前回,メソッド名が正しくないときにコンパイルエラー(意味エラー)が出るようにしましたが,あれはまだかなり適当な実装です.
どんなふうに適当なのかを見てみるために,今回は新たに文字列が使えるようにしてみましょう.
文字列型を導入しよう
まず,文字列リテラルが解析できるようにlang/MyTokenizer.tsに文字列リテラルのトークンを追加します.
import Tokenizer from "../lib/Tokenizer";
import {Token} from "../lib/TreeTypes";
export default function MyTokenizer(src:string) {
const tokenizer:Tokenizer=new Tokenizer(src);
function tokenize():Token[] {
const tokens:Token[]=[];
while(true) {
tokenizer.skip(/^\s*/);
if (tokenizer.eof()) return tokens;
const token=
tokenizer.read("number",/^[0-9]+/) ||
tokenizer.read("string",/^"[^"]*"+/) ||//追加
tokenizer.read("lpar",/^\(/) ||
tokenizer.read("rpar",/^\)/) ||
tokenizer.read("dot",/^\./) ||
tokenizer.read("comma",/^,/) ||
tokenizer.read("identifier",/^[A-Za-z]+/) ||
undefined;
if (!token) throw new Error("Tokenizer error while reading "+tokenizer.head());
else tokens.push(token);
}
}
return {tokenize};
}
次に,構文解析でも文字列リテラルを認識できるようにします.
まず,文字列リテラルを表す式をExpressionsで定義しておきます.
//追加
export class StringLiteral {
constructor(public value:string){}
}
export default function MyParser(tokens:Token[]){
//中略
function parseElement():ValueExpression {
if (parser.nextTokenIs("number")) return parseNumberLiteral();
if (parser.nextTokenIs("string")) return parseStringLiteral();//追加
if (parser.nextTokenIs("identifier")) return parseIdentifier();
throw parser.parseError();
}
function parseStringLiteral():StringLiteral {
const t=parser.readToken("string");
return new StringLiteral(t.text.replace(/^"/,"").replace(/"$/,""));
}
//中略
}
すると,何か所かでエラーになります.
ちなみに今更ですが,TypeScriptを書くときは,編集中にリアルタイムにエラーを通知してくれるエディタをおすすめします.私はAtomにTypeScriptのプラグインを入れていますが,一般的にはVSCodeとかでしょうか.
ここではあえてtscの結果を載せておきます.
lang/MyParser.ts:46:32 - error TS2304: Cannot find name 'StringLiteral'.
46 function parseStringLiteral():StringLiteral {
~~~~~~~~~~~~~
lang/MyParser.ts:48:14 - error TS2304: Cannot find name 'StringLiteral'.
48 return new StringLiteral(t.text.replace(/^"/,"").replace(/"$/,""));
~~~~~~~~~~~~~
StringLiteralが見つからないと言われるので,MyParser.tsのimportのところに追記します.
import { ValueExpression, NumberLiteral, Identifier, Call, MemberAccess, StringLiteral } from './Expressions';
//後略
すると,また違うところで怒られます.
lang/MyParser.ts:22:37 - error TS2322: Type 'StringLiteral' is not assignable to type 'ValueExpression'.
Type 'StringLiteral' is not assignable to type 'NumberLiteral'.
Types of property 'value' are incompatible.
Type 'string' is not assignable to type 'number'.
22 if (parser.nextTokenIs("string")) return parseStringLiteral();
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1行目に,
Type 'StringLiteral' is not assignable to type 'ValueExpression'.:「'StringLiteral'は'ValueExpression'に代入できない」と言われました.
ValueExpressionの定義はlang/Expressions.ts
にあります.
//前略
export type ValueExpression=NumberLiteral|Identifier|MemberAccess|Call;
ここにStringLiteralも仲間に入れてあげましょう.
//前略
export type ValueExpression=NumberLiteral|StringLiteral|Identifier|MemberAccess|Call;
さて,これで直……らないです.
lang/CodeGen.ts:16:23 - error TS2345: Argument of type 'StringLiteral' is not assignable to parameter of type 'never'.
16 throw invalid(expr);
~~~~
lang/Semantics.ts:18:23 - error TS2345: Argument of type 'StringLiteral' is not assignable to parameter of type 'never'.
18 throw invalid(expr);
~~~~
Found 2 errors.
ここで,前回の仕掛けが役に立ちました.lang/Semantics.tsを例にしてみましょう.
// 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 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);//ここでエラー
}
}
この関数は「ValueExpressionのオブジェクトをもらってきて,どのValueExpressionかによってチェックの処理を振り分ける」ということをやっていました.
先ほど,ValueExpressionの仲間にStringLiteralを加えてあげたので.「StringLiteralの処理も忘れないであげてください」というエラーが出てくれました.
とはいえ,今のところStringLiteral自身には(NumberLiteral同様)チェック項目はないので,単に判定部分を増やして空の処理を入れておきます.
// 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);
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);//ここでエラー
}
}
(追加するとまた「StringLiteralなんざ知らん」と言われますがそれはimportしておきましょう)
そして,同様にCodeGenにもStringLiteralの処理を追加します
import { ValueExpression, NumberLiteral, Identifier, MemberAccess, Call, StringLiteral} from "./Expressions";
function invalid(s:never){return new Error("${s} is invalid");}
export function generate(expr:ValueExpression):string {
const g=generate;
if (expr instanceof NumberLiteral) {
return `Num(${expr.value})`;
} else if (expr instanceof StringLiteral) {//追加
return `Str("${expr.value}")`;//追加
} else if (expr instanceof Identifier) {
return expr.text;
} else if (expr instanceof MemberAccess) {
return `${g(expr.left)}.${expr.name.text}`;
} else if (expr instanceof Call) {
const a=expr.args.map(g);
return `${g(expr.left)}(${a.join(",")})`;
} else {
throw invalid(expr);
}
}
さて、これでコンパイルエラーは消えたので、文字列を使ったサンプルプログラムを作って実行してみましょう。
"hello".add("world")
何の前触れもなくadd
とか出てきましたが、これは、数値のときと同じです。3.add(2)
が3+2
に相当していたので、"hello".add("world")
は"hello"+"world"
と同じ、つまり2つの文字列をくっつけて"helloworld"
という文字列にする、という式です。
実行すると……エラーになります。
% tsc
% node index.js test/str.txt
//前略
const {Num}=runtime;
return Str("hello").add(Str("world"));
ReferenceError: Str is not defined
at eval (eval at run (C:\bin\Dropbox\workspace\tinyfunc\index.js:42:18), <anonymous>:5:5)
at run (C:\bin\Dropbox\workspace\tinyfunc\index.js:43:17)
at test (C:\bin\Dropbox\workspace\tinyfunc\index.js:16:9)
at Object.<anonymous> (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)
at Function.Module._load (internal/modules/cjs/loader.js:530:3)
at Function.Module.runMain (internal/modules/cjs/loader.js:742:12)
jsのソースの生成はできましたが、生成されたソースを実行したときにエラーになりました。そういえばStr
なんていうオブジェクトはないでした。
Num
を定義しているのと同じ場所で定義してみましょう。lib/runtime.ts
です。
// INum, Numの定義は省略
interface IStr {
value:string;
add:(b:IStr)=>IStr,
};
export const Str=(value:string):IStr=>({
value,
add(b:IStr) {
return Str(value+b.value);
},
});
文字列にはadd
メソッドのみを定義しておきます。文字列に対して引き算や掛け算はできないという仕様にしておきます。JavaScriptだとできちゃう(文字列を数値に変換してから数値として計算)のですが、この言語ではできないことにします。(あ、これ今回の「理解度チェック」のヒント)
最後に、index.tsも変更します。
//runメソッドのみ
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);
//ここ{Num} →{Num, Str}
const js=`
const {Num, Str}=runtime;
return ${generate(tree)};
`;
console.log(js);
const func=new Function("runtime", js);
const res=func(runtime);
console.log(res);
}
実行してみます。
//前略
const {Num, Str}=runtime;
return Str("hello").add(Str("world"));
{ value: 'helloworld', add: [Function: add] }
見事、"helloworld"
が計算できました。
理解度チェック
※前回までの理解度チェックを済ませてからやってみてください。
で、今回のテーマは「メソッド名が正しくないときにコンパイルエラー(意味エラー)が出るようにしましたが,あれはまだかなり適当な実装」なのはどんなところが適当なのか、ということでした。
この状態で、「コンパイル時はエラーにならず、実行時になって初めてエラーになってしまう」コードを書いてみましょう。
次回答え合わせします.