ネコのために鐘は鳴る

寺院に住み着くパソコ〇好き

Unity2021以前のURPのシェーダーで "RenderPipeline" のタグがバグってる件について

Unity2021でURPのシェーダーを公式ドキュメント通りに書いて、Unity2022にアップデートするとバグります。描画されません。

何が悪いかというと、Tags の中に "RenderPipeline" = "UniversalRenderPipeline" と書くとおかしくなります。

Tags {
    "RenderPipeline" = "UniversalRenderPipeline"
}

と書くとおかしくなります。

正しくは

Tags {
    "RenderPipeline" = "UniversalPipeline"
}

です。どこのページだったか忘れましたが、Unity の公式リファレンスのサンプルコードが間違っていたし、他の第三者のいろんな記事で間違った方が書かれていたりします。(公式が間違ってるんだから仕方ない)

どうやら Unity がバグっていて、どんな名前が指定されていても無視され、全て UPR 用として認識されていたらしい。そして、"UniversalRenderPipeline" と公式が間違えていたのでそれが広まった。間違ったタグなのに Unity がタグを無視するバグのせいで、URP 用として認識されてしまう。

ここまでなら皆気づかず動いていたからよかったのだが、どうやら Unity2022 のどこかのバージョンでタグが無視されてしまうバグが修正された。 すると、間違ったタグ "UniversalRenderPipeline" を書いているシェーダーは URP 用として認識されないのでバグる。

「Unity2021までは "UniversalRenderPipeline" で、Unity2022以降は "UniversalPipeline" に変更された」という情報も見つけたのだが、これはたぶん正しくない。2021以前も "UniversalPipeline" が正しくて、Unity のバグのせいで間違ったタグでも URP 用として認識されていただけ。

つまり、結論として、Unity のバージョンによらず、URP 用のシェーダーには

Tags {
    "RenderPipeline" = "UniversalPipeline"
}

と書くのが正しい。

参考

https://discussions.unity.com/t/renderpipeline-tag-changed/925179/5

Ok, at least for URP it’s not so bad. Via the shader discord, Cyan told me that the tag was always UniversalPipeline, it’s just that a bug in previous versions would incorrectly also render UniversalRenderPipeline. That bug is detailed here - the renderer was ignoring filtering via the draw tag Opaque forward render pass renders shaders that are defined as incompatible w/URP

So for URP, you can have compatibility with both future and past versions - you need to change UniversalRenderPipeline to UniversalPipeline and it will work with both old and new versions (I tested on 2021). The tag value didn’t change, it just was not respected beforehand.

雑な意訳「タグは常に UniversalPipelineで、以前のバージョンにはバグがあっただけ。だから UniversalPipeline と書けばどっちのバージョンでもちゃんと動くよ。2021で試した。タグの値が変わったわけじゃない、以前はタグが機能してなかっただけ」

(Unity) GL.GetGPUProjectionMatrix による変換

Projection 行列の取得

自分で MVP 行列を掛けたりややこしいことをする人向けです。シェーダー内で Unity のマクロを使っている限りは内部的にいい感じにしてくれるため、この記事の内容は気にしなくてよいです。

Unity で Projection 行列は Camera.projectionMatrix で取得できますが、用途やプラットフォームによってはこの行列はそのまま使えません。 というのも、上記の行列は常に OpenGL 系の NDC (Normalized Device Coordinate) への変換行列となっています。現在のプラットフォームに対して正しい行列は GL.GetGPUProjectionMatrix メソッドを使う必要があります。

この辺までのふわっとした説明はネットで調べればいくらでも出てくるんですが、具体的な数値変換の内容があまりなかった (というか Unity の公式リファレンスが非常に分かりにくい解説だった) ため自分で調べました。

各プラットフォームでの NDC

前提として、Unity の OpenGLES の NDC は、

  • x = -1, 右 x = 1
  • y = -1, 上 x = 1
  • 手前 z = -1, 奥 z = 1

です。

また、Unity の DirectX, Vulkan, Metal では Reversed-Z の実装になっており、これらのプラットフォームでは

  • x = -1, 右 x = 1
  • y = -1, 上 x = 1
  • 手前 z = 1, 奥 z = 0

です。

Reversed-Z についての詳細はこちら。

https://developer.nvidia.com/content/depth-precision-visualized

