今回のスコープ
前回から一か月近く経ってる。
今回の実装内容自体が手間のかかるものであったという理由もありますが主な理由はゲームなど他のことをしていたためです(ぉぃ
さて、前回のあとがきでも書いたように制御構造も一通りできて次は配列かと思ったのですが、配列はオブジェクトとクラスの仕組みを作ってからの方が作りやすいだろうと思ったため、今回の内容はオブジェクトとクラスです。具体的には以下の内容です。
- ステップ9:オブジェクト
- まずは連想配列的な意味でのオブジェクト
- Shorthand property namesやスプレッドもサポート
- 次にメソッド。というかthisの扱い
- ステップ10:クラス
- 昔ながらのprototype。合わせてnew
- 今どきのclass
-
JOKE実装で使っちゃったから継承もサポート - Arrayで使う予定なのでgetter/setterもサポート
ステップ9:オブジェクト
ステップ9段階のコードは以下にあります。
https://github.com/junjis0203/joke/tree/step0009
オブジェクトリテラル
プログラム内でオブジェクトが定義できないと話が始まらないのでまずはオブジェクトリテラルです。
http://www.ecma-international.org/ecma-262/6.0/#sec-object-initializer
オブジェクトの作成は以下のように行っています。
- 空のオブジェクトを作成しスタックにプッシュ(
MAKE_OBJECT
) - 名前を
PUSH
- 値を
PUSH
- オブジェクト、名前、値をスタックからポップしプロパティとして設定(
PROP_DEFINE
1) - 2.~4.をリテラルに書かれてるプロパティ分繰り返す
Assemblerの該当部分(後述するスプレッドの処理も含みます)
Vmの該当部分
Shorthand property names
ES2015で導入されたShorthand property names、こういうやつですね。
const foo = 123;
const bar = 'abc';
const obj = {foo, bar};
構文解析的にIdentifier
の後ろに:
がなければShorthand property namesとみなすようにしました。
また、「展開」の処理はParser
内でしてしまうことでAssembler
やVm
は変更なしで動作するようにしました。
ちなみにComputed property namesも対応はできますがJOKE実装では使ってないしサポートしてません
スプレッド
スプレッドの仕様見当たらないな(。´・ω・)?
とよく考えたらオブジェクトのスプレッドはES2018からでした。まあでも便利なので(JOKE実装で使ってるので)こちらはサポートします。
https://www.ecma-international.org/ecma-262/9.0/index.html#sec-object-initializer
スプレッドで問題となるのは「プロパティの数がアセンブル時点でわからない」ということです。
初めに作成したオブジェクトリテラル実装では「プロパティの数をMAKE_OBJECT
の引数にし、VM実行時にスタックから引数分の名前と値を取り出す」ということをしており、スプレッドをなんとかこの仕組みに乗せれないかいろいろ試行錯誤したのですが2、結局「専用のCOPY_OBJECT命令を作った方が実装がシンプルになる」ということで既存部分を修正しました。
一度作ったものをなんとか使いまわそうとして変な実装になるよりかはそもそもの実装を変えてしまった方がいいという教訓を得ました。3
プロパティへの代入
プロパティの参照はステップ1で作っているので飛ばして代入処理です。
「変数への代入」と「オブジェクトプロパティへの代入」で処理を分けたくなかったので「変数とはスコープオブジェクトのプロパティである」のような一般化をしようと思いましたがめんどくさいのでやめました(プロパティへの代入を行うMEMBER_ASSIGN命令4を作りました)。後述のsetterのことを考えるとちゃんと分けてよかったのかなと思います。
余談ですがPythonやRubyは仕組みは違うもののどちらも「プロパティへの代入」はメソッド呼び出しとして行われます。いつものように「PythonやRubyってどうしてるっけ?」と参考にしようとしたら参考になりませんでした(笑)
this
さてthis
です。JavaScriptのthis
はややこしいことで有名ですがなんちゃって処理系なので「複雑な使い方」は当面サポートしない方向でいきます。つまり、
- オブジェクトからメソッドを取り出して関数的に呼ぶのはサポートしない
- Object.bindもサポートしない
- アロー関数での「thisを束縛しない」動作もサポートしない
- すなわち環境からthisを決定するのはサポートしない
this
に関する仕様(動作)はここら辺が参考になります。
http://www.ecma-international.org/ecma-262/6.0/#sec-function-calls
仕様を正確に実装していませんが(ぉぃ、以下のように実装を行いthis
を実現しました。この処理はステップ10でもう少し改造が入ります。
-
setProperty時に
value
がオブジェクトの場合はvalue
にthisValue
を設定する(関数はオブジェクトで定義されているのでthisValue
が設定される) -
callFunctionでオブジェクトに設定されてるthisValueを取り出し
context
に設定、呼び出す -
this
に対応する命令のTHISでは単にcontext
に設定されているthisValue
を取り出す(スタックにプッシュする)
ステップ10:クラス
オブジェクトと関連が深いからということでクラスについてもまとめて説明します。JavaScriptの場合、「クラス」というと話は長くなりますがES2015で定義されたclass
のこととします。
と言いつつ、実体は・・・なのですが、そもそもJOKEを作ってみようと思った因縁の機能(?)ですね。
ステップ10段階のコードは以下にあります。
https://github.com/junjis0203/joke/tree/step0010
prototype形式とnew
V8などの内部実装がどうなってるのかはわからないですが、とりあえずJOKEではclass
で定義されたクラスもprototype形式に変換して動かすことにしました。
ところでVSCodeだとprototype形式で書いているとこんなことが言われます。最近のエンジンは「クラス形式」に変換されてるっぽいですね。
ステップ10の前準備として「オブジェクト」に対応するJokeObject
5を継承した(このために継承も実装することになりました)JokeFunctionを定義し、prototype
を初めから持たせておきます。こうしておけば「prototypeにメソッドを設定する」処理はステップ9で作ったプロパティへの代入がそのまま動きます。
export class JokeFunction extends JokeObject {
constructor() {
super();
this.setProperty('prototype', new JokeObject());
}
new
は「空のオブジェクトを作り、それをthis
としてコンストラクタ関数に渡す(MDNでの説明ページ)」というものなのでその通りに実装しました。そこまで考えていなかったのですが(ぉぃ、ステップ9で作った「contextにthisを入れておく」仕組みがうまく使いまわせました。
case I.NEW:
{
const args = setupArguments();
const target = stack.pop();
const newObject = new JokeObject();
newObject.setProperty('constructor', target);
callFunction({...context, thisValue: newObject}, target, args);
stack.push(newObject);
}
break;
ややこしいのはメソッド(prototypeに設定されているプロパティ)の取得処理です。JavaScriptで書いているため「constructorが設定されてなくても素のconstructorが取得される6」という点、「this
がprototypeに設定されているのを書き換える必要がある(コメントに書いてあるようにこの実装は気に入ってません)」点などちゃんと動くまで苦労しました。
getProperty(name) {
let value;
// need because native "constructor" is found if not set
if (this.names.includes(name)) {
value = this.properties[name];
}
if (!value && this.names.includes('constructor')) {
const constructor = this.properties['constructor'];
value = constructor.searchProperty(name);
// change thisValue. any more better way?
if (value instanceof JokeObject) {
value = {
...value,
thisValue: this
};
}
}
return value;
}
class形式
さてclass
です。class
に関する動作仕様はこちらに書かれています。
http://www.ecma-international.org/ecma-262/6.0/#sec-runtime-semantics-classdefinitionevaluation
超長いし条件分岐もある( ゚Д゚)
というわけで読む気が起こるのも読み解くのも時間かかったわけですが、最終的に以下のように実装しました。仕様準拠ではないですが大体ちゃんと動作します。
-
構文解析の段階で
constructor
を分けておく -
アセンブラではまず
constructor
を関数として定義しその後にprototype
にメソッドを設定していく。継承については後で説明します。それと// TODO: cache
とありますが当面効率は無視です(開発記その1の方針参照)
こうすれば「prototypeで書いたのと同じこと」になるのでVMの変更はありません。
正確に言うとnew
付けないで関数呼び出しされたら例外投げるとかはあるのですがとりあえず保留です。
継承
めんどくさそうなので作る気がなかった継承ですが、実装に使ってしまったので諦めて仕様を読んでみたところおもしろいことがわかりました。
http://www.ecma-international.org/ecma-262/6.0/#sec-class-definitions
ClassHeritage :
extends LeftHandSideExpression
そう、extends
の後ろはLeftHandSideExpressionなのでハードコードなクラス名だけではなく変数でも関数呼び出しでも書けるのです!
つまり以下のコードは正しいJavaScriptプログラムです。
class Foo {}
const cls = Foo;
class Bar extends cls {}
const func = () => Foo;
class Baz extends func() {}
まあフレームワークの中ぐらいでしか使わない、というか今のところフレームワークの中でも見たことないですが(笑)
もう一つ文法的な話。super
をconsole.log
しようとするとSyntaxError
になるの何故なのかと思っていたのですがBNFを見て納得しました。単独にsuper
と書く文法は存在しない(super()
やsuper.method()
しか正しい文法ではない)のですね。
http://www.ecma-international.org/ecma-262/6.0/#sec-super-keyword
さて実装ですがAssemblerではスーパークラス(extends
の後ろにあるもの)を評価しておき、Vmでは単にスタックに着かれているものをsuperClass
プロパティに設定しておきます。
スーパークラスのコンストラクタ呼び出しに対応するSUPER_CALLとスーパークラスのプロパティ参照7に対応するSUPER_PROPERTYでは設定されているsuperClass
を使ってスーパークラスにアクセスします。
getter/setter
Array
のlength
は特別処理しようかなと考えていたのですがよく思うとgetter/setterとして一般化できるので実装しました。
getter/setterに関する仕様はこちら。要するにget
やset
を付けたメソッドを作ればいいようです(ちゃんと読み込むとそれだけではない気がする)
http://www.ecma-international.org/ecma-262/6.0/#sec-method-definitions-runtime-semantics-propertydefinitionevaluation
VmではSET_PROPERTY
時にsetterがあるか、GET_PROPERTY
時にgetterがあるかを調べるようにしました。
デバッグ機能
今までは-d
オプション一つで「構文解析結果」「アセンブル結果」「今実行している命令」を出力していましたが出力が多くなってきたのでそれぞれ別オプションに分けました。
また、「今実行している命令」は追加情報として「今のスタック」を表示するようになりました。クラスまで来ると「動きがおかしいのだけどスタックに積まれてるデータどうなってるの?」ということが多くなってくるので役立っています(今までは都度console.log
を入れていた)
あとがき
以上今回はオブジェクトとクラスの実装について説明しました。これでだいぶJavaScriptっぽいプログラムまで対応できるようになったと思います。
・・・「普通の使い方」についてはそこそこ動くと思うのですが「変わった書き方」や「仕様にも書かれている細かな文法エラー・実行時エラー」はあまりサポートできていません。まあそのうち。
ともかく次は満を持して配列(メソッド含む)です。
ただその前に、map
などに渡すコードバックはアロー関数で書きたいのでアロー関数を実装する予定です。
-
ステップ10終わりに名前の見直しを行ったのでHEADでは
DEFINE_PROPERTY
。 ↩ -
// len is added in runtime
というコメントは試行錯誤してたときの残骸コメントですね・・・ ↩ -
その際に大事なのは既存部分が壊れてないかを確認できるテストですね ↩
-
HEADでは
SET_PROPERTY
に名前変更。 ↩ -
ステップ1で「とりあえずのオブジェクト表現」を作りそのまま進めてきましたがリファクタリングしました。Jokeと付いているのは素の
Object
と被るためです。 ↩ -
JokeObjectは「JOKEを動かしているJavaScriptエンジン」での「Object(を継承したクラス)のインスタンス」のため、そちらが取得される。 ↩
-
使うことがあるかはわかりませんが仕様的には「スーパークラスのメソッド呼び出し」だけではなく、「(オーバーライドして
this
ではアクセスできないスーパークラスのプロパティ」を取ることができます。 ↩