UNPKG

ifvms

Version:

The Interactive Fiction Virtual Machines Suite - in Javascript

473 lines (407 loc) 9.98 kB
/* Abstract syntax trees for IF VMs ================================ Copyright (c) 2017 The ifvms.js team BSD licenced http://github.com/curiousdannii/ifvms.js */ 'use strict'; /* All AST nodes must use these functions, even constants (An exception is made for branch addresses and text literals which remain as primitives) toString() functions are used to generate JIT code Aside from Variable is currently generic and could be used for Glulx too TODO: Use strict mode for new Function()? When we can run through a whole game, test whether using common_func is faster (if its slower then not worth the file size saving) Can we eliminate the Operand class? Subclass Operand/Variable from Number? Replace calls to args() with arguments.join()? */ var utils = require( '../common/utils.js' ), Class = utils.Class, U2S = utils.U2S16, //S2U = utils.S2U16; // Generic/constant operand // Value is a constant Operand = Class.subClass({ init: function( engine, value ) { this.e = engine; this.v = value; }, toString: function() { return this.v; }, // Convert an Operand into a signed operand U2S: function() { return U2S( this.v ); }, }), // Variable operand // Value is the variable number // TODO: unrolling is needed -> retain immediate returns if optimisations are disabled Variable = Operand.subClass({ // Get a value toString: function() { var variable = this.v; // Indirect if ( this.indirect ) { return 'e.indirect(' + variable + ')'; } // Stack if ( variable === 0 ) { // If we've been passed a value we're setting a variable return 's[--e.sp]'; } // Locals if ( --variable < 15 ) { return 'l[' + variable + ']'; } // Globals return 'e.m.getUint16(' + ( this.e.globals + ( variable - 15 ) * 2 ) + ')'; }, // Store a value store: function( value ) { var variable = this.v; // Indirect variable if ( this.indirect ) { return 'e.indirect(' + variable + ',' + value + ')'; } // BrancherStorers need the value if ( this.returnval ) { return 'e.variable(' + variable + ',' + value + ')'; } // Stack if ( variable === 0 ) { // If we've been passed a value we're setting a variable return 't=' + value + ';s[e.sp++]=t'; } // Locals if ( --variable < 15 ) { return 'l[' + variable + ']=' + value; } // Globals return 'e.ram.setUint16(' + ( this.e.globals + ( variable - 15 ) * 2 ) + ',' + value + ')'; }, // Convert an Operand into a signed operand U2S: function() { return 'e.U2S(' + this + ')'; }, }), // Generic opcode // .func() must be set, which returns what .write() will actually return; it is passed the operands as its arguments Opcode = Class.subClass({ init: function( engine, context, code, pc, next, operands ) { this.e = engine; this.context = context; this.code = code; this.pc = pc; this.labels = [ this.pc + '/' + this.code ]; this.next = next; this.operands = operands; // Post-init function (so that they don't all have to call _super) if ( this.post ) { this.post(); } }, // Write out the opcode, passing .operands to .func(), with a JS comment of the pc/opcode toString: function() { return this.label() + ( this.func ? this.func.apply( this, this.operands ) : '' ); }, // Return a string of the operands separated by commas args: function( joiner ) { return this.operands.join( joiner ); }, // Generate a comment of the pc and code, possibly for more than one opcode label: function() { return '/* ' + this.labels.join() + ' */ '; }, }), // Stopping opcodes Stopper = Opcode.subClass({ stopper: 1, }), // Pausing opcodes (ie, set the pc at the end of the context) Pauser = Stopper.subClass({ post: function() { this.origfunc = this.func; this.func = this.newfunc; }, newfunc: function() { return 'e.stop=1;e.pc=' + this.next + ';' + this.origfunc.apply( this, arguments ); }, }), PauserStorer = Pauser.subClass({ storer: 1, post: function() { this.storer = this.operands.pop(); this.origfunc = this.func; this.func = this.newfunc; }, }), // Join multiple branchers together with varying logic conditions BrancherLogic = Class.subClass({ init: function( ops, code ) { this.ops = ops || []; this.code = code || '||'; }, toString: function() { var i = 0, ops = [], op; while ( i < this.ops.length ) { op = this.ops[i++]; // Accept either Opcodes or further BrancherLogics ops.push( op.func ? ( op.iftrue ? '' : '!(' ) + op.func.apply( op, op.operands ) + ( op.iftrue ? '' : ')' ) : op ); } return ( this.invert ? '(!(' : '(' ) + ops.join( this.code ) + ( this.invert ? '))' : ')' ); }, }), // Branching opcodes Brancher = Opcode.subClass({ // Flag for the disassembler brancher: 1, keyword: 'if', // Process the branch result now post: function() { var result, prev, // Calculate the offset brancher = this.operands.pop(), offset = brancher[1]; this.iftrue = brancher[0]; // Process the offset if ( offset === 0 || offset === 1 ) { result = 'e.ret(' + offset + ')'; } else { offset += this.next - 2; // Add this target to this context's list this.context.targets.push( offset ); result = 'e.pc=' + offset; } this.result = result + ';return'; this.offset = offset; this.cond = new BrancherLogic( [this] ); // TODO: re-enable /*if ( this.e.options.debug ) { // Stop if we must if ( debugflags.noidioms ) { return; } }*/ // Compare with previous statement if ( this.context.ops.length ) { prev = this.context.ops.pop(); // As long as no other opcodes have an offset property we can skip the instanceof check if ( /* prev instanceof Brancher && */ prev.offset === offset ) { // Goes to same offset so reuse the Brancher arrays this.cond.ops.unshift( prev.cond ); this.labels = prev.labels; this.labels.push( this.pc + '/' + this.code ); } else { this.context.ops.push( prev ); } } }, // Write out the brancher toString: function() { var result = this.result; // Account for Contexts if ( result instanceof Context ) { // Update the context to be a child of this context if ( this.e.options.debug ) { result.context = this.context; } result = result + ( result.stopper ? '; return' : '' ); // Extra line breaks for multi-op results if ( this.result.ops.length > 1 ) { result = '\n' + result + '\n'; if ( this.e.options.debug ) { result += this.context.spacer; } } } // Print out a label for all included branches and the branch itself return this.label() + this.keyword + this.cond + ' {' + result + '}'; }, }), // Brancher + Storer BrancherStorer = Brancher.subClass({ storer: 1, // Set aside the storer operand post: function() { BrancherStorer.super.post.call( this ); this.storer = this.operands.pop(); this.storer.returnval = 1; // Replace the func this.origfunc = this.func; this.func = this.newfunc; }, newfunc: function() { return this.storer.store( this.origfunc.apply( this, arguments ) ); }, }), // Storing opcodes Storer = Opcode.subClass({ // Flag for the disassembler storer: 1, // Set aside the storer operand post: function() { this.storer = this.operands.pop(); }, // Write out the opcode, passing it to the storer (if there still is one) toString: function() { var data = Storer.super.toString.call( this ); // If we still have a storer operand, use it // Otherwise (if it's been removed due to optimisations) just return func() return this.storer ? this.storer.store( data ) : data; }, }), // Routine calling opcodes Caller = Stopper.subClass({ // Fake a result variable result: { v: -1 }, // Write out the opcode toString: function() { // TODO: Debug: include label if possible return this.label() + 'e.call(' + this.operands.shift() + ',' + this.result.v + ',' + this.next + ',[' + this.args() + '])'; }, }), // Routine calling opcodes, storing the result CallerStorer = Caller.subClass({ // Flag for the disassembler storer: 1, post: function() { // We can't let the storer be optimised away here this.result = this.operands.pop(); }, }), // A generic context (a routine, loop body etc) Context = Class.subClass({ init: function( engine, pc ) { this.e = engine; this.pc = pc; this.pre = []; this.ops = []; this.post = []; this.targets = []; // Branch targets if ( engine.options.debug ) { this.spacer = ''; } }, toString: function() { if ( this.e.options.debug ) { // Indent the spacer further if needed if ( this.context ) { this.spacer = this.context.spacer + ' '; } // DEBUG: Pretty print! return this.pre.join( '' ) + ( this.ops.length > 1 ? this.spacer : '' ) + this.ops.join( ';\n' + this.spacer ) + this.post.join( '' ); } else { // Return the code return this.pre.join( '' ) + this.ops.join( ';' ) + this.post.join( '' ); } }, }), // A routine body RoutineContext = Context.subClass({ toString: function() { // TODO: Debug: If we have routine names, find this one's name // Add in some extra vars and return this.pre.unshift( 'var l=e.l,s=e.s,t=0;\n' ); return RoutineContext.super.toString.call( this ); }, }); // Opcode builder // Easily build a new opcode from a class function opcode_builder( Class, func, flags ) { flags = flags || {}; if ( func ) { /*if ( func.pop ) { flags.str = func; flags.func = common_func; } else {*/ flags.func = func; //} } return Class.subClass( flags ); } module.exports = { Operand: Operand, Variable: Variable, Opcode: Opcode, Stopper: Stopper, Pauser: Pauser, PauserStorer: PauserStorer, BrancherLogic: BrancherLogic, Brancher: Brancher, BrancherStorer: BrancherStorer, Storer: Storer, Caller: Caller, CallerStorer: CallerStorer, Context: Context, RoutineContext: RoutineContext, opcode_builder: opcode_builder, };