また、Unity で現在のプラットフォームが Reversed-Z かどうかは SystemInfo.usesReversedZBuffer で取得できます。

GL.GetGPUProjectionMatrix の中身

GL.GetGPUProjectionMatrix は

Matrix4x4 proj = camera.projectionMatrix;
Matrix4x4 projActual = GL.GetGPUProjectionMatrix(proj, false);

のように、GL 系の Projection 行列を現在のプラットフォーム用に補正した行列を返します。第二引数は RenderTexture に対して描画するかどうかを指定します。これの中身を調べるために、

Matrix4x4 mat1 = GL.GetGPUProjectionMatrix(Matrix4x4.identity, false);
Matrix4x4 mat2 = GL.GetGPUProjectionMatrix(Matrix4x4.identity, true);

のように、単位行列入力してどうなるかを各プラットフォームで調べました。

  • GL.GetGPUProjectionMatrix(Matrix4x4.identity, false)

DX11, DX12, Vulkan, Metal:

1    0    0    0
0    1    0    0
0    0 -0.5  0.5
0    0    0    1

GLES:

1    0    0    0
0    1    0    0
0    0    1    0
0    0    0    1
  • GL.GetGPUProjectionMatrix(Matrix4x4.identity, true)

DX11, DX12, Vulkan, Metal:

1    0    0    0
0   -1    0    0
0    0 -0.5  0.5
0    0    0    1

GLES:

1    0    0    0
0   -1    0    0
0    0    1    0
0    0    0    1

でした。

解説

GL.GetGPUProjectionMatrix は GLES 以外のプラットフォームにおいて、

x' = x

y' = y  (renderIntoTexture: false) 
   or 
   = -y (renderIntoTexture: true)

z' = -0.5 * z + 0.5

となるような変換行列を左から掛ける操作に相当します。GLES の場合は何も変換を行いません。

これは GL 系の NDC から Reversed-Z な NDC への射影となっていることがわかります。第二引数のブール値は、NDC のY軸は上が正なのに対して、Texture は通常下向きが正であるため、上下反転させるためのものです。

xorshift の乱数を逆順で巻き戻す

xorshift について

xorshift はビットシフトと排他的論理和だけで構成される軽量な疑似乱数生成アルゴリズムです。32ビット版と64ビット版があります。

C言語での32ビット版は以下のコードです。

// シード
uint32_t s;

uint32_t xorshift()
{
    s ^= (s << 13);
    s ^= (s >> 17);
    s ^= (s << 5);
    return s;
}

入力はシード値のみで、生成された疑似乱数が次のシード値となります。得られた疑似乱数列は0以外の32ビット空間の値を重複なく全て巡回します。つまり、周期は232-1です。 シードの初期値は0以外である必要があります。(シードが0だと0が生成される)

シフト数 (上記のコードの << 13, >> 17, << 5) はいくつか別パターンがありますが、ここでは上記の組で説明します。

逆順に巻き戻す

先に結論を書くと、逆順で巻き戻すコードは以下です。

// シード (正順のものと共通)
uint32_t s;

uint32_t xorshift_rev()
{
    s ^= (s << 5) ^ (s << 10) ^ (s << 15) ^ (s << 20) ^ (s << 25) ^ (s << 30);
    s ^= (s >> 17);
    s ^= (s << 13) ^ (s << 26);
    return s;
}

どうしてこれで元に戻るのかを詳しく見ます。

xorshift のアルゴリズム

(s を入力)

  1. t1 = s ^ (s << 13)
  2. t2 = t1 ^ (t1 >> 17)
  3. t3 = t2 ^ (t2 << 5)

(t3 を出力)

の3つの手順で構成されています。 まず、最後の手順3を巻き戻す方法を考えます。

31~30 29~25 24~20 19~15 14~10 9~5 4~0
t2 ? ? ? ? ? ? ?
t2 << 5 ? ? ? ? ? ? O
t3 G F E D C B A

O は0埋めされたビットを表しています。 今、t3 は生成された乱数値であるため既知で、t3 から t2 を求めることを考えます。t3 の0から4ビット目までを A、1から5ビット目までを B、というように置きます。

t3 は t2 と t2 << 5 を xor したもので、上記の表で上の2段を xor したら下の段になります。したがって、0~4ビット目に注目すると、? ^ O == A となります。 したがって、この ?A であることがわかります。(∵ A ^ O == A)

これで該当する場所を埋めます。

