-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathVM.as
More file actions
665 lines (518 loc) · 23.1 KB
/
VM.as
File metadata and controls
665 lines (518 loc) · 23.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
/* VM
Execute the compiled bytecode. Inspired heavily by JonesFORTH, to a lesser degree by Postscript and HotRuby.
See:
http://replay.waybackmachine.org/20090209211708/http://www.annexia.org/forth
Notes to self:
The Array class is one of the few core classes that is not final, which
means that you can create your own subclass of Array. Hmmm.... probably a bad idea.
*/
package org.sixsided.scripting.SJS {
//import org.sixsided.scripting.SJS.Inspector;
import flash.utils.getDefinitionByName;
import flash.utils.getQualifiedClassName;
import flash.geom.*;
import flash.display.*;
import flash.events.Event;
import flash.events.EventDispatcher;
public class VM extends EventDispatcher {
public static var _VM_ID:int = 0;
public static var registry:Object = { Math:Math, Date:Date, 'null':null }; // TweenLite, etc goes here
public static function register(key:String, value:*) : void {
VM.registry[key] = value;
}
public static const MAX_RECURSION_DEPTH : int = 64;
/***********************************************************
*
* OPCODE IDs
*
***********************************************************/
public static const NOP:String = 'NOP';
public static const DUP:String = 'DUP';
public static const DROP:String = 'DROP';
/*public static const DROPALL:String = 'DROPALL';*/
public static const SWAP:String = 'SWAP';
public static const INDEX:String = 'INDEX';
public static const LIT:String = 'LIT';
public static const VAL:String = 'VAL';
public static const ADD:String = 'ADD';
public static const SUB:String = 'SUB';
public static const MUL:String = 'MUL';
public static const DIV:String = 'DIV';
public static const MOD:String = 'MOD';
public static const NEG:String = 'NEG';
public static const EQL:String = 'EQL';
public static const GT:String = 'GT';
public static const LT:String = 'LT';
public static const GTE:String = 'GTE';
public static const LTE:String = 'LTE';
public static const AND:String = 'AND';
public static const OR:String = 'OR';
public static const NOT:String = 'NOT';
public static const CLOSURE:String = 'CLOSURE';
public static const MARK:String = 'MARK';
public static const CLEARTOMARK:String = 'CLEARTOMARK';
public static const ARRAY:String = 'ARRAY';
public static const HASH:String = 'HASH';
public static const JUMP:String = 'JUMP';
public static const JUMPFALSE:String = 'JUMPFALSE';
public static const CALL:String = 'CALL';
public static const RETURN:String = 'RETURN';
/* public static const TRACE:String = 'TRACE';*/
public static const PUT:String = 'PUT';
public static const PUTINDEX:String = 'PUTINDEX';
public static const GETINDEX:String = 'GETINDEX';
public static const GET:String = 'GET';
public static const LOCAL:String = 'LOCAL';
public static const NATIVE_NEW:String = 'NATIVE_NEW';
public static const AWAIT:String = 'AWAIT';
public static const PUSH_RESUME_PROMISE:String = 'PUSH_RESUME_PROMISE';
//public static const HALT:String = 'HALT';
/***********************************************************
*
* EVENTS
*
***********************************************************/
public static const EXECUTION_COMPLETE:String = 'VM.EXECUTION_COMPLETE';
/***********************************************************
*
* VM STATE
*
***********************************************************/
public var _vm_id:String = ""+_VM_ID++;;
public var running:Boolean;
public var tracing:Boolean = false;
public var call_stack:Array = []; // function call stack
public var os:Array = []; // operand stack
public var marks:Array = []; // stack indices for array / hash construction
public var system_dicts:Array = []; // context; e.g. add a movie clip and script "x += 10"
public var vm_globals:Object = {}; // global scope, like you're used to in browser JavaScript:
/************************************************************
**
** PRIVATE API
**
************************************************************/
private function log(...args) : void {
if(tracing) {
_vmTrace('| ' + args.join(' '));
}
}
private function _vmTrace(...args) : void {
trace('[VM#'+_vm_id+']', args.join(' '));
}
private function _vmUserTrace(...args) : void {
_vmTrace(ANSI.cyan(args.join(' ')));
}
private function get _osAsString() : String {
return os.map(function(e:*, ...args) : String {
if (e is Function) { return '*fn*'; } return e;
}).join(' ');
}
private function next_word() : * {
return current_call.next_word();
}
private function get current_call():StackFrame {
return call_stack[0];
}
/***********************************************************
*
* PUBLIC API
*
***********************************************************/
public function VM() {
setGlobal('_trace', _vmUserTrace);
setGlobal('verbose_vm', _vmSetVerbosity);
setGlobal('halt', halt);
setGlobal('pushscope', pushDict);
setGlobal('popscope', popDict);
}
// for hotloading -- define a "clone me" function externally
// but make it a noop in the clone so you don't get
// infinite recursion
/* public function clone() : VM {
var ret:VM = new VM;
ret.load(call_stack[0].code);
return ret;
}
*/
public function _vmSetVerbosity(on:Boolean) : void {
tracing = on;
}
public function setGlobal(k:String, v:*) : void {
vm_globals[k] = v;
}
public function setGlobals(o:Object) : void {
for(var k:String in o) vm_globals[k] = o[k];
}
public function pushDict(dict:Object) : void {
system_dicts.unshift(dict);
trace('pushDict: now have', system_dicts.length);
}
public function popDict() : void {
system_dicts.shift();
trace('popDict: now have', system_dicts.length);
}
// can say vm.load(one_liner).run() with no trouble
// prebind the ops for speed?
public function load(tokens:Array) : VM {
call_stack = [ new StackFrame(tokens, {}) ];
return this;
}
public function loadString(code:String) : VM {
load(code.split(' '));
return this;
};
public function halt() : void {
running = false;
}
// == run ==
// some notes:@C
// Opcodes can only legally be of type String, although we interleave other types of data with them.
// we loop until reaching the end of the last stack frame, or until halted (running = false).
// we wrap (op) in extra parentheses to quiet Flash's "function where object expected" warning.
// we stash the callframe at the top of the inner loop in case next_word exhausts the StackFrame, causing it to be popped at the end of the loop
// have to do checks in loop in case we:
// - popped a frame last time through
// - exhausted callframe during run loop -- probably if/else jumping to end
//trace('VM.run; call_stack depth:', cs.length, 'pc @', cs[0].pc, '/', cs[0].code.length, '(', cs[0].code.join(' '), ')');
//trace('... bailing at end of cycle, call_stack depth:', cs.length, 'pc @', cs[0].pc, '/', cs[0].code.length, '(', cs[0].code.join(' '), ')');
// log(' VM Finished run. os: ', '[' + os.join(', ') + ']', ' dicts: ', Inspector.inspect(system_dicts), 'traces:', Inspector.inspect(dbg_traces), "\n");
//log(w, ANSI.wrap(ANSI.BLUE, ' ( ' + _osAsString + ' ) '));
public function run() : void {
var cs:Array = call_stack;
var op:Function;
running = true;
while(cs.length) {
while(cs.length && !current_call.exhausted && running) {
op = this[current_call.next_word()];
if(!(op)) {
if(current_call.exhausted) continue;
else throw new Error('VM got unknown operator ``' + current_call.prev_word() + "''");
}
op();
if(!running) return; // bail from AWAIT instruction
}
cpop(); // automatically return at end of function even if no return statement
}
running = false;
dispatchEvent(new Event(EXECUTION_COMPLETE));
}
public function onComplete(fn:Function) : void {
addEventListener(EXECUTION_COMPLETE, function _doOnComplete(...args) : void {
removeEventListener(EXECUTION_COMPLETE, _doOnComplete);
fn();
});
}
/**************************************************
**
** INTERNALS
**
***************************************************/
// call_stack manipulation. We prefer unshift/shift to push/pop because it's convenient that top-of-stack is always stack[0]
private function cpush(code:Array,vars:Object) : void {
call_stack.unshift(new StackFrame(code, vars, call_stack[0]));
}
private function cpop() : void {
call_stack.shift();
}
private function fcall(fn:VmFunc, args:Array) : void {
if(call_stack.length > VM.MAX_RECURSION_DEPTH) {
throw new Error('org.sixsided.scripting.SJS.VM: too much recursion in' + fn.name);
}
call_stack.unshift(new StackFrame(fn.body,
conformArgumentListToVmFuncArgumentHash(args, fn),
fn.parentScope));
}
// stack manipulation
private function opush(op:*):void { os.unshift(op); log(op, '->', '(', _osAsString, ')'); };
private function opop():* { log(os[0], '<-', '(', _osAsString, ')'); return os.shift(); };
private function numpop():Number { return parseFloat(opop()); };
private function bin_ops():Array { return [opop(), opop() ].reverse(); };
private function pushmark():void { marks.unshift(os.length); };
private function yanktomark():Array{ return os.splice(0, os.length - Number(marks.shift())).reverse(); }; // fixme: hack, ditch shift-stacks for push-stacks
// var manipulation
/* find_var/set_var
* VM has four tiers of variables.
* 1) the chain of StackFrame vars as defined by lexical scope
* 2) the VM's globals, vm_globals
* 3) the system dicts, in the order they were added -- READ ONLY; set_var does not even look at these
* 4) the VM's static registry, VM.registry
*
* *** The only writable vars are the current callframe's and the vm globals
* *** ... that is, locals and globals for a given VM. Just like Javascript.
* ....... Could add a 'register' function for adding things to the registry.
*/
// so running in the root scope, the 'var' keyword indicates a temporary variable tha won't persist after
// the call_stack is exhausted, i.e. the code runs through to its end and the vm exits.
// simply setting a variable with x = n, however, will create a persistent global x.
private function frameWithVar(key:String) : StackFrame {
var sf:StackFrame = call_stack[0];
var safety:int = MAX_RECURSION_DEPTH;
while(sf && safety--) {
if(sf.vars.hasOwnProperty(key)) {
return sf;
}
sf = sf.parent;
}
return null;
}
public function set_var(key:String, value:*) : void {
var sf:StackFrame = frameWithVar(key);
if(sf) {
sf.vars[key] = value;
return;
}
vm_globals[key] = value;
};
public function findVar(key:String) : * {
var v:* = _find_var(key);
return (undefined === v) ? null : v; // duhh why?
}
private function _keys(o : Object ) : Array {
var ret = [];
for(var k:String in o) {
ret.push(k);
}
return ret;
}
private function _find_var(key:String) : * {
// locals?
var sf:StackFrame = frameWithVar(key);
if(sf) {
return sf.vars[key];
}
// globals?
if(vm_globals.hasOwnProperty(key)) {
return vm_globals[key];
}
// dicts? (in LIFO order)
for (var i:int = 0; i < system_dicts.length; i++) {
//trace('look for', key, 'in #' + i + '/' + system_dicts.length +':', _keys(system_dicts[i]).join(' '));
var g:Object = system_dicts[i];
if(g.hasOwnProperty(key)) {
return g[key];
}
}
// registry?
if(VM.registry.hasOwnProperty(key)) {
return VM.registry[key];
}
// not defined anywhere!
return undefined;
};
/***********************************************************
*
* OPCODES
*
***********************************************************/
public function callScriptFunction(fnName:String, args:Array=null) : void {
_vmTrace('callScriptFunction', fnName);
var fn:* = findVar(fnName);
if(fn is Function) {
fn.apply(null, args);
} else if(fn is VmFunc) {
fcall(fn, args);
run();
} else {
throw "Tried to callScriptFunction on object " + fn;
}
}
// wrap VM functions in AS3 closures so we can pass them to AS3
// as event listeners, etc, that will fire up the vm
private function wrapVmFunc(fn:VmFunc):Function{
var vm:VM = this;
return function(...args):void {
vm.fcall(fn, args);
vm.run(); // if called from within SJS code, recurses into VM::run(); if called from an AS callback, starts up the interpreter
}
}
// fixme: replace for/in with for(i...
private function conformArgumentListToVmFuncArgumentHash(func_args:Array, fn:VmFunc):Object {
var ret:Object = {};
for (var i:String in fn.args) {
var k:String = fn.args[i];
ret[k] = func_args.shift();
}
return ret;
}
private function NOP():void { }
//stack manipulation
private function DUP() :void{ var p:* = opop(); opush(p); opush(p); }
private function DROP() :void{ opop(); }
private function CLEARTOMARK() :void{ yanktomark(); }
private function SWAP() :void{ var a:* = opop(); var b : * = opop(); opush(a); opush(b); }
private function INDEX() :void{ var index :*= opop(); opush(os[index]); }
//values
private function LIT():void{ var v:* = next_word(); opush(v); }
private function VAL():void{ opush(findVar(next_word())); }
//arithmetic
private function ADD():void{ var o:Array = bin_ops(); opush(o[0] + o[1]); }
private function SUB():void{ var o:Array = bin_ops(); opush(o[0] - o[1]);}
private function MUL():void{ var o:Array = bin_ops(); opush(o[0] * o[1]); }
private function DIV():void{ var o:Array = bin_ops(); opush(o[0] / o[1]); }
private function MOD():void{ var modulus:Number = numpop(); opush(numpop() % modulus); }
private function NEG():void{ opush(-opop()); }
//relational
private function EQL():void{ opush(opop() == opop()); }
private function GT() :void{ var o:Array = bin_ops(); opush(o[0] > o[1]); }
private function LT() :void{ var o:Array = bin_ops(); opush(o[0] < o[1]); }
private function GTE():void{ var o:Array = bin_ops(); opush(o[0] >= o[1]); }
private function LTE():void{ var o:Array = bin_ops(); opush(o[0] <= o[1]); }
//short-circuit logic operators -- for a() && b(), don't evaluate b() if a is falsy
// for a() || b(), don't evaluate b if a is truthy
private function _short_circuit_if(value:Boolean):void {
var right:*= opop();
var left:*= opop();
if(!!left == value) {
opush(left);
} else {
cpush(right, {});
// Creates a callframe/scope.
// "a && v = 3" will set v in global scope if not defined in the enclosing scope.
}
}
private function AND():void{ _short_circuit_if(false); }
private function OR():void { _short_circuit_if(true); }
private function NOT():void{ opush(!opop()); }
//structures
private function MARK():void { pushmark(); }
private function ARRAY():void { opush(yanktomark()); }
private function HASH():void {
var i:int, dict:Object = {}, a:Array = yanktomark();
for(i=0; i < a.length; i+=2) {
dict[a[i]] = a[i+1];
}
opush(dict);
}
//flow control
private function JUMP():void{
current_call.pc += next_word();
}
private function JUMPFALSE():void{
var prevpc:int = current_call.pc;
var offset:int = next_word();
if(!opop()) {
current_call.pc += offset;
}
}
//functions
private function CLOSURE():void{
var closure:Function;
log(Inspector.inspect(os));
var body:Array= opop();
var args:Array= opop();
var name:String = opop();
// used to wrap vm functions in AS3 functions here
var vmf:VmFunc = new VmFunc(name, args, body, call_stack[0]);
set_var(name, vmf);
opush(vmf);
}
// TODO -- supply a "this" context for scripted functions?
// FIXME -- How to distinguish between functions returning nothing and functions
// returning undefined? For now, we don't. (function.length is the js way)
// we allow both wrapped and unwrapped functions because they're both useful:
// wrapped functions for passing to AS3 as e.g. event listeners which retain
// a reference to this VM in their closures;
// unwrapped functions so we can run code from another VM in our own context
private function CALL():void { // (closure args_array -- return_value
var func_args:* = opop();
var fn:* = opop();
var rslt:*;
//trace('VM.CALL', (fn is Function) ? 'native' : fn, 'args:', func_args);
if(fn is Function) {
rslt = fn.apply(NaN, func_args); // NaN is flash's default; presumably it means "don't rebind this"
if(rslt !== undefined) opush(rslt);
} else if(fn is VmFunc) {
fcall(fn, func_args);
} else {
trace('* * * * * VM.CALL tried to call nonfunction value "' + fn + '": ' + typeof(fn) + ' * * * * * *');
}
}
private function RETURN():void{
log('return');
cpop();
}
// getting and setting values
private function GET ():void {
var key:String = opop();
opush(findVar(key));
}
// v k PUT
private function PUT():void{ // (value key -- value )
var key:String = opop();
var value:* = opop();
log('PUT', value, key);
set_var(key, value);
// opush(value);
}
private function PUTLOCAL():void{ // (value key -- value )
// TODO: figure out scopes in parser/codegen
// or just generate PUTLOCAL anywhere you see "var x;" (gets null or undefined) or "var x = value":
var key:String = opop();
var value:* = opop();
log('PUTLOCAL', value, key);
call_stack[0].vars[key] = value;
// opush(value);
}
// value object key PUTINDEX
private function PUTINDEX():void{ // ( value object key -- value )
var key :* = opop();
var object:* = opop();
var value :* = opop();
object[key] = value;
// opush(value);
}
private function GETINDEX():void{ // aka "dot" (o k -- o[k])
var k:* = opop();
var o:* = opop();
// trace('GETINDEX', o, k);
opush(o[k]);
}
// LIT m LOCAL -- declares m as a var in current scope
private function LOCAL():void {
var key:String = opop();
current_call.vars[key] = undefined;
}
// NEW ( constructor [args] -- instance )
private function NATIVE_NEW():void {
var args:Array = opop();
var classname:String = opop();
var klass:Class = findVar(classname);
var instance:*;
log('++ new ', classname, '(' + args.join(', ') + ') //', klass + ': ' + getQualifiedClassName(klass));
switch(args.length) {
case 0: instance = new klass(); break;
case 1: instance = new klass(args[0]); break;
case 2: instance = new klass(args[0], args[1]); break;
case 3: instance = new klass(args[0], args[1], args[2]); break;
case 4: instance = new klass(args[0], args[1], args[2], args[3]); break;
case 5: instance = new klass(args[0], args[1], args[2], args[3], args[4]); break;
case 6: instance = new klass(args[0], args[1], args[2], args[3], args[4], args[5]); break;
case 7: instance = new klass(args[0], args[1], args[2], args[3], args[4], args[5], args[6]); break;
case 8: instance = new klass(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]); break;
case 9: instance = new klass(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]); break;
default: throw "NATIVE_NEW was given too many arguments: " + args.length;
}
opush(instance);
}
private function _resumeFromPromise(...promiseFulfillArgs) : void {
log('_resumeFromPromise', promiseFulfillArgs);
// convert all cases to 1-arg.
// 0: null
// 1: pass through
// N: pass as an array
if(promiseFulfillArgs.length == 0) {
opush(null);
} else if(promiseFulfillArgs.length == 1) {
opush(promiseFulfillArgs[0]);
} else {
opush(promiseFulfillArgs);
}
run();
}
private function AWAIT():void {
var p:Promise = opop();
halt();
p.onFulfill(_resumeFromPromise);
}
} // VM
} // package