LoginSignup
1
0

More than 3 years have passed since last update.

JavaScriptで関数型言語を作ろう(5)

Last updated at Posted at 2021-03-07

前回,やっと最初のプログラムが動いたところですが,試しにそのプログラムをこんな風にいじってみましょう(念のためファイル名を変えました)

test/error.txt
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です(ソース下段にいますね).

lang/Semantics.ts
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を呼びます.

index.ts
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.<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) messages: [ 'addd', ' is not defined' ] }

エラーが出るのは一緒ですが,エラーメッセージで出た場所に注目しましょう.「SemanticError: addd is not defined」とあるように,意味エラーが投げられています.場所もSemantics.jsからなので,意味解析の段階で,実行する前にエラーを検出していることがわかります.

今回のソースコードを置いておきます.

次回、意味エラーをもう少しちゃんと処理するための準備をしていきます。

理解度チェック(1)

前回の理解度チェックで作ったプログラムがコンパイルが通って、実行できるようにしましょう。

理解度チェック(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を実行させようとすると,別にさきほどと変わらず,意味エラーを検出してくれました.

test/error.txt
3.addd(2)
  • なぜこれでも意味エラーが検出されたかを考えてみましょう
  • この状態で「意味エラーがあるのにそれが検出されず,実行して初めてエラーになるプログラム」はどんなプログラムでしょうか.error.txtを書き換えて試してみましょう.
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0