31~30 29~25 24~20 19~15 14~10 9~5 4~0
t2 ? ? ? ? ? ? A
t2 << 5 ? ? ? ? ? A O
t3 G F E D C B A

次に、5~9ビット目に注目すると、? ^ A == B であるため、この ?A ^ B であることがわかります。(∵ (B ^ A) ^ A == B)

31~30 29~25 24~20 19~15 14~10 9~5 4~0
t2 ? ? ? ? ? B^A A
t2 << 5 ? ? ? ? B^A A O
t3 G F E D C B A

同様に上のビットに対しても繰り返していくと、最終的に以下のようになります。

31~30 29~25 24~20 19~15 14~10 9~5 4~0
t2 G^F^E^D^C^B^A F^E^D^C^B^A E^D^C^B^A D^C^B^A C^B^A B^A A
t2 << 5 F^E^D^C^B^A E^D^C^B^A D^C^B^A C^B^A B^A A O
t3 G F E D C B A

30~31ビット目に関しては5ビットのうちの溢れてしまう上位3ビットは無視して下位2ビットのみを表していると考えてください。

これでt2の全てのビットが分かりました。したがって、手順3 t3 = t2 ^ (t2 << 5) の逆は

t2 = t3 ^ (t3 << 5) ^ (t3 << 10) ^ (t3 << 15) ^ (t3 << 20) ^ (t3 << 25) ^ (t3 << 30)

となることがわかりました。

これで、手順2、手順1についても同様に逆手順を考えると、手順2 t2 = t1 ^ (t1 >> 17) の逆は

t1 = t2 ^ (t2 >> 17);

となり、手順1 t1 = s ^ (s << 13) の逆は

s = t1 ^ (t1 << 13) ^ (t1 << 26)

となります。これで、最初に示した xorshift を巻き戻すコードが得られました。

利用例

例えば、N問あるクイズをランダムで出題するとして、出題したクイズをメモリ上に記憶せずに、前の問題に戻れるようにしたいとか。

特に有用な利用法はないですが、巻き戻せるということを覚えておけば何かに役立つかもしれないですね。

(Unity) Android で HttpClient で通信するとインターネット権限が自動でつかない問題

筆者の動作確認: Unity 2021.3.29f1

忙しい人向けの結論

[Preserve]
internal sealed class MarkerForInternet : UnityWebRequest { }

上記のコードをどこかに書いておけば、UnityWebRequest を使っている判定になり、自動でインターネット権限をつけてくれる。

インターネット権限の付与

Unity で通信したいときは UnityWebRequest を使うと、Android の場合はビルド時に自動的にインターネット権限が付与される。 しかし、C# 標準ライブラリの HttpClient を使うと

SocketException: Access denied

のような例外が出る。あるいは、IL2CPP を通した場合、出る例外が少し違う。

WebException: Error: NameResolutionFailure

どちらにせよ、インターネット権限が自動では付与されないために発生しているようだ。

手動でインターネット権限をつけたい場合、Player Settings -> Other Settings -> Configuration -> Internet AccessAuto から Require に変更すれば権限が付く。

ここで問題が起こるのが、Unity 用のライブラリ開発者が HttpClient を使いたい場合。 パッケージ側からパッケージ利用者のアプリケーションのインターネット権限を付与することはできないため、利用者に Player Settings からインターネット権限を Require に設定してもらう必要が出てくる。これは非常に不親切。

パッケージ内にエディタ拡張などで権限を変更するような処理を書けば変更できるのかもしれないが、できればそんなことしたくない。プロジェクトの Internet AccessAuto のまま、UnityWebRequest を使った時と同じように権限が自動付与されてほしい。

解決策

考えた。 たぶん Unity のことだから UnityWebRequest を使って通信しているかどうかなんて真面目にチェックしていないだろう。UnityWebRequest クラスへの参照が存在しているかどうかで判断しているだけだろう。

[assembly: UseInternet(typeof(UnityWebRequest))]

internal sealed class UseInternetAttribute : Attribute
{
    public Type Type { get; }

    public UseInternetAttribute(Type type)
    {
        Type = type;
    }
}

上のコードをパッケージ内に書いたら、案の定パッケージを使う側のアプリに自動でインターネット権限がついた。解決。

と思ったら IL2CPP 環境で試したらインターネット権限も付与されず、解決してなかった。ストリップされてしまったのだろう。

