Repository: syuilo/aiscript
Pull request #196 (2022-08-20 03:01:36)

enhance: Chain combination
<!-- ℹ お読みください
PRありがとうございます！ PRを作成する前に、以下をご確認ください:
- 可能であればタイトルに、以下で示すようなPRの種類が分かるキーワードをプリフィクスしてください。
  - fix / refactor / feat / enhance / perf / chore
  - また、PRの粒度が適切であることを確認してください。ひとつのPRに複数の種類の変更や関心を含めることは避けてください。
- このPRによって解決されるIssueがある場合は、そのIssueへの参照を本文内に含めてください。
- CHANGELOG.mdに変更点を追記してください。リファクタリングなど、利用者に影響を与えない変更についてはこの限りではありません。
- この変更により新たに作成、もしくは更新すべきドキュメントがないか確認してください。
- 機能追加やバグ修正をした場合は、可能であればテストケースを追加してください。
- テスト、Lintが通っていることを予め確認してください。
  - `npm run test`、`npm run lint`でぞれぞれ実施可能です
- `npm run api`を実行してAPIレポートを更新し、差分がある場合はコミットしてください。
ご協力ありがとうございます🤗
-->
<!-- ℹ README
Thank you for your PR! Before creating a PR, please check the following:
- If possible, prefix the title with a keyword that identifies the type of this PR, as shown below.
  - fix / refactor / feat / enhance / perf / chore
  - Also, make sure that the granularity of this PR is appropriate. Please do not include more than one type of change or interest in a single PR.
- If there is an Issue which will be resolved by this PR, please include a reference to the Issue in the text.
- Please add the summary of the changes to CHANGELOG.md. However, this is not necessary for changes that do not affect the users, such as refactoring.
- Check if there are any documents that need to be created or updated due to this change.
- If you have added a feature or fixed a bug, please add a test case if possible.
- Please make sure that tests and Lint are passed in advance.
  - You can run it with `npm run test` and `npm run lint`.
- Run `npm run api` to update the API report and commit it if there are any diffs.
Thanks for your cooperation 🤗
-->

# What
<!-- このPRで何をしたのか？ どう変わるのか？ -->
<!-- What did you do with this PR? How will it change things? -->
以下の作業を行います。
- パーサーの変更
- ノード定義の変更
- 各種プラグインの変更
- infix-to-fncallの変更

~~一旦作業ブランチへのPRです。~~
masterへのPRです。

Related #194 

## パーサー処理の変更
これまでCall、IndexRef、PropCall、PropRefで個別に処理していた部分がChain系ルールのループとして処理できるようになります。
また、これまでPropAssign、IndexAssign、Inc、Decで処理していた部分がChain系ルールを利用した処理に変更されます。

Chain系ルールを導入することにより、パーサーから返されるノード(IRノード)が一部変更・削除されます。
- 変更
  - prop
  - index
  - call
  - inc
  - dec
  - assign
- 削除
  - propAssign
  - indexAssign
  - propCall

## ノードの型定義
src/node.tsをASTノードの定義(src/parser/node.ts)と、変形した後のIRノードの定義(src/node.ts)に分割します。
ASTノードは便宜上の理由で厳密には定義していません。ASTと変形後のノードを区別せずに扱えるようにしています。

## 変形プラグインの追加
ASTノードからIRノードへ変形するプラグインを追加します。

## infix-to-fncallおよび各種プラグインの修正
ノード定義が変更になった関係で修正が必要になった部分の修正を行います。

# Why
<!-- なぜそうするのか？ どういう意図なのか？ 何が困っているのか？ -->
<!-- Why do you do it? What are your intentions? What is the problem? -->

# Additional info (optional)
<!-- テスト観点など -->
<!-- Test perspective, etc -->


marihachi: @syuilo ノードの定義って今までみたいにコメントが付いてるほうが良い？
syuilo: 無いよりはあった方が良いかも
marihachi: 外から使うことが多いであろうIRノード側に付けとくか
marihachi: やっぱりノード定義の改修はまた別の機会にするかも
syuilo: IRノードって何かしら
marihachi: パーサーが生成したノードをASTノードとして、それを変形(attrの処理やchain)したものをIRノードとしてる。
syuilo: YOSASOU
chain対応インタプリタ書くわよ
marihachi: ありがとう
あと変形処理の追加が残ってるから、それが終わったらReadyにするよ

ノードの型定義は今のままでいくつもりだから、インタプリタの方はそれを使いつつ並行してもらえると助かる

syuilo: 余裕あればテスト追加しておいてもらえると助かる
marihachi: とりあえずはインタプリタレベルのテストがあれば良さそう？
syuilo: 👍🏻 
marihachi: IRノードで一部名前を変えた
marihachi: assign.destの中にあるchainが処理されない
marihachi: 深いネストにあるchainが処理されないな
marihachi: ```
a.b[a.b]()
```
が正しく変形されない
marihachi: いけたかも
marihachi: ノードの定義細かく分けるのやっぱやめたい
細かくすることでデメリットが多そう
syuilo: ChainElement と Expression ってどういう基準で分けてる？
marihachi: ChainElementは式のうちchain出来ないものは除外されてる
syuilo: アサインに関しては今まで通り assign と propAssign で分かれていた方がよさそう
assignは今まで通りアサインする変数名が含まれる
propAssign は dest として Expression が含まれる
marihachi: 従来の代入か否かについては、destにvarが入ってくるかどうかで判定できそうな気がする？
marihachi: 変形プラグインを増やす？
syuilo: > 従来の代入か否かについては、destにvarが入ってくるかどうかで判定できそうな気がする

