/** * Bootstrap procedure for Ulambda Scheme * * Copyright (C) 2017, 2018 Mike Gerwitz * * This file is part of Ulambda Scheme. * * Ulambda Scheme is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * Ideally, the user should be able to bootstrap Ulambda Scheme with nothing * more than what they already have installed on their computer, in the * environment that Ulambda was designed to run in---the web browser. * Node.js was used during official development, but that is a large system * that should not be a necessary dependency---it should be needed only for * convenience. * * To run this process on a local development environment using Node.js, see * `../bootstrap.js'. To run in your web browser, see `../bootstrap.html'. */ 'use strict'; /** * Bootstrap procedure for Ulambda Scheme * * This abstracts the bootstrap process in such a way that it can be run in * any JavaScript environment. Notably, we need to support not only Node.js * (which is convenient for development and automation), but also a web * browser, which allows users to bootstrap using only their runtime * environment and no additional tools. * * Prebirth and every compiler thereafter are designed to be able to be run * from the command line, accepting source code on standard input. Such a * concept does not exist in a browser environment, and therefore cannot * exist here; there is an awkward abstraction to work around that. */ class Bootstrap { /** * Initialize bootstrap process * * The file loader `getf' must accept a path to a file to load and * return a Promise representing the contents of that file. The logger * function `logf' must accept a string message and, as an optional * argument an Error. `prebirth' should be `Prebirth' from * `prebirth.js'. * * @param {function(string):Promise} getf file loader * @param {function(string,Error=}} logf logger * @param {Prebirth} prebirth Prebirth */ constructor( getf, logf, prebirth ) { this._getf = getf; this._logf = logf; this._prebirth = prebirth; } /** * Perform bootstrapping process * * This compiles each of the phases of Ulambda Scheme beginning with * Prebirth. This will evolve in complexity as we continue to move * forward. * * There is currently no final result from this method other than * log output and an indication of success or failure; that'll change as * we get further along and will produce the final compiler. * * @return {undefined} nothing yet. */ bootstrap() { this._strout( 'header' ); return this._birth() .then( birth => this._rebirth( birth ) ) .catch( e => this._error( e ) ) .then( status => this._log( "=> " + this._doneMessage( status ) ) ); } /** * Produce self-hosted Birth * * Prebirth will be used to compile Birth, which is written in * Prebirth Lisp. Birth will then be used to compile itself, becoming * self-hosting. * * This process is self-verifying: Birth compiled with both Prebirth and * Birth itself should produce output that is identical (with regards to * JavaScript's string representation). In practice, since Birth uses * only ASCII, this amounts to verifying that the outputs are * bytewise-identical. * * The result of this method will be a unary function that, given a * Birth Lisp source string, will compile that string into JavaScript. * * @return {Promise} Birth compiler */ _birth() { return this._loadPaths( [ [ "birth.scm", "Birth" ], [ "libprebirth.js", "libprebirth" ], ] ) .then( ( [ scm, lib ] ) => { this._strout( 'prebirthDesc' ); const preout = this._prebirth.compile( scm, lib ); return [ preout, scm, lib ]; } ) .then( ( [ birthjs, scm, lib ] ) => { this._strout( 'prebirthComplete', birthjs.length ); this._strout( 'birthCompiled' ); this._strout( 'birthSelfCompiling' ); const birthf = this._makeCompiler( birthjs, { "libprebirth.js": lib } ); const birthout = birthf( scm ); this._verifyBirthOutput( birthout, birthjs ); return birthf; } ); } /** * Verify that self-compiled Birth output BIRTHOUT matches that of * Prebirth-compiled Birth BIRTHJS * * @param {string} birthout self-compiled Birth * @param {string} birthjs Prebirth-compiled Birth * * @throws {Error} on non-match * * @return {undefined} */ _verifyBirthOutput( birthout, birthjs ) { if ( birthout === '' ) { throw Error( "Self-compilation yielded no output" ); } this._strout( 'birthVerify' ); if ( birthout !== birthjs ) { this._strout( 'birthVerifyFail' ); throw Error( "Birth self-compilation output does not match Prebirth!" ); } this._strout( 'birthVerifyOk' ); } /** * Create unary function wrapping the compiler JS with a stub * filesystem FS * * The unary function accepts a source file which is then passed to the * compiler via the stub filesystem on "/dev/stdin". The output of the * compiler is returned as a string. * * The stub filesystem should contain the contents of all files * dynamically loaded by the compiler JS. This abstraction allows the * bootstrapping process to work in any environment without regards to * whether a filesystem even exists, and regardless of whether loading * is a synchronous or asynchronous operation. * * @param {string} js JavaScript code of compiler (to be eval'd) * @param {Object} fs mapping of filename to content for stub filesystem * * @return {string} compiler output */ _makeCompiler( js, fs = {} ) { const birth = new Function( 'let __fsinit = this.__fsinit;' + 'let require = this.require;' + 'let birthout = "";\n' + 'const console = { log: str => birthout = str + "\\n" };\n' + js + "return birthout;" ); return scm => { fs[ "/dev/stdin" ] = scm; return birth.call( { __fsinit: fs } ); }; } /** * Compile Rebirth using Birth and yield unary compiler function * * This begins the recursive compilation of Rebirth, beginning with * the first generation Re¹birth, using the self-hosted Birth. The * first generation of Rebirth is written purely in Birth Lisp. The * resulting compiler has more features than Birth, which is then used * to compile itself again, producing a compiler with even more * features. This process repeats until the output does not change. * * @param {function(string):string} birth Birth * * @return {Promise} final Rebirth generation */ _rebirth( birth ) { return this._loadPaths( [ [ "rebirth.scm", "Rebirth" ], [ "rebirth/es.scm" ], [ "rebirth/relibprebirth.scm" ], [ "rebirth/macro.scm" ], ] ).then( ( [ scm, es, relibprebirth, macro ] ) => this._compileRebirth( birth, scm, { "rebirth/es.scm": es, "rebirth/relibprebirth.scm": relibprebirth, "rebirth/macro.scm": macro, } ) ); } /** * Recursively compile Rebirth until two consecutive generations match * and yield the unary compiler function for the final generation * * The first time this method is called, it should be called with Birth * as the unary compiler function COMPILE. It should each time be * provided with the Rebirth source code SCM and the necessary stub * filesystem FS (these are identical for each recursive invocation of * this method). * * Recursion terminates when the compiler COMPILE output matches that of * the previous generation PREV, at which point the unary compiler * function COMPILE will be yielded as the final generation (with the * final generation number being N-1 to account for the duplicate). * * @param {function(string):string} compile compiler (Birth or Rebirth) * @param {string} scm Rebirth source * @param {Object} fs stub filesystem for Rebirth * @param {number=} n target Rebirth generation id * @param {string=} prev previous Rebirth generation * * @throws {Error} if compiler COMPILE produces no output * * @return {Promise} final Rebirth generation */ _compileRebirth( compile, scm, fs, n = 1, prev = "" ) { this._strout( 'rebirthCompiling', n ); const birthout = compile( scm ); if ( birthout === '' ) { return Promise.reject( Error( "Rebirth compilation yielded no output" ) ); } this._strout( 'rebirthCompiled', n, birthout.length ); const rebirthf = this._makeCompiler( birthout, fs ); if ( birthout === prev ) { this._strout( 'rebirthDone', ( n - 1 ) ); return Promise.resolve( compile ); } // recurse, but just in case we're running in a browser, give a // change to repaint the log (otherwise we'd just hang until every // Rebirth is compiled) return new Promise( accept => setTimeout( () => accept( this._compileRebirth( rebirthf, scm, fs, ( n + 1 ), birthout ) ) ) ); } /** * Produce a promise for the file contents of each of `path' * * See also `#_loadPath'. * * @param {Array} paths file paths * * @return {Promise} resolved with file contents or failure */ _loadPaths( paths ) { return Promise.all( paths.map( ( [ path, desc ] ) => this._loadPath( path, desc ) ) ); } /** * Produce a promise for the file contents of `path' * * This action is logged with the description `desc' and the length of * the result. * * This uses the loader function provided via the constructor, which * must return a Promise. * * @param {string} path file path * @param {string=} desc file description for logging * * @return {Promise} promise of string file contents */ _loadPath( path, desc = "" ) { this._strout( 'loadingf', desc, path ); return this._getf( path ) .then( data => { this._strout( 'loadedf', path, data.length ); return data; } ); } /** * Promise to log a string identified by `id' * * All given arguments in `args' will be passed to the function handling * that identifier. * * @param {string} id string identifier (see `_strmap') * @param {Array} args string arguments * * @return {Promise} */ _strout( id, ...args ) { return Promise.resolve( this._log( this._str.apply( this, arguments ) ) ); } /** * Generate a string identified by `id' * * All given arguments in `args' will be passed to the function handling * that identifier. * * @param {string} id string identifier (see `_strmap') * @param {Array} args string arguments * * @return {string} generated string */ _str( id, ...args ) { const strf = Bootstrap._strmap[ id ]; if ( strf === undefined ) { throw Error( `Unknown strmap '${id}'` ); } return strf.apply( null, args ); } /** * Log string using logger function * * @param {string} str string to log * * @return {undefined} */ _log( str ) { this._logf( str ); } /** * Log error using logger function * * `e.message' will be used as the log string, with `e' itself being * passed as the second argument to the logger function. * * @param {Error} e error * * @throws {Error} e * * @return {undefined} */ _error( e ) { const str = this._str( 'fatal', e ); this._logf( str, e ); throw e; } /** * Return either success or failure message given `status' * * @param {boolean} status success/failure indicator * * @return {string} success/failure message */ _doneMessage( status ) { return ( status === false ) ? this._str( 'fail' ) : this._str( 'ok' ); } } /** * Output strings in an easily accessible map * * This both keeps the code a bit more easily comprehensible by removing * large strings from procedural logic, and allows for future localization. * * We can do better once we get to a localization stage---Error messages * aren't part of this map, for example. * * @type {string} */ Bootstrap._strmap = { header: () => "\\\\ // \\\\\\\n" + " \\\\ // \\\\\\\n" + " \\\\// Ulambda \\\\\\\n" + " \\\\\\ Scheme ///\n" + " \\\\\\ ///\n" + " \\\\\\ ///\n", loadingf: ( desc, path ) => ( desc ) ? `Loading ${desc} from ${path}...` : `Loading ${path}...`, loadedf: ( path, len ) => `Loaded ${path} (len=${len}).`, prebirthDesc: () => "+ Prebirth is a very basic Lisp dialect with a compiler\n" + "+ implemented in ECMAScript. Birth is the same\n" + "+ compiler, but re-implemented in Prebirth Lisp.", prebirthComplete: ( len ) => `Birth compilation complete (len=${len}).`, birthCompiled: () => "+ Birth has been compiled with Prebirth. Since Birth is\n" + "+ a re-implementation of Prebirth, it can now be used\n" + "+ to compile itself.", birthSelfCompiling: () => "Self-compiling Birth...", birthVerify: () => "Verifying self-compilation output...", birthVerifyFail: () => "\n" + "The self-compilation of Birth yielded output\n" + "that differs from Prebirth's compilation of Birth.\n" + "This verification step is a self-test to ensure\n" + "consistency between the two implementations.\n\n" + "Unfortunately, to fix this, you need to hack\n" + "Prebirth and/or Birth. Please report a bug!", birthVerifyOk: () => "Birth output matches that of Prebirth.\n" + "+ We are now bootstrapped using a very primitive\n" + "+ Birth Lisp. Birth can now be used to compile the\n" + "+ next generation of bootstrap compilers, Rebirth.", rebirthCompiling: n => "Compiling Re" + Bootstrap._supmap[ n ] + "birth...", rebirthCompiled: ( n, len ) => ( n > 1 ) ? `Compilation complete (len=${len}).` : `+ The first generation of Rebirth (Re¹birth) has been\n` + `+ compiled using Birth (len=${len}). The next step is\n` + `+ to have Re¹birth build itself, producing Re²birth.\n` + `+ This will repeat, each time producing a compiler with\n` + `+ additional features capable of compiling the next\n` + `+ generation. This process will end once two\n` + `+ consecutive generations yield identical output.`, rebirthDone: n => "+ Rebirth stopped changing after Re" + Bootstrap._supmap[ n ] + "birth, so that\n" + "+ generation will serve as our final one. The last\n" + "+ step is to use it to compile Ulambda.", fatal: ( e ) => "\n\n!!! " + e.toString() + "\n\n" + "Something has gone terribly wrong!\n" + "See the console for a stack trace.\n\n", ok: () => "Bootstrap successful (but not yet complete)!", fail: () => "Bootstrap failed.", }; /** * Map of number to Unicode superscript * * This may be implemented as either a string or an array; the notation * _supmap[n] will work the same in either case. * * @type {string} */ Bootstrap._supmap = "⁰¹²³⁴⁵⁶⁷⁸⁹"; // for use in a CommonJS (e.g. Node.js) environment if ( typeof module !== 'undefined' ) { module.exports = Bootstrap; }