要するに、UnityWebRequest クラスへの参照をアセンブリ内に存在させておけば、Unity は「UnityWebRequest を使っているのでインターネット権限が必要だ」と判断してくれるみたい。

ならば、UnityWebRequest クラスへの参照を含む実装がストリップによって消えなければいいのだ。 以下、IL2CPP 対応版

[Preserve]
internal sealed class MarkerForInternet : UnityWebRequest { }

UnityWebRequest を継承したクラスを定義しておいて、Preserve 属性を使って IL2CPP で消えないようにした。

これで、実際に問題なく動くことが確認できた。不要なクラスを定義せざるを得ないのは少し気持ち悪いが仕方ない。 継承クラスの定義でなくとも、UnityWebRequest が IL2CPP によって消えないコードの中で何かに使われていればいいようなので、自分なりの方法でアレンジしてほしい。

これでうまくいくのだがドキュメント記載の仕様ではないため、将来のバージョンでは使えないかもしれないので注意。

ちなみに

上記の UnityWebRequest の有無の判定が、C#コンパイル時なのかコンパイル後なのか気になった。

Source Generator などでよく使われるテクニックとして、Conditional 属性に存在しないシンボル名を書くことで、コンパイル時にのみコンパイラに情報を読み取らせてアセンブリには情報を残さないというテクニックがある。 先ほどの例に、[System.Diagnostics.Conditional("COMPILE_TIME_ONLY")] という属性をつけて、コンパイラには UnityWebRequest を認識させるがアセンブリには情報を残さないようにしてみた。

// ↓ これはインターネット権限がつかない

[assembly: UseInternet(typeof(UnityWebRequest))]

[System.Diagnostics.Conditional("COMPILE_TIME_ONLY")]
internal sealed class UseInternetAttribute : Attribute
{
    public Type Type { get; }

    public UseInternetAttribute(Type type)
    {
        Type = type;
    }
}

するとインターネット権限がつかない。コンパイル時に Roslyn でソースコードを舐めて UnityWebRequest の有無をチェックしているならこれでも権限がつくはずだが、どうやらダメなよう。 つまり、ソースコートではなくコンパイル後にアセンブリから情報を拾って UnityWebRequest の有無を見ているらしい。 なるほど、IL2CPP を通すとダメなわけだ。