あー確かに
そこでevalしないようにすればいいか
marihachi: masterへのPRに変更した
marihachi: ASTノードとIRノードの定義を変更した
syuilo: マージするか
codecov[bot]: # [Codecov](https://codecov.io/gh/syuilo/aiscript/pull/196?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo) Report
> Merging [#196](https://codecov.io/gh/syuilo/aiscript/pull/196?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo) (605086d) into [master](https://codecov.io/gh/syuilo/aiscript/commit/ad4878777ac989392265776b8bde2d2426566871?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo) (ad48787) will **increase** coverage by `5.43%`.
> The diff coverage is `72.77%`.

```diff
@@            Coverage Diff             @@
##           master     #196      +/-   ##
==========================================
+ Coverage   70.04%   75.48%   +5.43%     
==========================================
  Files          16       18       +2     
  Lines        2257     2688     +431     
  Branches      311      360      +49     
==========================================
+ Hits         1581     2029     +448     
+ Misses        670      653      -17     
  Partials        6        6              
```


| [Impacted Files](https://codecov.io/gh/syuilo/aiscript/pull/196?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo) | Coverage Δ | |
|---|---|---|
| [src/node.ts](https://codecov.io/gh/syuilo/aiscript/pull/196/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo#diff-c3JjL25vZGUudHM=) | `0.00% <0.00%> (ø)` | |
| [src/interpreter/util.ts](https://codecov.io/gh/syuilo/aiscript/pull/196/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo#diff-c3JjL2ludGVycHJldGVyL3V0aWwudHM=) | `39.42% <33.33%> (+3.28%)` | :arrow_up: |
| [src/parser/plugins/set-attribute.ts](https://codecov.io/gh/syuilo/aiscript/pull/196/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo#diff-c3JjL3BhcnNlci9wbHVnaW5zL3NldC1hdHRyaWJ1dGUudHM=) | `81.25% <81.81%> (-1.68%)` | :arrow_down: |
| [src/parser/plugins/validate-keyword.ts](https://codecov.io/gh/syuilo/aiscript/pull/196/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo#diff-c3JjL3BhcnNlci9wbHVnaW5zL3ZhbGlkYXRlLWtleXdvcmQudHM=) | `88.42% <81.81%> (-1.70%)` | :arrow_down: |
| [src/interpreter/index.ts](https://codecov.io/gh/syuilo/aiscript/pull/196/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo#diff-c3JjL2ludGVycHJldGVyL2luZGV4LnRz) | `91.13% <95.23%> (-0.17%)` | :arrow_down: |
| [src/parser/node.ts](https://codecov.io/gh/syuilo/aiscript/pull/196/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo#diff-c3JjL3BhcnNlci9ub2RlLnRz) | `97.92% <97.92%> (ø)` | |
| [src/parser/plugins/transform-chain.ts](https://codecov.io/gh/syuilo/aiscript/pull/196/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo#diff-c3JjL3BhcnNlci9wbHVnaW5zL3RyYW5zZm9ybS1jaGFpbi50cw==) | `98.72% <98.72%> (ø)` | |
| [src/interpreter/infix-to-fncall.ts](https://codecov.io/gh/syuilo/aiscript/pull/196/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo#diff-c3JjL2ludGVycHJldGVyL2luZml4LXRvLWZuY2FsbC50cw==) | `98.14% <100.00%> (ø)` | |
| [src/parser/index.ts](https://codecov.io/gh/syuilo/aiscript/pull/196/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo#diff-c3JjL3BhcnNlci9pbmRleC50cw==) | `91.42% <100.00%> (+0.38%)` | :arrow_up: |
| [src/parser/plugins/validate-type.ts](https://codecov.io/gh/syuilo/aiscript/pull/196/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo#diff-c3JjL3BhcnNlci9wbHVnaW5zL3ZhbGlkYXRlLXR5cGUudHM=) | `100.00% <100.00%> (ø)` | |
| ... and [3 more](https://codecov.io/gh/syuilo/aiscript/pull/196/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo) | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo)


Patch:
diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md
index fef02ec..97c32de 100644
--- a/etc/aiscript.api.md
+++ b/etc/aiscript.api.md
@@ -4,17 +4,35 @@
 
 ```ts
 
+// Warning: (ae-forgotten-export) The symbol "NodeBase" needs to be exported by the entry point index.d.ts
+//
+// @public (undocumented)
+type AddAssign = NodeBase & {
+    type: 'inc';
+    dest: Expression;
+    expr: Expression;
+};
+
+// Warning: (ae-forgotten-export) The symbol "NodeBase" needs to be exported by the entry point index.d.ts
+//
+// @public (undocumented)
+type AddAssign_2 = NodeBase_2 & {
+    type: 'inc';
+    dest: Expression_2;
+    expr: Expression_2;
+};
+
 // @public (undocumented)
 export class AiScript {
     constructor(vars: AiScript['vars'], opts?: AiScript['opts']);
     // (undocumented)
     abort(): void;
     // (undocumented)
-    static collectMetadata(script?: Node_2[]): Map<any, any> | undefined;
-    // Warning: (ae-forgotten-export) The symbol "Node" needs to be exported by the entry point index.d.ts
+    static collectMetadata(script?: N.Node[]): Map<any, any> | undefined;
+    // Warning: (ae-forgotten-export) The symbol "N" needs to be exported by the entry point index.d.ts
     //
     // (undocumented)
-    exec(script?: Node_2[]): Promise<void>;
+    exec(script?: N.Node[]): Promise<void>;
     // (undocumented)
     execFn(fn: VFn, args: Value[]): Promise<Value>;
     // (undocumented)
@@ -35,6 +53,20 @@ const ARR: (arr: VArr['value']) => {
     value: Value[];
 };
 
+// @public (undocumented)
+type Arr = NodeBase & {
+    type: 'arr';
+    value: Expression[];
+};
+
+// Warning: (ae-forgotten-export) The symbol "ChainProp" needs to be exported by the entry point index.d.ts
+//
+// @public (undocumented)
+type Arr_2 = NodeBase_2 & ChainProp & {
+    type: 'arr';
+    value: Expression_2[];
+};
+
 // @public (undocumented)
 function assertArray(val: Value): asserts val is VArr;
 
@@ -53,6 +85,20 @@ function assertObject(val: Value): asserts val is VObj;
 // @public (undocumented)
 function assertString(val: Value): asserts val is VStr;
 
+// @public (undocumented)
+type Assign = NodeBase & {
+    type: 'assign';
+    dest: Expression;
+    expr: Expression;
+};
+
+// @public (undocumented)
+type Assign_2 = NodeBase_2 & {
+    type: 'assign';
+    dest: Expression_2;
+    expr: Expression_2;
+};
+
 // @public (undocumented)
 type Attr_2 = {
     attr?: {
@@ -61,27 +107,158 @@ type Attr_2 = {
     }[];
 };
 
+// @public (undocumented)
+type Attribute = NodeBase & {
+    type: 'attr';
+    name: string;
+    value: Expression;
+};
+
+// @public (undocumented)
+type Attribute_2 = NodeBase_2 & {
+    type: 'attr';
+    name: string;
+    value: Expression_2;
+};
+
+// @public (undocumented)
+type Block = NodeBase & {
+    type: 'block';
+    statements: (Statement | Expression)[];
+};
+
+// @public (undocumented)
+type Block_2 = NodeBase_2 & ChainProp & {
+    type: 'block';
+    statements: (Statement_2 | Expression_2)[];
+};
+
 // @public (undocumented)
 const BOOL: (bool: VBool['value']) => {
     type: "bool";
     value: boolean;
 };
 
+// @public (undocumented)
+type Bool = NodeBase & {
+    type: 'bool';
+    value: boolean;
+};
+
+// @public (undocumented)
+type Bool_2 = NodeBase_2 & ChainProp & {
+    type: 'bool';
+    value: boolean;
+};
+
 // @public (undocumented)
 const BREAK: () => {
     type: "break";
     value: null;
 };
 
+// @public (undocumented)
+type Break = NodeBase & {
+    type: 'break';
+};
+
+// @public (undocumented)
+type Break_2 = NodeBase_2 & {
+    type: 'break';
+};
+
+// @public (undocumented)
+function CALL(target: Call_2['target'], args: Call_2['args'], loc?: {
+    start: number;
+    end: number;
+}): Call_2;
+
+// @public (undocumented)
+type Call = NodeBase & {
+    type: 'call';
+    target: Expression;
+    args: Expression[];
+};
+
+// @public (undocumented)
+type Call_2 = NodeBase_2 & {
+    type: 'call';
+    target: Expression_2;
+    args: Expression_2[];
+};
+
+// @public (undocumented)
+type CallChain = NodeBase_2 & {
+    type: 'callChain';
+    args: Expression_2[];
+};
+
+// @public (undocumented)
+type ChainMember = CallChain | IndexChain | PropChain;
+
 // @public (undocumented)
 const CONTINUE: () => {
     type: "continue";
     value: null;
 };
 
+// @public (undocumented)
+type Continue = NodeBase & {
+    type: 'continue';
+};
+
+// @public (undocumented)
+type Continue_2 = NodeBase_2 & {
+    type: 'continue';
+};
+
+// @public (undocumented)
+type Definition = NodeBase & {
+    type: 'def';
+    name: string;
+    varType?: TypeSource;
+    expr: Expression;
+    mut: boolean;
+    attr: Attribute[];
+};
+
+// @public (undocumented)
+type Definition_2 = NodeBase_2 & {
+    type: 'def';
+    name: string;
+    varType?: TypeSource;
+    expr: Expression_2;
+    mut: boolean;
+    attr?: Attribute_2[];
+};
+
+// @public (undocumented)
+type Each = NodeBase & {
+    type: 'forOf';
+    var: string;
+    items: Expression;
+    for: Statement | Expression;
+};
+
+// @public (undocumented)
+type Each_2 = NodeBase_2 & {
+    type: 'forOf';
+    var: string;
+    items: Expression_2;
+    for: Statement_2 | Expression_2;
+};
+
 // @public (undocumented)
 function eq(a: Value, b: Value): boolean;
 
+// @public (undocumented)
+type Expression = Infix | If | Fn | Match | Block | Tmpl | Str | Num | Bool | Null | Obj | Arr | Var | Call | Index | Prop;
+
+// @public (undocumented)
+type Expression_2 = Infix_2 | If_2 | Fn_2 | Match_2 | Block_2 | Tmpl_2 | Str_2 | Num_2 | Bool_2 | Null_2 | Obj_2 | Arr_2 | Var_2 | Call_2 | // IR
+Index_2 | // IR
+Prop_2;
+
 // @public (undocumented)
 const FALSE: {
     type: "bool";
@@ -96,6 +273,28 @@ const FN: (args: VFn['args'], statements: VFn['statements'], scope: VFn['scope']
     scope: Scope | undefined;
 };
 
+// @public (undocumented)
+type Fn = NodeBase & {
+    type: 'fn';
+    args: {
+        name: string;
+        type?: TypeSource;
+    }[];
+    ret?: TypeSource;
+    children: (Statement | Expression)[];
+};
+
+// @public (undocumented)
+type Fn_2 = NodeBase_2 & ChainProp & {
+    type: 'fn';
+    args: {
+        name: string;
+        type?: TypeSource;
+    }[];
+    ret?: TypeSource;
+    children: (Statement_2 | Expression_2)[];
+};
+
 // @public (undocumented)
 const FN_NATIVE: (fn: VFn['native']) => {
     type: "fn";
@@ -106,26 +305,224 @@ const FN_NATIVE: (fn: VFn['native']) => {
     }) => Value | Promise<Value> | void) | undefined;
 };
 
+// @public (undocumented)
+type For = NodeBase & {
+    type: 'for';
+    var?: string;
+    from?: Expression;
+    to?: Expression;
+    times?: Expression;
+    for: Statement | Expression;
+};
+
+// @public (undocumented)
+type For_2 = NodeBase_2 & {
+    type: 'for';
+    var?: string;
+    from?: Expression_2;
+    to?: Expression_2;
+    times?: Expression_2;
+    for: Statement_2 | Expression_2;
+};
+
 // @public (undocumented)
 function getLangVersion(input: string): string | null;
 
+// @public (undocumented)
+function hasChainProp(x: Node_3): x is Node_3 & ChainProp;
+
+// @public (undocumented)
+type If = NodeBase & {
+    type: 'if';
+    cond: Expression;
+    then: Statement | Expression;
+    elseif: {
+        cond: Expression;
+        then: Statement | Expression;
+    }[];
+    else?: Statement | Expression;
+};
+
+// @public (undocumented)
+type If_2 = NodeBase_2 & {
+    type: 'if';
+    cond: Expression_2;
+    then: Statement_2 | Expression_2;
+    elseif: {
+        cond: Expression_2;
+        then: Statement_2 | Expression_2;
+    }[];
+    else?: Statement_2 | Expression_2;
+};
+
+// @public (undocumented)
+function INDEX(target: Index_2['target'], index: Index_2['index'], loc?: {
+    start: number;
+    end: number;
+}): Index_2;
+
+// @public (undocumented)
+type Index = NodeBase & {
+    type: 'index';
+    target: Expression;
+    index: Expression;
+};
+
+// @public (undocumented)
+type Index_2 = NodeBase_2 & {
+    type: 'index';
+    target: Expression_2;
+    index: Expression_2;
+};
+
+// @public (undocumented)
+type IndexChain = NodeBase_2 & {
+    type: 'indexChain';
+    index: Expression_2;
+};
+
+// @public (undocumented)
+type Infix = NodeBase & {
+    type: 'infix';
+    operands: Expression[];
+    operators: InfixOperator[];
+};
+
+// @public (undocumented)
+type Infix_2 = NodeBase_2 & {
+    type: 'infix';
+    operands: Expression_2[];
+    operators: InfixOperator_2[];
+};
+
+// @public (undocumented)
+type InfixOperator = "||" | "&&" | "==" | "!=" | "<=" | ">=" | "<" | ">" | "+" | "-" | "*" | "|" | "%";
+
+// @public (undocumented)
+type InfixOperator_2 = "||" | "&&" | "==" | "!=" | "<=" | ">=" | "<" | ">" | "+" | "-" | "*" | "|" | "%";
+
+// @public (undocumented)
+function isExpression(x: Node_2): x is Expression;
+
+// @public (undocumented)
+function isExpression_2(x: Node_3): x is Expression_2;
+
+// @public (undocumented)
+function isStatement(x: Node_2): x is Statement;
+
+// @public (undocumented)
+function isStatement_2(x: Node_3): x is Statement_2;
+
 // @public (undocumented)
 function jsToVal(val: any): Value;
 
 // @public (undocumented)
-function nodeToString(node: Node_2): string;
+type Loc = {
+    start: number;
+    end: number;
+};
+
+// @public (undocumented)
+type Loop = NodeBase & {
+    type: 'loop';
+    statements: (Statement | Expression)[];
+};
+
+// @public (undocumented)
+type Loop_2 = NodeBase_2 & {
+    type: 'loop';
+    statements: (Statement_2 | Expression_2)[];
+};
+
+// @public (undocumented)
+type Match = NodeBase & {
+    type: 'match';
+    about: Expression;
+    qs: {
+        q: Expression;
+        a: Statement | Expression;
+    }[];
+    default?: Statement | Expression;
+};
+
+// @public (undocumented)
+type Match_2 = NodeBase_2 & ChainProp & {
+    type: 'match';
+    about: Expression_2;
+    qs: {
+        q: Expression_2;
+        a: Statement_2 | Expression_2;
+    }[];
+    default?: Statement_2 | Expression_2;
+};
+
+// @public (undocumented)
+type Meta = NodeBase & {
+    type: 'meta';
+    name: string | null;
+    value: Expression;
+};
+
+// @public (undocumented)
+type Meta_2 = NodeBase_2 & {
+    type: 'meta';
+    name: string | null;
+    value: Expression_2;
+};
+
+// @public (undocumented)
+type Namespace = NodeBase & {
+    type: 'ns';
+    name: string;
+    members: (Definition | Namespace)[];
+};
+
+// @public (undocumented)
+type Namespace_2 = NodeBase_2 & {
+    type: 'ns';
+    name: string;
+    members: (Definition_2 | Namespace_2)[];
+};
+
+// @public (undocumented)
+type Node_2 = Namespace | Meta | Statement | Expression;
+
+// @public (undocumented)
+type Node_3 = Namespace_2 | Meta_2 | Statement_2 | Expression_2 | ChainMember;
 
 // @public (undocumented)
 const NULL: {
     type: "null";
 };
 
+// @public (undocumented)
+type Null = NodeBase & {
+    type: 'null';
+};
+
+// @public (undocumented)
+type Null_2 = NodeBase_2 & ChainProp & {
+    type: 'null';
+};
+
 // @public (undocumented)
 const NUM: (num: VNum['value']) => {
     type: "num";
     value: number;
 };
 
+// @public (undocumented)
+type Num = NodeBase & {
+    type: 'num';
+    value: number;
+};
+
+// @public (undocumented)
+type Num_2 = NodeBase_2 & ChainProp & {
+    type: 'num';
+    value: number;
+};
+
 // @public (undocumented)
 const OBJ: (obj: VObj['value']) => {
     type: "obj";
@@ -133,7 +530,19 @@ const OBJ: (obj: VObj['value']) => {
 };
 
 // @public (undocumented)
-export function parse(input: string): Node_2[];
+type Obj = NodeBase & {
+    type: 'obj';
+    value: Map<string, Expression>;
+};
+
+// @public (undocumented)
+type Obj_2 = NodeBase_2 & ChainProp & {
+    type: 'obj';
+    value: Map<string, Expression_2>;
+};
+
+// @public (undocumented)
+export function parse(input: string): N.Node[];
 
 // @public (undocumented)
 export class Parser {
@@ -141,13 +550,41 @@ export class Parser {
     // (undocumented)
     addPlugin(plugin: ParserPlugin): void;
     // (undocumented)
-    static parse(input: string): Node_2[];
+    static parse(input: string): N.Node[];
     // (undocumented)
-    parse(input: string): Node_2[];
+    parse(input: string): N.Node[];
 }
 
+// Warning: (ae-forgotten-export) The symbol "Ast" needs to be exported by the entry point index.d.ts
+//
+// @public (undocumented)
+export type ParserPlugin = (nodes: Ast.Node[]) => Ast.Node[];
+
+// @public (undocumented)
+function PROP(target: Prop_2['target'], name: Prop_2['name'], loc?: {
+    start: number;
+    end: number;
+}): Prop_2;
+
+// @public (undocumented)
+type Prop = NodeBase & {
+    type: 'prop';
+    target: Expression;
+    name: string;
+};
+
+// @public (undocumented)
+type Prop_2 = NodeBase_2 & {
+    type: 'prop';
+    target: Expression_2;
+    name: string;
+};
+
 // @public (undocumented)
-export type ParserPlugin = (nodes: Node_2[]) => Node_2[];
+type PropChain = NodeBase_2 & {
+    type: 'propChain';
+    name: string;
+};
 
 // @public (undocumented)
 const RETURN: (v: VReturn['value']) => {
@@ -155,23 +592,80 @@ const RETURN: (v: VReturn['value']) => {
     value: Value;
 };
 
+// @public (undocumented)
+type Return = NodeBase & {
+    type: 'return';
+    expr: Expression;
+};
+
+// @public (undocumented)
+type Return_2 = NodeBase_2 & {
+    type: 'return';
+    expr: Expression_2;
+};
+
 // @public (undocumented)
 export class SemanticError extends Error {
     constructor(details?: string);
 }
 
+// @public (undocumented)
+type Statement = Definition | Return | Each | For | Loop | Break | Continue | Assign | AddAssign | SubAssign;
+
+// @public (undocumented)
+type Statement_2 = Definition_2 | Return_2 | Attribute_2 | // AST
+Each_2 | For_2 | Loop_2 | Break_2 | Continue_2 | Assign_2 | AddAssign_2 | SubAssign_2;
+
 // @public (undocumented)
 const STR: (str: VStr['value']) => {
     type: "str";
     value: string;
 };
 
+// @public (undocumented)
+type Str = NodeBase & {
+    type: 'str';
+    value: string;
+};
+
+// @public (undocumented)
+type Str_2 = NodeBase_2 & ChainProp & {
+    type: 'str';
+    value: string;
+};
+
+// @public (undocumented)
+type SubAssign = NodeBase & {
+    type: 'dec';
+    dest: Expression;
+    expr: Expression;
+};
+
+// @public (undocumented)
+type SubAssign_2 = NodeBase_2 & {
+    type: 'dec';
+    dest: Expression_2;
+    expr: Expression_2;
+};
+
 // @public (undocumented)
 class SyntaxError_2 extends Error {
     constructor(details?: string);
 }
 export { SyntaxError_2 as SyntaxError }
 
+// @public (undocumented)
+type Tmpl = NodeBase & {
+    type: 'tmpl';
+    tmpl: (string | Expression)[];
+};
+
+// @public (undocumented)
+type Tmpl_2 = NodeBase_2 & ChainProp & {
+    type: 'tmpl';
+    tmpl: (string | Expression_2)[];
+};
+
 // @public (undocumented)
 const TRUE: {
     type: "bool";
@@ -191,7 +685,6 @@ declare namespace utils {
         assertArray,
         eq,
         valToString,
-        nodeToString,
         valToJs,
         jsToVal,
         getLangVersion
@@ -203,7 +696,7 @@ export { utils }
 function valToJs(val: Value): any;
 
 // @public (undocumented)
-function valToString(val: Value, simple?: boolean): any;
+function valToString(val: Value, simple?: boolean): string;
 
 // @public (undocumented)
 type Value = (VNull | VBool | VNum | VStr | VArr | VObj | VFn | VReturn | VBreak | VContinue) & Attr_2;
@@ -240,6 +733,18 @@ declare namespace values {
 }
 export { values }
 
+// @public (undocumented)
+type Var = NodeBase & {
+    type: 'var';
+    name: string;
+};
+
+// @public (undocumented)
+type Var_2 = NodeBase_2 & ChainProp & {
+    type: 'var';
+    name: string;
+};
+
 // @public (undocumented)
 type VArr = {
     type: 'arr';
@@ -306,6 +811,10 @@ type VStr = {
     value: string;
 };
 
+// Warnings were encountered during analysis:
+//
+// src/node.ts:82:2 - (ae-forgotten-export) The symbol "TypeSource" needs to be exported by the entry point index.d.ts
+
 // (No @packageDocumentation comment for this package)
 
 ```
diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts
index 653100f..e5b7254 100644
--- a/src/interpreter/index.ts
+++ b/src/interpreter/index.ts
@@ -3,7 +3,7 @@
  */
 
 import autobind from 'autobind-decorator';
-import { Node, NNs } from '../node';
+import * as N from '../node';
 import { Scope } from './scope';
 import { AiScriptError } from './error';
 import { std } from './lib/std';
@@ -54,7 +54,7 @@ export class AiScript {
 	}
 
 	@autobind
-	public async exec(script?: Node[]) {
+	public async exec(script?: N.Node[]) {
 		if (script == null || script.length === 0) return;
 
 		await this.collectNs(script);
@@ -70,10 +70,10 @@ export class AiScript {
 	}
 
 	@autobind
-	public static collectMetadata(script?: Node[]) {
+	public static collectMetadata(script?: N.Node[]) {
 		if (script == null || script.length === 0) return;
 
-		function nodeToJs(node: Node): any {
+		function nodeToJs(node: N.Node): any {
 			switch (node.type) {
 				case 'arr': return node.value.map(item => nodeToJs(item));
 				case 'bool': return node.value;
@@ -115,7 +115,7 @@ export class AiScript {
 	}
 
 	@autobind
-	private async collectNs(script: Node[]): Promise<void> {
+	private async collectNs(script: N.Node[]): Promise<void> {
 		for (const node of script) {
 			switch (node.type) {
 				case 'ns': {
@@ -131,7 +131,7 @@ export class AiScript {
 	}
 
 	@autobind
-	private async collectNsMember(ns: NNs): Promise<void> {
+	private async collectNsMember(ns: N.Namespace): Promise<void> {
 		const scope = this.scope.createChildScope();
 
 		for (const node of ns.members) {
@@ -148,7 +148,7 @@ export class AiScript {
 				}
 
 				default: {
-					throw new Error('invalid ns member type: ' + node.type);
+					throw new Error('invalid ns member type: ' + (node as N.Node).type);
 				}
 			}
 		}
@@ -174,7 +174,7 @@ export class AiScript {
 	}
 
 	@autobind
-	private async _eval(node: Node, scope: Scope): Promise<Value> {
+	private async _eval(node: N.Node, scope: Scope): Promise<Value> {
 		if (this.stop) return NULL;
 		this.log('node', { node: node });
 		this.stepCount++;
@@ -184,10 +184,10 @@ export class AiScript {
 
 		switch (node.type) {
 			case 'call': {
-				const val = scope.get(node.name);
-				assertFunction(val);
+				const callee = await this._eval(node.target, scope);
+				assertFunction(callee);
 				const args = await Promise.all(node.args.map(async expr => await this._eval(expr, scope)));
-				return this._fn(val, args);
+				return this._fn(callee, args);
 			}
 
 			case 'if': {
@@ -304,39 +304,29 @@ export class AiScript {
 			}
 
 			case 'assign': {
-				scope.assign(node.name, await this._eval(node.expr, scope));
-				return NULL;
-			}
-
-			case 'propAssign': {
-				const obj = scope.get(node.obj);
-				let x = obj;
-				const lastProp = node.path[node.path.length - 1];
-				for (const prop of node.path.slice(0, node.path.length - 1)) {
-					assertObject(x);
-					if (!x.value.has(prop)) {
-						x = NULL;
-						break;
-					} else {
-						x = x.value.get(prop)!;
-					}
+				const v = await this._eval(node.expr, scope);
+				if (node.dest.type === 'var') {
+					scope.assign(node.dest.name, v);
+					return NULL;
+				} else if (node.dest.type === 'index') {
+					const assignee = await this._eval(node.dest.target, scope);
+					assertArray(assignee);
+					const i = await this._eval(node.dest.index, scope);
+					assertNumber(i);
+					assignee.value[i.value - 1] = v; // TODO: 存在チェック
+					return NULL;
+				} else if (node.dest.type === 'prop') {
+					const assignee = await this._eval(node.dest.target, scope);
+					assertObject(assignee);
+					assignee.value.set(node.dest.name, v);
+					return NULL;
+				} else {
+					throw new AiScriptError('The left-hand side of an assignment expression must be a variable or a property/index access.');
 				}
-				assertObject(x);
-				x.value.set(lastProp, await this._eval(node.expr, scope));
-				return NULL;
-			}
-
-			case 'indexAssign': {
-				const arr = scope.get(node.arr);
-				assertArray(arr);
-				const i = await this._eval(node.i, scope);
-				assertNumber(i);
-				arr.value[i.value - 1] = await this._eval(node.expr, scope); // TODO: 存在チェック
-				return NULL;
 			}
 
 			case 'inc': {
-				const target = scope.get(node.name);
+				const target = await this._eval(node.dest, scope);
 				assertNumber(target);
 				const v = await this._eval(node.expr, scope);
 				assertNumber(v);
@@ -345,7 +335,7 @@ export class AiScript {
 			}
 
 			case 'dec': {
-				const target = scope.get(node.name);
+				const target = await this._eval(node.dest, scope);
 				assertNumber(target);
 				const v = await this._eval(node.expr, scope);
 				assertNumber(v);
@@ -372,43 +362,21 @@ export class AiScript {
 			}
 
 			case 'prop': {
-				const obj = scope.get(node.obj);
-				let x = obj;
-				for (const prop of node.path) {
-					assertObject(x);
-					if (!x.value.has(prop)) {
-						x = NULL;
-						break;
-					} else {
-						x = x.value.get(prop)!;
-					}
-				}
-				return x;
-			}
-
-			case 'propCall': {
-				const obj = scope.get(node.obj);
-				let x = obj;
-				for (const prop of node.path) {
-					assertObject(x);
-					if (!x.value.has(prop)) {
-						x = NULL;
-						break;
-					} else {
-						x = x.value.get(prop)!;
-					}
+				const obj = await this._eval(node.target, scope);
+				assertObject(obj);
+				if (obj.value.has(node.name)) {
+					return obj.value.get(node.name)!;
+				} else {
+					return NULL; // エラーにしてもよさそう
 				}
-				assertFunction(x);
-				const args = await Promise.all(node.args.map(async expr => await this._eval(expr, scope)));
-				return this._fn(x, args);
 			}
 
 			case 'index': {
-				const arr = scope.get(node.arr);
-				assertArray(arr);
-				const i = await this._eval(node.i, scope);
+				const target = await this._eval(node.target, scope);
+				assertArray(target);
+				const i = await this._eval(node.index, scope);
 				assertNumber(i);
-				return arr.value[i.value - 1]; // TODO: 存在チェック
+				return target.value[i.value - 1]; // TODO: 存在チェック
 			}
 
 			case 'fn': {
@@ -471,7 +439,7 @@ export class AiScript {
 	}
 
 	@autobind
-	private async _run(program: Node[], scope: Scope): Promise<Value> {
+	private async _run(program: N.Node[], scope: Scope): Promise<Value> {
 		this.log('block:enter', { scope: scope.name });
 
 		let v: Value = NULL;
diff --git a/src/interpreter/infix-to-fncall.ts b/src/interpreter/infix-to-fncall.ts
index fb09cf6..7249ba3 100644
--- a/src/interpreter/infix-to-fncall.ts
+++ b/src/interpreter/infix-to-fncall.ts
@@ -1,4 +1,4 @@
-import { Node, NInfix, NCall } from '../node';
+import * as N from '../node';
 import { AiScriptError } from './error';
 
 /**
@@ -11,15 +11,15 @@ import { AiScriptError } from './error';
  */
 type InfixTree = {
 	type: 'infixTree';
-	left: InfixTree | Node;
-	right: InfixTree | Node;
+	left: InfixTree | N.Node;
+	right: InfixTree | N.Node;
 	info: {
 		func: string; // 対応する関数名
 		priority: number; // 優先度（高いほど優先して計算される値）
 	};
 };
 
-function INFIX_TREE(left: InfixTree | Node, right: InfixTree | Node, info: InfixTree["info"]): InfixTree {
+function INFIX_TREE(left: InfixTree | N.Node, right: InfixTree | N.Node, info: InfixTree["info"]): InfixTree {
 	return { type: 'infixTree', left, right, info };
 }
 
@@ -46,7 +46,7 @@ function INFIX_TREE(left: InfixTree | Node, right: InfixTree | Node, info: Infix
  * - NOTE: 右結合性の演算子としては代入演算子などが挙げられる
  * - NOTE: 比較の演算子などは非結合性とされる
  */
-function insertTree(currTree: InfixTree | Node, nextTree: InfixTree | Node, nextOpInfo: InfixTree["info"]): InfixTree {
+function insertTree(currTree: InfixTree | N.Node, nextTree: InfixTree | N.Node, nextOpInfo: InfixTree["info"]): InfixTree {
 	if (currTree.type !== 'infixTree') {
 		return INFIX_TREE(currTree, nextTree, nextOpInfo);
 	}
@@ -62,15 +62,15 @@ function insertTree(currTree: InfixTree | Node, nextTree: InfixTree | Node, next
 /**
  * 中置演算子式を表す木を対応する関数呼び出しの構造体に変換する
  */
-function treeToNode(tree: InfixTree | Node): Node {
+function treeToNode(tree: InfixTree | N.Node): N.Node {
 	if (tree.type !== 'infixTree') {
 		return tree;
 	}
 	return {
 		type: 'call',
-		name: tree.info.func,
+		target: { type: 'var', name: tree.info.func },
 		args: [treeToNode(tree.left), treeToNode(tree.right)],
-	} as NCall;
+	} as N.Call;
 }
 
 const infoTable: Record<string, InfixTree["info"]> = {
@@ -92,7 +92,7 @@ const infoTable: Record<string, InfixTree["info"]> = {
 /**
  * NInfix を関数呼び出し形式に変換する
  */
-export function infixToFnCall(node: NInfix): Node {
+export function infixToFnCall(node: N.Infix): N.Node {
 	const infos = node.operators.map(op => {
 		const info = infoTable[op];
 		if (info == null) {
diff --git a/src/interpreter/util.ts b/src/interpreter/util.ts
index f853105..f2db2d7 100644
--- a/src/interpreter/util.ts
+++ b/src/interpreter/util.ts
@@ -1,4 +1,3 @@
-import { Node } from '../node';
 import { Value, VStr, VNum, VBool, VFn, VObj, VArr, STR, NUM, ARR, OBJ, NULL, BOOL } from './value';
 import { AiScriptError } from './error';
 
@@ -38,16 +37,16 @@ export function assertArray(val: Value): asserts val is VArr {
 	}
 }
 
-export function eq(a: Value, b: Value) {
+export function eq(a: Value, b: Value): boolean {
 	if (a.type === 'fn' || b.type === 'fn') return false;
 	if (a.type === 'null' && b.type === 'null') return true;
 	if (a.type === 'null' || b.type === 'null') return false;
 	return (a.value === b.value);
 }
 
-export function valToString(val: Value, simple = false) {
+export function valToString(val: Value, simple = false): string {
 	if (simple) {
-		if (val.type === 'num') return val.value;
+		if (val.type === 'num') return val.value.toString();
 		if (val.type === 'bool') return val.value ? 'true' : 'false';
 		if (val.type === 'str') return `"${val.value}"`;
 		if (val.type === 'arr') return `[${val.value.map(item => valToString(item, true)).join(', ')}]`;
@@ -64,20 +63,6 @@ export function valToString(val: Value, simple = false) {
 	return `${val.type}<${label}>`;
 }
 
-export function nodeToString(node: Node) {
-	const label =
-		node.type === 'num' ? node.value :
-		node.type === 'bool' ? node.value :
-		node.type === 'str' ? node.value :
-		node.type === 'fn' ? null :
-		node.type === 'null' ? null :
-		node.type === 'def' ? node.name :
-		node.type === 'var' ? node.name :
-		node.type === 'call' ? node.name :
-		null;
-	return label ? `${node.type.toUpperCase()} (${label})` : node.type.toUpperCase();
-}
-
 export function valToJs(val: Value): any {
 	switch (val.type) {
 		case 'arr': return val.value.map(item => valToJs(item));
diff --git a/src/node.ts b/src/node.ts
index ba5a45e..d73e01e 100644
--- a/src/node.ts
+++ b/src/node.ts
@@ -1,3 +1,9 @@
+/**
+ * IRノード
+ *
+ * IRノードはASTノードをインタプリタ等から操作しやすい構造に変形したものです。
+*/
+
 import { TypeSource } from './type';
 
 export type Loc = {
@@ -5,268 +11,244 @@ export type Loc = {
 	end: number;
 };
 
-export type NDef = {
-	type: 'def'; // 変数宣言
-	loc?: Loc; // コード位置
-	name: string; // 変数名
-	varType?: TypeSource; // 変数の型
-	expr: Node; // 式
-	mut: boolean; // ミュータブルか否か
-	attr: NAttr[]; // 付加された属性
-};
-
-export type NAssign = {
-	type: 'assign'; // 再代入
-	loc?: Loc; // コード位置
-	name: string; // 変数名
-	expr: Node; // 式
-};
-
-export type NPropAssign = {
-	type: 'propAssign'; // プロパティ再代入
-	loc?: Loc; // コード位置
-	obj: string; // オブジェクト変数名
-	path: string[]; // プロパティパス
-	expr: Node; // 式
-};
-
-export type NIndexAssign = {
-	type: 'indexAssign'; // 配列要素再代入
-	loc?: Loc; // コード位置
-	arr: string; // 配列変数名
-	i: Node; // インデックス
-	expr: Node; // 式
+export type Node = Namespace | Meta | Statement | Expression;
+
+export type Statement =
+	Definition |
+	Return |
+	Each |
+	For |
+	Loop |
+	Break |
+	Continue |
+	Assign |
+	AddAssign |
+	SubAssign;
+
+const statementTypes = [
+	'def', 'return', 'forOf', 'for', 'loop', 'break', 'continue', 'assign', 'inc', 'dec'
+];
+export function isStatement(x: Node): x is Statement {
+	return statementTypes.includes(x.type);
+}
+
+export type Expression =
+	Infix |
+	If |
+	Fn |
+	Match |
+	Block |
+	Tmpl |
+	Str |
+	Num |
+	Bool |
+	Null |
+	Obj |
+	Arr |
+	Var |
+	Call |
+	Index |
+	Prop;
+
+const expressionTypes = [
+	'infix', 'if', 'fn', 'match', 'block', 'tmpl', 'str', 'num', 'bool', 'null', 'obj', 'arr', 'var', 'cal', 'index', 'prop'
+];
+export function isExpression(x: Node): x is Expression {
+	return expressionTypes.includes(x.type);
+}
+
+type NodeBase = {
+	loc?: { // コード位置
+		start: number;
+		end: number;
+	};
+};
+
+export type Namespace = NodeBase & {
+	type: 'ns'; // 名前空間
+	name: string; // 空間名
+	members: (Definition | Namespace)[]; // メンバー
 };
 
-export type NInc = {
-	type: 'inc'; // インクリメント
-	loc?: Loc; // コード位置
-	name: string; // 変数名
-	expr: Node; // 式
+export type Meta = NodeBase & {
+	type: 'meta'; // メタデータ定義
+	name: string | null; // 名
+	value: Expression; // 値
 };
 
-export type NDec = {
-	type: 'dec'; // デクリメント
-	loc?: Loc; // コード位置
+export type Definition = NodeBase & {
+	type: 'def'; // 変数宣言文
 	name: string; // 変数名
-	expr: Node; // 式
-};
-
-export type NCall = {
-	type: 'call'; // 関数呼び出し
-	loc?: Loc; // コード位置
-	name: string; // 関数名
-	args: Node[]; // 引数(式の配列)
+	varType?: TypeSource; // 変数の型
+	expr: Expression; // 式
+	mut: boolean; // ミュータブルか否か
+	attr: Attribute[]; // 付加された属性
 };
 
-export type NReturn = {
-	type: 'return'; // return
-	loc?: Loc; // コード位置
-	expr: Node; // 式
+export type Attribute = NodeBase & {
+	type: 'attr'; // 属性
+	name: string; // 属性名
+	value: Expression; // 値
 };
 
-export type NBreak = {
-	type: 'break'; // break
-	loc?: Loc; // コード位置
+export type Return = NodeBase & {
+	type: 'return'; // return文
+	expr: Expression; // 式
 };
 
-export type NContinue = {
-	type: 'continue'; // continue
-	loc?: Loc; // コード位置
+export type Each = NodeBase & {
+	type: 'forOf'; // each文
+	var: string; // イテレータ変数名
+	items: Expression; // 配列
+	for: Statement | Expression; // 本体処理
 };
 
-export type NIf = {
-	type: 'if'; // if文
-	loc?: Loc; // コード位置
-	cond: Node; // 条件式
-	then: Node; // then節
-	elseif: {
-		cond: Node;
-		then: Node;
-	}[]; // elseif節
-	else?: Node; // else節
+export type For = NodeBase & {
+	type: 'for'; // for文
+	var?: string; // イテレータ変数名
+	from?: Expression; // 開始値
+	to?: Expression; // 終値
+	times?: Expression; // 回数
+	for: Statement | Expression; // 本体処理
 };
 
-export type NLoop = {
+export type Loop = NodeBase & {
 	type: 'loop'; // loop文
-	loc?: Loc; // コード位置
-	statements: Node[]; // 処理
+	statements: (Statement | Expression)[]; // 処理
 };
 
-export type NFor = {
-	type: 'for'; // for文
-	loc?: Loc; // コード位置
-	var?: string; // イテレータ変数名
-	from?: Node; // 開始値
-	to?: Node; // 終値
-	times?: Node; // 回数
-	for: Node; // 本体処理
+export type Break = NodeBase & {
+	type: 'break'; // break文
 };
 
-export type NForOf = {
-	type: 'forOf'; // for of文
-	loc?: Loc; // コード位置
-	var: string; // イテレータ変数名
-	items: Node; // 配列
-	for: Node; // 本体処理
+export type Continue = NodeBase & {
+	type: 'continue'; // continue文
 };
 
-export type NVar = {
-	type: 'var'; // 変数
-	loc?: Loc; // コード位置
-	name: string; // 変数名
+export type AddAssign = NodeBase & {
+	type: 'inc'; // 加算代入文
+	dest: Expression; // 代入先
+	expr: Expression; // 式
 };
 
-export type NNull = {
-	type: 'null'; // nullリテラル
-	loc?: Loc; // コード位置
+export type SubAssign = NodeBase & {
+	type: 'dec'; // 減算代入文
+	dest: Expression; // 代入先
+	expr: Expression; // 式
 };
 
-export type NBool = {
-	type: 'bool'; // 真理値リテラル
-	loc?: Loc; // コード位置
-	value: boolean; // 真理値
+export type Assign = NodeBase & {
+	type: 'assign'; // 代入文
+	dest: Expression; // 代入先
+	expr: Expression; // 式
 };
 
-export type NNum = {
-	type: 'num'; // 数値リテラル
-	loc?: Loc; // コード位置
-	value: number; // 数値
-};
+export type InfixOperator = "||" | "&&" | "==" | "!=" | "<=" | ">=" | "<" | ">" | "+" | "-" | "*" | "|" | "%";
 
-export type NStr = {
-	type: 'str'; // 文字列リテラル
-	loc?: Loc; // コード位置
-	value: string; // 文字列
+// TODO: remove by transform plugin
+export type Infix = NodeBase & {
+	type: 'infix'; // 中置演算子式
+	operands: Expression[]; // 項のリスト
+	operators: InfixOperator[]; // 演算子のリスト
 };
 
-export type NArr = {
-	type: 'arr'; // 配列リテラル
-	loc?: Loc; // コード位置
-	value: Node[]; // アイテム
+export type If = NodeBase & {
+	type: 'if'; // if式
+	cond: Expression; // 条件式
+	then: Statement | Expression; // then節
+	elseif: {
+		cond: Expression; // elifの条件式
+		then: Statement | Expression;// elif節
+	}[];
+	else?: Statement | Expression; // else節
 };
 
-export type NFn = {
-	type: 'fn'; // 関数リテラル
-	loc?: Loc; // コード位置
+export type Fn = NodeBase & {
+	type: 'fn'; // 関数
 	args: {
 		name: string; // 引数名
 		type?: TypeSource; // 引数の型
 	}[];
 	ret?: TypeSource; // 戻り値の型
-	children: Node[]; // 関数の本体処理
+	children: (Statement | Expression)[]; // 本体処理
 };
 
-export type NObj = {
-	type: 'obj'; // オブジェクトリテラル
-	loc?: Loc; // コード位置
-	value: Map<string, Node>; // オブジェクト
+export type Match = NodeBase & {
+	type: 'match'; // パターンマッチ
+	about: Expression; // 対象
+	qs: {
+		q: Expression; // 条件
+		a: Statement | Expression; // 結果
+	}[];
+	default?: Statement | Expression; // デフォルト値
 };
 
-export type NProp = {
-	type: 'prop'; // プロパティアクセス
-	loc?: Loc; // コード位置
-	obj: string; // オブジェクト変数名
-	path: string[]; // プロパティパス
+export type Block = NodeBase & {
+	type: 'block'; // ブロックまたはeval式
+	statements: (Statement | Expression)[]; // 処理
 };
 
-export type NPropCall = {
-	type: 'propCall'; // プロパティアクセス(関数呼び出し)
-	loc?: Loc; // コード位置
-	obj: string; // オブジェクト変数名
-	path: string[]; // プロパティパス
-	args: Node[]; // 引数(式の配列)
+export type Tmpl = NodeBase & {
+	type: 'tmpl'; // テンプレート
+	tmpl: (string | Expression)[]; // 処理
 };
 
-export type NIndex = {
-	type: 'index'; // 配列要素アクセス
-	loc?: Loc; // コード位置
-	arr: string; // 配列変数名
-	i: Node; // インデックス
+export type Str = NodeBase & {
+	type: 'str'; // 文字列リテラル
+	value: string; // 文字列
 };
 
-export type NBlock = {
-	type: 'block'; // ブロック
-	loc?: Loc; // コード位置
-	statements: Node[]; // 処理
+export type Num = NodeBase & {
+	type: 'num'; // 数値リテラル
+	value: number; // 数値
 };
 
-export type NTmpl = {
-	type: 'tmpl'; // テンプレート
-	loc?: Loc; // コード位置
-	tmpl: (string | Node)[]; // 処理
+export type Bool = NodeBase & {
+	type: 'bool'; // 真理値リテラル
+	value: boolean; // 真理値
 };
 
-export type NNs = {
-	type: 'ns'; // 名前空間
-	loc?: Loc; // コード位置
-	name: string; // 空間名
-	members: Node[]; // メンバー
+export type Null = NodeBase & {
+	type: 'null'; // nullリテラル
 };
 
-export type NMatch = {
-	type: 'match'; // パターンマッチ
-	loc?: Loc; // コード位置
-	about: Node; // 対象
-	qs: {
-		q: Node; // 条件
-		a: Node; // 結果
-	}[];
-	default?: Node; // デフォルト値
+export type Obj = NodeBase & {
+	type: 'obj'; // オブジェクト
+	value: Map<string, Expression>; // プロパティ
 };
 
-export type NMeta = {
-	type: 'meta'; // メタデータ定義
-	loc?: Loc; // コード位置
-	name: string | null; // 名
-	value: Node; // 値
+export type Arr = NodeBase & {
+	type: 'arr'; // 配列
+	value: Expression[]; // アイテム
 };
 
-export type NAttr = {
-	type: 'attr'; // 属性
-	loc?: Loc; // コード位置
-	name: string; // 属性名
-	value: Node; // 値
+export type Var = NodeBase & {
+	type: 'var'; // 変数
+	name: string; // 変数名
 };
 
-export type NInfix = {
-	type: 'infix'; // 中置演算子式
-	loc?: Loc; // コード位置
-	operands: Node[]; // 項
-	operators: string[]; // 演算子
+// chain node example:
+// call > fn
+// call > var(fn)
+// index > arr
+// index > var(arr)
+// prop > prop(obj) > var(obj)
+// call > prop(fn) > obj
+
+export type Call = NodeBase & {
+	type: 'call'; // 関数呼び出し
+	target: Expression; // 対象
+	args: Expression[]; // 引数
 };
 
-// TODO: analyze前のNodeとanalyze後のNodeが区別されておらず気持ち悪いのをなんとかしたい
-export type Node =
-	NDef |
-	NAssign |
-	NPropAssign |
-	NIndexAssign |
-	NInc |
-	NDec |
-	NCall |
-	NReturn |
-	NBreak |
-	NContinue |
-	NIf |
-	NLoop |
-	NFor |
-	NForOf |
-	NVar |
-	NNull |
-	NBool |
-	NNum |
-	NStr |
-	NArr |
-	NFn |
-	NObj |
-	NProp |
-	NPropCall |
-	NIndex |
-	NBlock |
-	NTmpl |
-	NMatch |
-	NNs |
-	NMeta |
-	NAttr |
-	NInfix;
+export type Index = NodeBase & {
+	type: 'index'; // 配列要素アクセス
+	target: Expression; // 対象
+	index: Expression; // インデックス
+};
+
+export type Prop = NodeBase & {
+	type: 'prop'; // プロパティアクセス
+	target: Expression; // 対象
+	name: string; // プロパティ名
+};
diff --git a/src/parser/index.ts b/src/parser/index.ts
index f79c950..b0ac431 100644
--- a/src/parser/index.ts
+++ b/src/parser/index.ts
@@ -1,12 +1,14 @@
 import * as aiscript from '..';
-import { Node } from '../node';
+import * as N from '../node';
+import * as Ast from './node';
 import * as parser from './parser.js';
 
 import { validateKeyword } from './plugins/validate-keyword';
 import { validateType } from './plugins/validate-type';
 import { setAttribute } from './plugins/set-attribute';
+import { transformChain } from './plugins/transform-chain';
 
-export type ParserPlugin = (nodes: Node[]) => Node[];
+export type ParserPlugin = (nodes: Ast.Node[]) => Ast.Node[];
 
 export class Parser {
 	private static instance?: Parser;
@@ -17,10 +19,11 @@ export class Parser {
 			validateKeyword,
 			validateType,
 			setAttribute,
+			transformChain,
 		];
 	}
 
-	public static parse(input: string): Node[] {
+	public static parse(input: string): N.Node[] {
 		if (Parser.instance == null) {
 			Parser.instance = new Parser();
 		}
@@ -31,8 +34,8 @@ export class Parser {
 		this.plugins.push(plugin);
 	}
 
-	public parse(input: string): Node[] {
-		let nodes: Node[];
+	public parse(input: string): N.Node[] {
+		let nodes: Ast.Node[];
 
 		// generate a node tree
 		try {
@@ -56,12 +59,12 @@ export class Parser {
 			nodes = plugin(nodes);
 		}
 
-		return nodes;
+		return nodes as N.Node[];
 	}
 }
 
 // alias of legacy api
 
-export function parse(input: string): Node[] {
+export function parse(input: string): N.Node[] {
 	return Parser.parse(input);
 }
diff --git a/src/parser/node.ts b/src/parser/node.ts
new file mode 100644
index 0000000..3d36ce0
--- /dev/null
+++ b/src/parser/node.ts
@@ -0,0 +1,289 @@
+/**
+ * ASTノード
+ *
+ * パーサーが生成する直接的な処理結果です。
+ * パーサーが生成しやすい形式になっているため、インタプリタ等では操作しにくい構造になっていることがあります。
+ * この処理結果がプラグインによって処理されるとIRノードとなります。
+*/
+
+import { TypeSource } from '../type';
+
+export type Node = Namespace | Meta | Statement | Expression | ChainMember;
+
+export type Statement =
+	Definition |
+	Return |
+	Attribute | // AST
+	Each |
+	For |
+	Loop |
+	Break |
+	Continue |
+	Assign |
+	AddAssign |
+	SubAssign;
+
+const statementTypes = [
+	'def', 'return', 'attr', 'forOf', 'for', 'loop', 'break', 'continue', 'assign', 'inc', 'dec'
+];
+export function isStatement(x: Node): x is Statement {
+	return statementTypes.includes(x.type);
+}
+
+export type Expression =
+	Infix |
+	If |
+	Fn |
+	Match |
+	Block |
+	Tmpl |
+	Str |
+	Num |
+	Bool |
+	Null |
+	Obj |
+	Arr |
+	Var |
+	Call | // IR
+	Index | // IR
+	Prop; // IR
+
+const expressionTypes = [
+	'infix', 'if', 'fn', 'match', 'block', 'tmpl', 'str', 'num', 'bool', 'null', 'obj', 'arr', 'var', 'cal', 'index', 'prop'
+];
+export function isExpression(x: Node): x is Expression {
+	return expressionTypes.includes(x.type);
+}
+
+type NodeBase = {
+	__AST_NODE: never; // phantom type
+	loc?: {
+		start: number;
+		end: number;
+	};
+};
+
+export type Namespace = NodeBase & {
+	type: 'ns';
+	name: string;
+	members: (Definition | Namespace)[];
+};
+
+export type Meta = NodeBase & {
+	type: 'meta';
+	name: string | null;
+	value: Expression;
+};
+
+export type Definition = NodeBase & {
+	type: 'def';
+	name: string;
+	varType?: TypeSource;
+	expr: Expression;
+	mut: boolean;
+	attr?: Attribute[]; // IR
+};
+
+export type Attribute = NodeBase & {
+	type: 'attr';
+	name: string;
+	value: Expression;
+};
+
+export type Return = NodeBase & {
+	type: 'return';
+	expr: Expression;
+};
+
+export type Each = NodeBase & {
+	type: 'forOf';
+	var: string;
+	items: Expression;
+	for: Statement | Expression;
+};
+
+export type For = NodeBase & {
+	type: 'for';
+	var?: string;
+	from?: Expression;
+	to?: Expression;
+	times?: Expression;
+	for: Statement | Expression;
+};
+
+export type Loop = NodeBase & {
+	type: 'loop';
+	statements: (Statement | Expression)[];
+};
+
+export type Break = NodeBase & {
+	type: 'break';
+};
+
+export type Continue = NodeBase & {
+	type: 'continue';
+};
+
+export type AddAssign = NodeBase & {
+	type: 'inc';
+	dest: Expression;
+	expr: Expression;
+};
+
+export type SubAssign = NodeBase & {
+	type: 'dec';
+	dest: Expression;
+	expr: Expression;
+};
+
+export type Assign = NodeBase & {
+	type: 'assign';
+	dest: Expression;
+	expr: Expression;
+};
+
+export type InfixOperator = "||" | "&&" | "==" | "!=" | "<=" | ">=" | "<" | ">" | "+" | "-" | "*" | "|" | "%";
+
+export type Infix = NodeBase & {
+	type: 'infix';
+	operands: Expression[];
+	operators: InfixOperator[];
+};
+
+export type If = NodeBase & {
+	type: 'if';
+	cond: Expression;
+	then: Statement | Expression;
+	elseif: {
+		cond: Expression;
+		then: Statement | Expression;
+	}[];
+	else?: Statement | Expression;
+};
+
+export type Fn = NodeBase & ChainProp & {
+	type: 'fn';
+	args: {
+		name: string;
+		type?: TypeSource;
+	}[];
+	ret?: TypeSource; 
+	children: (Statement | Expression)[];
+};
+
+export type Match = NodeBase & ChainProp & {
+	type: 'match';
+	about: Expression;
+	qs: {
+		q: Expression;
+		a: Statement | Expression;
+	}[];
+	default?: Statement | Expression;
+};
+
+export type Block = NodeBase & ChainProp & {
+	type: 'block';
+	statements: (Statement | Expression)[];
+};
+
+export type Tmpl = NodeBase & ChainProp & {
+	type: 'tmpl';
+	tmpl: (string | Expression)[];
+};
+
+export type Str = NodeBase & ChainProp & {
+	type: 'str';
+	value: string;
+};
+
+export type Num = NodeBase & ChainProp & {
+	type: 'num';
+	value: number;
+};
+
+export type Bool = NodeBase & ChainProp & {
+	type: 'bool';
+	value: boolean;
+};
+
+export type Null = NodeBase & ChainProp & {
+	type: 'null';
+};
+
+export type Obj = NodeBase & ChainProp & {
+	type: 'obj';
+	value: Map<string, Expression>;
+};
+
+export type Arr = NodeBase & ChainProp & {
+	type: 'arr';
+	value: Expression[];
+};
+
+export type Var = NodeBase & ChainProp & {
+	type: 'var';
+	name: string;
+};
+
+// AST
+type ChainProp = {
+	chain?: ChainMember[];
+};
+
+// AST
+export function hasChainProp(x: Node): x is Node & ChainProp {
+	return ((x as any).chain != null);
+}
+
+// AST
+export type ChainMember = CallChain | IndexChain | PropChain;
+
+// AST
+export type CallChain = NodeBase & {
+	type: 'callChain';
+	args: Expression[];
+};
+
+// AST
+export type IndexChain = NodeBase & {
+	type: 'indexChain';
+	index: Expression;
+};
+
+// AST
+export type PropChain = NodeBase & {
+	type: 'propChain';
+	name: string;
+};
+
+// IR
+export type Call = NodeBase & {
+	type: 'call';
+	target: Expression;
+	args: Expression[];
+};
+export function CALL(target: Call['target'], args: Call['args'], loc?: { start: number, end: number }): Call {
+	return { type: 'call', target, args, loc, } as Call;
+}
+
+// IR
+export type Index = NodeBase & {
+	type: 'index';
+	target: Expression;
+	index: Expression;
+};
+
+export function INDEX(target: Index['target'], index: Index['index'], loc?: { start: number, end: number }): Index {
+	return { type: 'index', target, index, loc, } as Index;
+}
+
+// IR
+export type Prop = NodeBase & {
+	type: 'prop';
+	target: Expression;
+	name: string;
+};
+
+export function PROP(target: Prop['target'], name: Prop['name'], loc?: { start: number, end: number }): Prop {
+	return { type: 'prop', target, name, loc, } as Prop;
+}
diff --git a/src/parser/parser.peggy b/src/parser/parser.peggy
index 392587c..cbff96b 100644
--- a/src/parser/parser.peggy
+++ b/src/parser/parser.peggy
@@ -111,24 +111,22 @@ Statement
 	/ Loop        // "loop"
 	/ Break       // "break"
 	/ Continue    // "continue"
-	/ Assign      // NAME_WITH_NAMESPACE "="
-	/ PropAssign  // NAME_WITH_NAMESPACE "."
-	/ IndexAssign // NAME_WITH_NAMESPACE "["
-	/ Inc         // NAME_WITH_NAMESPACE "+="
-	/ Dec         // NAME_WITH_NAMESPACE "-="
+	/ Assign      // Expr "=" | Expr "+=" | Expr "-="
 	/ Expr
 
 // list of expression
 
 Expr
 	= Infix
-	/ Term
+	/ Expr2
 
-// list of term expression
-
-Term
+Expr2
 	= If          // "if"
-	/ Fn          // "@("
+	/ Chain       // Expr3 "(" | Expr3 "[" | Expr3 "."
+	/ Expr3
+
+Expr3
+	= Fn          // "@("
 	/ Match       // "match"
 	/ Eval        // "eval"
 	/ Tmpl        // "`"
@@ -138,10 +136,6 @@ Term
 	/ Null        // "null"
 	/ Obj         // "{"
 	/ Arr         // "["
-	/ Call        // NAME_WITH_NAMESPACE "("
-	/ IndexRef    // NAME_WITH_NAMESPACE "["
-	/ PropCall    // NAME_WITH_NAMESPACE "."
-	/ PropRef     // NAME_WITH_NAMESPACE "."
 	/ VarRef      // NAME_WITH_NAMESPACE
 	/ "(" _* e:Expr _* ")" { return e; }
 
@@ -191,10 +185,15 @@ VarDef
 
 // output statement
 
-// NOTE: syntax sugar for print()
+// NOTE: out is syntax sugar for print(expr)
 Out
 	= "<:" _* expr:Expr
-{ return createNode('call', { name: 'print', args: [expr] }); }
+{
+	return createNode('var', {
+		name: 'print',
+		chain: [createNode('callChain', { args: [expr] })],
+	});
+}
 
 // attribute statement
 
@@ -291,33 +290,15 @@ Continue
 // assign statement
 
 Assign
-	= name:NAME_WITH_NAMESPACE _* "=" _* expr:Expr
-{ return createNode('assign', { name, expr }); }
-
-// property assign statement
-
-PropAssign
-	= head:NAME_WITH_NAMESPACE tails:("." name:NAME { return name; })+ _* "=" _* expr:Expr
-{ return createNode('propAssign', { obj: head, path: tails, expr }); }
-
-
-// indexer assign statement
-
-IndexAssign
-	= v:NAME_WITH_NAMESPACE "[" _* i:Expr _* "]" _* "=" _* expr:Expr
-{ return createNode('indexAssign', { arr: v, i: i, expr }); }
-
-// increment statement
-
-Inc
-	= name:NAME_WITH_NAMESPACE _* "+=" _* expr:Expr
-{ return createNode('inc', { name, expr }); }
-
-// decrement statement
-
-Dec
-	= name:NAME_WITH_NAMESPACE _* "-=" _* expr:Expr
-{ return createNode('dec', { name, expr }); }
+	= dest:Expr _* op:("+=" / "-=" / "=") _* expr:Expr
+{
+	if (op === '+=')
+		return createNode('inc', { dest, expr });
+	else if (op === '-=')
+		return createNode('dec', { dest, expr });
+	else
+		return createNode('assign', { dest, expr });
+}
 
 
 
@@ -328,7 +309,7 @@ Dec
 // infix expression
 
 Infix
-	= head:Term tail:(InfixSp* op:Op InfixSp* term:Term { return {op, term}; })+
+	= head:Expr2 tail:(InfixSp* op:Op InfixSp* term:Expr2 { return {op, term}; })+
 {
 	return createNode('infix', {
 		operands: [head, ...tail.map(i => i.term)],
@@ -344,6 +325,28 @@ Op
 	= ("||" / "&&" / "==" / "!=" / "<=" / ">=" / "<" / ">" / "+" / "-" / "*" / "/" / "%") ![-+*/%&|=~<>!?]
 { return text(); }
 
+// chain
+
+Chain
+	= e:Expr3 chain:(CallChain / IndexChain / PropChain)+
+{ return { ...e, chain }; }
+
+CallChain
+	= "(" _* args:CallArgs? _* ")"
+{ return createNode('callChain', { args: args || [] }); }
+
+CallArgs
+	= head:Expr tails:(SEP expr:Expr { return expr; })*
+{ return [head, ...tails]; }
+
+IndexChain
+	= "[" _* index:Expr _* "]"
+{ return createNode('indexChain', { index }); }
+
+PropChain
+	= "." name:NAME
+{ return createNode('propChain', { name }); }
+
 // if statement
 
 If
@@ -387,35 +390,7 @@ Eval
 	= "eval" _* "{" _* s:Statements _* "}"
 { return createNode('block', { statements: s }); }
 
-// function call expression
-
-Call
-	= name:NAME_WITH_NAMESPACE "(" _* args:CallArgs? _* ")"
-{ return createNode('call', { name, args: args || [] }); }
-
-CallArgs
-	= head:Expr tails:(SEP expr:Expr { return expr; })*
-{ return [head, ...tails]; }
-
-// reference of indexer expression
-
-IndexRef
-	= v:NAME_WITH_NAMESPACE "[" _* i:Expr _* "]"
-{ return createNode('index', { arr: v, i: i }); }
-
-// property call
-
-PropCall
-	= head:NAME_WITH_NAMESPACE tails:("." name:NAME { return name; })+ "(" _* args:CallArgs? _* ")"
-{ return createNode('propCall', { obj: head, path: tails, args: args || [] }); }
-
-// reference of property expression
-
-PropRef
-	= head:NAME_WITH_NAMESPACE tails:("." name:NAME { return name; })+
-{ return createNode('prop', { obj: head, path: tails }); }
-
-// reference of variable expression
+// variable reference expression
 
 VarRef
 	= name:NAME_WITH_NAMESPACE
diff --git a/src/parser/plugins/set-attribute.ts b/src/parser/plugins/set-attribute.ts
index 185b3da..30905a4 100644
--- a/src/parser/plugins/set-attribute.ts
+++ b/src/parser/plugins/set-attribute.ts
@@ -1,14 +1,21 @@
 import * as aiscript from '../..';
-import { Node, NAttr } from '../../node';
+import * as Ast from '../node';
 
-export function setAttribute(nodes: Node[]): Node[] {
-	const result: Node[] = [];
-	const stockedAttrs: NAttr[] = [];
+export function setAttribute(node: Ast.Expression[]): Ast.Expression[]
+export function setAttribute(node: Ast.Statement[]): Ast.Statement[]
+export function setAttribute(node: (Ast.Statement | Ast.Expression)[]): (Ast.Statement | Ast.Expression)[]
+export function setAttribute(node: Ast.Node[]): Ast.Node[]
+export function setAttribute(nodes: Ast.Node[]): Ast.Node[] {
+	const result: Ast.Node[] = [];
+	const stockedAttrs: Ast.Attribute[] = [];
 
 	for (const node of nodes) {
 		if (node.type === 'attr') {
 			stockedAttrs.push(node);
 		} else if (node.type === 'def') {
+			if (node.attr == null) {
+				node.attr = [];
+			}
 			node.attr.push(...stockedAttrs);
 			// clear all
 			stockedAttrs.splice(0, stockedAttrs.length);
diff --git a/src/parser/plugins/transform-chain.ts b/src/parser/plugins/transform-chain.ts
new file mode 100644
index 0000000..ff6ae21
--- /dev/null
+++ b/src/parser/plugins/transform-chain.ts
@@ -0,0 +1,157 @@
+import * as Ast from '../node';
+
+function visitNode(node: Ast.Expression): Ast.Expression
+function visitNode(node: Ast.Statement): Ast.Statement
+function visitNode(node: Ast.Statement | Ast.Expression): Ast.Statement | Ast.Expression
+function visitNode(node: Ast.Node): Ast.Node
+function visitNode(node: Ast.Node): Ast.Node {
+	let result: Ast.Node = node;
+
+	// transform chain
+	if (Ast.hasChainProp(node) && node.chain != null) {
+		const { chain, ...hostNode } = node;
+		let parent: Ast.Expression = hostNode;
+		for (const item of chain) {
+			switch (item.type) {
+				case 'callChain': {
+					parent = Ast.CALL(parent, item.args, item.loc);
+					break;
+				}
+				case 'indexChain': {
+					parent = Ast.INDEX(parent, item.index, item.loc);
+					break;
+				}
+				case 'propChain': {
+					parent = Ast.PROP(parent, item.name, item.loc);
+					break;
+				}
+				default: {
+					break;
+				}
+			}
+		}
+		result = parent;
+	}
+
+	// nested nodes
+	switch (result.type) {
+		case 'def': {
+			result.expr = visitNode(result.expr);
+			break;
+		}
+		case 'return': {
+			result.expr = visitNode(result.expr);
+			break;
+		}
+		case 'forOf': {
+			result.items = visitNode(result.items);
+			result.for = visitNode(result.for);
+			break;
+		}
+		case 'for': {
+			if (result.from != null) {
+				result.from = visitNode(result.from);
+			}
+			if (result.to != null) {
+				result.to = visitNode(result.to);
+			}
+			if (result.times != null) {
+				result.times = visitNode(result.times);
+			}
+			result.for = visitNode(result.for);
+			break;
+		}
+		case 'loop': {
+			visitNodes(result.statements);
+			break;
+		}
+		case 'inc':
+		case 'dec':
+		case 'assign': {
+			result.expr = visitNode(result.expr);
+			result.dest = visitNode(result.dest);
+			break;
+		}
+		case 'infix': {
+			visitNodes(result.operands);
+			break;
+		}
+		case 'if': {
+			result.cond = visitNode(result.cond);
+			result.then = visitNode(result.then);
+			for (const prop of result.elseif) {
+				prop.cond = visitNode(prop.cond);
+				prop.then = visitNode(prop.then);
+			}
+			if (result.else != null) {
+				result.else = visitNode(result.else);
+			}
+			break;
+		}
+		case 'fn': {
+			visitNodes(result.children);
+			break;
+		}
+		case 'match': {
+			result.about = visitNode(result.about);
+			for (const prop of result.qs) {
+				prop.q = visitNode(prop.q);
+				prop.a = visitNode(prop.a);
+			}
+			if (result.default != null) {
+				result.default = visitNode(result.default);
+			}
+			break;
+		}
+		case 'block': {
+			visitNodes(result.statements);
+			break;
+		}
+		case 'tmpl': {
+			for (let i = 0; i < result.tmpl.length; i++) {
+				const item = result.tmpl[i];
+				if (typeof item !== 'string') {
+					result.tmpl[i] = visitNode(item);
+				}
+			}
+			break;
+		}
+		case 'obj': {
+			for (const item of result.value) {
+				result.value.set(item[0], visitNode(item[1]));
+			}
+			break;
+		}
+		case 'arr': {
+			visitNodes(result.value);
+			break;
+		}
+		case 'call': {
+			result.target = visitNode(result.target);
+			visitNodes(result.args);
+			break;
+		}
+		case 'index': {
+			result.target = visitNode(result.target);
+			result.index = visitNode(result.index);
+			break;
+		}
+		case 'prop': {
+			result.target = visitNode(result.target);
+			break;
+		}
+	}
+
+	return result;
+}
+
+function visitNodes(nodes: Ast.Node[]) {
+	for (let i = 0; i < nodes.length; i++) {
+		nodes[i] = visitNode(nodes[i]);
+	}
+}
+
+export function transformChain(nodes: Ast.Node[]): Ast.Node[] {
+	visitNodes(nodes);
+	return nodes;
+}
diff --git a/src/parser/plugins/validate-keyword.ts b/src/parser/plugins/validate-keyword.ts
index 4883e52..d38ef29 100644
--- a/src/parser/plugins/validate-keyword.ts
+++ b/src/parser/plugins/validate-keyword.ts
@@ -1,5 +1,5 @@
 import * as aiscript from '../..';
-import { Node } from '../../node';
+import * as Ast from '../node';
 
 const reservedWord = [
 	'null',
@@ -40,42 +40,82 @@ const reservedWord = [
 	// 'out',
 ];
 
-export function validateKeyword(nodes: Node[]): Node[] {
-	for (const node of nodes) {
-		switch (node.type) {
-			case 'def':
-			case 'assign':
-			case 'ns': {
-				if (reservedWord.includes(node.name)) {
-					throw new aiscript.SemanticError(`Reserved word "${node.name}" cannot be used as variable name.`);
+function throwReservedWordError(name: string) {
+	throw new aiscript.SemanticError(`Reserved word "${name}" cannot be used as variable name.`);
+}
+
+function validate(node: Ast.Node): void {
+	switch (node.type) {
+		case 'def':
+		case 'ns': {
+			if (reservedWord.includes(node.name)) {
+				throwReservedWordError(node.name);
+			}
+			break;
+		}
+		case 'inc':
+		case 'dec':
+		case 'assign': {
+			if (node.dest.type === 'var') {
+				if (reservedWord.includes(node.dest.name)) {
+					throwReservedWordError(node.dest.name);
 				}
-				break;
 			}
-			case 'fn': {
-				validateKeyword(node.children);
-				break;
+			validate(node.dest);
+			validate(node.expr);
+			break;
+		}
+		case 'fn': {
+			for (const inner of node.children) {
+				validate(inner);
 			}
-			case 'block': {
-				validateKeyword(node.statements);
-				break;
+			break;
+		}
+		case 'block': {
+			for (const inner of node.statements) {
+				validate(inner);
 			}
-			case 'if': {
-				if (node.then.type === 'block') {
-					validateKeyword(node.then.statements);
+			break;
+		}
+		case 'if': {
+			if (node.then.type === 'block') {
+				for (const inner of node.then.statements) {
+					validate(inner);
 				}
-				for (const n of node.elseif) {
-					if (n.then.type === 'block') {
-						validateKeyword(n.then.statements);
+			}
+			for (const n of node.elseif) {
+				if (n.then.type === 'block') {
+					for (const inner of n.then.statements) {
+						validate(inner);
 					}
 				}
-				if (node.else?.type === 'block') {
-					validateKeyword(node.else.statements);
+			}
+			if (node.else?.type === 'block') {
+				for (const inner of node.else.statements) {
+					validate(inner);
 				}
-				break;
 			}
-			// TODO: match
+			break;
 		}
+		// TODO: match
 	}
 
+	if (Ast.hasChainProp(node)) {
+		if (node.chain != null) {
+			for (const item of node.chain) {
+				if (item.type === 'propChain') {
+					if (reservedWord.includes(item.name)) {
+						throwReservedWordError(item.name);
+					}
+				}
+			}
+		}
+	}
+}
+
+export function validateKeyword(nodes: Ast.Node[]): Ast.Node[] {
+	for (const inner of nodes) {
+		validate(inner);
+	}
 	return nodes;
 }
diff --git a/src/parser/plugins/validate-type.ts b/src/parser/plugins/validate-type.ts
index 31a5df0..8fcc1a3 100644
--- a/src/parser/plugins/validate-type.ts
+++ b/src/parser/plugins/validate-type.ts
@@ -1,9 +1,9 @@
 import * as aiscript from '../..';
 import { SemanticError } from '../../error';
-import { Node } from '../../node';
+import * as Ast from '../node';
 import { getTypeBySource } from '../../type';
 
-function validate(node: Node) {
+function validate(node: Ast.Node) {
 	switch (node.type) {
 		case 'def': {
 			if (node.varType != null) {
@@ -30,7 +30,7 @@ function validate(node: Node) {
 	// TODO: ブロックも全部スキャン
 }
 
-export function validateType(nodes: Node[]): Node[] {
+export function validateType(nodes: Ast.Node[]): Ast.Node[] {
 	for (const node of nodes) {
 		validate(node);
 	}


Test patch:
diff --git a/test/index.ts b/test/index.ts
index ebded28..24f35ec 100644
--- a/test/index.ts
+++ b/test/index.ts
@@ -7,13 +7,14 @@ import * as assert from 'assert';
 import { parse, Parser, utils } from '../src';
 import { AiScript } from '../src/interpreter';
 import { NUM, STR, NULL, ARR, OBJ, BOOL } from '../src/interpreter/value';
-import { NAttr, Node } from '../src/node';
+import * as N from '../src/node';
 
 const exe = (program: string): Promise<any> => new Promise((ok, err) => {
 	const aiscript = new AiScript({}, {
 		out(value) {
 			ok(value);
 		},
+		maxStep: 9999,
 	});
 
 	const parser = new Parser();
@@ -279,6 +280,17 @@ it('var', async () => {
 	eq(res, NUM(42));
 });
 
+it('参照が繋がらない', async () => {
+	const res = await exe(`
+	var f = @() { "a" }
+	var g = f
+	f = @() { "b" }
+
+	<: g()
+	`);
+	eq(res, STR('a'));
+});
+
 describe('Cannot put multiple statements in a line', () => {
 	it('var def', async () => {
 		try {
@@ -641,6 +653,96 @@ it('Array item assign', async () => {
 	eq(res, ARR([STR('ai'), STR('taso'), STR('kawaii')]));
 });
 
+it('chain access (prop + index + call)', async () => {
+	const res = await exe(`
+	let obj = {
+		a: {
+			b: [@(name) { name }, @(str) { "chan" }, @() { "kawaii" }];
+		};
+	}
+
+	<: obj.a.b[1]("ai")
+	`);
+	eq(res, STR('ai'));
+});
+
+it('chained assign left side (prop + index)', async () => {
+	const res = await exe(`
+	let obj = {
+		a: {
+			b: ["ai", "chan", "kawaii"];
+		};
+	}
+
+	obj.a.b[2] = "taso"
+
+	<: obj
+	`);
+	eq(res, OBJ(new Map([
+		['a', OBJ(new Map([
+			['b', ARR([STR('ai'), STR('taso'), STR('kawaii')])]
+		]))]
+	])));
+});
+
+it('chained assign right side (prop + index + call)', async () => {
+	const res = await exe(`
+	let obj = {
+		a: {
+			b: ["ai", "chan", "kawaii"];
+		};
+	}
+
+	var x = null
+	x = obj.a.b[1]
+
+	<: x
+	`);
+	eq(res, STR('ai'));
+});
+
+it('chained inc/dec left side (index + prop)', async () => {
+	const res = await exe(`
+	let arr = [
+		{
+			a: 1;
+			b: 2;
+		}
+	]
+
+	arr[1].a += 1
+	arr[1].b -= 1
+
+	<: arr
+	`);
+	eq(res, ARR([
+		OBJ(new Map([
+			['a', NUM(2)],
+			['b', NUM(1)]
+		]))
+	]));
+});
+
+it('chained inc/dec left side (prop + index)', async () => {
+	const res = await exe(`
+	let obj = {
+		a: {
+			b: [1, 2, 3];
+		};
+	}
+
+	obj.a.b[2] += 1
+	obj.a.b[3] -= 1
+
+	<: obj
+	`);
+	eq(res, OBJ(new Map([
+		['a', OBJ(new Map([
+			['b', ARR([NUM(1), NUM(3), NUM(2)])]
+		]))]
+	])));
+});
+
 describe('Template syntax', () => {
 	it('Basic', async () => {
 		const res = await exe(`
@@ -1651,8 +1753,8 @@ describe('lang version', () => {
 
 describe('Attribute', () => {
 	it('single attribute with function (str)', async () => {
-		let node: Node;
-		let attr: NAttr;
+		let node: N.Node;
+		let attr: N.Attribute;
 		const parser = new Parser();
 		const nodes = parser.parse(`
 		#[Event "Recieved"]
@@ -1674,8 +1776,8 @@ describe('Attribute', () => {
 	});
 
 	it('multiple attributes with function (obj, str, bool)', async () => {
-		let node: Node;
-		let attr: NAttr;
+		let node: N.Node;
+		let attr: N.Attribute;
 		const parser = new Parser();
 		const nodes = parser.parse(`
 		#[Endpoint { path: "/notes/create"; }]
@@ -1723,8 +1825,8 @@ describe('Attribute', () => {
 	// TODO: attribute target does not exist
 
 	it('single attribute (no value)', async () => {
-		let node: Node;
-		let attr: NAttr;
+		let node: N.Node;
+		let attr: N.Attribute;
 		const parser = new Parser();
 		const nodes = parser.parse(`
 		#[serializable]
@@ -1746,7 +1848,7 @@ describe('Attribute', () => {
 
 describe('Location', () => {
 	it('function', async () => {
-		let node: Node;
+		let node: N.Node;
 		const parser = new Parser();
 		const nodes = parser.parse(`
 		@f(a) { a }