前回,やっと最初のプログラムが動いたところですが,試しにそのプログラムをこんな風にいじってみましょう(念のためファイル名を変えました)
3.addd(2)
実行するとこんなエラーが出ます.
% tsc
% node index.js test/error.txt
(中略)
TypeError: Num(...).addd is not a function
at eval (eval at run (C:\bin\Dropbox\workspace\tinyfunc\index.js:40:18), <anonymous>:5:19)
at run (C:\bin\Dropbox\workspace\tinyfunc\index.js:41:17)
at test (C:\bin\Dropbox\workspace\tinyfunc\index.js:15:9)
at Object.<anonymous> (C:\bin\Dropbox\workspace\tinyfunc\index.js:10:5)
at Module._compile (internal/modules/cjs/loader.js:689:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
at Module.load (internal/modules/cjs/loader.js:599:32)
at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
at Function.Module._load (internal/modules/cjs/loader.js:530:3)
at Function.Module.runMain (internal/modules/cjs/loader.js:742:12)
エラーの原因はもちろん,add
と書くところを(わざと)間違えてaddd
としたから,なのですが,このエラーが発生した場所(.txt上の場所じゃなくて,処理系上の場所)に注目してみましょう.
at run (C:\bin\Dropbox\workspace\tinyfunc\index.js:41:17)
とあります.TypeScriptから変換されたjsファイルの中なので,ちょっとわかりにくいのですが,
function run(src) {
const t = MyTokenizer_1.default(src);
const tokens = t.tokenize();
tokens.forEach((token, i) => console.log(`${i}:[${token.type}] ${token.text}`));
const p = MyParser_1.default(tokens);
const tree = p.parse();
console.dir(tree, { depth: 10 });
const js = `
const {Num}=runtime;
return ${CodeGen_1.generate(tree)};
`;
console.log(js);
const func = new Function("runtime", js);
const res = func(runtime);//ここ!
console.log(res);
}
「ここ!」と書いたこの場所は,前回追加した,生成されたコード(js
の中身)から関数(func
)を生成して,呼び出しているところです.
ここで大事な点(というか,問題点)は「エラーのあるコードなのに,コードは実行されてしまった」という事実です.本連載で目指すプログラミング言語は,型などのチェックをちゃんと行って,事前にエラーになることがわかっていたら実行しない,という機能を目指したいと考えていますので,今回は
存在しないメソッド名を使って呼び出そうとしていたら,実行する前にコンパイルエラーを出す.
という機能を追加していきます.
このような,「構文的には合っているけど,もろもろの事情(?)で実行できないコードなんじゃないか?」ということを調べることを**「意味解析」と言います.「もろもろの事情」には,今回取り上げる「存在しないメソッドを呼び出している」ももちろん含まれます.「存在しないメソッドを呼び出している」などの間違い(エラー)を意味エラー**と呼ぶこともあります.
意味エラーチェック(Semantics.ts)
それでは,今回追加するlang/Semantics.ts
を見てみましょう.Semanticsとは「意味論」とかいう意味です.先述した「意味エラー」はSemanticErrorです(ソース下段にいますね).
import { ValueExpression, NumberLiteral, Identifier, MemberAccess, Call } from "./Expressions";
function invalid(s:never){return new Error("${s} is invalid");}
const numberTypeMembers=new Set(["add","sub"]);
export function check(expr: ValueExpression) {
const E=(...messages:any[])=>new SemanticError( ...messages);
console.log("Checking", expr);
if (expr instanceof NumberLiteral) {
} else if (expr instanceof Identifier) {
} else if (expr instanceof MemberAccess) {
check(expr.left);
if (!numberTypeMembers.has(expr.name.text)) {
throw E(expr.name.text, " is not defined");
}
} else if (expr instanceof Call) {
check(expr.left);
} else {
throw invalid(expr);
}
}
export class SemanticError extends Error {
public messages:any[];
constructor(...messages:any[]) {
super("SemanticError: "+messages.join(" "));
this.messages=messages;
}
}
さて,このファイルの主役はcheck
という関数ですが,前回と似たような構造が出てきます.
if (expr instanceof NumberLiteral) {
} else if (expr instanceof Identifier) {
} else if (expr instanceof MemberAccess) {//中身略
} else if (expr instanceof Call) {//中身略
} else {
throw invalid(expr);
}
つまり,ValueExpression
(式)であるところのexpr
がどんな種類であるかによって,処理を振り分ける部分です.これも前回書きましたが,ValueExpression
には現状次の4種類があります.
-
NumberLiteral
(数値定数) -
Identifier
(変数) -
MemberAccess
(object.x
のようなプロパティへのアクセス) -
Call
(f(x)
のような関数呼び出し)
で,余談.前回の愚痴を解決する方法が見つかったので.今回は上の4つのパターンをifまたはelse-if で全部列挙して,最後のelseでthrow invalid(expr);
としています.invalid
の定義は上のほうにあります.
function invalid(s:never){return new Error("${s} is invalid");}
ここで,never
型というのが使われていますが,これは「どんな値も代入できない」型のようです.つまり,throw invalid(expr);
に到達可能であると判定されると,コンパイルエラーになってくれる,というものです.ところが,ValueExpression
としてあり得る4パターンをそこまでのelse-ifで網羅しているから,throw invalid(expr);
には到達不能なため,エラーにはならないという仕組みのようです.さらにもし,4パターンの中に書き洩らしがあったら,ちゃんとエラーになってくれるという優れもの.今後ValueExpression
の種類は増えて行きますので,全パターン網羅できているかのチェックは重要になってきます.
閑話休題.
処理の中身を見ていくと,まず,NumberLiteral
には特にチェック項目はありませんし,Identifier
はそもそもまだ構文として書けていないので空っぽになっています.
次にMemberAccess
についてのチェック項目ですが,これがまさに今回やりたいことで,さきほどの.addd
などの存在しないメソッド呼出を見つけたらエラーを出すようにしています.
else if (expr instanceof MemberAccess) {
check(expr.left);
if (!numberTypeMembers.has(expr.name.text)) {
throw E(expr.name.text, " is not defined");
}
numberTypeMembers
は,上のほうで
const numberTypeMembers=new Set(["add","sub"]);
と定義されているSet
(集合)オブジェクトです.要素は"add"
と"sub"
ですので,それ以外の名前が来たら例外(SemanticError
)を投げます.
Call
については,check(expr.left)
という処理をしています.これはMemberAccess
にも同じものがあります.あとで理解度チェックで改めて問いますが,再帰的にチェックを行うものです.
} else if (expr instanceof Call) {
check(expr.left);
}
最後に,index.ts
からこのcheck
を呼び出してみましょう.構文解析ができた状態で,tree
に式オブジェクトが入っているはずなので,この状態でcheck
を呼びます.
import * as fs from "fs";
import MyTokenizer from "./lang/MyTokenizer";
import MyParser from "./lang/MyParser";
import { generate } from "./lang/CodeGen";
import * as runtime from "./lang/runtime";
import { ParseError } from "./lib/TreeTypes";
import { check } from "./lang/Semantics";//追加
//中略
function run(src:string) {
const t=MyTokenizer(src);
const tokens=t.tokenize();
tokens.forEach((token,i)=>console.log(`${i}:[${token.type}] ${token.text}`));
const p=MyParser(tokens);
const tree=p.parse();
console.dir(tree, {depth:10});
check(tree);//追加
const js=`
const {Num}=runtime;
return ${generate(tree)};
`;
console.log(js);
const func=new Function("runtime", js);
const res=func(runtime);
console.log(res);
}
~~~
さて,実行してみます.
% tsc
% node index.js test/error.txt
(中略)
{ Error: SemanticError: addd is not defined
at E (C:\bin\Dropbox\workspace\tinyfunc\lang\Semantics.js:7:32)
at check (C:\bin\Dropbox\workspace\tinyfunc\lang\Semantics.js:16:19)
at Object.check (C:\bin\Dropbox\workspace\tinyfunc\lang\Semantics.js:20:9)
at run (C:\bin\Dropbox\workspace\tinyfunc\index.js:36:17)
at test (C:\bin\Dropbox\workspace\tinyfunc\index.js:16:9)
at Object. (C:\bin\Dropbox\workspace\tinyfunc\index.js:11:5)
at Module._compile (internal/modules/cjs/loader.js:689:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
at Module.load (internal/modules/cjs/loader.js:599:32)
at tryModuleLoad (internal/modules/cjs/loader.js:538:12) messages: [ 'addd', ' is not defined' ] }
エラーが出るのは一緒ですが,エラーメッセージで出た場所に注目しましょう.「`SemanticError: addd is not defined`」とあるように,意味エラーが投げられています.場所も`Semantics.js`からなので,意味解析の段階で,実行する前にエラーを検出していることがわかります.
今回の[ソースコード](https://github.com/hoge1e3/tinyfunc/releases/tag/semantics-1)を置いておきます.
[次回](https://qiita.com/hoge1e3/items/5319c3b258b61a78c185)、意味エラーをもう少しちゃんと処理するための準備をしていきます。
## 理解度チェック(1)
[前回](https://qiita.com/hoge1e3/items/544efc02eac3718b33b4)の理解度チェックで作ったプログラムがコンパイルが通って、実行できるようにしましょう。
## 理解度チェック(2)
`check(expr.left)`の役割について理解を深めるため,これを呼ばなかったらどんな不具合があるか試してみましょう.`MemberAccess`の部分で呼ばれている`check(expr.left)`をコメントにしてみます.(`Call`のほうはそのまま).あとコンパイル(tsc)を忘れずに.
~~~~Semantics.ts
//checkメソッド内
} else if (expr instanceof MemberAccess) {
//check(expr.left);//わざとコメントにする
if (!numberTypeMembers.has(expr.name.text)) {
throw E(expr.name.text, " is not defined");
}
} else if (expr instanceof Call) {
check(expr.left);//こっちはそのまま
}
ところが,このプログラムでこのerror.txt
を実行させようとすると,別にさきほどと変わらず,意味エラーを検出してくれました.
3.addd(2)
- なぜこれでも意味エラーが検出されたかを考えてみましょう
- この状態で「意味エラーがあるのにそれが検出されず,実行して初めてエラーになるプログラム」はどんなプログラムでしょうか.
error.txt
を書き換えて試してみましょう.