(C#) ジェネリック型制約違いのオーバーロード

小ネタです。

C#ジェネリックメソッドには型制約をつけることができます。 この時、C# ではシグネチャ (メソッド名、引数、戻り値など) が同じでジェネリック型制約のみが異なるメソッドはオーバーロードとして書くことができません。

public static void Foo<T>(this T value) where T : class
{
}

// コンパイルエラー!!
public static void Foo<T>(this T value) where T : struct, Enum
{
}

C# の文法的にどうしようもないので、以下のコードで妥協しました。

public static void Foo<T>(this T value, ClassMarker _ = default)
    where T : class
{
}

public static void Foo<T>(this T value, EnumMarker _ = default)
    where T : struct, Enum
{
}

public struct ClassMarker { }
public struct EnumMarker { }

マーカー用の型を作って、メソッドの二番目の引数に追加してデフォルト引数を追加しておきます。

こうするとシグネチャが異なるのでオーバーロードでき、なおかつメソッド使用者側は二番目の引数を省略できるので2つとも同じ書き方ができます。

ライブラリとして public に露出するメソッドとしてはよくないかもしれませんが、使いどころによっては便利に書けるかもしれないです。

public enum Animal 
{
    Dog, Cat,
}

// クラス引数ののオーバーロード
"hello".Foo();
// enum 引数のオーバーロード
Animal.Dog.Foo();

(C#) ValueTuple のサイズとレイアウト

突然ですが、C#sizeof((byte, int)) の値はいくつでしょう?(64bit 環境)

byte が1バイト、int が4バイト、つまり (byte, int) は5バイト……ではないです。

 

答えは8バイト。アライメント上に乗るようにパディングがあるからですね。

0 1 2 3 4 5 6 7
byte - - - int (0) int (1) int (2) int (3)

 

では、sizeof((byte, int, short)) の値はいくつでしょう?(64bit 環境)

byte, int, short がそれぞれ1, 4, 2 バイトで、アライメント上に乗せると4, 4, 4 で12バイト?

 

いいえ、答えは8バイトです。レイアウトは以下のようになります。

0 1 2 3 4 5 6 7
byte - short (0) short(1) int (0) int (1) int (2) int (3)

はい。お察しの通り、ValueTuple はフィールドレイアウトが順番通りではないです。

(byte, int, short) の場合、byte, int, short の順番にアライメントに乗せるよりも、順番を入れ替えて byte, short, int の順にそろえたほうが構造体のサイズを小さくできるためこのようになります。

 

なぜ順番が入れ替わるのでしょう? たとえば (byte, int) 型の場合、(byte, int) は糖衣構文で実際の型は ValueTuple<byte, int> という型になります。

これは以下のように定義されています。

[StructLayout(LayoutKind.Auto)]
public struct ValueTuple<T1, T2>
{
    public T1 Item1;
    public T2 Item2;
}

StructLayout(LayoutKind.Auto) が指定されているので、コンパイラが都合よく自由にメモリレイアウトを決めるということです。 これによって効率よく配置されてメモリサイズを削減するようになる利点があります。

しかし、レイアウトはコンパイラが決めるためネイティブとの相互運用には使えなくなってしまうのです。 実際にはアライメントを考慮すればコンパイラが決めるレイアウトはわかるのですが、仕様上これは保証されません。

ネイティブライブラリとの相互運用で、構造体の型定義をサボって楽するための匿名型として ValueTuple は使えないんですね。

おつらい。

WGSL 仕様メモ

WGSL

WGSL (WebGPU Shader Language) の文法や仕様の日本語解説が少ないので自分用メモ。仕様を網羅しているわけではなく、自分が書く上で必要そうな部分を適当に抜粋してまとめた。

Tour of WGSLWGSL の仕様 から日本語に訳して簡単にまとめた。 仕様は長すぎて辛いけど困ったら見るべき。全部書いてある。

定数 (const)

const の型は abstract-int と abstract-float として扱われ、精度は実際の変数に代入する場所で決まる。

変数

let はイミュータブルな変数の宣言。ミュータブル変数は var で宣言する。 varvar<AS> で宣言する。ASアドレス空間 (address spaces)

var<function>

関数内のローカル変数のデフォルトは var<function> であり、普通ローカル変数では単に var で宣言する。 初期値を省略した場合、ゼロ初期化される。

var<private>

シェーダーモジュール内でのグローバル変数。同一モジュール内の全ての関数からアクセス可能。 宣言時の初期値は const-expression でなければならない。省略した場合はゼロ初期化される。 モジュールグローバルでのみ宣言可能。

var<workgroup>

workgroup 内で共有されるミュータブル変数。各呼び出し (invocation) の間では atomicworkgroupBarrier で同期する必要がある。 モジュールグローバルでのみ宣言可能。 初期化式は必要なく、各ワーキンググループの実行時に自動でゼロ初期化される。

var<uniform>

ユニフォーム変数

var<storage>

(自分が使ってないので調べてない)

var<handle>

(自分が使ってないので調べてない)

let

イミュータブルな変数宣言。初期化子が必須。関数内ローカルでのみ宣言可能。 ポインタ型にすることが可能。

属性

@ から始まるのが属性。

@vertex fn vs_main() -> @builtin(position) vec4<f32> {
    ...
}

行列

行列の引数省略時はゼロ初期化。引数は列優先。メモリ上の表現も列優先。

const zero_init = mat3x4<f32>();
/*
| 0 0 0 |
| 0 0 0 |
| 0 0 0 |
| 0 0 0 |
*/

const column_wise = mat3x2<f32>(vec2<f32>(1, 2), vec2<f32>(4, 5), vec2<f32>(6, 7));
/*
| 1 4 6 |
| 2 5 7 |
*/

const scalar_wise = mat2x3<f32>(1, 2, 3, 4, 5, 6);
/*
| 1 4 |
| 2 5 |
| 3 6 |
*/

配列

  • Fixed-Sized Array array<T, N>
  • Runtime-Sized Array array<T>

Fixed-Sized Array の配列長 N は定数式ならリテラルでなくともよい。

const numbers = array<i32, 7>(1, 4, 2, 3);

const three = 3;
const foo: array<i32, three * 2 + 1> = numbers;

Runtime-Sized Array は Storage Buffer のリソースとしてのみ使える。 バッファ全体を表すか、バッファ全体を表す構造体の最後のメンバーとして使う。

素数は実行時に決定され、変数に関連する BufferBinding のサイズに収まる範囲で可能な限り大きくなる。 実行時の配列長は arrayLength で取得できる。

インデックスによって値を取得できるが、配列自体を変数に代入することはできない。

@group(0) @binding(0) var<storage> weights: array<f32>;

struct PointLight { position: vec3f, color: vec3f, }
struct LightData {
  meanIntensity: f32,
  point: array<PointLight>,
}
@group(0) @binding(1) var<storage> lights: LightData;

fn number_of_lights() -> u32 {
    return arrayLength(&lights.point);
}

fn get_point_light(i: i32) -> PointLight {
    return lights.point[i];
}

fn cannot_copy_whole_array() {
    // 通常の変数に代入はできない
    // var all_point_lights = lights.point; // Error
}

構造体 (struct)

struct Data {
  a: f32,
  b: f32,
}

constructible な構造体の場合、() で初期化できる。 constructible とは

  • スカラー型 (u32, f32, etc...)
  • ベクトル型 (vec2<f32>, vec3<f32>, etc...)
  • 行列型 (mat2x2<f32>, mat3x3<f32>, etc...)
  • 要素の型が constructible な固定長配列型 (array<f32, 3>, etc...)
  • メンバーの型が全て constructible な構造体型
const data = Data(1.0, 2.0);

原子性 (atomicity)

uniform, storage, workgroup は並列実行において共有されている。 uniform は読み取り専用なので問題は起こらないが、storageworkgroup は書き込みも可能であるため、競合に注意する必要がある。

atomic<T>i32u32 に対してのみ使える。 atomic<T>workgroupstorageアドレス空間の変数でのみ使える。

var<workgroup> counter: atomic<u32>;

atomic<T> は constructible ではないため、以下の状況では直接使用できない。

  • expression に書けない
  • 関数の引数、戻り値に書けない
  • 変数に代入できない
  • 変数の初期化式に書けない

ビルトイン関数でのみ操作可能で、atomicLoadatomicStore で読み書きできる。 他にも atomicAdd, atomicMin, atomicMax, atomicAnd, atomicOr, atomicXor などがある。

fn atomicLoad(atomic_ptr: ptr<AS, atomic<T>, read_write>) -> T
fn atomicStore(atomic_ptr: ptr<AS, atomic<T>, read_write>, v: T)

ポインタ (pointer)

ポインタは ptr<AS, T, AM> で表される。

  • ASアドレス空間 (address spaces)
    • uniform
    • storage
    • workgroup
    • private
    • function
    • handle
  • Tポインターが指す型
  • AM はアクセスモード。省略可能
    • read (default)
    • read_write

アロー演算子 a->b はない。カッコで囲んで (*a).b のように書く。

ユーザー定義関数の引数にポインタを使う時の制約

  • アドレス空間functionprivate でなければならない
  • 変数全体へのポインタのみ使える。複合変数の一部のみのアドレスを渡すことはできない
struct Data {
    aaa: u32,
    bbb: u32,
}
var<private> data: Data;

fn foo() {
  //bar(&data.aaa); // Error: 複合変数の一部のみのアドレスを渡すことはできない
}

式 (expression)

三つの評価フェーズがある。

  • Shader creation time
  • Pipeline creation time
  • Shader execution time

Shader creation time

const-expressions の値を確定します。

Pipeline creation time

override-expressions の値を確定します。

  • override 宣言された値
  • GPUProgrammableStage.constants からの値

Shader execution time

runtime-expressions の値を確定します。

  • letvar 宣言された値
  • 全ての呼び出し関数
  • 変数値
  • 変数への参照やポインタ

リテラル

サフィックスのないリテラルは abstract-int か abstract-float になり、具体的な型付きの変数に束縛される時に型が決まる。

サフィックス
u u32 1u
i i32 1i
f f32 1.0f

@const ビルトイン関数

  • radians(x: T) -> T
  • const_assert(x: bool)
const x = 10;
const_assert(x == 10);  // false ならコンパイルエラー

制御フロー

if

if (condition) { ... }

または

if condition { ... }

switch

switch (condition) { ... }

または

switch condition { ... }

let a = 4;
switch a {
    case 1, 2, 3: {    
    }
    default: {
    }
}

for

for(var i = 0; i < 10; i++) {
}