今回は珍しく,前回 の理解度チェックの答え合わせから.
前回,こんなプログラムに対してHelloWorld
を出力させるところまでは確認できました.
"Hello".add("World")
また,次のように存在しないメソッドを含むプログラムを実行させると,SemanticError: addd is not defined
のような意味エラーをちゃんと出してくれる,というところも確認できていると思います.
"Hello".addd("World")
では,次はどうでしょう.確か,前回「文字列にはaddしかなくて,subやらmulやらは使えない仕様です」と断っておいたので,エラーになってほしいところです.
"Hello".sub("World")
こうなりました:
const {Num, Str}=runtime;
return Str("hello").sub(Str("world"));
TypeError: Str(...).sub is not a function
at eval (eval at run (C:\bin\Dropbox\workspace\tinyfunc\index.js:42:18), <anonymous>:5:25)
at run (C:\bin\Dropbox\workspace\tinyfunc\index.js:43:17)
at test (C:\bin\Dropbox\workspace\tinyfunc\index.js:16:9)
(略)
やった!エラーが出た! と思った方は残念でした.これ,前回我々がちゃんと仕込んだSemanticError
ではありません.その証拠に
const {Num, Str}=runtime;
return Str("hello").sub(Str("world"));
というコードが生成されてしまっています.SemanticError
が出た場合,その場で例外となるので,コードが生成されない点に注意しましょう.
このプログラムにおいては「コードがされちゃって,実行したら実行時エラーになった」にすぎず,処理系がちゃんと事前にエラーをチェックできていません.
では,なぜ"Hello".addd("World")
はちゃんとSemanticError
になったのに,"Hello".sub("World")
は我々の仕込んだエラーチェックをすり抜けて実行されて(実行時エラーになって)しまったのでしょうか.
ポイントはSemantics.ts
です.
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 StringLiteral) {//追加
} 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);
}
}
この if (!numberTypeMembers.has(expr.name.text)) {
の部分で,.add
やら.addd
やら.sub
やらがメンバ名として正しいかどうかチェックしているのですが,numberTypeMembers
の中身が
const numberTypeMembers=new Set(["add","sub"]);
,つまり「add
かsub
ならOK,それ以外はSemanticError
」というチェックをしています.(この部分は理解度チェックをしている方はもう少し増えてますね)
しかし,文字列型にはadd
しかないので,sub
がOKのわけがないんですが,この処理ではOKにしてしまっています.
そもそも文字列型のメンバーのチェックでnumberTypeMembers
を使ってはまずいですよね.
では,どうするのか,というのが今回のテーマです.
式の型チェック
} else if (expr instanceof MemberAccess) {
check(expr.left);
//ここで存在しないメンバーのチェックをしている
if (!numberTypeMembers.has(expr.name.text)) {
throw E(expr.name.text, " is not defined");
}
exprがMemberAccess ,つまりa.b
のような形の式の場合の処理だけ抜き出してみました.
MemberAccessには,a.b
のa
の側をleft
,b
の側の名前をname
で管理しています.
-
expr.left
の部分に数値が来ていればadd
やらsub
やら,他にもmul
やらdiv
やらも使ってよい -
expr.left
の部分に文字列が来ていればadd
以外はダメ
という場合分けが必要になってきます.
そこで,expr.left
の部分がどんな型なのかを調べる関数を新たに定義しましょう.Semantics.ts
の適当なところ(check
メソッドの上あたり)にgetType
という関数を作成しましょう.
export function getType(expr: ValueExpression):Type {
//式exprの型を返す
}
で,これはエラーになります.
lang/Semantics.ts:5:48 - error TS2304: Cannot find name 'Type'.
5 export function getType(expr: ValueExpression):Type {
~~~~
Type
なんていう型はないですよね.はい,では今から作りましょう.lang/Types.ts
というファイルを新規作成しましょう.
import { Identifier } from "./Expressions";
export class StructType {
constructor(public name:Identifier, public members:Member[]) {}
}
export class Member {
constructor(public name:Identifier) {}
}
export type Type=StructType;
StructType
って何かというと,今問題になっている数値型や文字列型を表します.struct
というとC言語の「構造体」を思い出すので,数値なのに構造体? と思うかもしれませんが,この言語では,3.add
みたいに,数値でも構造体っぽく「ドット+名前」でアクセスできるので,構造体扱いにしています.
この.add
の部分を「メンバー」と呼びます.StructType
には複数のメンバーを定義でき,それらがmembers
というフィールドに格納されます.
Member
の定義は,とりあえず名前だけです.型やメンバーの名前はExpressions.tsで定義したIdentifier
を使っています.
最後の行で「Type
は,StructType
のことです」,と書いてあります.後でStructType
以外のものもType
の仲間として追加される予定です.
では,これを使って,数値型や文字列型を定義します.
//今まで書いたものの下に追加
const nMembers=[
new Member(new Identifier("add")),
new Member(new Identifier("sub")),
];
export const numberType=new StructType(new Identifier("Number"), nMembers);
const sMembers=[
new Member(new Identifier("add")),
];
export const stringType=new StructType(new Identifier("String"), sMembers);
そして,これを使って,Semantics.tsのgetTypeを定義してみましょう.
import { Type, stringType, numberType } from "./Types";
//中略
export function getType(expr: ValueExpression):Type {
if (expr instanceof StringLiteral) {
return stringType;
} else {
return numberType;
}
}
//後略
とりあえず,文字列リテラル("abc"
とか)は文字列型,それ以外は数値型にしておきましょう(「おい,そんなんで大丈夫か」と思った方,お見事.大丈夫じゃないです! 理解度チェックで大丈夫じゃない例を挙げてみましょう)
そして,StructType
からメンバーを取り出す関数も作っておきます.なお,指定した型の指定した名前のメンバーがない場合はSemanticsError
にしています.
//中略
export function getMember(type:StructType, name:Identifier):Member {
for (const member of type.members) {
if (member.name.text===name.text) return member;
}
throw new SemanticError("member is not defined");
}
//後略
最後に,check
の部分を書き換えましょう.
//中略
export function check(expr: ValueExpression) {
const E=(...messages:any[])=>new SemanticError( ...messages);
console.log("Checking", expr);
if (expr instanceof NumberLiteral) {
} else if (expr instanceof StringLiteral) {//追加
} else if (expr instanceof Identifier) {
} else if (expr instanceof MemberAccess) {
check(expr.left);
const leftType=getType(expr.left);//expr.leftの型を取得
getMember(leftType, expr.name);//その型からメンバーを取得
} else if (expr instanceof Call) {
check(expr.left);
} else {
throw invalid(expr);
}
}
//後略
では,これで実行してみます(tscを忘れないように).
まず数値型
3.add(2)
↑これは正しく実行されて5になります
3.sub(2)
↑これもOK(以前の理解度チェックでちゃんと引き算の仕組みを作っていれば)
3.addd(2)
↑これはちゃんとSemanticsError
になってくれるのでOK.
"hello".add("world")
↑これは正しく実行されてhelloworldになります
"hello".addd("world")
↑これはちゃんとSemanticsError
になってくれるのでOK.
さて,今回問題になっていた(SemanticsError
にならなくて,実行時エラーになっちゃうもの)を実行してみましょう.
"hello".sub("world")
(中略)
Checking StringLiteral { value: 'hello' }
SemanticError: SemanticError: member is not defined
at getMember (C:\bin\Dropbox\workspace\tinyfunc\lang\Semantics.js:23:11)
at check (C:\bin\Dropbox\workspace\tinyfunc\lang\Semantics.js:38:9)
at Object.check (C:\bin\Dropbox\workspace\tinyfunc\lang\Semantics.js:44:9)
at run (C:\bin\Dropbox\workspace\tinyfunc\index.js:36:17)
~~~
ちゃんとコードが生成されず,`SemanticsError`になりました.
# 理解度チェック
(1) メンバーが見つからなかったときのエラーを親切にしてみましょう:
Before:
~~~
SemanticError: SemanticError: member is not defined
~~~
After:
SemanticError: SemanticError: member 'sub' is not defined in type 'String'
(2) この状態でも相変わらず「`SemanticsError`にならなくて,実行時エラーになっちゃうもの」は存在します.どんなコードでしょう? 前回まで書いたコードの中にヒントがあるかもしれません.
[ここまでのソースコード](https://github.com/hoge1e3/tinyfunc/releases/tag/typecheck-1)