├── .gitignore ├── bin └── gbasm ├── lib ├── data │ ├── Constant.js │ ├── Variable.js │ ├── Label.js │ ├── DataBlock.js │ ├── Binary.js │ ├── Instruction.js │ ├── Macro.js │ └── Section.js ├── Errors.js ├── linker │ ├── Macro.js │ ├── Optimizer.js │ ├── FileLinker.js │ └── Linker.js ├── parser │ ├── TokenStream.js │ ├── Expression.js │ └── Lexer.js ├── SourceFile.js ├── Compiler.js └── Generator.js ├── .eslintrc.js ├── package.json ├── LICENSE ├── .jshintrc ├── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /bin/gbasm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../index'); 3 | 4 | -------------------------------------------------------------------------------- /lib/data/Constant.js: -------------------------------------------------------------------------------- 1 | // Compile Time Constants ----------------------------------------------------- 2 | // ---------------------------------------------------------------------------- 3 | function Constant(file, name, value, index) { 4 | this.file = file; 5 | this.name = name; 6 | this.value = value; 7 | this.index = index; 8 | this.references = 0; 9 | } 10 | 11 | 12 | // Exports -------------------------------------------------------------------- 13 | module.exports = Constant; 14 | 15 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "eslint:recommended", 3 | "env": { 4 | "es6": true, 5 | "node": true 6 | }, 7 | "rules": { 8 | "indent": ["error", 4, { 9 | "SwitchCase": 1 10 | }], 11 | "quotes": ["error", "single"], 12 | "no-var": "error", 13 | "no-else-return": "error", 14 | "object-shorthand": ["error", "always"], 15 | "no-case-declarations": "off", 16 | "prefer-arrow-callback": "error", 17 | "prefer-template": "error", 18 | "prefer-const": "error" 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /lib/data/Variable.js: -------------------------------------------------------------------------------- 1 | // Variable Definitions ------------------------------------------------------- 2 | // ---------------------------------------------------------------------------- 3 | function Variable(file, name, section, size, index) { 4 | 5 | this.file = file; 6 | this.name = name; 7 | this.offset = -1; 8 | this.label = null; 9 | this.section = section; 10 | this.size = size; 11 | this.index = index; 12 | this.references = 0; 13 | 14 | this.section.add(this); 15 | 16 | } 17 | 18 | 19 | // Methods -------------------------------------------------------------------- 20 | Variable.prototype = { 21 | 22 | toJSON() { 23 | return { 24 | type: 'Variable', 25 | name: this.name, 26 | offset: this.offset, 27 | size: this.size, 28 | references: this.references 29 | }; 30 | } 31 | 32 | }; 33 | 34 | 35 | // Exports -------------------------------------------------------------------- 36 | module.exports = Variable; 37 | 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gbasm", 3 | "version": "0.1.00", 4 | "description": "A GameBoy Assembler.", 5 | "keywords": [ 6 | "gameboy", 7 | "assembler", 8 | "z80", 9 | "intel", 10 | "8080", 11 | "assembly" 12 | ], 13 | "main": "index.js", 14 | "files": [ 15 | "bin", 16 | "lib", 17 | "index.js", 18 | "package.json", 19 | "README.md", 20 | "LICENSE" 21 | ], 22 | "bin": "./bin/gbasm", 23 | "repository": { 24 | "type": "git", 25 | "url": "git://github.com/BonsaiDen/gbasm.git" 26 | }, 27 | "author": "Ivo Wetzel ", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/BonsaiDen/gbasm/issues" 31 | }, 32 | "dependencies": { 33 | "colors": "~0.6.2" 34 | }, 35 | "devDependencies": { 36 | "eslint-config-standard": "^10.2.1", 37 | "eslint-plugin-import": "^2.7.0", 38 | "eslint-plugin-node": "^5.1.1", 39 | "eslint-plugin-promise": "^3.5.0", 40 | "eslint-plugin-standard": "^3.0.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Ivo Wetzel. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /lib/data/Label.js: -------------------------------------------------------------------------------- 1 | // Label Definitions ---------------------------------------------------------- 2 | // ---------------------------------------------------------------------------- 3 | function Label(file, name, section, parent, index) { 4 | 5 | this.file = file; 6 | this.name = name; 7 | this.offset = -1; 8 | this.label = null; 9 | this.section = section; 10 | this.parent = parent; 11 | this.children = []; 12 | this.isLocal = !!parent; 13 | this.index = index; 14 | this.references = 0; 15 | 16 | if (this.parent) { 17 | this.parent.children.push(this); 18 | } 19 | 20 | this.section.add(this); 21 | 22 | } 23 | 24 | 25 | // Methods -------------------------------------------------------------------- 26 | Label.prototype = { 27 | 28 | toJSON() { 29 | return { 30 | type: 'Label', 31 | name: this.name, 32 | offset: this.offset, 33 | isLocal: this.isLocal, 34 | references: this.references 35 | }; 36 | } 37 | 38 | }; 39 | 40 | 41 | // Exports -------------------------------------------------------------------- 42 | module.exports = Label; 43 | 44 | -------------------------------------------------------------------------------- /lib/data/DataBlock.js: -------------------------------------------------------------------------------- 1 | // DataBlocks ----------------------------------------------------------------- 2 | // ---------------------------------------------------------------------------- 3 | function DataBlock(values, section, isByte, size, index) { 4 | 5 | this.values = values; 6 | this.resolvedValues = []; 7 | this.offset = -1; 8 | this.label = null; 9 | this.section = section; 10 | this.bits = isByte ? 8 : 16; 11 | this.isFixedSize = size !== null; 12 | this.index = index; 13 | 14 | if (this.isFixedSize) { 15 | this.size = size; 16 | 17 | } else { 18 | this.size = values.length * (isByte ? 1 : 2); 19 | } 20 | 21 | this.section.add(this); 22 | 23 | } 24 | 25 | 26 | // Methods -------------------------------------------------------------------- 27 | DataBlock.prototype = { 28 | 29 | toJSON() { 30 | return { 31 | type: 'DataBlock', 32 | offset: this.offset, 33 | size: this.size, 34 | values: this.resolvedValues 35 | }; 36 | 37 | } 38 | 39 | }; 40 | 41 | 42 | // Exports -------------------------------------------------------------------- 43 | module.exports = DataBlock; 44 | 45 | -------------------------------------------------------------------------------- /lib/data/Binary.js: -------------------------------------------------------------------------------- 1 | // Dependencies --------------------------------------------------------------- 2 | // ---------------------------------------------------------------------------- 3 | const fs = require('fs'), 4 | path = require('path'), 5 | includeError = require('../Errors').IncludeError; 6 | 7 | 8 | // Binary Includes ------------------------------------------------------------ 9 | // ---------------------------------------------------------------------------- 10 | function Binary(file, src, section, index) { 11 | 12 | this.file = file; 13 | 14 | if (src.charCodeAt(0) === 47) { 15 | this.src = path.join(this.file.compiler.base, src.substring(1)); 16 | 17 | } else { 18 | this.src = path.join(path.dirname(this.file.path), src); 19 | } 20 | 21 | this.section = section; 22 | this.offset = -1; 23 | this.index = index; 24 | 25 | try { 26 | this.size = fs.statSync(this.src).size; 27 | 28 | } catch(err) { 29 | includeError(this.file, err, 'include binary data', this.src, index); 30 | } 31 | 32 | this.section.add(this); 33 | 34 | } 35 | 36 | 37 | Binary.prototype = { 38 | 39 | getBuffer() { 40 | return fs.readFileSync(this.src); 41 | }, 42 | 43 | toJSON() { 44 | return { 45 | type: 'Binary', 46 | src: this.src, 47 | offset: this.offset, 48 | size: this.size 49 | }; 50 | } 51 | 52 | }; 53 | 54 | 55 | // Exports -------------------------------------------------------------------- 56 | module.exports = Binary; 57 | 58 | -------------------------------------------------------------------------------- /lib/data/Instruction.js: -------------------------------------------------------------------------------- 1 | // CPU Instructions ----------------------------------------------------------- 2 | // ---------------------------------------------------------------------------- 3 | function Instruction(mnemonic, section, cycles, code, arg, isByte, isSigned, isBit, index) { 4 | 5 | this.mnemonic = mnemonic; 6 | this.section = section; 7 | this.offset = -1; 8 | this.label = null; 9 | this.size = code.length + (arg ? (isByte ? 1 : 2) : 0); 10 | this.cycles = cycles; 11 | 12 | this.raw = code; 13 | this.arg = arg; 14 | this.resolvedArg = null; 15 | 16 | this.bits = isByte ? 8 : 16; 17 | this.isSigned = !!isSigned; 18 | this.isBit = !!isBit; 19 | this.index = index; 20 | 21 | this.section.add(this); 22 | 23 | } 24 | 25 | 26 | // Methods -------------------------------------------------------------------- 27 | Instruction.prototype = { 28 | 29 | rewrite(mnemonic, cycles, code, arg, isByte, isSigned, isBit) { 30 | 31 | this.mnemonic = mnemonic; 32 | this.size = code.length + (arg ? (isByte ? 1 : 2) : 0); 33 | this.cycles = cycles; 34 | this.raw = code; 35 | this.arg = arg; 36 | this.resolvedArg = null; 37 | this.bits = isByte ? 8 : 16; 38 | this.isSigned = !!isSigned; 39 | this.isBit = !!isBit; 40 | 41 | }, 42 | 43 | remove() { 44 | this.section.removeEntry(this); 45 | }, 46 | 47 | toJSON() { 48 | return { 49 | type: 'Instruction', 50 | mnemonic: this.mnemonic, 51 | offset: this.offset, 52 | size: this.size, 53 | cycles: this.cycles, 54 | arg: this.resolvedArg 55 | }; 56 | } 57 | 58 | }; 59 | 60 | 61 | // Exports -------------------------------------------------------------------- 62 | module.exports = Instruction; 63 | 64 | -------------------------------------------------------------------------------- /lib/Errors.js: -------------------------------------------------------------------------------- 1 | // Error Definitions ---------------------------------------------------------- 2 | // ---------------------------------------------------------------------------- 3 | module.exports = { 4 | 5 | ReferenceError(file, msg, index) { 6 | file.error({ 7 | index, 8 | name: 'ReferenceError', 9 | message: msg 10 | }); 11 | }, 12 | 13 | AddressError(file, msg, index) { 14 | file.error({ 15 | index, 16 | name: 'AddressError', 17 | message: msg 18 | }); 19 | }, 20 | 21 | ArgumentError(file, msg, index) { 22 | file.error({ 23 | index, 24 | name: 'ArgumentError', 25 | message: msg 26 | }); 27 | }, 28 | 29 | MacroError(file, msg, index, macro) { 30 | 31 | if (macro) { 32 | msg += ` ( MACRO declared in ${ macro.file.getPath(macro.index).yellow } )`; 33 | } 34 | 35 | file.error({ 36 | index, 37 | name: 'MacroError', 38 | message: msg 39 | }); 40 | 41 | }, 42 | 43 | ExpressionError(file, msg, index) { 44 | file.error({ 45 | index, 46 | name: 'ExpressionError', 47 | message: msg 48 | }); 49 | }, 50 | 51 | DeclarationError(file, msg, index, existing) { 52 | 53 | if (existing) { 54 | msg = `Redeclaration of ${ msg}`; 55 | msg += ` ( first declared in ${ existing.file.getPath(existing.index).yellow } )`; 56 | 57 | } else { 58 | msg = `Declaration of ${ msg}`; 59 | } 60 | 61 | file.error({ 62 | index, 63 | name: 'DeclarationError', 64 | message: msg 65 | }); 66 | 67 | }, 68 | 69 | IncludeError(file, err, msg, filePath, index) { 70 | 71 | msg = `Unable to ${ msg } "${ filePath }", `; 72 | 73 | if (err.errno === 34) { 74 | msg += 'file was not found'; 75 | 76 | } else { 77 | msg += 'file could not be read'; 78 | } 79 | 80 | file.error({ 81 | index, 82 | name: 'IncludeError', 83 | message: msg 84 | }); 85 | 86 | }, 87 | 88 | ParseError(file, msg, expected, index) { 89 | 90 | msg = `Unexpected ${ msg}`; 91 | 92 | if (expected) { 93 | msg += `, expected ${ expected } instead`; 94 | } 95 | 96 | file.error({ 97 | index, 98 | name: 'ParseError', 99 | message: msg 100 | }); 101 | 102 | }, 103 | 104 | SectionError(file, msg, index) { 105 | 106 | msg = `${msg}`; 107 | 108 | file.error({ 109 | index, 110 | name: 'SectionError', 111 | message: msg 112 | }); 113 | 114 | } 115 | 116 | 117 | }; 118 | 119 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | 3 | // JShint 4 | "passfail" : false, // if the scan should stop on first error 5 | 6 | // Tolerate features 7 | "eqnull" : false, // if == null comparisons should be tolerated 8 | "sub" : true, // if all forms of subscript notation are tolerated 9 | "asi" : false, // if automatic semicolon insertion should be tolerated 10 | "scripturl" : false, // if script-targeted URLs should be tolerated 11 | "shadow" : false, // if variable shadowing should be tolerated 12 | "smarttabs" : false, // if smarttabs should be tolerated (http://www.emacswiki.org/emacs/SmartTabs) 13 | "supernew" : false, // if `new function () { ... };` and `new Object;` should be tolerated 14 | 15 | // Required 16 | "strict" : false, // require the "use strict"; pragma 17 | "curly" : true, // if curly braces around all blocks should be required 18 | "eqeqeq" : true, // if === should be required 19 | 20 | // Define Environments 21 | "node" : true, // if the Node.js environment globals should be predefined 22 | "nonstandard" : false, // if non-standard (but widely adopted) globals should be predefined 23 | "couch" : false, // if CouchDB globals should be predefined 24 | "devel" : false, // if logging globals should be predefined (console,// alert, etc.) 25 | "browser" : false, // if the standard browser globals should be predefined 26 | "dojo" : false, // if Dojo Toolkit globals should be predefined 27 | "rhino" : false, // if the Rhino environment globals should be predefined 28 | "jquery" : false, // if jQuery globals should be predefined 29 | "mootools" : false, // if MooTools globals should be predefined 30 | "prototypejs" : false, // if Prototype and Scriptaculous globals should be predefined 31 | "wsh" : false, // if the Windows Scripting Host environment globals should be predefined 32 | 33 | // Allow features 34 | "expr" : true, // if ExpressionStatement should be allowed as Programs 35 | "loopfunc" : true, // if functions should be allowed to be defined within loops 36 | "onecase" : true, // if one case switch statements should be allowed 37 | "boss" : false, // if advanced usage of assignments should be allowed 38 | "debug" : false, // if debugger statements should be allowed 39 | "es5" : false, // if ES5 syntax should be allowed 40 | "esnext" : false, // if es.next specific syntax should be allowed 41 | "evil" : false, // if eval should be allowed 42 | "globalstrict": false, // if global "use strict"; should be allowed (also // enables 'strict') 43 | "proto" : false, // if the `__proto__` property should be allowed 44 | "iterator" : false, // if the `__iterator__` property should be allowed 45 | 46 | // Disallow certain features 47 | "nonew" : false, // if using `new` for side-effects should be disallowed 48 | "latedef" : "nofunc", // if the use before definition should not be tolerated 49 | "laxbreak" : true, // if line breaks should not be checked 50 | "regexp" : true, // if the . should not be allowed in regexp literals 51 | "noarg" : true, // if arguments.caller and arguments.callee should be disallowed 52 | "noempty" : true, // if empty blocks should be disallowed 53 | "bitwise" : false, // if bitwise operators should not be allowed 54 | "plusplus" : false, // if increment/decrement should not be allowed 55 | 56 | // Functions 57 | "funcscope" : true, // if only function scope should be used for scope tests 58 | "onevar" : false, // if only one var statement per function should be 59 | "unused" : "vars", 60 | "validthis" : true, // if 'this' inside a non-constructor function is valid This is a function scoped option only. 61 | 62 | // Loops 63 | "forin" : false, // if for in statements must filter 64 | 65 | // Names 66 | "newcap" : true, // if constructor names must be capitalized 67 | "nomen" : false, // if names should be checked 68 | 69 | // General 70 | "undef" : true, // if variables should be declared before used 71 | "regexdash" : true, // if unescaped first/last dash (-) inside brackets 72 | "immed" : true, // if immediate invocations must be wrapped in parens 73 | 74 | // Whitespace and Syntax 75 | "trailing" : true, // if trailing whitespace rules apply 76 | "multistr" : false, // allow multiline strings 77 | "white" : false, // if strict whitespace rules apply 78 | "lastsemic" : false // if semicolons may be ommitted for the trailing statements inside of a one-line blocks. 79 | } 80 | 81 | 82 | -------------------------------------------------------------------------------- /lib/linker/Macro.js: -------------------------------------------------------------------------------- 1 | // Macro Logic Impelemations -------------------------------------------------- 2 | // ---------------------------------------------------------------------------- 3 | function Macro(name, func, args) { 4 | this.name = name; 5 | this.func = func; 6 | this.args = args; 7 | } 8 | 9 | Macro.Argument = function(name, type) { 10 | this.name = name; 11 | this.type = type; 12 | }; 13 | 14 | 15 | // Macro Definitions ---------------------------------------------------------- 16 | // ---------------------------------------------------------------------------- 17 | const Macros = { 18 | 19 | // Constants -------------------------------------------------------------- 20 | PI: Math.PI, 21 | E: Math.E, 22 | LN2: Math.LN2, 23 | LN10: Math.LN10, 24 | LOG2E: Math.LOG2E, 25 | LOG10E: Math.LOG10E, 26 | 27 | 28 | // String ----------------------------------------------------------------- 29 | STRUPR: new Macro('STRUPR', ((str) => { 30 | return str.toUpperCase(); 31 | 32 | }), [new Macro.Argument('Text', 'string')]), 33 | 34 | STRLWR: new Macro('STRLWR', ((str) => { 35 | return str.toLowerCase(); 36 | 37 | }), [new Macro.Argument('Text', 'string')]), 38 | 39 | STRSUB: new Macro('STRSUB', ((str, from, length) => { 40 | return str.substr(from, from + length); 41 | 42 | }), [ 43 | new Macro.Argument('Text', 'string'), 44 | new Macro.Argument('Index', 'number'), 45 | new Macro.Argument('Length', 'number') 46 | ]), 47 | 48 | STRIN: new Macro('STRIN', ((str, key) => { 49 | return str.indexOf(key) !== -1 ? 1 : 0; 50 | 51 | }), [ 52 | new Macro.Argument('Text', 'string'), 53 | new Macro.Argument('Key', 'string') 54 | ]), 55 | 56 | STRPADR: new Macro('STRPADR', ((str, chr, len) => { 57 | return str + new Array(len - str.length + 1).join(chr); 58 | 59 | }), [ 60 | new Macro.Argument('Text', 'string'), 61 | new Macro.Argument('Padding', 'string'), 62 | new Macro.Argument('Length', 'number') 63 | ]), 64 | 65 | STRPADL: new Macro('STRPADL', ((str, chr, len) => { 66 | return new Array(len - str.length + 1).join(chr) + str; 67 | 68 | }), [ 69 | new Macro.Argument('Text', 'string'), 70 | new Macro.Argument('Padding', 'string'), 71 | new Macro.Argument('Length', 'number') 72 | ]), 73 | 74 | 75 | // Math ------------------------------------------------------------------- 76 | SIN: new Macro('SIN', ((value) => { 77 | return Math.sin(value); 78 | 79 | }), [new Macro.Argument('radians', 'number')]), 80 | 81 | COS: new Macro('COS', ((value) => { 82 | return Math.cos(value); 83 | 84 | }), [new Macro.Argument('radians', 'number')]), 85 | 86 | TAN: new Macro('TAN', ((value) => { 87 | return Math.tan(value); 88 | 89 | }), [new Macro.Argument('radians', 'number')]), 90 | 91 | ASIN: new Macro('ASIN', ((value) => { 92 | return Math.asin(value); 93 | 94 | }), [new Macro.Argument('radians', 'number')]), 95 | 96 | ACOS: new Macro('ACOS', ((value) => { 97 | return Math.acos(value); 98 | 99 | }), [new Macro.Argument('radians', 'number')]), 100 | 101 | ATAN: new Macro('ATAN', ((value) => { 102 | return Math.atan(value); 103 | 104 | }), [new Macro.Argument('radians', 'number')]), 105 | 106 | ATAN2: new Macro('ATAN2', ((y, x) => { 107 | return Math.atan2(y, x); 108 | 109 | }), [ 110 | new Macro.Argument('y', 'number'), 111 | new Macro.Argument('x', 'number') 112 | ]), 113 | 114 | LOG: new Macro('LOG', ((value) => { 115 | return Math.log(value); 116 | 117 | }), [new Macro.Argument('number', 'number')]), 118 | 119 | EXP: new Macro('EXP', ((value) => { 120 | return Math.exp(value); 121 | 122 | }), [new Macro.Argument('number', 'number')]), 123 | 124 | FLOOR: new Macro('FLOOR', ((value) => { 125 | return Math.floor(value); 126 | 127 | }), [new Macro.Argument('number', 'number')]), 128 | 129 | CEIL: new Macro('CEIL', ((value) => { 130 | return Math.ceil(value); 131 | 132 | }), [new Macro.Argument('number', 'number')]), 133 | 134 | ROUND: new Macro('ROUND', ((value) => { 135 | return Math.round(value); 136 | 137 | }), [new Macro.Argument('number', 'number')]), 138 | 139 | SQRT: new Macro('SQRT', ((value) => { 140 | return Math.sqrt(value); 141 | 142 | }), [new Macro.Argument('number', 'number')]), 143 | 144 | MAX: new Macro('MAX', ((a, b) => { 145 | return Math.max(a, b); 146 | 147 | }), [ 148 | new Macro.Argument('a', 'number'), 149 | new Macro.Argument('b', 'number') 150 | ]), 151 | 152 | MIN: new Macro('MIN', ((a, b) => { 153 | return Math.min(a, b); 154 | 155 | }), [ 156 | new Macro.Argument('a', 'number'), 157 | new Macro.Argument('b', 'number') 158 | ]), 159 | 160 | ABS: new Macro('ABS', ((value) => { 161 | return Math.abs(value); 162 | 163 | }), [new Macro.Argument('Number', 'number')]), 164 | 165 | RAND: new Macro('RAND', ((from, to) => { 166 | return from + Math.floor(Math.random() * (to - from)); 167 | 168 | }), [ 169 | new Macro.Argument('from', 'number'), 170 | new Macro.Argument('to', 'number') 171 | ]) 172 | 173 | }; 174 | 175 | 176 | // Helpers -------------------------------------------------------------------- 177 | Macro.isDefined = function(name) { 178 | return Macros.hasOwnProperty(name); 179 | }; 180 | 181 | Macro.get = function(name) { 182 | return Macros[name]; 183 | }; 184 | 185 | 186 | // Exports -------------------------------------------------------------------- 187 | module.exports = Macro; 188 | 189 | -------------------------------------------------------------------------------- /lib/parser/TokenStream.js: -------------------------------------------------------------------------------- 1 | // Dependencies --------------------------------------------------------------- 2 | // ---------------------------------------------------------------------------- 3 | const Errors = require('../Errors'); 4 | 5 | 6 | // Token streaming interface for the Parser ----------------------------------- 7 | // ---------------------------------------------------------------------------- 8 | function TokenStream(file, tokens) { 9 | this.file = file; 10 | this.tokens = tokens; 11 | this.token = this.tokens[0]; 12 | this.last = null; 13 | this.index = 0; 14 | } 15 | 16 | TokenStream.prototype = { 17 | 18 | next() { 19 | this.index++; 20 | this.last = this.token; 21 | if (this.index < this.tokens.length) { 22 | this.token = this.tokens[this.index]; 23 | } 24 | return this.last; 25 | }, 26 | 27 | peak(type) { 28 | return this.token && this.is(type, this.token); 29 | }, 30 | 31 | expect(type) { 32 | 33 | if (!this.token || type === 'EOF') { 34 | this.error('end of input', type); 35 | 36 | } else if (this.is(type, this.token)) { 37 | return this.value(type, this.next()); 38 | 39 | } else { 40 | this.error(this.token.type, type); 41 | } 42 | 43 | }, 44 | 45 | get(type) { 46 | 47 | if (!this.token || type === 'EOF') { 48 | this.error('end of input', type); 49 | 50 | } else if (this.is(type, this.token)) { 51 | return this.value(type, this.next()); 52 | 53 | } else { 54 | this.error(this.token.type, type); 55 | } 56 | 57 | }, 58 | 59 | value(type, token) { 60 | 61 | switch(type) { 62 | case 'ACCUMULATOR': 63 | case 'REGISTER_C': 64 | case 'REGISTER_HL': 65 | case 'REGISTER_HL_INCREMENT': 66 | case 'REGISTER_HL_DECREMENT': 67 | case 'REGISTER_SP': 68 | case 'REGISTER_8BIT': 69 | case 'REGISTER_8BIT_NON_ACC': 70 | case 'REGISTER_DOUBLE': 71 | case 'REGISTER_STACKABLE': 72 | case 'FLAG': 73 | return token.value.toLowerCase(); 74 | 75 | case 'NUMBER_8BIT': 76 | case 'NUMBER_SIGNED_8BIT': 77 | case 'NUMBER_BIT_INDEX': 78 | case 'NUMBER_16BIT': 79 | case 'OFFSET': 80 | case 'STRING': 81 | case 'NAME': 82 | case 'EXPRESSION': 83 | case 'MACRO_ARG': 84 | return token; 85 | 86 | case 'NUMBER': 87 | case 'ZERO_PAGE_LOCATION': 88 | return token.value; 89 | 90 | case 'POWER_OF_TWO': 91 | return token.value; 92 | 93 | default: 94 | return type; 95 | 96 | } 97 | 98 | }, 99 | 100 | is(type, token) { 101 | 102 | const tokenType = token.type; 103 | if (tokenType === 'DIRECTIVE') { 104 | return type === token.value; 105 | 106 | } 107 | return type === tokenType || isTokenOfType(type, tokenType) || isTokenOfValue(type, token.value); 108 | 109 | }, 110 | 111 | error(msg, type) { 112 | 113 | let index = this.token.index; 114 | if (this.token.type === 'NEWLINE') { 115 | index = this.last.index; 116 | } 117 | 118 | Errors.ParseError(this.file, msg, type, index); 119 | 120 | } 121 | 122 | }; 123 | 124 | // Helper --------------------------------------------------------------------- 125 | function isTokenOfType(type, tokenType) { 126 | 127 | switch(type) { 128 | case 'NUMBER_8BIT': 129 | case 'NUMBER_SIGNED_8BIT': 130 | case 'NUMBER_BIT_INDEX': 131 | return tokenType === 'NUMBER' || tokenType === 'EXPRESSION' 132 | || tokenType === 'NAME'; 133 | 134 | case 'NUMBER_16BIT': 135 | return tokenType === 'LABEL_LOCAL_REF' 136 | || tokenType === 'EXPRESSION' 137 | || tokenType === 'NAME' 138 | || tokenType === 'NUMBER'; 139 | 140 | case 'EXPRESSION': 141 | return tokenType === 'EXPRESSION' 142 | || tokenType === 'NAME'; 143 | 144 | case 'POWER_OF_TWO': 145 | return tokenType === 'NUMBER'; 146 | 147 | default: 148 | return false; 149 | 150 | } 151 | 152 | } 153 | 154 | function isTokenOfValue(type, value) { 155 | 156 | value = typeof value === 'string' ? value.toLowerCase() : value; 157 | switch(type) { 158 | case 'ACCUMULATOR': 159 | return value === 'a'; 160 | 161 | case 'REGISTER_C': 162 | return value === 'c'; 163 | 164 | case 'REGISTER_HL': 165 | return value === 'hl'; 166 | 167 | case 'REGISTER_HL_INCREMENT': 168 | return value === 'hli'; 169 | 170 | case 'REGISTER_HL_DECREMENT': 171 | return value === 'hld'; 172 | 173 | case 'REGISTER_8BIT': 174 | return value === 'a' || value === 'b' || value === 'c' 175 | || value === 'd' || value === 'e' || value === 'h' 176 | || value === 'l'; 177 | 178 | case 'REGISTER_8BIT_NON_ACC': 179 | return value === 'b' || value === 'c' 180 | || value === 'd' || value === 'e' || value === 'h' 181 | || value === 'l'; 182 | 183 | case 'REGISTER_DOUBLE': 184 | return value === 'hl' || value === 'de' || value === 'bc'; 185 | 186 | case 'REGISTER_STACKABLE': 187 | return value === 'hl' || value === 'de' 188 | || value === 'bc' || value === 'af'; 189 | 190 | case 'REGISTER_SP': 191 | return value === 'sp'; 192 | 193 | case 'FLAG': 194 | return value === 'c' || value === 'nc' 195 | || value === 'z' || value === 'nz'; 196 | 197 | case 'ZERO_PAGE_LOCATION': 198 | return value === 0x00 || value === 0x08 || value === 0x10 199 | || value === 0x18 || value === 0x20 || value === 0x28 200 | || value === 0x30 || value === 0x38; 201 | 202 | case 'POWER_OF_TWO': 203 | return value === 2 || value === 4 || value === 8 || value === 16 204 | || value === 32 || value === 64 || value === 128; 205 | 206 | default: 207 | return false; 208 | 209 | } 210 | 211 | } 212 | 213 | 214 | // Exports -------------------------------------------------------------------- 215 | module.exports = TokenStream; 216 | 217 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Dependencies --------------------------------------------------------------- 2 | // ---------------------------------------------------------------------------- 3 | const fs = require('fs'), 4 | path = require('path'), 5 | Compiler = require('./lib/Compiler'); 6 | 7 | 8 | // Command Line Interface------------------------------------------------------ 9 | // ---------------------------------------------------------------------------- 10 | const args = process.argv.slice(2), 11 | options = { 12 | outfile: 'game.gb', 13 | optimize: false, 14 | mapfile: null, 15 | symfile: null, 16 | jsonfile: null, 17 | unused: false, 18 | unsafe: false, 19 | silent: false, 20 | version: false, 21 | verbose: false, 22 | info: false, 23 | help: false, 24 | files: [] 25 | }; 26 | 27 | for(let i = 0, l = args.length; i < l; i++) { 28 | const arg = args[i]; 29 | switch(arg) { 30 | case '-o': 31 | case '--outfile': 32 | options.outfile = getString(arg, args, ++i); 33 | break; 34 | 35 | case '-O': 36 | case '--optimize': 37 | options.optimize = true; 38 | break; 39 | 40 | case '-m': 41 | case '--mapfile': 42 | options.mapfile = getString(arg, args, ++i); 43 | break; 44 | 45 | case '-s': 46 | case '--symfile': 47 | options.symfile = getString(arg, args, ++i); 48 | break; 49 | 50 | case '--info': 51 | options.info = getString(arg, args, ++i); 52 | break; 53 | 54 | case '-S': 55 | case '--silent': 56 | options.silent = true; 57 | break; 58 | 59 | case '-j': 60 | case '--jsonfile': 61 | options.jsonfile = getString(arg, args, ++i); 62 | break; 63 | 64 | case '--unused': 65 | options.unused = true; 66 | break; 67 | 68 | case '--unsafe': 69 | options.unsafe = true; 70 | break; 71 | 72 | case '--version': 73 | options.version = true; 74 | break; 75 | 76 | case '-v': 77 | case '--verbose': 78 | options.verbose = true; 79 | break; 80 | 81 | case '-d': 82 | case '--debug': 83 | options.debug = true; 84 | break; 85 | 86 | case '--help': 87 | options.help = true; 88 | break; 89 | 90 | default: 91 | if (arg.substring(0, 1) === '-') { 92 | error(`Unknown option: ${arg}`); 93 | 94 | } else { 95 | options.files.push(arg); 96 | } 97 | break; 98 | 99 | } 100 | } 101 | 102 | // Version Information 103 | if (options.version) { 104 | 105 | const p = path.join(__dirname, 'package.json'), 106 | version = JSON.parse(fs.readFileSync(p).toString()).version; 107 | 108 | process.stdout.write(`v${version}\n`); 109 | 110 | // Display Help 111 | } else if (options.help) { 112 | usage(); 113 | 114 | // Source String Information 115 | } else if (options.info !== false) { 116 | process.stdout.write(Compiler.infoFromSource(options.info)); 117 | 118 | // Compile Files 119 | } else if (options.files.length) { 120 | 121 | // Compile 122 | const c = new Compiler(options.silent, options.verbose, options.debug); 123 | c.compile(options.files, !options.optimize); 124 | 125 | // Optimize 126 | if (options.optimize) { 127 | c.optimize(options.unsafe); 128 | } 129 | 130 | // Report unused labels and variables 131 | if (options.unused) { 132 | c.unused(); 133 | } 134 | 135 | // Generate ROM image 136 | if (options.outfile === 'stdout') { 137 | process.stdout.write(c.generate()); 138 | 139 | } else { 140 | fs.writeFileSync(options.outfile, c.generate()); 141 | } 142 | 143 | // Generate symbol file 144 | if (options.symfile) { 145 | if (options.symfile === 'stdout') { 146 | process.stdout.write(c.symbols(true)); 147 | 148 | } else { 149 | fs.writeFileSync(options.symfile, c.symbols(false)); 150 | } 151 | } 152 | 153 | // Generate mapping file 154 | if (options.mapfile) { 155 | if (options.mapfile === 'stdout') { 156 | process.stdout.write(c.mapping(true)); 157 | 158 | } else { 159 | fs.writeFileSync(options.mapfile, c.mapping(false)); 160 | } 161 | } 162 | 163 | // Generate json dump file 164 | if (options.jsonfile) { 165 | if (options.jsonfile === 'stdout') { 166 | process.stdout.write(c.json()); 167 | 168 | } else { 169 | fs.writeFileSync(options.jsonfile, c.json()); 170 | } 171 | } 172 | 173 | // Usage 174 | } else { 175 | usage(); 176 | } 177 | 178 | 179 | // Helpers -------------------------------------------------------------------- 180 | function getString(name, args, index) { 181 | const s = args[index]; 182 | if (s === undefined || s.substring(0, 1) === '-') { 183 | error(`Expected string argument for ${name}`); 184 | 185 | } else { 186 | return s; 187 | } 188 | } 189 | 190 | function usage() { 191 | process.stdout.write([ 192 | 'Usage: gbasm [options] [sources]', 193 | '', 194 | ' --outfile, -o : The name of the output rom file (default: game.gb)', 195 | ' --optimize, -O: Enable instruction optimizations', 196 | ' --unsafe: Turn on unsafe optimizations', 197 | ' --mapfile, -m : Generates a ASCII overview of the mapped ROM space', 198 | ' --symfile, -s : Generates a symbol map compatible with debuggers', 199 | ' --jsonfile, -j : Generates a JSON data dump of all sections with their data, labels, instructions etc.', 200 | ' --silent, -S: Surpresses all logging', 201 | ' --debug, -d: Enable support for custom "msg" and "brk" debug opcodes for use with BGB', 202 | ' --unused: Report unused labels and variables', 203 | ' --info : Parse the input string and return byte count and cycles information', 204 | ' --verbose, -v: Turn on verbose logging', 205 | ' --version: Displays version information', 206 | ' --help: Displays this help text', 207 | '' 208 | ].join('\n')); 209 | } 210 | 211 | function error(message) { 212 | process.stderr.write(`Error: ${message}\n`); 213 | process.exit(1); 214 | } 215 | 216 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A JavaScript Gameboy Assembler 2 | 3 | **gbasm** is a JavaScript based compiler for Gameboy z80 assembly code. 4 | 5 | `gbasm` is mainly being developed for and tested with [Tuff](https://github.com/BonsaiDen/Tuff.gb). 6 | 7 | 8 | ### Installation and Usage 9 | 10 | 1. Install [Node.js](https://nodejs.org) 11 | 2. Now install `gbasm` by running `npm install -g gbasm` 12 | 13 | 14 | ``` 15 | Usage: gbasm [options] [sources] 16 | 17 | --outfile, -o : The name of the output rom file (default: game.gb) 18 | --optimize, -O: Enable instruction optimizations 19 | --mapfile, -m : Generates a ASCII overview of the mapped ROM space 20 | --symfile, -s : Generates a symbol map compatible with debuggers 21 | --jsonfile, -j : Generates a JSON data dump of all sections with their data, labels, instructions etc. 22 | --silent, -S: Surpresses all logging 23 | --debug, -d: Enable support for custom "msg" debug opcodes', 24 | --verbose, -v: Surpresses all logging 25 | --version: Displays version information 26 | --help: Displays this help text 27 | ``` 28 | 29 | 30 | ## Output Options 31 | 32 | 33 | - __ `--outfile` / `-o` __ 34 | 35 | Specifies the filename of the generated ROM image. 36 | 37 | 38 | - __ `--optimize` / `-O` __ 39 | 40 | Turns on assembly optimizations which are automatically performed during linkage. 41 | 42 | 43 | - __ `--mapfile` / `-m`__ 44 | 45 | Generates a ASCII overview of the mapped ROM areas. 46 | 47 | 48 | - __ `--symfile` / `-s` __ 49 | 50 | Generates a symbol map file for use with Debuggers (e.g. [bgb](http://bgb.bircd.org/)) 51 | 52 | 53 | - __ `--debug` / `-d` __ 54 | 55 | Enables support for custom `msg` opcodes for use with Debuggers (e.g. [bgb](http://bgb.bircd.org/)) 56 | 57 | ```asm 58 | ; This will log "Debug Message" when run in the debugger 59 | msg "Debug Message" 60 | ``` 61 | 62 | > Note: The `msg` opcode will be ignored when compiling without the flag. 63 | 64 | 65 | - __ `--jsonfile` / `-j` __ 66 | 67 | Generates a *json* file that contains the fully linked ROM data serialized into a detailed format useable for further, custom processing. 68 | 69 | 70 | 71 | ## Compatibility Notes 72 | 73 | **gbasm** is mostly compatible with [rgbds](https://github.com/bentley/rgbds) 74 | but there are some deviations and additions: 75 | 76 | ### General 77 | 78 | - *gbasm* is a multipass compiler, meaning the all sources files and definitions 79 | are parsed before resolving any names or sizes. 80 | 81 | ### Syntax 82 | 83 | - The *load accumulator and increment/decrement hl* type instructions only take `hli` and `hld` as their second operand 84 | - Memory operands do only support `[` and `]` in their syntax 85 | - All names and labels which start with an underscore are treated as being local / private to the file they were defined in 86 | 87 | ### Macros 88 | 89 | - Most of the pre-defined macros from `rgbds` are available (e.g. `COS`, `STRLWR` etc.) 90 | - User defined macros come in two flavors: 91 | 92 | 1. __Expression Macros__ 93 | 94 | These macros contain only a single expression statement and can be used as values everywhere a built-in macro could be used: 95 | 96 | ```asm 97 | MACRO add(@a, @b) 98 | @a + @b 99 | ENDMACRO 100 | 101 | DB add(2, 5) ; essentially DB 7 102 | ``` 103 | 104 | Expression Macros can take `Numbers` and `Strings` as their arguments. 105 | 106 | 2. __Expansion Macros__ 107 | 108 | These are macros in the classical sense which just expand into additional assembler code: 109 | 110 | ```asm 111 | MACRO header() 112 | DB $11,$22,$33,$44,$55 113 | DW $1234,$4567 114 | ENDMACRO 115 | 116 | header(); expands into the DB and DW diretives above 117 | ``` 118 | 119 | In addition to `Strings` and `Numbers`, expansion macros can also take `Registers` as their arguments. 120 | 121 | ```asm 122 | MACRO ld16(@number, @a, @b) 123 | ld @a,@number >> 8 124 | ld @b,@number & $ff 125 | ENDMACRO 126 | 127 | ld16($1234, b, c); turns into ld b,$12 and ld c,$34 128 | ``` 129 | 130 | ### Instructions 131 | 132 | **gbasm** supports additional meta instructions at the source code level, which will be compiled down to multiple native instructions. 133 | 134 | These aim at increasing the readability of the source. 135 | 136 | #### **addw** 137 | 138 | Adds a 8-bit operand to a 16-bit register using only the `Accumulator`: 139 | 140 | ```asm 141 | ; ld a,$ff 142 | ; add a,l 143 | ; ld l,a 144 | ; adc a,h 145 | ; sub l 146 | ; ld h,a 147 | addw hl,$ff 148 | addw bc,$ff 149 | addw de,$ff 150 | 151 | ; add a,l 152 | ; ld l,a 153 | ; adc a,h 154 | ; sub l 155 | ; ld h,a 156 | addw hl,a 157 | addw bc,a 158 | addw de,a 159 | 160 | ; ld a,reg 161 | ; add a,l 162 | ; ld l,a 163 | ; adc a,h 164 | ; sub l 165 | ; ld h,a 166 | addw hl,reg 167 | addw bc,reg 168 | addw de,reg 169 | ``` 170 | 171 | #### **incx** 172 | 173 | Extended increment of a memory address, using the `Accumulator` as an intermediate register (destroying its contents): 174 | 175 | ```asm 176 | ; ld a,[$0000] 177 | ; inc a 178 | ; ld [$0000],a 179 | incx [$0000] 180 | ``` 181 | 182 | #### **decx** 183 | 184 | Extended decrement of a memory address, using the `Accumulator` as an intermediate register (destroying its contents): 185 | 186 | ```asm 187 | ; ld a,[$0000] 188 | ; dec a 189 | ; ld [$0000],a 190 | decx [$0000] 191 | ``` 192 | 193 | #### **ldxa** 194 | 195 | Extended memory loads using the `Accumulator` as an intermediate register (destroying its contents): 196 | 197 | ```asm 198 | ; ld a,[hli] 199 | ; ld R,a 200 | ldxa b,[hli] 201 | ldxa c,[hli] 202 | ldxa d,[hli] 203 | ldxa e,[hli] 204 | ldxa h,[hli] 205 | ldxa l,[hli] 206 | 207 | ; ld a,[hld] 208 | ; ld R,a 209 | ldxa b,[hld] 210 | ldxa c,[hld] 211 | ldxa d,[hld] 212 | ldxa e,[hld] 213 | ldxa h,[hld] 214 | ldxa l,[hld] 215 | 216 | ; ld a,R 217 | ; ld [hli],a 218 | ldxa [hli],b 219 | ldxa [hli],c 220 | ldxa [hli],d 221 | ldxa [hli],e 222 | ldxa [hli],h 223 | ldxa [hli],l 224 | 225 | ; ld a,R 226 | ; ld [hld],a 227 | ldxa [hld],b 228 | ldxa [hld],c 229 | ldxa [hld],d 230 | ldxa [hld],e 231 | ldxa [hld],h 232 | ldxa [hld],l 233 | 234 | ; ld a,$ff 235 | ; ld [$0000],a 236 | ldxa [$0000],$ff 237 | 238 | ; ld a,$ff 239 | ; ld [hli],a 240 | ldxa [hli],$ff 241 | 242 | ; ld a,$ff 243 | ; ld [hld],a 244 | ldxa [hld],$ff 245 | 246 | ; ld a,R 247 | ; ld [$0000],a 248 | ldxa [$0000],b 249 | ldxa [$0000],c 250 | ldxa [$0000],d 251 | ldxa [$0000],e 252 | ldxa [$0000],h 253 | ldxa [$0000],l 254 | 255 | ; ld a,[hli] 256 | ; ld [$0000],a 257 | ldxa [$0000],[hli] 258 | 259 | ; ld a,[hld] 260 | ; ld [$0000],a 261 | ldxa [$0000],[hld] 262 | 263 | ; ld a,[$0000] 264 | ; ld [$0000],a 265 | ldxa [$0000],[$0000] 266 | 267 | ; ld a,[$0000] 268 | ; ld R,a 269 | ldxa b,[$0000] 270 | ldxa c,[$0000] 271 | ldxa d,[$0000] 272 | ldxa e,[$0000] 273 | ldxa h,[$0000] 274 | ldxa l,[$0000] 275 | 276 | ; ld a,[$0000] 277 | ; ld [hli],a 278 | ldxa [hli],[$0000] 279 | 280 | ; ld a,[$0000] 281 | ; ld [hld],a 282 | ldxa [hld],[$0000] 283 | ``` 284 | 285 | 286 | ## License 287 | 288 | Licensed under MIT. 289 | 290 | -------------------------------------------------------------------------------- /lib/linker/Optimizer.js: -------------------------------------------------------------------------------- 1 | // Dependencies --------------------------------------------------------------- 2 | // ---------------------------------------------------------------------------- 3 | const Token = require('../parser/Lexer').Token; 4 | 5 | 6 | // Assembly Instruction Optimizer --------------------------------------------- 7 | // ---------------------------------------------------------------------------- 8 | function Optimizer(instr, unsafe, next, nextTwo) { 9 | 10 | const opCode = instr.raw[0]; 11 | switch(opCode) { 12 | 13 | // ld a,0 -> xor a 14 | // 15 | // -> save 1 byte and 3 T-states 16 | case 0x3E: 17 | if (instr.resolvedArg === 0x00) { 18 | instr.rewrite('xor', 4, [0x07 + 0xA8]); 19 | return 1; 20 | } 21 | break; 22 | 23 | // cp 0 -> or a 24 | // 25 | // save 1 byte and 3 T-states 26 | case 0xFE: 27 | if (instr.resolvedArg === 0x00) { 28 | instr.rewrite('or', 4, [0x07 + 0xB0]); 29 | return 1; 30 | } 31 | break; 32 | 33 | // ld a,[someLabel] -> ldh a,$XX 34 | case 0xFA: 35 | // Transform memory loads into high loads if argument is 36 | // in the range of 0xff00-0xffff 37 | if (instr.resolvedArg >= 0xff00 && instr.resolvedArg <= 0xffff) { 38 | 39 | instr.rewrite( 40 | 'ldh', 12, [0xF0], 41 | new Token('NUMBER', instr.resolvedArg & 0xff, instr.index), 42 | true 43 | ); 44 | 45 | return 1; 46 | 47 | } 48 | break; 49 | 50 | // ld [someLabel],a -> ldh $XX,a 51 | case 0xEA: 52 | // Transform memory loads into high loads if argument is 53 | // in the range of 0xff00-0xffff 54 | if (instr.resolvedArg >= 0xff00 && instr.resolvedArg <= 0xffff) { 55 | 56 | instr.rewrite( 57 | 'ldh', 12, [0xE0], 58 | new Token('NUMBER', instr.resolvedArg & 0xff, instr.index), 59 | true 60 | ); 61 | 62 | return 1; 63 | 64 | } 65 | break; 66 | 67 | // jp c,label -> jr c,label 68 | // jp nc,label -> jr nc,label 69 | // jp z,label -> jr z,label 70 | // jp nz,label -> jr nz,label 71 | // jp label -> jr label 72 | case 0xDA: 73 | case 0xD2: 74 | case 0xCA: 75 | case 0xC2: 76 | case 0xC3: 77 | 78 | // Transform jp instructions into jrs if the target is in range 79 | // of one signed byte 80 | const offset = instr.resolvedArg - instr.offset; 81 | if (offset >= -127 && offset <= 128) { 82 | 83 | // Without flags 84 | if (opCode === 0xC3) { 85 | 86 | // We need to check for padding here in case the JP is 87 | // used in a jump table, otherwise we'll screw up the 88 | // alignment causing all kinds of havoc 89 | 90 | // Only replace if the next instruction is NOT a nop 91 | if (unsafe && (!next || next.raw[0] !== 0x00)) { 92 | instr.rewrite('jr', 12, [0x18], instr.arg, true, true); 93 | return 1; 94 | } 95 | 96 | // With flags 97 | } else { 98 | instr.rewrite('jr', 12, [opCode - 0xA2], instr.arg, true, true); 99 | return 1; 100 | } 101 | 102 | } 103 | break; 104 | 105 | // call LABEL 106 | // ret 107 | // -> 108 | // jp LABEL 109 | // 110 | // save 1 byte and 17 T-states 111 | case 0xCD: 112 | 113 | // Transform call instructions which are directly followed by a ret 114 | // into a simple jp 115 | if (next && next.raw[0] === 0xC9 && next.label === null) { 116 | instr.rewrite('jp', 16, [0xC3], instr.arg); 117 | return 2; 118 | } 119 | break; 120 | 121 | // ld b,$XX 122 | // ld c,$XX 123 | // -> 124 | // ld bc,$XXXX 125 | // 126 | // -> save 1 byte and 4 T-states 127 | case 0x06: 128 | if (next && next.raw[0] === 0x0E && next.label === null) { 129 | instr.rewrite( 130 | 'ld', 12, [0x01], 131 | new Token( 132 | 'NUMBER', 133 | (instr.resolvedArg << 8) | next.resolvedArg, 134 | instr.index 135 | ) 136 | ); 137 | return 2; 138 | } 139 | break; 140 | 141 | // ld d,$XX 142 | // ld e,$XX 143 | // -> 144 | // ld de,$XXXX 145 | // 146 | // -> save 1 byte and 4 T-states 147 | case 0x16: 148 | if (next && next.raw[0] === 0x1E && next.label === null) { 149 | instr.rewrite( 150 | 'ld', 12, [0x11], 151 | new Token( 152 | 'NUMBER', 153 | (instr.resolvedArg << 8) | next.resolvedArg, 154 | instr.index 155 | ) 156 | ); 157 | return 2; 158 | } 159 | break; 160 | 161 | // ld h,$XX 162 | // ld l,$XX 163 | // -> 164 | // ld hl,$XXXX 165 | // 166 | // -> save 1 byte and 4 T-states 167 | case 0x26: 168 | if (next && next.raw[0] === 0x2E && next.label === null) { 169 | instr.rewrite( 170 | 'ld', 12, [0x21], 171 | new Token( 172 | 'NUMBER', 173 | (instr.resolvedArg << 8) | next.resolvedArg, 174 | instr.index 175 | ) 176 | ); 177 | return 2; 178 | } 179 | break; 180 | 181 | // srl a 182 | // srl a 183 | // srl a 184 | // -> 185 | // rrca 186 | // rrca 187 | // rrca 188 | // and %00011111 189 | // 190 | // save 1 byte and 5 T-states 191 | case 0xCB: 192 | if (next && nextTwo && instr.raw[1] === 0x38 + 0x07) { 193 | if (next.raw[0] === 0xCB && 194 | next.raw[1] === 0x38 + 0x07 && 195 | next.label === null && 196 | nextTwo.raw[0] === 0xCB && 197 | nextTwo.raw[1] === 0x38 + 0x07 && 198 | nextTwo.label === null) { 199 | 200 | instr.rewrite( 201 | 'rrca,rrca,rrca,and 0x1F', 202 | 4 * 3 + 8, 203 | [ 204 | 0x0F, 205 | 0x0F, 206 | 0x0F, 207 | 0xE6, 0x1F 208 | ] 209 | ); 210 | return 3; 211 | } 212 | } 213 | break; 214 | } 215 | 216 | return 0; 217 | 218 | } 219 | 220 | 221 | // Exports -------------------------------------------------------------------- 222 | module.exports = Optimizer; 223 | 224 | -------------------------------------------------------------------------------- /lib/data/Macro.js: -------------------------------------------------------------------------------- 1 | // Dependencies --------------------------------------------------------------- 2 | // ---------------------------------------------------------------------------- 3 | const Errors = require('../Errors'), 4 | Expression = require('../parser/Expression'), 5 | Parser = require('../parser/Parser'), 6 | TokenStream = require('../parser/TokenStream'), 7 | Lexer = require('../parser/Lexer'); 8 | 9 | 10 | // Macro Definitions ---------------------------------------------------------- 11 | // ---------------------------------------------------------------------------- 12 | function Macro(file, name, args, tokens, index) { 13 | 14 | this.file = file; 15 | this.name = name; 16 | 17 | const argMap = {}; 18 | this.args = args.map((arg) => { 19 | 20 | const name = arg.value; 21 | if (argMap.hasOwnProperty(name)) { 22 | Errors.DeclarationError( 23 | file, 24 | `macro argument @${ name}`, 25 | arg.index, 26 | argMap[name] 27 | ); 28 | 29 | } else { 30 | argMap[name] = arg; 31 | return new Macro.Argument(name, 'any'); 32 | } 33 | }); 34 | 35 | // Tokens in this macro 36 | this.tokens = tokens; 37 | 38 | // Index in the source file 39 | this.index = index; 40 | 41 | // Wether this macro can be called in an expression context or not 42 | // (i.e. it does return a value) 43 | this.isExpression = tokens.length === 2 && tokens[0].type === 'EXPRESSION'; 44 | 45 | Macro.Map[name] = this; 46 | 47 | } 48 | 49 | Macro.Argument = function(name, type) { 50 | this.name = name; 51 | this.type = type; 52 | }; 53 | 54 | Macro.RegisterArgument = function(name) { 55 | this.name = name; 56 | }; 57 | 58 | 59 | // Statics -------------------------------------------------------------------- 60 | Macro.LabelPrefix = 0; 61 | Macro.Map = {}; 62 | 63 | Macro.isDefined = function(name) { 64 | return Macro.Map.hasOwnProperty(name); 65 | }; 66 | 67 | Macro.get = function(name) { 68 | return Macro.Map[name]; 69 | }; 70 | 71 | 72 | // Methods -------------------------------------------------------------------- 73 | Macro.prototype = { 74 | 75 | getExpressionForArguments(args) { 76 | return expressionWithArguments( 77 | this.file, 78 | this.tokens[0].value, 79 | getArgumentMap(this.args, args), 80 | false 81 | ); 82 | }, 83 | 84 | expand(macroName, section, offset, args, debug) { 85 | 86 | const argumentMap = getArgumentMap(this.args, args); 87 | const prefix = Macro.LabelPrefix++; 88 | 89 | // Clone all tokens and expressions and replace macro agruments 90 | // with their computed values 91 | const tokens = this.tokens.map((original) => { 92 | 93 | const token = original.clone(); 94 | 95 | if (token.value instanceof Expression.Call 96 | || token.value instanceof Expression.Node) { 97 | 98 | token.value = expressionWithArguments( 99 | this.file, 100 | token.value, 101 | argumentMap, 102 | true 103 | ); 104 | 105 | } else if (token.type === 'MACRO_ARG') { 106 | replaceTokenWithArgument(this.file, token, argumentMap, true); 107 | 108 | } else if (token.type == 'LABEL_GLOBAL_DEF') { 109 | token.value = `macro_expansion_${prefix}-${token.value}`; 110 | 111 | } else if (token.type == 'LABEL_LOCAL_REF' || token.type == 'LABEL_LOCAL_DEF') { 112 | const to = `.macro_expansion_${prefix}-${token.value.replace(/^./, '')}`; 113 | token.value = to; 114 | } 115 | 116 | return token; 117 | 118 | }); 119 | 120 | // Create a proxy for the section and its source file that allows the 121 | // parser to insert tokens into the already existing section entries 122 | const proxyFile = createFileProxy(macroName, section, offset), 123 | stream = new TokenStream(proxyFile, tokens), 124 | parser = new Parser(proxyFile, stream, debug); 125 | 126 | // Parse the marco tokens into the existing file / section 127 | parser.parse(); 128 | 129 | } 130 | 131 | }; 132 | 133 | 134 | // Helpers -------------------------------------------------------------------- 135 | function createFileProxy(macroName, section, offset) { 136 | 137 | function SourceFileProxy(section) { 138 | 139 | this.currentSection = createSectionProxy(section, offset); 140 | 141 | this.addSection = function(name) { 142 | Errors.DeclarationError( 143 | this, 144 | 'a SECTION cannot be defined within a macro', 145 | name.index 146 | ); 147 | }; 148 | 149 | /* 150 | var addLabel = section.file.addLabel; 151 | 152 | // TODO figure out how to resolve macro local names and have them stacked 153 | this.addLabel = function(name, parent, index) { 154 | name = macroName + '::' + name; 155 | console.log(name); 156 | return addLabel.call(this, name, parent, index); 157 | }; 158 | */ 159 | 160 | } 161 | 162 | SourceFileProxy.prototype = section.file; 163 | 164 | return new SourceFileProxy(section); 165 | 166 | } 167 | 168 | function createSectionProxy(section, offset) { 169 | 170 | function SectionProxy(offset) { 171 | this.add = function(entry) { 172 | this.addWithOffset(entry, offset); 173 | offset++; 174 | }; 175 | } 176 | 177 | SectionProxy.prototype = section; 178 | 179 | return new SectionProxy(offset); 180 | 181 | } 182 | 183 | function getArgumentMap(expected, provided) { 184 | 185 | const map = {}; 186 | expected.forEach((arg, index) => { 187 | map[arg.name] = provided[index]; 188 | }); 189 | 190 | return map; 191 | 192 | } 193 | 194 | function expressionWithArguments(file, expr, argumentMap, allowRegisters) { 195 | 196 | const cloned = expr.clone(); 197 | 198 | function walker(node) { 199 | if (node.type === 'MACRO_ARG') { 200 | replaceTokenWithArgument(file, node, argumentMap, allowRegisters); 201 | 202 | } else if (node.type === 'EXPRESSION') { 203 | node.walk(walker); 204 | } 205 | } 206 | 207 | cloned.walk(walker); 208 | 209 | return cloned; 210 | 211 | } 212 | 213 | function replaceTokenWithArgument(file, token, argumentMap, allowRegisters) { 214 | 215 | if (argumentMap.hasOwnProperty(token.value)) { 216 | 217 | const arg = argumentMap[token.value]; 218 | if (arg instanceof Macro.RegisterArgument) { 219 | 220 | if (allowRegisters !== true) { 221 | Errors.ArgumentError( 222 | file, 223 | 'Use of register arguments is not supported within expression macros', 224 | token.index 225 | ); 226 | } 227 | 228 | token.value = arg.name; 229 | token.type = 'NAME'; 230 | 231 | } else { 232 | 233 | const raw = argumentMap[token.value]; 234 | 235 | // Names and other things 236 | if (raw instanceof Lexer.Token) { 237 | token.value = raw.value; 238 | token.type = raw.type; 239 | 240 | // Numbers and strings 241 | } else { 242 | token.value = raw; 243 | token.type = (typeof token.value).toUpperCase(); 244 | } 245 | 246 | } 247 | 248 | } else { 249 | Errors.ArgumentError( 250 | file, 251 | `Use of undefined macro argument @${ token.value } in expression`, 252 | token.index 253 | ); 254 | } 255 | 256 | } 257 | 258 | 259 | // Exports -------------------------------------------------------------------- 260 | module.exports = Macro; 261 | 262 | -------------------------------------------------------------------------------- /lib/parser/Expression.js: -------------------------------------------------------------------------------- 1 | // Expression Parser ---------------------------------------------------------- 2 | // ---------------------------------------------------------------------------- 3 | function Expression(lexer, tokens) { 4 | 5 | this.index = 0; 6 | this.token = tokens[0]; 7 | this.list = tokens; 8 | this.lexer = lexer; 9 | 10 | return this.parseBinary(0); 11 | 12 | } 13 | 14 | // Methods -------------------------------------------------------------------- 15 | Expression.prototype = { 16 | 17 | parseBinary(p) { 18 | 19 | // We always start with a unary expression 20 | let t = this.parseUnary(); 21 | 22 | // Now we collect additional binary operators on right as long as their 23 | // precedence is higher then the initial one 24 | while(this.isBinary(this.token) && this.prec(this.token.value) > p) { 25 | 26 | // We found a binary operator 27 | const op = new ExpressionBinaryOp(this.token.value, this.token.index); 28 | this.next(); 29 | 30 | // Now we check it's associativity 31 | const associativity = (op.id === '^' || op.id === '**') ? 0 : 1; 32 | 33 | // And parse another binaryExpression to it's right 34 | const t1 = this.parseBinary(this.prec(op.id) + associativity); 35 | 36 | // Then we combine our current expression with the operator and 37 | // the expression after it to a binary epxression node 38 | t = new ExpressionNode(op, t, t1); 39 | 40 | } 41 | 42 | return t; 43 | 44 | }, 45 | 46 | parseUnary() { 47 | 48 | // Unary expressions 49 | if (this.isUnary(this.token)) { 50 | const op = new ExpressionUnaryOp(this.token.value, this.token.index); 51 | this.next(); 52 | return new ExpressionNode(op, this.parseBinary(this.prec(op.id)), null); 53 | 54 | // Parenthesis 55 | } else if (this.token.type === 'LPAREN') { 56 | this.next(); 57 | const t = this.parseBinary(0); 58 | this.expect('RPAREN'); 59 | return t; 60 | 61 | // Values / Calls 62 | } else if (this.token.type !== 'RPAREN' 63 | && this.token.type !== 'OPERATOR' 64 | && this.token.type !== 'COMMA') { 65 | 66 | let e = new ExpressionLeaf(this.token); 67 | 68 | // Names can be part of a macro call 69 | if (this.token.type === 'NAME') { 70 | 71 | this.next(); 72 | 73 | // Check for a potential call expression 74 | if (this.token && this.token.type === 'LPAREN') { 75 | this.next(); 76 | 77 | const args = []; 78 | if (this.token && this.token.type !== 'RPAREN') { 79 | 80 | // Grab it's first argument 81 | args.push(this.parseBinary(0)); 82 | 83 | // Now as long as there is a COMMA after the expression 84 | // we consume it and parse another expression 85 | while(this.token.type === 'COMMA') { 86 | this.next(); 87 | args.push(this.parseBinary(0)); 88 | } 89 | 90 | } 91 | 92 | // Eventually we build the call expression from the name 93 | // and it's argument 94 | e = new ExpressionCall(e.value, args); 95 | this.expect('RPAREN'); 96 | 97 | } 98 | 99 | } else { 100 | this.next(); 101 | } 102 | 103 | return e; 104 | 105 | } 106 | throw new TypeError(`Unexpected token when evaluating expression: ${ this.token.type}`); 107 | 108 | 109 | }, 110 | 111 | isBinary(token) { 112 | return token 113 | && token.type === 'OPERATOR' 114 | && token.value !== '!' 115 | && token.value !== '~'; 116 | }, 117 | 118 | isUnary(token) { 119 | return token 120 | && token.type === 'OPERATOR' 121 | && (token.value === '-' || token.value === '!' || token.value === '~' || token.value === '+'); 122 | }, 123 | 124 | next() { 125 | this.index++; 126 | this.token = this.list[this.index]; 127 | }, 128 | 129 | expect(type) { 130 | 131 | if (this.token === undefined) { 132 | this.lexer.error('end of expression', null, this.list[this.list.length - 1].index); 133 | 134 | } else if (this.token.type !== type) { 135 | this.lexer.error(this.token.type, type, this.token.index); 136 | 137 | } else { 138 | this.next(); 139 | } 140 | 141 | }, 142 | 143 | prec(op) { 144 | 145 | switch (op) { 146 | case '||': 147 | return 1; 148 | 149 | case '&&': 150 | return 2; 151 | 152 | case '|': 153 | return 3; 154 | 155 | case '^': 156 | return 4; 157 | 158 | case '&': 159 | return 5; 160 | 161 | case '==': 162 | case '!=': 163 | return 6; 164 | 165 | case '<': 166 | case '>': 167 | case '<=': 168 | case '>=': 169 | return 7; 170 | 171 | case '<<': 172 | case '>>': 173 | return 8; 174 | 175 | case '+': 176 | case '-': 177 | case '!': 178 | case '~': 179 | return 9; 180 | 181 | case '*': 182 | case '/': 183 | case '%': 184 | return 11; 185 | 186 | case '**': 187 | return 12; 188 | 189 | default: 190 | return 0; 191 | } 192 | 193 | } 194 | 195 | }; 196 | 197 | 198 | // Expression Nodes ----------------------------------------------------------- 199 | // ---------------------------------------------------------------------------- 200 | function ExpressionBinaryOp(id, index) { 201 | this.id = id; 202 | this.index = index; 203 | } 204 | 205 | function ExpressionUnaryOp(id, index) { 206 | this.id = id; 207 | this.index = index; 208 | } 209 | 210 | function ExpressionNode(op, left, right) { 211 | 212 | // These two fields are required in order to have ExpressionNode work at 213 | // macro call sites. They provide compatibility with the Token interface. 214 | this.type = 'EXPRESSION'; 215 | this.value = this; 216 | 217 | this.op = op; 218 | this.left = left; 219 | this.right = right; 220 | 221 | } 222 | 223 | ExpressionNode.prototype = { 224 | 225 | walk(callback) { 226 | this.left.walk(callback); 227 | this.right.walk(callback); 228 | }, 229 | 230 | clone() { 231 | return new ExpressionNode(this.op, this.left.clone(), this.right.clone()); 232 | } 233 | 234 | }; 235 | 236 | function ExpressionLeaf(value) { 237 | this.value = value; 238 | } 239 | 240 | ExpressionLeaf.prototype = { 241 | 242 | walk(callback) { 243 | callback(this.value); 244 | }, 245 | 246 | clone() { 247 | return new ExpressionLeaf(this.value.clone()); 248 | } 249 | 250 | }; 251 | 252 | function ExpressionCall(callee, args) { 253 | this.callee = callee; 254 | this.args = args; 255 | } 256 | 257 | ExpressionCall.prototype = { 258 | 259 | walk(callback) { 260 | this.args.forEach((arg) => { 261 | callback(arg.value); 262 | }); 263 | }, 264 | 265 | clone() { 266 | return new ExpressionCall(this.callee, this.args.map((arg) => { 267 | return arg.clone(); 268 | })); 269 | } 270 | 271 | }; 272 | 273 | 274 | // Exports -------------------------------------------------------------------- 275 | Expression.Node = ExpressionNode; 276 | Expression.Leaf = ExpressionLeaf; 277 | Expression.Call = ExpressionCall; 278 | 279 | module.exports = Expression; 280 | 281 | -------------------------------------------------------------------------------- /lib/SourceFile.js: -------------------------------------------------------------------------------- 1 | // Dependencies --------------------------------------------------------------- 2 | // ---------------------------------------------------------------------------- 3 | const fs = require('fs'), 4 | path = require('path'), 5 | Parser = require('./parser/Parser'), 6 | Lexer = require('./parser/Lexer'), 7 | 8 | // Linking 9 | BuiltinMacro = require('./linker/Macro'), 10 | Linker = require('./linker/Linker'), 11 | 12 | // Entries 13 | Section = require('./data/Section'), 14 | Label = require('./data/Label'), 15 | Constant = require('./data/Constant'), 16 | Variable = require('./data/Variable'), 17 | DataBlock = require('./data/DataBlock'), 18 | Binary = require('./data/Binary'), 19 | Instruction = require('./data/Instruction'), 20 | Macro = require('./data/Macro'), 21 | 22 | // Errors 23 | Errors = require('./Errors'); 24 | 25 | 26 | // Assembly Source File Abstraction ------------------------------------------- 27 | // ---------------------------------------------------------------------------- 28 | function SourceFile(compiler, parent, file, section, index) { 29 | 30 | this.compiler = compiler; 31 | this.parent = parent; 32 | this.parentIndex = index; 33 | this.path = file; 34 | this.name = file instanceof Buffer ? 'memory' : file.substring(compiler.base.length + 1); 35 | this.buffer = file instanceof Buffer ? file : fs.readFileSync(this.path); 36 | 37 | // Code 38 | this.currentSection = section; 39 | this.instructions = []; 40 | this.relativeJumpTargets = []; 41 | this.sections = []; 42 | this.labels = []; 43 | this.variables = []; 44 | this.binaryIncludes = []; 45 | this.dataBlocks = []; 46 | this.unresolvedSizes = []; 47 | this.names = {}; 48 | 49 | } 50 | 51 | SourceFile.prototype = { 52 | 53 | parse(debug) { 54 | this.parser = new Parser(this, new Lexer(this), debug); 55 | this.parser.parse(); 56 | }, 57 | 58 | include(file, index) { 59 | 60 | // Relative includes 61 | if (file.substring(0, 1) === '/') { 62 | file = path.join(this.compiler.base, file.substring(1)); 63 | 64 | } else { 65 | file = path.join(path.dirname(this.path), file); 66 | } 67 | 68 | // Check for circular includes 69 | let p = this; 70 | while(p) { 71 | if (p.path === file) { 72 | Errors.ParseError(this, `circular inclusion of "${ this.name }"`, null, index); 73 | } 74 | p = p.parent; 75 | } 76 | 77 | // Parse the file 78 | try { 79 | const includedFile = this.compiler.includeFile(this, file, this.currentSection, index); 80 | this.currentSection = includedFile.currentSection; 81 | 82 | } catch(err) { 83 | if (err instanceof TypeError) { 84 | throw err; 85 | 86 | } else { 87 | Errors.IncludeError(this, err, 'include source file', this.path, index); 88 | } 89 | } 90 | 91 | }, 92 | 93 | symbols() { 94 | 95 | const symbols = []; 96 | symbols.push.apply(symbols, this.variables.map((v) => { 97 | return v; 98 | })); 99 | 100 | symbols.push.apply(symbols, this.labels.map((l) => { 101 | return l; 102 | })); 103 | 104 | return symbols; 105 | 106 | }, 107 | 108 | 109 | // Entries ---------------------------------------------------------------- 110 | addSection(name, segment, bank, offset) { 111 | this.currentSection = new Section(this, name, segment.value, bank, offset, segment.index); 112 | this.sections.push(this.currentSection); 113 | }, 114 | 115 | addBinaryInclude(src, index) { 116 | this.binaryIncludes.push(new Binary(this, src.value, this.currentSection, index)); 117 | }, 118 | 119 | addDataBlock(values, isByte, size, index) { 120 | 121 | this.checkForSection('Data', index); 122 | 123 | const data = new DataBlock(values, this.currentSection, isByte, size, index); 124 | this.dataBlocks.push(data); 125 | 126 | if (typeof data.size === 'object') { 127 | this.unresolvedSizes.push(data); 128 | } 129 | 130 | }, 131 | 132 | addInstruction(mnemonic, cycles, code, arg, isByte, isSigned, isBit, index) { 133 | 134 | this.checkForSection('Instruction', index); 135 | 136 | const instr = new Instruction( 137 | mnemonic, this.currentSection, 138 | cycles, code, arg, isByte, isSigned, isBit, 139 | index 140 | ); 141 | 142 | this.instructions.push(instr); 143 | 144 | if (instr.mnemonic === 'jr' && instr.arg.type === 'OFFSET') { 145 | this.relativeJumpTargets.push(instr); 146 | } 147 | 148 | }, 149 | 150 | addVariable(name, size, index) { 151 | 152 | this.checkForSection('Variable', index, true); 153 | this.checkForDefinition('variable', name, index); 154 | 155 | const variable = new Variable(this, name, this.currentSection, size, index); 156 | this.names[name] = variable; 157 | this.variables.push(variable); 158 | 159 | if (typeof variable.size === 'object') { 160 | this.unresolvedSizes.push(variable); 161 | } 162 | 163 | }, 164 | 165 | addLabel(name, parent, index) { 166 | 167 | this.checkForSection('Label', index); 168 | 169 | // Check for duplicate global lables 170 | if (!parent) { 171 | this.checkForDefinition('global label', name, index); 172 | 173 | // Check for duplicate local labels 174 | } else if (parent) { 175 | 176 | for(let i = 0, l = parent.children.length; i < l; i++) { 177 | if (parent.children[i].name === name) { 178 | Errors.DeclarationError( 179 | this, 180 | `Local label "${ name }"`, 181 | index, parent.children[i] 182 | ); 183 | } 184 | } 185 | 186 | } 187 | 188 | const label = new Label(this, name, this.currentSection, parent, index); 189 | this.names[name] = label; 190 | this.labels.push(label); 191 | 192 | return label; // return label for parent label assignments in Parser 193 | 194 | }, 195 | 196 | addConstant(name, value, index) { 197 | this.checkForDefinition('constant', name, index); 198 | this.names[name] = new Constant(this, name, value, index); 199 | }, 200 | 201 | addMacro(name, args, tokens, index) { 202 | this.checkForDefinition('macro', name, index); 203 | this.names[name] = new Macro(this, name, args, tokens, index); 204 | }, 205 | 206 | addMacroCall(expression, index) { 207 | 208 | if (!this.currentSection) { 209 | Errors.MacroError(this, 'Macro call must be made within a section', index); 210 | } 211 | 212 | this.currentSection.add(expression.value); 213 | 214 | }, 215 | 216 | 217 | // Entry Validation ------------------------------------------------------- 218 | checkForDefinition(entryType, name, index) { 219 | 220 | if (BuiltinMacro.isDefined(name)) { 221 | Errors.DeclarationError(this, `shadows built-in macro ${ name}`, index); 222 | 223 | } else { 224 | 225 | const existing = Linker.resolveName(name, this); 226 | if (existing && !existing.parent) { 227 | Errors.DeclarationError( 228 | this, 229 | `${entryType } "${ name }"`, 230 | index, existing 231 | ); 232 | } 233 | 234 | } 235 | 236 | }, 237 | 238 | checkForSection(entryType, index) { 239 | if (!this.currentSection) { 240 | Errors.AddressError(this, `${entryType } was not declared within any section`, index); 241 | } 242 | }, 243 | 244 | 245 | // Getters ---------------------------------------------------------------- 246 | getPath(index, nameOnly) { 247 | 248 | const i = this.getLineInfo(index), 249 | offset = index !== undefined ? ` (line ${ i.line + 1 }, col ${ i.col })` : ''; 250 | 251 | if (this.parent && !nameOnly) { 252 | return `[${ this.name }]${ offset }\n included from ${ this.parent.getPath(this.parentIndex, true) }`; 253 | 254 | } 255 | return `[${ this.name }]${ offset}`; 256 | 257 | 258 | }, 259 | 260 | getLineInfo(index) { 261 | 262 | let line = 0, 263 | col = 0, 264 | i = 0; 265 | 266 | while(i < index) { 267 | 268 | const ch = this.buffer[i++]; 269 | if (ch === 10 || ch === 13) { 270 | line++; 271 | col = 0; 272 | 273 | } else { 274 | col++; 275 | } 276 | 277 | } 278 | 279 | return { 280 | line, 281 | col 282 | }; 283 | 284 | }, 285 | 286 | getLineSource(index) { 287 | const i = this.getLineInfo(index); 288 | return { 289 | line: i.line, 290 | col: i.col, 291 | source: `${new Array(61).join('-').grey 292 | }\n\n ${ 293 | this.buffer.toString().split(/[\n\r]/)[i.line].white 294 | }\n ${ 295 | new Array(i.col).join(' ') }${'^---'.red}` 296 | }; 297 | }, 298 | 299 | 300 | // Logging ---------------------------------------------------------------- 301 | error(error) { 302 | this.compiler.error(this, error); 303 | }, 304 | 305 | warning(index, message) { 306 | this.compiler.warning(this, index, message); 307 | }, 308 | 309 | log(message) { 310 | this.compiler.log(this, message); 311 | } 312 | 313 | }; 314 | 315 | 316 | // Exports -------------------------------------------------------------------- 317 | module.exports = SourceFile; 318 | 319 | -------------------------------------------------------------------------------- /lib/data/Section.js: -------------------------------------------------------------------------------- 1 | // Dependencies --------------------------------------------------------------- 2 | // ---------------------------------------------------------------------------- 3 | const Label = require('./Label'), 4 | DataBlock = require('./DataBlock'), 5 | Instruction = require('./Instruction'), 6 | Variable = require('./Variable'), 7 | Binary = require('./Binary'), 8 | 9 | // Errors 10 | Errors = require('../Errors'); 11 | 12 | 13 | // ROM/RAM Sections ----------------------------------------------------------- 14 | // ---------------------------------------------------------------------------- 15 | function Section(file, name, segment, bank, offset, index) { 16 | 17 | this.file = file; 18 | this.nameIndex = name.index; 19 | this.name = name.value; 20 | this.segment = segment; 21 | this.bank = bank; 22 | this.size = 0; 23 | 24 | // Wether or not this section has a custom base address 25 | this.hasCustomBaseOffset = offset !== null; 26 | 27 | // Specified offset address in code 28 | this.baseOffset = offset; 29 | 30 | // Resolved offset address in ROM, can exceeded 16bit address space 31 | this.resolvedOffset = offset; 32 | 33 | // Offset value used for labels to bring them back into the 34 | // 16bit address range 35 | this.bankOffset = 0; 36 | 37 | // Start offset of the nearest segment 38 | this.startOffest = 0; 39 | 40 | // Internal offset value used when caculating label addresses 41 | this.endOffset = 0; 42 | 43 | // Flags 44 | this.isRam = false; 45 | 46 | // Instructions, Data and everything else that was declared in this Section 47 | this.entries = []; 48 | 49 | // Expansion Macro Calls in this Section 50 | this.macros = []; 51 | 52 | // Check for valid segment name 53 | if (Section.Segments.hasOwnProperty(this.segment)) { 54 | this.initialize(); 55 | 56 | } else { 57 | Errors.ParseError( 58 | this.file, 59 | `section name "${ this.segment }"`, `one of ${ Section.SegmentNames.join(', ')}`, 60 | index 61 | ); 62 | } 63 | 64 | // TODO check for multiple sections being defined at the the same offset 65 | 66 | } 67 | 68 | 69 | // Section Definitions -------------------------------------------------------- 70 | Section.Segments = { 71 | 72 | ROM0: { 73 | baseOffset: 0x0000, 74 | size: 0x4000, 75 | isRam: false, 76 | index: 0 77 | }, 78 | 79 | ROMX: { 80 | baseOffset: 0x4000, 81 | bankSize: 0x4000, 82 | maxBank: 127, 83 | size: 0x4000, 84 | isRam: false, 85 | isBanked: true, 86 | index: 1 87 | }, 88 | 89 | WRAM0: { 90 | baseOffset: 0xC000, 91 | size: 0x1000, 92 | isRam: true, 93 | index: 2 94 | }, 95 | 96 | WRAMX: { 97 | baseOffset: 0xD000, 98 | size: 0x1000, 99 | bankSize: 0x0000, 100 | maxBank: 1, 101 | isRam: true, 102 | isBanked: true, 103 | index: 3 104 | }, 105 | 106 | HRAM: { 107 | baseOffset: 0xFF80, 108 | size: 0x80, 109 | isRam: true, 110 | isBanked: false, 111 | index: 4 112 | }, 113 | 114 | RAM: { 115 | baseOffset: 0xA000, 116 | size: 0x2000, 117 | isRam: true, 118 | index: 5 119 | }, 120 | 121 | RAMX: { 122 | baseOffset: 0xA000, 123 | size: 0x2000, 124 | bankSize: 0x2000, 125 | maxBank: 7, 126 | isRam: true, 127 | isBanked: true, 128 | isZeroBanked: true, 129 | index: 6 130 | } 131 | 132 | }; 133 | 134 | Section.SegmentNames = Object.keys(Section.Segments).sort(); 135 | 136 | 137 | // Section Methods ------------------------------------------------------------ 138 | Section.prototype = { 139 | 140 | add(entry) { 141 | 142 | if (this.isRam) { 143 | if (entry instanceof Instruction) { 144 | throw new TypeError('Instruction is not allowed in RAM segment'); 145 | 146 | } else if (entry instanceof DataBlock) { 147 | if (entry.values.length) { 148 | throw new TypeError('Initialized DataBlock not allowed in RAM segment'); 149 | } 150 | 151 | } else if (entry instanceof Binary) { 152 | throw new TypeError('Binary include not not allowed in RAM segment'); 153 | } 154 | 155 | } else { 156 | if (entry instanceof Variable) { 157 | throw new TypeError('Variable can not be put in ROM segment'); 158 | } 159 | } 160 | 161 | this.entries.push(entry); 162 | 163 | }, 164 | 165 | addWithOffset(entry, offset) { 166 | 167 | if (this.isRam) { 168 | if (entry instanceof Instruction) { 169 | throw new TypeError('Instruction is not allowed in RAM segment'); 170 | 171 | } else if (entry instanceof DataBlock) { 172 | if (entry.values.length) { 173 | throw new TypeError('Initialized DataBlock not allowed in RAM segment'); 174 | } 175 | 176 | } else if (entry instanceof Binary) { 177 | throw new TypeError('Binary include not not allowed in RAM segment'); 178 | } 179 | 180 | } else { 181 | if (entry instanceof Variable) { 182 | throw new TypeError('Variable can not be put in ROM segment'); 183 | } 184 | } 185 | 186 | this.entries.splice(offset, 0, entry); 187 | 188 | }, 189 | 190 | calculateOffsets() { 191 | 192 | let offset = this.resolvedOffset, 193 | lastLabel = null; 194 | 195 | const labelOffset = this.bankOffset, 196 | endOffset = this.endOffset; 197 | 198 | for(let i = 0, l = this.entries.length; i < l; i++) { 199 | 200 | const entry = this.entries[i]; 201 | if (entry instanceof Label) { 202 | // Remove bank offsets when calculating label addresses 203 | entry.offset = offset - labelOffset; 204 | lastLabel = entry; 205 | 206 | } else { 207 | entry.label = lastLabel; 208 | entry.offset = offset; 209 | offset += entry.size; 210 | 211 | if (offset > endOffset) { 212 | Errors.SectionError( 213 | this.file, 214 | `Entry exceeds section by ${hex(offset - endOffset)} bytes, section ranges from address ${hex(labelOffset)} to ${hex(endOffset)}, but entry would end at address ${hex(offset)}.`, 215 | entry.index 216 | ) 217 | } 218 | 219 | lastLabel = null; 220 | 221 | } 222 | 223 | } 224 | 225 | this.size = offset - this.resolvedOffset; 226 | 227 | }, 228 | 229 | removeEntry(entry) { 230 | const index = this.entries.indexOf(entry); 231 | if (index !== -1) { 232 | this.entries.splice(index, 1); 233 | } 234 | }, 235 | 236 | initialize() { 237 | 238 | const segmentDefaults = Section.Segments[this.segment]; 239 | 240 | // Default Bank 241 | if (this.bank === null && segmentDefaults.isBanked) { 242 | this.bank = 1; 243 | 244 | } else if (this.bank === null) { 245 | this.bank = 0; 246 | } 247 | 248 | // Check if the segment is banked 249 | if (this.bank > 0 && !segmentDefaults.isBanked) { 250 | // TODO fix column index in error message 251 | Errors.AddressError( 252 | this.file, 253 | 'Unexpected bank index on non-bankable section', 254 | this.nameIndex 255 | ); 256 | 257 | // Check for negative bank indicies 258 | } else if (this.bank < 0) { 259 | // TODO fix column index in error message 260 | Errors.AddressError( 261 | this.file, 262 | 'Negative bank indexes are not allowed', 263 | this.nameIndex 264 | ); 265 | 266 | // Check for max bank 267 | } else if (segmentDefaults.isBanked && (this.bank < 1 || this.bank > segmentDefaults.maxBank)) { 268 | // TODO fix column index in error message 269 | Errors.AddressError( 270 | this.file, 271 | `Invalid bank index, must be between 1 and ${ segmentDefaults.maxBank}`, 272 | this.nameIndex 273 | ); 274 | } 275 | 276 | 277 | // Set default offset if not specified 278 | if (this.baseOffset === null) { 279 | 280 | // If we're in bank 0 we just use the base offset 281 | if (this.bank === 0) { 282 | this.bankOffset = 0; 283 | this.baseOffset = segmentDefaults.baseOffset; 284 | 285 | // Otherwise we use the base offset + bank * bankSize 286 | // and also setup our bankOffset in order to correct label offsets 287 | } else { 288 | this.baseOffset = segmentDefaults.baseOffset + (this.bank - 1) * segmentDefaults.bankSize; 289 | this.bankOffset = this.baseOffset - segmentDefaults.baseOffset; 290 | } 291 | 292 | // Caculate end of segment als data must lie in >= offset && <= endOffset 293 | this.startOffest = this.baseOffset; 294 | this.endOffset = this.baseOffset + segmentDefaults.size; 295 | 296 | // For sections with specified offsets we still need to correct for banking 297 | } else { 298 | 299 | if (this.bank === 0) { 300 | 301 | this.bankOffset = 0; 302 | this.endOffset = segmentDefaults.baseOffset + segmentDefaults.size; 303 | this.startOffest = segmentDefaults.baseOffset; 304 | 305 | if (this.baseOffset < segmentDefaults.baseOffset || this.baseOffset > this.endOffset) { 306 | // TODO fix column index in error message 307 | Errors.AddressError( 308 | this.file, 309 | `Section offset out of range, must be between ${hex(segmentDefaults.baseOffset)} and ${hex(this.endOffset)}`, 310 | this.nameIndex 311 | ); 312 | } 313 | 314 | } else { 315 | 316 | const baseBankOffset = segmentDefaults.baseOffset + (this.bank - 1) * segmentDefaults.bankSize; 317 | this.endOffset = segmentDefaults.baseOffset + (this.bank - 1) * segmentDefaults.bankSize + segmentDefaults.size; 318 | this.bankOffset = this.baseOffset - segmentDefaults.baseOffset - (this.baseOffset - baseBankOffset); 319 | this.startOffest = segmentDefaults.baseOffset + (this.bank - 1) * segmentDefaults.bankSize; 320 | 321 | if (this.baseOffset < baseBankOffset || this.baseOffset > this.endOffset) { 322 | // TODO fix column index in error message 323 | Errors.AddressError( 324 | this.file, 325 | `Section offset out of range, must be between ${hex(baseBankOffset)} and ${hex(this.endOffset)}`, 326 | this.nameIndex 327 | ); 328 | } 329 | 330 | } 331 | 332 | } 333 | 334 | // Set initial resolved offset 335 | this.resolvedOffset = this.baseOffset; 336 | 337 | // Set storage flags 338 | this.isRam = segmentDefaults.isRam; 339 | 340 | }, 341 | 342 | toString(resolved) { 343 | 344 | if (resolved) { 345 | return `${this.name } in ${ this.segment }[${ this.bank }] at ${ 346 | hex(this.resolvedOffset) }-${ 347 | hex(this.resolvedOffset + this.size)}`; 348 | 349 | } 350 | return `${this.name } in ${ this.segment }[${ this.bank }] at ${ 351 | hex(this.baseOffset - this.bankOffset) }-${ 352 | hex(this.baseOffset - this.bankOffset + this.size)}`; 353 | 354 | }, 355 | 356 | toJSON() { 357 | 358 | return { 359 | type: 'Section', 360 | file: this.file.name, 361 | name: this.name, 362 | segment: this.segment, 363 | bank: this.bank, 364 | start: this.startOffest, 365 | end: this.endOffset, 366 | offset: this.resolvedOffset, 367 | bankOffset: this.bankOffset, 368 | writeable: this.isRam, 369 | entries: this.entries.map((entry) => { 370 | if (typeof entry.toJSON === 'function') { 371 | 372 | const data = entry.toJSON(); 373 | 374 | // rewrite label offsets to be in ROM address space 375 | if (data.type === 'Label') { 376 | data.offset += this.bankOffset; 377 | } 378 | 379 | return data; 380 | 381 | } 382 | return 0; 383 | 384 | }) 385 | }; 386 | 387 | } 388 | 389 | }; 390 | 391 | 392 | // Helpers -------------------------------------------------------------------- 393 | function hex(value) { 394 | value = value.toString(16).toUpperCase(); 395 | return `$${(new Array(4 - value.length + 1).join('0')) + value}`; 396 | } 397 | 398 | 399 | // Exports -------------------------------------------------------------------- 400 | module.exports = Section; 401 | 402 | -------------------------------------------------------------------------------- /lib/Compiler.js: -------------------------------------------------------------------------------- 1 | // Dependencies --------------------------------------------------------------- 2 | // ---------------------------------------------------------------------------- 3 | const path = require('path'), 4 | Linker = require('./linker/Linker'), 5 | Generator = require('./Generator'), 6 | SourceFile = require('./SourceFile'); 7 | 8 | require('colors'); 9 | 10 | 11 | // Assembly Code Compiler ----------------------------------------------------- 12 | // ---------------------------------------------------------------------------- 13 | function Compiler(silent, verbose, debug) { 14 | this.files = []; 15 | this.context = null; 16 | this.base = ''; 17 | this.silent = !!silent; 18 | this.verbose = !!verbose; 19 | this.debug = !!debug; 20 | } 21 | 22 | 23 | // Statics -------------------------------------------------------------------- 24 | Compiler.infoFromSource = function(source) { 25 | 26 | const c = new Compiler(false, false), 27 | file = new SourceFile( 28 | c, 29 | null, 30 | new Buffer(`SECTION "Memory",ROM0\n${ source }\n`), 31 | null, 32 | 0 33 | ); 34 | 35 | let opCycles = 0, 36 | opBytes = 0, 37 | dataBytes = 0; 38 | 39 | file.parse(); 40 | 41 | file.instructions.forEach((i) => { 42 | opCycles += i.cycles; 43 | opBytes += i.size; 44 | }); 45 | 46 | file.dataBlocks.forEach((d) => { 47 | dataBytes += d.size; 48 | }); 49 | 50 | return `${opCycles } cycles, ${ opBytes + dataBytes 51 | } bytes (${ opBytes } ops, ${ dataBytes } data)`; 52 | 53 | }; 54 | 55 | 56 | // Methods -------------------------------------------------------------------- 57 | Compiler.prototype = { 58 | 59 | // API -------------------------------------------------------------------- 60 | compile(files, verify) { 61 | 62 | // Set base path from first file to be included 63 | this.base = path.join(process.cwd(), path.dirname(files[0])); 64 | this.files.length = 0; 65 | 66 | // Parse and link 67 | this.parse(files); 68 | this.link(verify); 69 | 70 | }, 71 | 72 | optimize(unsafe) { 73 | Linker.optimize(this.files, unsafe); 74 | }, 75 | 76 | generate() { 77 | 78 | const start = Date.now(), 79 | rom = Generator.generateRom(this.files); 80 | 81 | if (rom.errors.length) { 82 | rom.errors.forEach((error) => { 83 | 84 | if (error === Generator.Errors.INVALID_ROM_SIZE) { 85 | this.error(null, 'Invalid ROM size specified in ROM header'); 86 | 87 | } else if (error === Generator.Errors.INVALID_RAM_SIZE) { 88 | this.error(null, 'Invalid RAM size specified in ROM header'); 89 | 90 | } else if (error === Generator.Errors.INVALID_CARTRIDGE_TYPE) { 91 | this.error(null, 'Invalid cartridge type size specified in ROM header'); 92 | 93 | } else if (error === Generator.Errors.MAPPER_UNSUPPORTED_RAM_SIZE) { 94 | this.error(null, 'Mapper set in ROM header does not support the RAM size specified in the ROM header'); 95 | 96 | } else if (error === Generator.Errors.MAPPER_UNSUPPORTED_ROM_SIZE) { 97 | this.error(null, 'Mapper set in ROM header does not support the ROM size specified in the ROM header'); 98 | } 99 | 100 | }); 101 | 102 | } else if (rom.warnings.length) { 103 | rom.warnings.forEach((warning) => { 104 | 105 | if (warning === Generator.Warnings.ROM_IS_PADDED) { 106 | this.warning(null, 0, 'Generated ROM image is bigger than the ROM size that was specified in the ROM header'); 107 | 108 | } else if (warning === Generator.Warnings.HEADER_SIZE_TOO_SMALL) { 109 | this.warning(null, 0, 'ROM size in header is smaller then the minimum required size for the generated ROM (automatically extended)'); 110 | 111 | } else if (warning === Generator.Warnings.INVALID_LOGO_DATA) { 112 | this.warning(null, 0, 'ROM contains invalid logo data (automatically patched to contain the correct values)'); 113 | } 114 | 115 | }); 116 | } 117 | 118 | // Reporting 119 | if (this.verbose) { 120 | this.log(null, `Generated rom in ${ Date.now() - start }ms`); 121 | } 122 | 123 | this.log(null, `Title: ${ rom.title}`); 124 | this.log(null, `Mapper: ${ rom.type.Mapper}`); 125 | 126 | if (rom.rom.size > 32768) { 127 | this.log(null, `ROM: ${ rom.rom.size } bytes ( ${ rom.rom.size } bytes in ${ rom.rom.banks } bank(s) )`); 128 | 129 | } else { 130 | this.log(null, `ROM: ${ rom.rom.size } bytes ( standard ROM only, 32768 bytes )`); 131 | } 132 | 133 | if (rom.ram.size > 0) { 134 | this.log(null, `RAM: ${ rom.ram.size + 8192 } bytes ( internal 8192 bytes, ${ rom.ram.size } bytes in ${ rom.ram.banks } bank(s) )`); 135 | 136 | } else if (rom.ram.size) { 137 | this.log(null, `RAM: ${ rom.ram.size + 8192 } bytes ( internal RAM only, 8192 bytes )`); 138 | } 139 | 140 | this.log(null, `BATTERY: ${ rom.type.Battery ? 'Yes' : 'None'}`); 141 | process.stdout.write('\n'); 142 | 143 | return rom.buffer; 144 | 145 | }, 146 | 147 | json() { 148 | return JSON.stringify(Linker.getAllSections(this.files).map((l) => { 149 | return l.toJSON(); 150 | 151 | }), null, ' '); 152 | }, 153 | 154 | symbols() { 155 | 156 | const symbols = []; 157 | 158 | this.files.forEach((file) => { 159 | symbols.push.apply(symbols, file.symbols()); 160 | }); 161 | 162 | symbols.sort((a, b) => { 163 | return a.offset - b.offset; 164 | }); 165 | 166 | return symbols.map((s) => { 167 | const name = s.parent ? s.parent.name + s.name : s.name; 168 | return `${padHexValue(s.section.bank, 2, '0') }:${ padHexValue(s.offset, 4, '0') } ${ name}`; 169 | 170 | }).join('\n'); 171 | 172 | }, 173 | 174 | unused() { 175 | 176 | this.files.forEach((f) => { 177 | 178 | f.labels.filter((l) => { 179 | return l.references === 0; 180 | 181 | }).map((f) => { 182 | this.warning(f.file, f.index, `Unused label: ${ f.name}`); 183 | }); 184 | 185 | f.variables.filter((v) => { 186 | return v.references === 0; 187 | 188 | }).map((v) => { 189 | this.warning(v.file, v.index, `Unused variable: ${ v.name}`); 190 | }); 191 | 192 | }); 193 | 194 | }, 195 | 196 | mapping() { 197 | 198 | function pad(value, size, ch) { 199 | return value + (new Array(size - value.length + 1).join(ch)); 200 | } 201 | 202 | function rpad(value, size, ch) { 203 | return (new Array(size - value.length + 1).join(ch)) + value; 204 | } 205 | 206 | function row(from, to, free, used, name) { 207 | 208 | from = `$${ rpad(from.toString(16), 4, '0')}`; 209 | to = `$${ rpad(to.toString(16), 4, '0')}`; 210 | free = `(${ rpad(free.toString(), 5, ' ') } bytes)`; 211 | 212 | if (used) { 213 | return ` - ${ from }-${ to } ######## ${ free } (${ name })`; 214 | 215 | } 216 | return (` - ${ from }-${ to } ........ ${ free}`).grey; 217 | 218 | 219 | } 220 | 221 | const segmentMap = {}, 222 | segmentList = []; 223 | 224 | Linker.getAllSections(this.files).map((s) => { 225 | 226 | const id = `${s.segment }_${ s.bank}`; 227 | if (!segmentMap.hasOwnProperty(id)) { 228 | segmentMap[id] = { 229 | start: s.startOffest - s.bankOffset, 230 | end: s.endOffset - s.bankOffset, 231 | name: s.segment, 232 | bank: s.bank, 233 | size: s.endOffset - s.startOffest, 234 | used: s.size, 235 | usage: [[s.resolvedOffset - s.bankOffset, s.size, s.name]] 236 | }; 237 | 238 | segmentList.push(segmentMap[id]); 239 | 240 | } else { 241 | segmentMap[id].used += s.size; 242 | segmentMap[id].usage.push([s.resolvedOffset - s.bankOffset, s.size, s.name]); 243 | segmentMap[id].usage.sort((a, b) => { 244 | return a[0] - b[0]; 245 | }); 246 | } 247 | 248 | }); 249 | 250 | segmentList.sort((a, b) => { 251 | if (a.start === b.start) { 252 | return a.bank - b.bank; 253 | 254 | } 255 | return a.start - b.start; 256 | 257 | }); 258 | 259 | const map = segmentList.map((segment) => { 260 | 261 | const usage = [], 262 | first = segment.usage[0]; 263 | 264 | if (first[0] > segment.start) { 265 | usage.push(row(0, first[0] - 1, first[0], false)); 266 | } 267 | 268 | segment.usage.forEach((u, index) => { 269 | 270 | const next = segment.usage[index + 1]; 271 | usage.push(row(u[0], u[0] + u[1] - 1, u[1], true, u[2])); 272 | 273 | if (next && next[0] > u[0] + u[1]) { 274 | usage.push(row(u[0] + u[1], next[0] - 1, next[0] - (u[0] + u[1]), false)); 275 | } 276 | 277 | }); 278 | 279 | const last = segment.usage[segment.usage.length - 1]; 280 | if (last[0] + last[1] < segment.end) { 281 | usage.push(row(last[0] + last[1], segment.end - 1, segment.end - (last[0] + last[1]), false)); 282 | } 283 | 284 | let header; 285 | if (segment.bank) { 286 | header = pad(`${segment.name }[${ segment.bank }]`, 10, ' '); 287 | 288 | } else { 289 | header = pad(segment.name, 10, ' '); 290 | } 291 | 292 | return `${(` ${ 293 | header } @ $${ 294 | rpad(segment.start.toString(16), 4, '0') 295 | } (${ rpad(segment.used.toString(), 5, ' ') 296 | } of ${ rpad(segment.size.toString(), 5, ' ') 297 | } bytes used)` 298 | + ` ( ${ rpad((segment.size - segment.used).toString(), 5, ' ') 299 | } free)`).cyan }\n\n${ usage.join('\n') }\n\n`; 300 | 301 | }); 302 | 303 | return map.join('\n'); 304 | 305 | }, 306 | 307 | 308 | // Internals -------------------------------------------------------------- 309 | includeFile(parent, file, section, index) { 310 | const sourceFile = new SourceFile(this, parent, file, section, index); 311 | this.files.push(sourceFile); 312 | sourceFile.parse(this.debug); 313 | return sourceFile; 314 | }, 315 | 316 | parse(files) { 317 | 318 | const start = Date.now(); 319 | 320 | files.forEach((file) => { 321 | this.includeFile(null, path.join(process.cwd(), file), 0, null, 0, 0); 322 | }); 323 | 324 | this.verbose && this.log(null, `Parsed ${ this.files.length } file(s) in ${ Date.now() - start }ms`); 325 | 326 | }, 327 | 328 | link(verify) { 329 | const start = Date.now(); 330 | Linker.init(this.files); 331 | Linker.link(this.files, verify, this.debug); 332 | this.verbose && this.log(null, `Linked ${ this.files.length } file(s) in ${ Date.now() - start }ms`); 333 | }, 334 | 335 | 336 | // Error Handling and Logging --------------------------------------------- 337 | error(file, error) { 338 | 339 | if (!this.silent) { 340 | if (file) { 341 | process.stderr.write( 342 | `\n${ 343 | file.getLineSource(error.index).source 344 | }\n\n${ 345 | (`${error.name }: `).red 346 | }${error.message 347 | }\n${ (` at ${ file.getPath(error.index)}`).yellow }\n\n` 348 | ); 349 | 350 | } else { 351 | process.stderr.write(`${'[error]'.red } ${ error.red }\n`); 352 | } 353 | } 354 | 355 | process.exit(1); 356 | 357 | }, 358 | 359 | warning(file, index, message) { 360 | if (!this.silent) { 361 | if (file) { 362 | process.stdout.write( 363 | `${message 364 | }\n${ (` at ${ file.getPath(index)}`).yellow }\n\n` 365 | ); 366 | 367 | } else { 368 | process.stdout.write(`${'[warning]'.yellow } ${ message.grey }\n`); 369 | } 370 | } 371 | }, 372 | 373 | log(file, message) { 374 | 375 | file = file ? file.getPath(undefined, undefined, true): '[gbasm]'; 376 | 377 | if (!this.silent) { 378 | process.stdout.write(`${file.blue } ${ message }\n`); 379 | } 380 | 381 | } 382 | 383 | }; 384 | 385 | 386 | // Helpers -------------------------------------------------------------------- 387 | function padHexValue(value, size, pad) { 388 | const s = value.toString(16).toUpperCase(); 389 | return new Array((size + 1) - s.length).join(pad) + s; 390 | } 391 | 392 | 393 | // Exports -------------------------------------------------------------------- 394 | module.exports = Compiler; 395 | 396 | -------------------------------------------------------------------------------- /lib/Generator.js: -------------------------------------------------------------------------------- 1 | // ROM Generation Logic ------------------------------------------------------- 2 | // ---------------------------------------------------------------------------- 3 | const Generator = { 4 | 5 | // Static Methods --------------------------------------------------------- 6 | generateRom(files) { 7 | 8 | // Generate code and data for all files 9 | let buffer = Generator.getRomBuffer(files); 10 | files.forEach((file) => { 11 | Generator.generateFile(file, buffer); 12 | }); 13 | 14 | // Pad if ROM size in header is bigger than generated buffer 15 | const rom = Generator.parseRom(buffer); 16 | if (rom.rom.size > buffer.length) { 17 | buffer = Generator.getPaddedRomBuffer(buffer, rom.rom.size); 18 | rom.warnings.push(Generator.Warnings.ROM_IS_PADDED); 19 | 20 | // Warn if generated buffer is bigger than the specified header size 21 | } else if (buffer.length > rom.rom.size) { 22 | rom.warnings.push(Generator.Warnings.HEADER_SIZE_TOO_SMALL); 23 | } 24 | 25 | rom.buffer = buffer; 26 | 27 | return rom; 28 | 29 | }, 30 | 31 | generateFile(file, buffer) { 32 | 33 | // Write instructions to ROM 34 | file.instructions.forEach((instr) => { 35 | 36 | let index = instr.offset; 37 | for(let i = 0; i < instr.raw.length; i++) { 38 | buffer[index++] = instr.raw[i]; 39 | } 40 | 41 | // Write arguments 42 | if (instr.resolvedArg) { 43 | if (instr.bits === 8) { 44 | buffer[index] = instr.resolvedArg; 45 | 46 | } else if (instr.bits === 16) { 47 | buffer[index] = instr.resolvedArg & 0xff; 48 | buffer[index + 1] = (instr.resolvedArg >> 8) & 0xff; 49 | } 50 | } 51 | 52 | }); 53 | 54 | // Write data blocks to ROM 55 | file.dataBlocks.forEach((data) => { 56 | 57 | let index = data.offset, i; 58 | 59 | // Empty DS 60 | if (data.size > data.resolvedValues.length * (data.bits / 8)) { 61 | for(i = 0; i < data.size; i++) { 62 | buffer[index++] = 0; 63 | } 64 | 65 | // DB / DS 66 | } else if (data.bits === 8) { 67 | for(i = 0; i < data.resolvedValues.length; i++) { 68 | buffer[index++] = data.resolvedValues[i]; 69 | } 70 | 71 | // DW 72 | } else if (data.bits === 16) { 73 | for(i = 0; i < data.resolvedValues.length; i++) { 74 | buffer[index++] = data.resolvedValues[i] & 0xff; 75 | buffer[index++] = (data.resolvedValues[i] >> 8) & 0xff; 76 | } 77 | } 78 | 79 | }); 80 | 81 | // Copy binary includes to ROM 82 | file.binaryIncludes.forEach((binary) => { 83 | binary.getBuffer().copy(buffer, binary.offset); 84 | }); 85 | 86 | }, 87 | 88 | 89 | // Size Calculation ------------------------------------------------------- 90 | getRomBuffer(files) { 91 | const buffer = new Buffer(Generator.getRequiredRomSize(files)); 92 | for(let i = 0; i < buffer.length; i++) { 93 | buffer[i] = 0; 94 | } 95 | return buffer; 96 | }, 97 | 98 | getPaddedRomBuffer(buffer, size) { 99 | 100 | const paddingSize = size - buffer.length, 101 | paddingBuffer = new Buffer(paddingSize); 102 | 103 | for(let i = 0; i < paddingSize; i++) { 104 | paddingBuffer[i] = 0; 105 | } 106 | 107 | return Buffer.concat([buffer, paddingBuffer]); 108 | 109 | }, 110 | 111 | getRequiredRomSize(files) { 112 | return files.map(Generator.getRequiredFileSize).sort((a, b) => { 113 | return b - a; 114 | 115 | })[0] || 0x8000; // Minimum ROM size is 32kbyte 116 | }, 117 | 118 | getRequiredFileSize(file) { 119 | 120 | let v = file.sections.filter((s) => { 121 | return s.segment === 'ROM0' || s.segment === 'ROMX'; 122 | 123 | }).map((s) => { 124 | return Math.floor(s.baseOffset / 0x4000); 125 | 126 | }).sort((a, b) => { 127 | return b - a; 128 | 129 | })[0] || 1; 130 | 131 | // Get nearest upper power of two 132 | v |= v >> 1; 133 | v |= v >> 2; 134 | v |= v >> 4; 135 | v |= v >> 8; 136 | v |= v >> 16; 137 | v++; 138 | 139 | // Returns 32kb, 64kb, 128kb, 256kb etc. 140 | return v * 0x4000; 141 | 142 | }, 143 | 144 | 145 | // ROM Handling ----------------------------------------------------------- 146 | parseRom(buffer) { 147 | 148 | const rom = Generator.getRomInfo(buffer), 149 | g = Generator; 150 | 151 | // Validate logo 152 | for(let i = 0; i < g.NINTENDO_LOGO.length; i++) { 153 | if (g.NINTENDO_LOGO[i] !== rom.logo[i]) { 154 | rom.warnings.push(Generator.Warnings.INVALID_LOGO_DATA); 155 | Generator.applyLogo(buffer); 156 | break; 157 | } 158 | } 159 | 160 | // Validate cartridge type 161 | if (g.TYPES.hasOwnProperty(rom.type)) { 162 | rom.type = g.TYPES[rom.type]; 163 | 164 | } else { 165 | rom.errors.push(Generator.Errors.INVALID_CARTRIDGE_TYPE); 166 | return rom; 167 | } 168 | 169 | // Validate ROM size 170 | if (g.ROM_SIZES.hasOwnProperty(rom.rom)) { 171 | 172 | // Check if cartridge type supports it 173 | if (g.ROM_SIZES[rom.rom] > g.ROM_SIZES[g.MAX_ROM_SIZE[rom.type.Mapper]][0]) { 174 | rom.errors.push(Generator.Errors.MAPPER_UNSUPPORTED_ROM_SIZE); 175 | return rom; 176 | 177 | } 178 | rom.rom = { 179 | size: g.ROM_SIZES[rom.rom][0] * 1024, 180 | banks: g.ROM_SIZES[rom.rom][1] 181 | }; 182 | 183 | 184 | } else { 185 | rom.errors.push(Generator.Errors.INVALID_ROM_SIZE); 186 | return rom; 187 | } 188 | 189 | // Validate RAM size 190 | if (g.RAM_SIZES.hasOwnProperty(rom.ram)) { 191 | 192 | // Check if cartridge type supports it 193 | if (g.RAM_SIZES[rom.ram] > g.RAM_SIZES[g.MAX_RAM_SIZE[rom.type.Mapper]][0]) { 194 | rom.errors.push(Generator.Errors.MAPPER_UNSUPPORTED_RAM_SIZE); 195 | return rom; 196 | 197 | } 198 | rom.ram = { 199 | size: g.RAM_SIZES[rom.ram][0] * 1024, 200 | banks: g.RAM_SIZES[rom.ram][1] 201 | }; 202 | 203 | 204 | } else { 205 | rom.errors.push(Generator.Errors.INVALID_RAM_SIZE); 206 | return rom; 207 | } 208 | 209 | // Validate country code 210 | if (g.COUNTRY_CODES.indexOf(rom.countryCode) === -1) { 211 | rom.errors.push(Generator.Errors.INVALID_COUNTRY_CODE); 212 | return rom; 213 | } 214 | 215 | 216 | // Set Checksums 217 | Generator.setRomChecksums(buffer); 218 | 219 | // Checksums 220 | rom.headerChecksum = buffer[0x14D]; 221 | rom.romChecksum = (buffer[0x14D] << 8) & buffer[0x14E]; 222 | 223 | return rom; 224 | 225 | }, 226 | 227 | getRomInfo(buffer) { 228 | return { 229 | 230 | // General 231 | logo: buffer.slice(0x104, 0x134), 232 | title: buffer.slice(0x134, 0x143).toString('ascii'), 233 | colorGameBoyFlag: buffer[0x143], 234 | 235 | // Super Gameboy related 236 | sgbLicenseeCode: (buffer[0x144] << 8) & buffer[0x145], 237 | sgbFlag: buffer[0x146], 238 | 239 | // Cartrige info 240 | type: buffer[0x147], 241 | rom: buffer[0x148], 242 | ram: buffer[0x149], 243 | countryCode: buffer[0x14A], 244 | licenseeCode: buffer[0x14B], // 33 = super gameboy, will use the code from above 245 | versionNumber: buffer[0x14C], 246 | 247 | // Checksums 248 | headerChecksum: buffer[0x14D], 249 | romChecksum: (buffer[0x14D] << 8) & buffer[0x14E], 250 | 251 | // Warning and Errors generated 252 | warnings: [], 253 | errors: [] 254 | 255 | }; 256 | }, 257 | 258 | setRomChecksums(buffer) { 259 | 260 | // Header 261 | let checksum = 0, i; 262 | for(i = 0x134; i < 0x14D; i++) { 263 | checksum = (((checksum - buffer[i]) & 0xff) - 1) & 0xff; 264 | } 265 | 266 | buffer[0x14D] = checksum; 267 | 268 | // ROM 269 | checksum = 0; 270 | for(i = 0; i < buffer.length; i++) { 271 | if (i !== 0x14E && i !== 0x14F) { 272 | checksum += buffer[i]; 273 | } 274 | } 275 | 276 | buffer[0x14E] = (checksum >> 8) & 0xff; 277 | buffer[0x14F] = checksum & 0xff; 278 | 279 | }, 280 | 281 | applyLogo(buffer) { 282 | for(let i = 0; i < Generator.NINTENDO_LOGO.length; i++) { 283 | buffer[i + 0x104] = Generator.NINTENDO_LOGO[i]; 284 | } 285 | }, 286 | 287 | 288 | // Error and Warning Constants -------------------------------------------- 289 | Warnings: { 290 | ROM_IS_PADDED: 1, 291 | HEADER_SIZE_TOO_SMALL: 2, 292 | INVALID_LOGO_DATA: 3 293 | }, 294 | 295 | Errors: { 296 | INVALID_ROM_SIZE: 1, 297 | INVALID_RAM_SIZE: 2, 298 | INVALID_CARTRIDGE_TYPE: 3, 299 | MAPPER_UNSUPPORTED_RAM_SIZE: 4, 300 | MAPPER_UNSUPPORTED_ROM_SIZE: 5, 301 | INVALID_COUNTRY_CODE: 6 302 | }, 303 | 304 | 305 | // ROM Constants ---------------------------------------------------------- 306 | // ------------------------------------------------------------------------ 307 | TYPES: { 308 | 0x00: cartridgeType('ROM'), 309 | 0x01: cartridgeType('MBC1'), 310 | 0x02: cartridgeType('MBC1+RAM'), 311 | 0x03: cartridgeType('MBC1+RAM+BATTERY'), 312 | 0x05: cartridgeType('MBC2'), 313 | 0x06: cartridgeType('MBC2+BATTERY'), 314 | 0x08: cartridgeType('ROM+RAM'), 315 | 0x09: cartridgeType('ROM+RAM+BATTERY'), 316 | 0x0B: cartridgeType('MMM01'), 317 | 0x0C: cartridgeType('MMM01+RAM'), 318 | 0x0D: cartridgeType('MMM01+RAM+BATTERY'), 319 | 0x0F: cartridgeType('MBC3+TIMER+BATTERY'), 320 | 0x10: cartridgeType('MBC3+TIMER+RAM+BATTERY'), 321 | 0x11: cartridgeType('MBC3'), 322 | 0x12: cartridgeType('MBC3+RAM'), 323 | 0x13: cartridgeType('MBC3+RAM+BATTERY'), 324 | 0x15: cartridgeType('MBC4'), 325 | 0x16: cartridgeType('MBC4+RAM'), 326 | 0x17: cartridgeType('MBC4+RAM+BATTERY'), 327 | 0x19: cartridgeType('MBC5'), 328 | 0x1A: cartridgeType('MBC5+RAM'), 329 | 0x1B: cartridgeType('MBC5+RAM+BATTERY'), 330 | 0x1C: cartridgeType('MBC5+RUMBLE'), 331 | 0x1D: cartridgeType('MBC5+RUMBLE+RAM'), 332 | 0x1E: cartridgeType('MBC5+RUMBLE+RAM+BATTERY'), 333 | 0xFC: cartridgeType('ROM+POCKET CAMERA'), 334 | 0xFD: cartridgeType('ROM+BANDAI TAMA5'), 335 | 0xFE: cartridgeType('HuC3'), 336 | 0xFF: cartridgeType('HuC1+RAM+BATTERY') 337 | }, 338 | 339 | NINTENDO_LOGO: [ 340 | 0xCE, 0xED, 0x66, 0x66, 0xCC, 0x0D, 0x00, 0x0B, 341 | 0x03, 0x73, 0x00, 0x83, 0x00, 0x0C, 0x00, 0x0D, 342 | 0x00, 0x08, 0x11, 0x1F, 0x88, 0x89, 0x00, 0x0E, 343 | 0xDC, 0xCC, 0x6E, 0xE6, 0xDD, 0xDD, 0xD9, 0x99, 344 | 0xBB, 0xBB, 0x67, 0x63, 0x6E, 0x0E, 0xEC, 0xCC, 345 | 0xDD, 0xDC, 0x99, 0x9F, 0xBB, 0xB9, 0x33, 0x3E 346 | ], 347 | 348 | ROM_SIZES: { 349 | 0x00: [ 32, 0], 350 | 0x01: [ 64, 4], 351 | 0x02: [ 128, 8], 352 | 0x03: [ 256, 16], 353 | 0x04: [ 512, 32], 354 | 0x05: [1024, 64], // Only 63 banks used by MBC1 355 | 0x06: [2048, 128], // Only 125 banks used by MBC1 356 | 0x07: [4096, 256], 357 | 0x52: [1152, 72], 358 | 0x53: [1280, 80], 359 | 0x54: [1536, 96] 360 | }, 361 | 362 | RAM_SIZES: { 363 | 0x00: [ 0, 0], // None (must always be set with MBC2 even though it has 512x4 bits RAM) 364 | 0x01: [ 2, 1], // 1 Bank (only one quarter is used) 365 | 0x02: [ 8, 1], // 1 Bank (Full) 366 | 0x03: [32, 4], // 4 Banks 367 | 0x04: [128, 16] // 16 Banks 368 | }, 369 | 370 | MAX_ROM_SIZE: { 371 | ROM: 0x00, 372 | MBC1: 0x06, 373 | MBC2: 0x03, 374 | MBC3: 0x06, 375 | MBC4: 0xFF, // ??? 376 | MBC5: 0x07 377 | }, 378 | 379 | MAX_RAM_SIZE: { 380 | ROM: 0x00, 381 | MBC1: 0x03, 382 | MBC2: 0x00, // 512x4 bits RAM built into the MBC2 chip, only the lower 4 bits can be read 383 | MBC3: 0x03, 384 | MBC4: 0xFF, // ??? 385 | MBC5: 0x04 386 | }, 387 | 388 | DESTINATION: { 389 | 0x00: 'Japanese', 390 | 0x01: 'Non-Japanese' 391 | }, 392 | 393 | LICENSEES: { 394 | 0x33: 'Super Gameboy', 395 | 0x79: 'Accolade', 396 | 0xA4: 'Konami' 397 | }, 398 | 399 | COUNTRY_CODES: [ 400 | 0x00, 401 | 0x01 402 | ] 403 | 404 | }; 405 | 406 | 407 | // Helper --------------------------------------------------------------------- 408 | function cartridgeType(ident) { 409 | return { 410 | Mapper: ident.split('+')[0], 411 | Ram: ident.indexOf('RAM') !== -1, 412 | Battery: ident.indexOf('BATTERY') !== -1, 413 | Timer: ident.indexOf('TIMER') !== -1, 414 | Rumble: ident.indexOf('RUMBLE') !== -1, 415 | Camera: ident === 'ROM+POCKET CAMERA', 416 | BandaiTama: ident === 'ROM+BANDAI TAMA5', 417 | }; 418 | } 419 | 420 | 421 | // Exports -------------------------------------------------------------------- 422 | module.exports = Generator; 423 | 424 | -------------------------------------------------------------------------------- /lib/linker/FileLinker.js: -------------------------------------------------------------------------------- 1 | // Dependencies --------------------------------------------------------------- 2 | // ---------------------------------------------------------------------------- 3 | const Instruction = require('../data/Instruction'), 4 | optimize = require('./Optimizer'), 5 | Expression = require('../parser/Expression'), 6 | Linker = require('./Linker'), 7 | Errors = require('../Errors'); 8 | 9 | 10 | // Source File Linking Logic -------------------------------------------------- 11 | // ---------------------------------------------------------------------------- 12 | const FileLinker = { 13 | 14 | // Static Methods --------------------------------------------------------- 15 | init(file, debug) { 16 | 17 | // Recursively expand macros in all sections 18 | file.sections.forEach((f) => FileLinker.expandMacros(f, debug)); 19 | 20 | // Resolve any outstanding sizes for data and variables 21 | if (file.unresolvedSizes.length) { 22 | 23 | file.unresolvedSizes.forEach((entry) => { 24 | 25 | const size = Linker.resolveValue( 26 | file, 27 | entry.size, 28 | entry.offset, 29 | entry.index, 30 | false, 31 | [] 32 | ); 33 | 34 | entry.size = typeof size === 'string' ? size.length : size; 35 | 36 | }); 37 | 38 | // Clear the list 39 | file.unresolvedSizes.length = 0; 40 | 41 | } 42 | 43 | // Now recalculate the offsets of all entries within all sections 44 | file.sections.forEach((section) => { 45 | section.calculateOffsets(); 46 | }); 47 | 48 | // For relative jumps, we check if the target is a OFFSET and switch 49 | // it with the actual instruction it points to. 50 | // This is required to preserve relative jump target through code 51 | // optimization were instructions and thus their size and address might 52 | // be altered. 53 | if (file.relativeJumpTargets.length) { 54 | 55 | file.relativeJumpTargets.forEach((instr) => { 56 | 57 | const target = findInstructionByOffset(file, instr.offset, instr.arg.value); 58 | if (!target) { 59 | Errors.AddressError( 60 | file, 61 | 'Invalid jump offset, must point at the address of a valid instruction', 62 | instr.index 63 | ); 64 | 65 | } else { 66 | instr.arg = target; 67 | } 68 | 69 | }); 70 | 71 | // Clear the list 72 | file.relativeJumpTargets.length = 0; 73 | 74 | } 75 | 76 | }, 77 | 78 | link(file) { 79 | FileLinker.resolveInstructions(file); 80 | FileLinker.resolveDataBlocks(file); 81 | }, 82 | 83 | expandMacros(section, debug) { 84 | 85 | let expanded, 86 | depth = 0; 87 | 88 | do { 89 | 90 | expanded = false; 91 | 92 | for(let i = 0, l = section.entries.length; i < l; i++) { 93 | 94 | const entry = section.entries[i]; 95 | if (entry instanceof Expression.Call) { 96 | 97 | // Remove the macro entry 98 | section.entries.splice(i, 1); 99 | i--; 100 | 101 | const macro = Linker.resolveMacro(entry, section.file, 0, []); 102 | if (macro.isBuiltin) { 103 | Errors.ArgumentError( 104 | section.file, 105 | `Cannot expand built-in MACRO ${ macro.name}`, 106 | entry.callee.index 107 | ); 108 | 109 | } else if (macro.isExpression) { 110 | Errors.ArgumentError( 111 | section.file, 112 | `Cannot expand user defined expression MACRO ${ macro.name}`, 113 | entry.callee.index 114 | ); 115 | 116 | } else { 117 | // Parse and expand the macro body into the current position in the file 118 | expanded = true; 119 | macro.callee.expand( 120 | macro.name, section, i + 1, macro.args, debug 121 | ); 122 | } 123 | 124 | 125 | // Break out when there are too many levels of recursion 126 | if (depth > 32) { 127 | Errors.MacroError( 128 | section.file, 129 | 'Maximum macro expansion depth reached (32 levels)', 130 | entry.callee.index 131 | ); 132 | } 133 | 134 | } 135 | 136 | } 137 | 138 | depth++; 139 | 140 | } while(expanded); 141 | 142 | }, 143 | 144 | 145 | // Name Resolution -------------------------------------------------------- 146 | resolveInstructions(file) { 147 | 148 | // Re-sort all instructions since macros only append 149 | file.instructions.sort((a, b) => { 150 | return a.offset - b.offset; 151 | }); 152 | 153 | for(let i = 0, l = file.instructions.length; i < l; i++) { 154 | 155 | const instr = file.instructions[i]; 156 | if (!instr.arg) { 157 | continue; 158 | } 159 | 160 | // Handle targets of relative jump instructions 161 | let value; 162 | if (instr.arg instanceof Instruction) { 163 | value = instr.arg.offset - instr.offset; 164 | 165 | // Resolve the value of the instructions argument 166 | } else { 167 | value = Linker.resolveValue( 168 | file, 169 | instr.arg, 170 | instr.offset, 171 | instr.arg.index, 172 | instr.mnemonic === 'jr', 173 | [] 174 | ); 175 | } 176 | 177 | // Check if we could resolve the value 178 | if (value === null) { 179 | Errors.ReferenceError( 180 | file, 181 | `"${ instr.arg.value }" could not be resolved`, 182 | instr.index 183 | ); 184 | 185 | // Validate signed argument range 186 | } else if (instr.isSigned && (value < -127 || value > 128)) { 187 | 188 | if (instr.mnemonic === 'jr') { 189 | Errors.AddressError( 190 | file, 191 | `Invalid relative jump value of ${ value } bytes, must be -127 to 128 bytes`, 192 | instr.index 193 | ); 194 | 195 | } else { 196 | Errors.ArgumentError( 197 | file, 198 | `Invalid signed byte argument value of ${ value }, must be between -127 and 128`, 199 | instr.index 200 | ); 201 | } 202 | 203 | } else if (instr.isBit && (value < 0 || value > 7)) { 204 | Errors.ArgumentError( 205 | file, 206 | `Invalid bit index value of ${ value }, must be between 0 and 7`, 207 | instr.index 208 | ); 209 | 210 | } else if (instr.bits === 8 && (value < -127 || value > 255)) { 211 | Errors.ArgumentError( 212 | file, 213 | `Invalid byte argument value of ${ value }, must be between -128 and 255`, 214 | instr.index 215 | ); 216 | 217 | } else if (instr.bits === 16 && (value < -32767 || value > 65535)) { 218 | if (instr.mnemonic === 'jp' || instr.mnemonic === 'call') { 219 | Errors.AddressError( 220 | file, 221 | `Invalid jump address value of ${ value }, must be between 0 and 65535`, 222 | instr.index 223 | ); 224 | 225 | } else { 226 | Errors.ArgumentError( 227 | file, 228 | `Invalid word argument value of ${ value }, must be between -32767 and 65535`, 229 | instr.index 230 | ); 231 | } 232 | 233 | // Convert signed values to twos complement 234 | } else if (value < 0) { 235 | if (instr.bits === 8) { 236 | 237 | // Correct jump offsets for relative jumps 238 | if (instr.mnemonic === 'jr') { 239 | if (value < 0) { 240 | value -= 2; 241 | } 242 | } 243 | 244 | value = 256 - Math.abs(value); 245 | 246 | } else { 247 | value = 65536 - Math.abs(value); 248 | } 249 | 250 | } else { 251 | 252 | // Correct jump offsets for relative jumps 253 | if (instr.mnemonic === 'jr') { 254 | if (value > 0) { 255 | value -= 2; 256 | } 257 | } 258 | 259 | } 260 | 261 | // Replace arg with resolved value 262 | instr.resolvedArg = value; 263 | 264 | } 265 | 266 | }, 267 | 268 | resolveDataBlocks(file) { 269 | 270 | file.dataBlocks.forEach((data) => { 271 | 272 | for(let i = 0, l = data.values.length; i < l; i++) { 273 | 274 | let value = data.values[i]; 275 | 276 | // Resolve the correct value 277 | const resolved = Linker.resolveValue( 278 | file, 279 | value, 280 | value.offset, 281 | value.index, 282 | false, 283 | [] 284 | ); 285 | 286 | // DS can also store strings by splitting them 287 | if (data.isFixedSize) { 288 | 289 | // Only strings can be contained in fixed sized sections 290 | if (typeof resolved !== 'string') { 291 | Errors.ArgumentError( 292 | file, 293 | 'Only string values are allow for fixed sized data storage', 294 | data.index 295 | ); 296 | 297 | } else if (resolved.length > data.size) { 298 | Errors.ArgumentError( 299 | file, 300 | `String length of ${ resolved.length 301 | } exceeds allocated storage size of ${ data.size } bytes`, 302 | data.index 303 | ); 304 | } 305 | 306 | // Pad strings with 0x00 307 | value = new Array(data.size); 308 | for(let e = 0; e < data.size; e++) { 309 | if (e < resolved.length) { 310 | value[e] = resolved.charCodeAt(e); 311 | 312 | } else { 313 | value[e] = 0; 314 | } 315 | } 316 | 317 | data.resolvedValues = value; 318 | 319 | // Check bit width 320 | } else if (data.bits === 8 && (resolved < -127 || resolved > 255)) { 321 | Errors.ArgumentError( 322 | file, 323 | `Invalid byte argument value of ${ resolved 324 | } for data storage, must be between -128 and 255`, 325 | data.index 326 | ); 327 | 328 | } else if (data.bits === 16 && (resolved < -32767 || resolved > 65535)) { 329 | Errors.ArgumentError( 330 | file, 331 | `Invalid word argument value of ${ resolved 332 | } for data storage, must be between -32767 and 65535`, 333 | data.index 334 | ); 335 | 336 | // Convert signed values to twos complement 337 | } else if (resolved < 0) { 338 | if (data.bits === 8) { 339 | data.resolvedValues[i] = 256 - Math.abs(resolved); 340 | 341 | } else { 342 | data.resolvedValues[i] = 65536 - Math.abs(resolved); 343 | } 344 | 345 | } else { 346 | data.resolvedValues[i] = resolved; 347 | } 348 | 349 | } 350 | 351 | }); 352 | 353 | }, 354 | 355 | resolveLocalLabel(file, localLabel) { 356 | 357 | // Find the first global label which sits infront of the target localLabel 358 | let i, l, parent = null; 359 | 360 | for(i = 0, l = file.labels.length; i < l; i++) { 361 | 362 | const label = file.labels[i]; 363 | if (!label.parent) { 364 | // TODO this doesn't work nicely with macros 365 | // TODO one "fix" could be to simply increase the index of all 366 | // tokens within a macro by the max index in the file it originates in 367 | // TODO this wouldn't work cross file though 368 | // TODO so we need to add the index of the macro call site instead 369 | if (label.index > localLabel.index ) { 370 | break; 371 | 372 | } else { 373 | parent = label; 374 | 375 | // For macro tokens, we check for a prefix match here since the 376 | // will have the same index value which would cause us 377 | // to always choose the label from the last expasion 378 | if (parent.name.substring(0, parent.name.indexOf('-')) === localLabel.value.substring(1, localLabel.value.indexOf('-'))) { 379 | break; 380 | } 381 | } 382 | } 383 | 384 | } 385 | 386 | if (parent) { 387 | 388 | // Now find the first children with the labels name 389 | for(i = 0, l = parent.children.length; i < l; i++) { 390 | if (parent.children[i].name === localLabel.value) { 391 | return parent.children[i]; 392 | } 393 | } 394 | 395 | } 396 | 397 | return null; 398 | 399 | }, 400 | 401 | 402 | // Optimization ----------------------------------------------------------- 403 | optimize(file, unsafe) { 404 | 405 | let optimized, 406 | droppedInstructions = 0; 407 | 408 | do { 409 | 410 | optimized = false; 411 | 412 | for(let i = 0, l = file.instructions.length; i < l; i++) { 413 | 414 | const affectedInstructions = optimize( 415 | file.instructions[i], 416 | unsafe, 417 | i < l - 1 ? file.instructions[i + 1] : null, 418 | i < l - 2 ? file.instructions[i + 2] : null, 419 | i < l - 3 ? file.instructions[i + 3] : null 420 | ); 421 | 422 | if (affectedInstructions > 0) { 423 | 424 | optimized = true; 425 | 426 | // If more than 1 instruction is affected remove 427 | // the superfluous ones 428 | if (affectedInstructions > 1) { 429 | 430 | // Reduce total instruction count 431 | l -= affectedInstructions - 1; 432 | 433 | file.instructions.splice( 434 | i + 1, 435 | affectedInstructions - 1 436 | 437 | ).forEach((instr) => { 438 | instr.remove(); 439 | }); 440 | 441 | droppedInstructions += affectedInstructions - 1; 442 | 443 | } 444 | 445 | } 446 | 447 | } 448 | 449 | } while(optimized); 450 | 451 | // If we dropped any instructions recalculate the offsets of all 452 | // entries within all sections 453 | if (droppedInstructions > 0) { 454 | file.sections.forEach((section) => { 455 | section.calculateOffsets(); 456 | }); 457 | } 458 | 459 | } 460 | 461 | }; 462 | 463 | 464 | // Helpers -------------------------------------------------------------------- 465 | function findInstructionByOffset(file, address, offset) { 466 | 467 | // Correct for instruction size 468 | if (offset < 0) { 469 | offset -= 1; 470 | } 471 | 472 | const target = address + offset; 473 | 474 | let min = 0, 475 | max = file.instructions.length; 476 | 477 | while(max >= min) { 478 | 479 | const mid = min + Math.round((max - min) * 0.5), 480 | instr = file.instructions[mid]; 481 | 482 | if (instr.offset === target) { 483 | return instr; 484 | 485 | } else if (instr.offset < target) { 486 | min = mid + 1; 487 | 488 | } else if (instr.offset > target) { 489 | max = mid - 1; 490 | } 491 | 492 | } 493 | 494 | return null; 495 | 496 | } 497 | 498 | 499 | // Exports -------------------------------------------------------------------- 500 | module.exports = FileLinker; 501 | 502 | -------------------------------------------------------------------------------- /lib/linker/Linker.js: -------------------------------------------------------------------------------- 1 | // Dependencies --------------------------------------------------------------- 2 | // ---------------------------------------------------------------------------- 3 | const BuiltinMacro = require('./Macro'), 4 | Errors = require('../Errors'), 5 | Token = require('../parser/Lexer').Token, 6 | Variable = require('../data/Variable'), 7 | Constant = require('../data/Constant'), 8 | Section = require('../data/Section'), 9 | Label = require('../data/Label'), 10 | Macro = require('../data/Macro'), 11 | Expression = require('../parser/Expression'); 12 | 13 | let FileLinker = null; 14 | 15 | // Global Linking Logic ------------------------------------------------------- 16 | // ---------------------------------------------------------------------------- 17 | const Linker = { 18 | 19 | init(files) { 20 | 21 | // Order sections based on their offsets / banks 22 | const sections = Linker.getAllSections(files); 23 | sections.sort((a, b) => { 24 | 25 | if (a.segment === b.segment) { 26 | 27 | if (a.bank === b.bank) { 28 | 29 | if (a.hasCustomBaseOffset && b.hasCustomBaseOffset) { 30 | return a.baseOffset - b.baseOffset; 31 | 32 | } else if (a.hasCustomBaseOffset) { 33 | return -1; 34 | 35 | } 36 | return 1; 37 | 38 | 39 | } 40 | return a.bank - b.bank; 41 | 42 | 43 | } 44 | return Section.Segments[a.segment].index 45 | - Section.Segments[b.segment].index; 46 | 47 | 48 | }); 49 | 50 | }, 51 | 52 | link(files, verify, debug) { 53 | 54 | // First calculate all initial addresses for all files 55 | files.forEach((f) => FileLinker.init(f, debug)); 56 | 57 | // Now order and re-arrange the existing section and assign bases 58 | // addresses of sections without specific offset adresses 59 | const sections = Linker.getAllSections(files), 60 | sectionsLastAddresses = {}; 61 | 62 | sections.forEach((section) => { 63 | 64 | const id = `${section.segment }#${ section.bank}`; 65 | 66 | // Place sections without a specified offset after other sections 67 | // in the machting segment / bank 68 | if (!section.hasCustomBaseOffset) { 69 | section.resolvedOffset = sectionsLastAddresses[id] || section.resolvedOffset; 70 | section.calculateOffsets(); 71 | } 72 | 73 | sectionsLastAddresses[id] = section.resolvedOffset + section.size; 74 | 75 | }); 76 | 77 | // Check for overlapping sections 78 | if (verify) { 79 | this.checkOverlap(sections); 80 | } 81 | 82 | // Link all files with the newly calculated addresses 83 | files.forEach(FileLinker.link); 84 | 85 | }, 86 | 87 | checkOverlap(sections) { 88 | 89 | for(let i = 0; i < sections.length; i++) { 90 | for(let e = i + 1; e < sections.length; e++) { 91 | 92 | const b = sections[i], 93 | a = sections[e]; 94 | 95 | if (a.resolvedOffset + a.size > b.resolvedOffset 96 | && a.resolvedOffset <= b.resolvedOffset + b.size - 1) { 97 | 98 | if (a.resolvedOffset > b.resolvedOffset) { 99 | Errors.AddressError( 100 | a.file, 101 | `Section overlaps with previously defined section ${ b.toString(true) 102 | }. Previous section is ${ (b.resolvedOffset + b.size) - a.resolvedOffset } byte(s) too long`, 103 | a.nameIndex 104 | ); 105 | 106 | } else { 107 | Errors.AddressError( 108 | a.file, 109 | `Section overlaps with previously defined section ${ b.toString(true) 110 | }. Section is ${ a.size } byte(s) long, but only ${ 111 | b.resolvedOffset - a.resolvedOffset 112 | } bytes(s) are available until the start of the next section.`, 113 | a.nameIndex 114 | ); 115 | } 116 | 117 | } 118 | 119 | } 120 | } 121 | 122 | }, 123 | 124 | 125 | // Name / Value / Expression Resolution ----------------------------------- 126 | resolveValue(sourceFile, value, sourceOffset, sourceIndex, relativeOffset, stack, returnReference) { 127 | 128 | // Check for circular references during value resolution 129 | if (stack.indexOf(value) === -1) { 130 | stack.push(value); 131 | 132 | switch(value.type) { 133 | case 'NUMBER': 134 | case 'STRING': 135 | return value.value; 136 | 137 | case 'NAME': 138 | return Linker.resolveNameValue(sourceFile, value, sourceOffset, sourceIndex, relativeOffset, stack, returnReference); 139 | 140 | case 'LABEL_LOCAL_REF': 141 | return Linker.resolveLocalLabel(sourceFile, value, relativeOffset, sourceOffset); 142 | 143 | case 'EXPRESSION': 144 | const resolved = Linker.resolveExpression( 145 | value.value, sourceFile, sourceOffset, sourceIndex, 146 | relativeOffset, stack 147 | ); 148 | return typeof resolved === 'number' ? resolved | 0 : resolved; 149 | 150 | case 'OFFSET': 151 | return relativeOffset ? value.value : sourceOffset + value.value; 152 | 153 | default: 154 | throw new TypeError(`Unresolved ${ value.type }(${ value.value })`); 155 | } 156 | 157 | } else { 158 | Errors.ReferenceError( 159 | stack[0].file, 160 | `Circular reference of "${ 161 | stack[0].value 162 | }" to itself via ${ 163 | stack.slice(1).reverse().map((s) => { 164 | return `${s.value } in ${ s.file.getPath()}`; 165 | 166 | }).join(' -> ')}`, 167 | stack[0].index 168 | ); 169 | } 170 | 171 | }, 172 | 173 | resolveLocalLabel(sourceFile, value, relativeOffset, sourceOffset) { 174 | 175 | const resolved = FileLinker.resolveLocalLabel(sourceFile, value); 176 | if (resolved) { 177 | 178 | resolved.references++; 179 | 180 | if (relativeOffset) { 181 | return resolved.offset - sourceOffset; 182 | 183 | } 184 | return resolved.offset; 185 | 186 | } 187 | Errors.ReferenceError( 188 | value.file, 189 | `Local label "${ value.value }" not found in current scope`, 190 | value.index 191 | ); 192 | 193 | 194 | }, 195 | 196 | resolveNameValue(sourceFile, value, sourceOffset, sourceIndex, relativeOffset, stack, returnReference) { 197 | 198 | const resolved = Linker.resolveName(value.value, sourceFile); 199 | if (resolved) { 200 | 201 | resolved.references++; 202 | 203 | // Recursively resolve constants 204 | if (resolved instanceof Constant) { 205 | if (resolved.value instanceof Token) { 206 | return Linker.resolveValue( 207 | resolved.file, resolved.value, 208 | sourceOffset, resolved.value.index, 209 | relativeOffset, stack, 210 | returnReference 211 | ); 212 | 213 | } 214 | return resolved.value; 215 | 216 | 217 | // Resolve Variable Values and Label Addresses 218 | } else if (resolved instanceof Variable || resolved instanceof Label) { 219 | 220 | // Force the callee to use the reference it passed in 221 | // This is needed for macros where we otherwise would pass 222 | // in the pre-computed label and variable addresses even 223 | // though the are subject to change during linkage 224 | if (returnReference || resolved.offset === -1) { 225 | return null; 226 | 227 | } else if (relativeOffset) { 228 | return resolved.offset - sourceOffset; 229 | 230 | } 231 | return resolved.offset; 232 | 233 | 234 | // Resolve builtint macro handlers 235 | } else if (resolved instanceof BuiltinMacro) { 236 | return resolved; 237 | 238 | // Resolve other pre-defined, built-in values 239 | } 240 | return resolved; 241 | 242 | 243 | // Error on missing local names 244 | } else if (value.value.charCodeAt(0) === 95) { 245 | 246 | const resolvedGlobal = Linker.resolveName( 247 | value.value, sourceFile, true 248 | ); 249 | 250 | if (resolvedGlobal) { 251 | Errors.ReferenceError( 252 | value.file, 253 | `Local name "${ 254 | value.value 255 | }" was not declared in current file, but found in ${ 256 | resolvedGlobal.file.getPath(resolvedGlobal.index, true)}`, 257 | value.index 258 | ); 259 | 260 | } else { 261 | Errors.ReferenceError( 262 | value.file, 263 | `Local name "${ value.value }" was not declared`, 264 | value.index 265 | ); 266 | } 267 | 268 | // Error on missing global names 269 | } else { 270 | // TODO Show the reference path 271 | Errors.ReferenceError( 272 | value.file, 273 | `"${ value.value }" was not declared`, 274 | value.index 275 | ); 276 | } 277 | 278 | }, 279 | 280 | resolveName(name, file, global) { 281 | 282 | // Check if their is a builtin macro with the specified name 283 | if (BuiltinMacro.isDefined(name)) { 284 | return BuiltinMacro.get(name); 285 | 286 | // Names prefixed with _ will only be looked up in their own file 287 | } else if (!global && name.charCodeAt(0) === 95) { 288 | return file.names[name]; 289 | 290 | // All other names will be searched globally 291 | } 292 | 293 | const files = file.compiler.files; 294 | for(let i = 0, l = files.length; i < l; i++) { 295 | file = files[i]; 296 | 297 | const value = file.names[name]; 298 | if (value) { 299 | return value; 300 | } 301 | } 302 | 303 | return null; 304 | 305 | 306 | 307 | }, 308 | 309 | resolveExpression(node, sourceFile, sourceOffset, sourceIndex, relativeOffset, stack) { 310 | 311 | // Binary Expressions 312 | if (node instanceof Expression.Node) { 313 | 314 | const left = Linker.resolveExpression( 315 | node.left, sourceFile, sourceOffset, sourceIndex, 316 | relativeOffset, stack 317 | ); 318 | 319 | if (node.right) { 320 | 321 | const right = Linker.resolveExpression( 322 | node.right, sourceFile, sourceOffset, sourceIndex, 323 | relativeOffset, stack 324 | ); 325 | 326 | if (typeof left !== typeof right) { 327 | Errors.ExpressionError( 328 | sourceFile, 329 | `Incompatible operand types ${ 330 | (typeof left).toUpperCase() 331 | } and ${ 332 | (typeof right).toUpperCase() 333 | } for binary operator ${ node.op.id}`, 334 | node.op.index 335 | ); 336 | 337 | } else { 338 | return Linker.evaluateBinaryOperator(node.op.id, left, right); 339 | } 340 | 341 | } else if (typeof left === 'number') { 342 | return Linker.evaluateUnaryOperator(node.op.id, left); 343 | 344 | } else{ 345 | Errors.ExpressionError( 346 | sourceFile, 347 | `Invalid operand type ${ 348 | (typeof left).toUpperCase() 349 | } for unary operator ${ node.op.id}`, 350 | node.left.value.index 351 | ); 352 | } 353 | 354 | // Raw Values 355 | } else if (node instanceof Expression.Leaf) { 356 | return Linker.resolveValue( 357 | sourceFile, node.value, sourceOffset, 358 | node.value.index, false, stack, 359 | false 360 | ); 361 | 362 | // Macro Calls 363 | } else if (node instanceof Expression.Call) { 364 | 365 | const macro = Linker.resolveMacro(node, sourceFile, sourceOffset, stack); 366 | 367 | // Builtin macros are basically just plain JavaScript functions 368 | if (macro.isBuiltin) { 369 | return macro.callee.func.apply(null, macro.args); 370 | 371 | // For use in expressions, only macros which return a value can be 372 | // used 373 | } else if (macro.isExpression) { 374 | 375 | const expr = macro.callee.getExpressionForArguments(macro.args); 376 | return Linker.resolveExpression( 377 | expr, sourceFile, sourceOffset, sourceIndex, 378 | relativeOffset, stack 379 | ); 380 | 381 | // Expansion macros are not suited 382 | } 383 | Errors.MacroError( 384 | sourceFile, 385 | `User defined expansion MACRO ${ macro.name 386 | } cannot be used as a value`, 387 | node.callee.index, 388 | macro.callee 389 | ); 390 | 391 | } 392 | 393 | }, 394 | 395 | resolveMacro(node, sourceFile, sourceOffset, stack) { 396 | 397 | let callee = null; 398 | 399 | // Check for builtin macros 400 | if (!BuiltinMacro.isDefined(node.callee.value)) { 401 | 402 | if (!Macro.isDefined(node.callee.value)) { 403 | Errors.ExpressionError( 404 | sourceFile, 405 | `Call of undefined MACRO function "${ node.callee.value }"`, 406 | node.callee.index 407 | ); 408 | 409 | } else { 410 | callee = Macro.get(node.callee.value); 411 | } 412 | 413 | } else { 414 | callee = BuiltinMacro.get(node.callee.value); 415 | } 416 | 417 | if (node.args.length > callee.args.length) { 418 | Errors.ExpressionError( 419 | sourceFile, 420 | `Too many arguments for ${ 421 | callee.name 422 | }, macro takes at most ${ 423 | callee.args.length 424 | } arguments`, 425 | node.args[0].value.index 426 | ); 427 | 428 | } else if (node.args.length < callee.args.length) { 429 | Errors.ExpressionError( 430 | sourceFile, 431 | `Too few arguments for ${ 432 | callee.name 433 | }, macro takes at least ${ 434 | callee.args.length 435 | } arguments`, 436 | node.callee.index 437 | ); 438 | 439 | } else { 440 | 441 | const args = node.args.map((arg, index) => { 442 | 443 | return Linker.resolveMacroArgument( 444 | arg, callee.args[index], 445 | sourceFile, 446 | sourceOffset, 447 | stack 448 | ); 449 | 450 | }); 451 | 452 | return { 453 | name: node.callee.value, 454 | callee, 455 | args, 456 | isBuiltin: callee instanceof BuiltinMacro, 457 | isExpression: !!callee.isExpression 458 | }; 459 | 460 | } 461 | 462 | }, 463 | 464 | resolveMacroArgument(arg, expected, sourceFile, sourceOffset, stack) { 465 | 466 | // Support Register Arguments 467 | if (expected.type === 'any' 468 | && arg.value && arg.value.type === 'NAME' 469 | && isRegisterArgument(arg.value.value)) { 470 | 471 | return new Macro.RegisterArgument(arg.value.value); 472 | 473 | } 474 | 475 | let value; 476 | 477 | // Macro call arguments 478 | if (arg instanceof Expression.Call) { 479 | value = Linker.resolveExpression( 480 | arg, sourceFile, sourceOffset, 481 | arg.callee.index, false, stack 482 | ); 483 | 484 | // Other values 485 | } else { 486 | value = Linker.resolveValue( 487 | sourceFile, arg.value, sourceOffset, 488 | arg.value.index, false, stack, 489 | true 490 | ); 491 | } 492 | 493 | 494 | // In case we resolve labels, we cannot assume a final offset here 495 | // and have to defer the resolution of the value 496 | if (expected.type !== 'any' && typeof value !== expected.type) { 497 | 498 | // Error out if we couldn't resolve the value i.e. a address that 499 | // has no valid offset just yet 500 | if (value === null) { 501 | Errors.MacroError( 502 | sourceFile, 503 | `Cannot resolve computed address for static macro argument ${ 504 | expected.name }.`, 505 | arg.value.index 506 | ); 507 | } 508 | 509 | Errors.ExpressionError( 510 | sourceFile, 511 | `Invalid type for MACRO argument: ${ 512 | expected.name 513 | } is expected to be of type ${ expected.type.toUpperCase() 514 | } but was ${ (typeof value).toUpperCase() 515 | } instead`, 516 | arg.value.index 517 | ); 518 | 519 | // Address values might not have been resolved yet 520 | } else if (value === null) { 521 | return arg.value; 522 | 523 | } else { 524 | return value; 525 | } 526 | 527 | 528 | 529 | }, 530 | 531 | evaluateBinaryOperator(op, left, right) { 532 | switch(op) { 533 | 534 | // Binary 535 | case '&': 536 | return (left & right) | 0; 537 | 538 | case '|': 539 | return (left | right) | 0; 540 | 541 | 542 | case '^': 543 | return (left ^ right) | 0; 544 | 545 | // Math 546 | case '+': 547 | if (typeof left === 'string') { 548 | return left + right; 549 | } 550 | return left + right; 551 | 552 | case '-': 553 | return left - right; 554 | 555 | case '*': 556 | return left * right; 557 | 558 | case '/': 559 | return left / right; 560 | 561 | case '%': 562 | return left % right; 563 | 564 | case '**': 565 | return Math.pow(left, right); 566 | 567 | // Shift 568 | case '>>': 569 | return (left >> right) | 0; 570 | 571 | case '<<': 572 | return (left << right) | 0; 573 | 574 | // Comparisons 575 | case '>': 576 | return left > right ? 1 : 0; 577 | 578 | case '>=': 579 | return left >= right ? 1 : 0; 580 | 581 | case '<': 582 | return left < right ? 1 : 0; 583 | 584 | case '<=': 585 | return left <= right ? 1 : 0; 586 | 587 | case '==': 588 | return left === right ? 1 : 0; 589 | 590 | case '!=': 591 | return left !== right ? 1 : 0; 592 | 593 | default: 594 | throw new TypeError(`Unimplemented binary operator: ${ op}`); 595 | } 596 | }, 597 | 598 | evaluateUnaryOperator(op, arg) { 599 | switch(op) { 600 | case '!': 601 | return !arg ? 1 : 0; 602 | 603 | case '-': 604 | return -arg; 605 | 606 | case '~': 607 | return (~arg) | 0; 608 | 609 | default: 610 | throw new TypeError(`Unimplemented unary operator: ${ op}`); 611 | } 612 | }, 613 | 614 | 615 | // Optimization ----------------------------------------------------------- 616 | optimize(files, unsafe) { 617 | 618 | // Optimize instructions 619 | files.forEach((file) => { 620 | FileLinker.optimize(file, unsafe); 621 | }); 622 | 623 | // Now relink with the changed addresses 624 | Linker.link(files, true); 625 | 626 | }, 627 | 628 | getAllSections(files) { 629 | 630 | const sections = []; 631 | files.forEach((file) => { 632 | sections.push.apply(sections, file.sections); 633 | }); 634 | 635 | return sections; 636 | 637 | } 638 | 639 | }; 640 | 641 | 642 | // Helpers -------------------------------------------------------------------- 643 | function isRegisterArgument(arg) { 644 | return arg === 'a' || arg === 'b' 645 | || arg === 'c' || arg === 'd' 646 | || arg === 'e' || arg === 'h' 647 | || arg === 'l' || arg === 'hl' 648 | || arg === 'de' || arg === 'bc' 649 | || arg === 'af' || arg === 'sp'; 650 | } 651 | 652 | 653 | // Exports -------------------------------------------------------------------- 654 | module.exports = Linker; 655 | 656 | 657 | // After Dependencies --------------------------------------------------------- 658 | FileLinker = require('./FileLinker'); 659 | 660 | -------------------------------------------------------------------------------- /lib/parser/Lexer.js: -------------------------------------------------------------------------------- 1 | // Dependencies --------------------------------------------------------------- 2 | // ---------------------------------------------------------------------------- 3 | const TokenStream = require('./TokenStream'), 4 | Errors = require('../Errors'), 5 | Expression = require('./Expression'); 6 | 7 | 8 | // Assembly Code Lexer -------------------------------------------------------- 9 | // ---------------------------------------------------------------------------- 10 | function Lexer(file) { 11 | this.file = file; 12 | this.tokens = []; 13 | this.lastToken = null; 14 | this.parenDepth = 0; 15 | this.expressionStack = []; 16 | this.inMacroArgs = false; 17 | this.inMacroBody = false; 18 | this.parse(file.buffer); 19 | return new TokenStream(this.file, this.tokens); 20 | } 21 | 22 | Lexer.prototype = { 23 | 24 | // Parsing ---------------------------------------------------------------- 25 | parse(buffer) { 26 | 27 | let index = 0; 28 | while(index < buffer.length) { 29 | 30 | const ch = buffer[index++]; 31 | 32 | // Newlines 33 | if (isNewline(ch)) { 34 | this.token('NEWLINE', '', index - 1); 35 | 36 | // Skip Whitespace 37 | } else if (isWhitespace(ch)) { 38 | index = this.skipWhitespace(buffer, index); 39 | 40 | // Skip Comments 41 | } else if (ch === 59) { 42 | index = this.skipComment(buffer, index); 43 | 44 | // Parse Names 45 | } else if (isNameStart(ch)) { 46 | index = this.parseName(buffer, index, ch); 47 | 48 | // Parse Parenthesis 49 | } else if (ch === 40) { 50 | this.token('LPAREN', '(', index); 51 | 52 | } else if (ch === 41) { 53 | this.token('RPAREN', ')', index); 54 | 55 | // Parse braces 56 | } else if (ch === 91) { 57 | this.token('[', '[', index); 58 | 59 | } else if (ch === 93) { 60 | this.token(']', ']', index); 61 | 62 | // Comma 63 | } else if (ch === 44) { 64 | this.token('COMMA', ',', index); 65 | 66 | // Parse Decimal Numbers 67 | } else if (isDecimal(ch)) { 68 | index = this.parseDecimal(buffer, index, ch, false); 69 | 70 | // Parse Negative Decimal Numbers 71 | } else if (ch === 45 && isDecimal(buffer[index])) { 72 | index = this.parseDecimal(buffer, index + 1, buffer[index], true); 73 | 74 | // Parse Binary Numbers 75 | } else if (ch === 37 && isBinary(buffer[index])) { 76 | index = this.parseBinary(buffer, index, buffer[index]); 77 | 78 | // Parse Hexadecimal Numbers 79 | } else if (ch === 36 && isHex(buffer[index])) { 80 | index = this.parseHex(buffer, index, buffer[index]); 81 | 82 | // Parse Strings 83 | } else if (ch === 34 || ch === 39) { 84 | index = this.parseString(buffer, index, ch); 85 | 86 | // Parse Operators 87 | } else if (isOperator(ch)) { 88 | index = this.parseOperator(buffer, index, ch); 89 | 90 | // Parse offsets 91 | } else if (ch === 64) { 92 | index = this.parseOffsetOrMacroArg(buffer, index, buffer[index]); 93 | 94 | // Parse local labels 95 | } else if (ch === 46 && isNameStart(buffer[index])) { 96 | index = this.parseLocalLabel(buffer, index + 1, buffer[index]); 97 | 98 | } else { 99 | this.error(`Unexpected character "${ String.fromCharCode(ch) }" (${ ch })`, null, index); 100 | } 101 | 102 | } 103 | 104 | this.token('EOF', '', index); 105 | 106 | }, 107 | 108 | parseOperator(buffer, index, ch) { 109 | 110 | const next = buffer[index], 111 | at = index; 112 | 113 | // Double character operators 114 | if (ch === 62 && next === 62) { 115 | this.token('OPERATOR', '>>', index); 116 | return index + 1; 117 | 118 | } else if (ch === 60 && next === 60) { 119 | this.token('OPERATOR', '<<', index); 120 | return index + 1; 121 | 122 | } else if (ch === 38 && next === 38) { 123 | this.token('OPERATOR', '&&', index); 124 | return index + 1; 125 | 126 | } else if (ch === 124 && next === 124) { 127 | this.token('OPERATOR', '||', index); 128 | return index + 1; 129 | 130 | } else if (ch === 61 && next === 61) { 131 | this.token('OPERATOR', '==', index); 132 | return index + 1; 133 | 134 | } else if (ch === 33 && next === 61) { 135 | this.token('OPERATOR', '!=', index); 136 | return index + 1; 137 | 138 | } else if (ch === 62 && next === 61) { 139 | this.token('OPERATOR', '>=', index); 140 | return index + 1; 141 | 142 | } else if (ch === 60 && next === 61) { 143 | this.token('OPERATOR', '<=', index); 144 | return index + 1; 145 | 146 | } else if (ch === 42 && next=== 42) { 147 | this.token('OPERATOR', '**', at); 148 | return index + 1; 149 | 150 | // Single character operators 151 | } else if (ch === 60) { 152 | this.token('OPERATOR', '<', at); 153 | return index; 154 | 155 | } else if (ch === 62) { 156 | this.token('OPERATOR', '>', at); 157 | return index; 158 | 159 | } else if (ch === 33) { 160 | this.token('OPERATOR', '!', at); 161 | return index; 162 | 163 | } else if (ch === 43) { 164 | this.token('OPERATOR', '+', at); 165 | return index; 166 | 167 | } else if (ch === 45) { 168 | this.token('OPERATOR', '-', at); 169 | return index; 170 | 171 | } else if (ch === 42) { 172 | this.token('OPERATOR', '*', at); 173 | return index; 174 | 175 | } else if (ch === 47) { 176 | this.token('OPERATOR', '/', at); 177 | return index; 178 | 179 | } else if (ch === 37) { 180 | this.token('OPERATOR', '%', at); 181 | return index; 182 | 183 | } else if (ch === 38) { 184 | this.token('OPERATOR', '&', at); 185 | return index; 186 | 187 | } else if (ch === 124) { 188 | this.token('OPERATOR', '|', at); 189 | return index; 190 | 191 | } else if (ch === 126) { 192 | this.token('OPERATOR', '~', at); 193 | return index; 194 | 195 | } else if (ch === 94) { 196 | this.token('OPERATOR', '^', at); 197 | return index; 198 | 199 | } 200 | this.error(`invalid operator "${ String.fromCharCode(ch) }"`, null, index); 201 | 202 | 203 | }, 204 | 205 | parseLocalLabel(buffer, index, ch) { 206 | 207 | let name = `.${ String.fromCharCode(ch)}`; 208 | const at = index; 209 | 210 | ch = buffer[index]; 211 | 212 | while(isNamePart(ch)) { 213 | name += String.fromCharCode(ch); 214 | ch = buffer[++index]; 215 | } 216 | 217 | // Definition 218 | if (ch === 58) { 219 | this.token('LABEL_LOCAL_DEF', name, at - 1); 220 | return index + 1; 221 | } 222 | 223 | // Reference 224 | this.token('LABEL_LOCAL_REF', name, at - 1); 225 | return index; 226 | 227 | 228 | }, 229 | 230 | parseOffsetOrMacroArg(buffer, index, sign) { 231 | 232 | if (sign === 45) { 233 | this.token('OFFSET_SIGN', '-', index); 234 | return index + 1; 235 | 236 | } else if (sign === 43) { 237 | this.token('OFFSET_SIGN', '+', index); 238 | return index + 1; 239 | 240 | } else if (this.inMacroBody || this.inMacroArgs) { 241 | if (isNameStart(sign)) { 242 | return this.parseMacroArgName(buffer, index + 1, sign); 243 | } 244 | 245 | this.error(String.fromCharCode(sign), 'valid argument name instead', index + 1); 246 | 247 | } else { 248 | // TODO also inform that the macro argument might not be inside a macro 249 | this.error(String.fromCharCode(sign), 'valid direction specifier (- or +) for offset', index + 1); 250 | } 251 | 252 | }, 253 | 254 | parseHex(buffer, index, digit) { 255 | 256 | let number = ''; 257 | 258 | const at = index; 259 | while(isHex(digit)) { 260 | 261 | number += String.fromCharCode(digit); 262 | digit = buffer[++index]; 263 | 264 | // Ignore interleaved underscore characters 265 | if (digit === 95) { 266 | digit = buffer[++index]; 267 | } 268 | 269 | } 270 | 271 | this.token('NUMBER', parseInt(number, 16), at); 272 | 273 | return index; 274 | 275 | }, 276 | 277 | parseBinary(buffer, index, digit) { 278 | 279 | let number = ''; 280 | 281 | const at = index; 282 | while(isBinary(digit)) { 283 | 284 | number += String.fromCharCode(digit); 285 | digit = buffer[++index]; 286 | 287 | // Ignore interleaved underscore characters 288 | if (digit === 95) { 289 | digit = buffer[++index]; 290 | } 291 | 292 | } 293 | 294 | this.token('NUMBER', parseInt(number, 2), at); 295 | 296 | return index; 297 | 298 | }, 299 | 300 | parseDecimal(buffer, index, digit, isNegative) { 301 | 302 | let number = String.fromCharCode(digit); 303 | 304 | digit = buffer[index]; 305 | 306 | const at = index; 307 | while(isDecimal(digit)) { 308 | 309 | number += String.fromCharCode(digit); 310 | digit = buffer[++index]; 311 | 312 | // Ignore interleaved underscore characters 313 | if (digit === 95) { 314 | digit = buffer[++index]; 315 | } 316 | 317 | } 318 | 319 | // Floating point 320 | if (digit === 46) { 321 | 322 | number += String.fromCharCode(digit); 323 | digit = buffer[++index]; 324 | 325 | while(isDecimal(digit)) { 326 | 327 | number += String.fromCharCode(digit); 328 | digit = buffer[++index]; 329 | 330 | // Ignore interleaved underscore characters 331 | if (digit === 95) { 332 | digit = buffer[++index]; 333 | } 334 | 335 | } 336 | 337 | if (isNegative) { 338 | this.token('NUMBER', -parseFloat(number, 10), at - 1); 339 | 340 | } else { 341 | this.token('NUMBER', parseFloat(number, 10), at); 342 | } 343 | 344 | // Integer 345 | } else if (isNegative) { 346 | this.token('NUMBER', -parseInt(number, 10), at - 1); 347 | 348 | } else { 349 | this.token('NUMBER', parseInt(number, 10), at); 350 | } 351 | 352 | return index; 353 | 354 | }, 355 | 356 | parseName(buffer, index, ch) { 357 | 358 | let name = String.fromCharCode(ch); 359 | ch = buffer[index]; 360 | 361 | const at = index; 362 | while(isNamePart(ch)) { 363 | name += String.fromCharCode(ch); 364 | ch = buffer[++index]; 365 | } 366 | 367 | // Label Definition 368 | if (ch === 58) { 369 | if (!this.name(name, true, at)) { 370 | this.error(`invalid label name ${ name}`, null, at); 371 | 372 | } else { 373 | return index + 1; 374 | } 375 | 376 | } else { 377 | this.name(name, false, at); 378 | return index; 379 | } 380 | 381 | }, 382 | 383 | parseMacroArgName(buffer, index, ch) { 384 | 385 | let name = String.fromCharCode(ch); 386 | ch = buffer[index]; 387 | 388 | const at = index; 389 | while(isNamePart(ch)) { 390 | name += String.fromCharCode(ch); 391 | ch = buffer[++index]; 392 | } 393 | 394 | this.token('MACRO_ARG', name, at - 1); 395 | return index; 396 | 397 | }, 398 | 399 | parseString(buffer, index, delimiter) { 400 | 401 | let string = '', 402 | ch = buffer[index]; 403 | 404 | const at = index; 405 | while(ch !== delimiter) { 406 | 407 | // Escape sequences 408 | if (ch === 92) { 409 | ch = buffer[++index]; 410 | switch(ch) { 411 | case 34: // " 412 | string += '"'; 413 | break; 414 | 415 | case 39: // ' 416 | string += '\''; 417 | break; 418 | 419 | case 92: // \\ 420 | string += '\\'; 421 | break; 422 | 423 | case 114: // CR 424 | string += '\r'; 425 | break; 426 | 427 | case 110: // LF 428 | string += '\n'; 429 | break; 430 | 431 | case 118: // VT 432 | string += '\v'; 433 | break; 434 | 435 | case 116: // HT 436 | string += '\t'; 437 | break; 438 | 439 | case 98: // BEL 440 | string += '\b'; 441 | break; 442 | 443 | case 48: // 0 444 | string += '\0'; 445 | break; 446 | 447 | default: 448 | this.error('invalid escape sequence', null, index); 449 | break; 450 | } 451 | 452 | 453 | } else { 454 | string += String.fromCharCode(ch); 455 | } 456 | 457 | ch = buffer[++index]; 458 | 459 | } 460 | 461 | this.token('STRING', string, at); 462 | 463 | return index + 1; 464 | 465 | }, 466 | 467 | skipComment(buffer, index) { 468 | while(!isNewline(buffer[index])) { 469 | index++; 470 | } 471 | return index; 472 | }, 473 | 474 | skipWhitespace(buffer, index) { 475 | while(index < buffer.length && isWhitespace(buffer[index])) { 476 | index++; 477 | } 478 | return index; 479 | }, 480 | 481 | 482 | // Tokens ----------------------------------------------------------------- 483 | token(type, value, index) { 484 | 485 | // Combine offset labels with their argument 486 | if (this.lastToken && this.lastToken.type === 'OFFSET_SIGN') { 487 | 488 | // We require a number here 489 | if (type !== 'NUMBER') { 490 | this.error(type, `a number after @${ this.lastToken.value}`, index); 491 | 492 | } else { 493 | this.lastToken.type = 'OFFSET'; 494 | this.lastToken.value = this.lastToken.value === '-' ? -value : value; 495 | } 496 | 497 | // Combine macro tokens with their name 498 | } else if (this.lastToken && this.lastToken.type === 'MACRO_DEF') { 499 | 500 | // We require a name here 501 | if (type !== 'NAME') { 502 | this.error(type, 'a valid name for a MACRO definiton', index); 503 | 504 | } else { 505 | this.lastToken.type = 'MACRO'; 506 | this.lastToken.value = value; 507 | 508 | // Mark the parser as beging inside the macro arguments 509 | // This allows for MACRO_ARG tokens to be parsed 510 | this.inMacroArgs = true; 511 | } 512 | 513 | } else { 514 | const token = new Token(type, value, index, this.file); 515 | this.expr(this.lastToken, token); 516 | this.lastToken = token; 517 | } 518 | 519 | }, 520 | 521 | expr(token, next) { 522 | 523 | // Collect expression tokens 524 | if (token && !this.inMacroArgs && isExpression(token.type, next.type, this.parenDepth)) { 525 | 526 | // We need to keep track of the paren depth 527 | // since we only consume COMMAs as part of a expression 528 | // when inside of parenthesis 529 | if (token.type === 'LPAREN') { 530 | this.parenDepth++; 531 | 532 | } else if (token.type === 'RPAREN') { 533 | this.parenDepth--; 534 | } 535 | 536 | this.expressionStack.push(token); 537 | 538 | // Wait for macro argument definitions to close 539 | } else if (this.inMacroArgs) { 540 | 541 | if (next.type === 'RPAREN') { 542 | this.inMacroArgs = false; 543 | this.inMacroBody = true; 544 | } 545 | 546 | this.tokens.push(next); 547 | 548 | // Parse the expression stack tokens 549 | } else { 550 | 551 | // If we have an expression stack... 552 | if (this.expressionStack.length > 0) { 553 | 554 | // ...push the last token onto it and parse the expression 555 | // into a binary tree 556 | this.expressionStack.push(token); 557 | 558 | // Replace the last token in the token list 559 | // with a expression token 560 | // We cannot modify the existing token in-place since it is 561 | // actually part of the binary expression tree 562 | this.tokens[this.tokens.length - 1] = new Token( 563 | 'EXPRESSION', 564 | new Expression(this, this.expressionStack), 565 | this.expressionStack[0].index, 566 | this.file 567 | ); 568 | 569 | this.expressionStack.length = 0; 570 | 571 | } 572 | 573 | // Reset the paren depth and push the next token (the one after the 574 | // expression) into the token list 575 | this.parenDepth = 0; 576 | this.tokens.push(next); 577 | 578 | } 579 | 580 | }, 581 | 582 | name(value, isLabel, index) { 583 | 584 | if (isInstruction(value)) { 585 | this.token('INSTRUCTION', value, index); 586 | return false; 587 | 588 | } else if (isDirective(value)) { 589 | this.token('DIRECTIVE', value, index); 590 | return true; 591 | 592 | } else if (value === 'MACRO') { 593 | this.token('MACRO_DEF', value, index); 594 | return true; 595 | 596 | } else if (value === 'ENDMACRO') { 597 | 598 | if (!this.inMacroBody) { 599 | this.error(`${value } outside of MACRO definition`, null, index); 600 | } 601 | 602 | this.inMacroBody = false; 603 | this.token('ENDMACRO', value, index); 604 | return true; 605 | 606 | } 607 | this.token(isLabel ? 'LABEL_GLOBAL_DEF' : 'NAME', value, index); 608 | return true; 609 | 610 | 611 | }, 612 | 613 | 614 | // Error Handling --------------------------------------------------------- 615 | error(msg, expected, index) { 616 | Errors.ParseError(this.file, msg, expected, index); 617 | } 618 | 619 | }; 620 | 621 | 622 | // Helper --------------------------------------------------------------------- 623 | function Token(type, value, index, file) { 624 | this.type = type; 625 | this.value = value; 626 | this.index = index; 627 | this.file = file; 628 | } 629 | 630 | Token.prototype = { 631 | 632 | clone() { 633 | return new Token(this.type, this.value, this.index, this.file); 634 | } 635 | 636 | }; 637 | 638 | function isNameStart(c) { 639 | // 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcedfghijklmnopqrstuvwxyz_' 640 | return c === 95 // _ 641 | || (c >= 97 && c <= 122) // a-z 642 | || (c >= 65 && c <= 90); // A-Z 643 | } 644 | 645 | function isNamePart(c) { 646 | // 'abcedfghijklmnopqrstuvwxyz_0123456789' 647 | return c === 95 // _ 648 | || (c >= 97 && c <= 122) // a-z 649 | || (c >= 65 && c <= 90) // A-Z 650 | || (c >= 48 && c <= 57); // 0-9 651 | } 652 | 653 | function isWhitespace(c) { 654 | // space \n \r \t \v 655 | return c === 32 || c === 10 || c === 13 || c === 9 || c === 11; 656 | } 657 | 658 | function isDecimal(c) { 659 | // 0 - 9 660 | return c >= 48 && c <= 57; 661 | } 662 | 663 | function isHex(c) { 664 | return (c >= 48 && c <= 57) // 0 - 9 665 | || (c >= 97 && c <= 102) // a - f 666 | || (c >= 65 && c <= 70); // A - F 667 | } 668 | 669 | function isBinary(c) { 670 | // 0 or 1 671 | return c === 48 || c === 49; 672 | } 673 | 674 | function isNewline(c) { 675 | return c === 13 || c === 10; 676 | } 677 | 678 | function isOperator(c) { 679 | // '+-*/><|&^~!%' 680 | return c === 43 || c === 45 || c === 42 || c === 47 || c === 62 681 | || c === 60 || c === 124 || c === 38 || c === 94 || c === 126 682 | || c === 33 || c === 37; 683 | } 684 | 685 | function isInstruction(value) { 686 | switch(value.length) { 687 | case 2: 688 | switch(value) { 689 | case 'if': 690 | case 'cp': 691 | case 'di': 692 | case 'ei': 693 | case 'jp': 694 | case 'jr': 695 | case 'or': 696 | case 'rl': 697 | case 'rr': 698 | case 'ld': 699 | return true; 700 | 701 | default: 702 | return false; 703 | 704 | } 705 | 706 | case 3: 707 | switch(value) { 708 | case 'adc': 709 | case 'add': 710 | case 'and': 711 | case 'bit': 712 | case 'ccf': 713 | case 'cpl': 714 | case 'daa': 715 | case 'dec': 716 | case 'inc': 717 | case 'ldh': 718 | case 'nop': 719 | case 'pop': 720 | case 'res': 721 | case 'ret': 722 | case 'rla': 723 | case 'rlc': 724 | case 'rra': 725 | case 'rrc': 726 | case 'rst': 727 | case 'sbc': 728 | case 'scf': 729 | case 'set': 730 | case 'sla': 731 | case 'sra': 732 | case 'srl': 733 | case 'sub': 734 | case 'xor': 735 | case 'msg': 736 | case 'brk': 737 | case 'mul': 738 | case 'div': 739 | return true; 740 | 741 | default: 742 | return false; 743 | 744 | } 745 | 746 | case 4: 747 | switch(value) { 748 | case 'incx': 749 | case 'decx': 750 | case 'addw': 751 | case 'ldxa': 752 | case 'halt': 753 | case 'push': 754 | case 'call': 755 | case 'reti': 756 | case 'ldhl': 757 | case 'rlca': 758 | case 'rrca': 759 | case 'stop': 760 | case 'swap': 761 | return true; 762 | 763 | default: 764 | return false; 765 | } 766 | 767 | default: 768 | return false; 769 | 770 | } 771 | } 772 | 773 | 774 | function isDirective(value) { 775 | return value === 'DB' || value === 'DW' 776 | || value === 'DS' || value === 'EQU' 777 | || value === 'EQUS' || value === 'BANK' 778 | || value === 'INCBIN' || value === 'SECTION' 779 | || value === 'INCLUDE'; 780 | } 781 | 782 | 783 | function isExpression(token, next, parenDepth) { 784 | 785 | if (parenDepth === 0 && (token === 'COMMA' || next === 'COMMA') ) { 786 | return false; 787 | } 788 | 789 | switch(token) { 790 | case 'LPAREN': 791 | return checkParenExpressionLeft(next); 792 | 793 | case 'RPAREN': 794 | return checkParenExpressionRight(next); 795 | 796 | case 'OPERATOR': 797 | return checkOperatorExpression(next); 798 | 799 | case 'NUMBER': 800 | case 'STRING': 801 | case 'LABEL_LOCAL_REF': 802 | return checkValueExpression(next); 803 | 804 | case 'MACRO_ARG': 805 | case 'NAME': 806 | return checkNameExpression(next); 807 | 808 | case 'COMMA': 809 | return checkCommaExpression(next); 810 | 811 | default: 812 | return false; 813 | 814 | } 815 | 816 | } 817 | 818 | function checkParenExpressionLeft(next) { 819 | return next === 'NAME' || next === 'LABEL_LOCAL_REF' || next === 'NUMBER' 820 | || next === 'STRING' || next === 'OPERATOR' || next === 'LPAREN' 821 | || next === 'RPAREN' || next === 'MACRO_ARG'; 822 | } 823 | 824 | function checkParenExpressionRight(next) { 825 | return next === 'RPAREN' || next === 'OPERATOR'; 826 | } 827 | 828 | function checkOperatorExpression(next) { 829 | return next === 'LPAREN' || next === 'NUMBER' || next === 'STRING' 830 | || next === 'LABEL_LOCAL_REF' || next === 'NAME' || next === 'MACRO_ARG'; 831 | } 832 | 833 | function checkValueExpression(next) { 834 | return next === 'RPAREN' || next === 'OPERATOR' || next === 'COMMA'; 835 | } 836 | 837 | function checkNameExpression(next) { 838 | return next === 'LPAREN' || next === 'RPAREN' || next === 'OPERATOR' 839 | || next === 'COMMA'; 840 | } 841 | 842 | function checkCommaExpression(next) { 843 | return next === 'LPAREN' || next === 'NAME' || next === 'STRING' 844 | || next === 'NUMBER' || next === 'MACRO_ARG'; 845 | } 846 | 847 | 848 | // Exports -------------------------------------------------------------------- 849 | module.exports = Lexer; 850 | module.exports.Token = Token; 851 | 852 | --------------------------------------------------------------------------------