diff --git a/README.md b/README.md index f7343a5..0ca8302 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,10 @@ The primary compiler target is JavaScript. There's a lot more to be said, but that story will evolve over time. ``` -\/ Ulambda - \ Scheme +\\ // \\\ + \\ // \\\ + \\// Ulambda \\\ + \\\ Scheme /// + \\\ /// + \\\ /// ``` diff --git a/build-aux/bootstrap/Bootstrap.js b/build-aux/bootstrap/Bootstrap.js index caad92c..2c23efd 100644 --- a/build-aux/bootstrap/Bootstrap.js +++ b/build-aux/bootstrap/Bootstrap.js @@ -74,7 +74,7 @@ class Bootstrap * * This compiles each of the phases of Ulambda Scheme beginning with * Prebirth. This will evolve in complexity as we continue to move - * forward. Each step of the process is self-verifying. + * 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 @@ -86,7 +86,36 @@ class Bootstrap { this._strout( 'header' ); - this._loadPaths( [ + 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" ], ] ) @@ -94,11 +123,9 @@ class Bootstrap { this._strout( 'prebirthDesc' ); - return [ - this._prebirth.compile( scm, lib ), - scm, - lib - ]; + const preout = this._prebirth.compile( scm, lib ); + + return [ preout, scm, lib ]; } ) .then( ( [ birthjs, scm, lib ] ) => { @@ -106,43 +133,173 @@ class Bootstrap this._strout( 'birthCompiled' ); this._strout( 'birthSelfCompiling' ); - const birth = new Function( - 'let __fsinit = this.__fsinit;' + - 'let birthout = "";\n' + - 'const console = { log: str => birthout = str + "\\n" };\n' + - birthjs + - "return birthout;" - ); - - const birthout = birth.call( { - __fsinit: { // stub fs - "/dev/stdin": scm, - "libprebirth.js": lib, - }, + const birthf = this._makeCompiler( birthjs, { + "libprebirth.js": lib } ); - if ( birthout === '' ) { - throw Error( "Self-compilation yielded no output" ); - } + const birthout = birthf( scm ); - this._strout( 'birthVerify' ); + this._verifyBirthOutput( birthout, birthjs ); - if ( birthout !== birthjs ) { - this._strout( 'birthVerifyFail' ); + return birthf; + } ); + } - throw Error( - "Birth self-compilation output does not match Prebirth!" - ); - } - this._strout( 'birthVerifyOk' ); + /** + * 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" ); + } - return birthout; - } ) - .catch( e => this._error( e ) ) - .then( status => - this._log( "=> " + this._doneMessage( status ) ) + 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 + ) ) ) + ); } @@ -174,12 +331,12 @@ class Bootstrap * 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 + * @param {string} path file path + * @param {string=} desc file description for logging * * @return {Promise} promise of string file contents */ - _loadPath( path, desc ) + _loadPath( path, desc = "" ) { this._strout( 'loadingf', desc, path ); @@ -297,38 +454,39 @@ class Bootstrap */ Bootstrap._strmap = { header: () => - " _ _ \n" + - " ,--0---0--. \n" + - "| |\n" + - "| |\n" + - " `---------' \n", + "\\\\ // \\\\\\\n" + + " \\\\ // \\\\\\\n" + + " \\\\// Ulambda \\\\\\\n" + + " \\\\\\ Scheme ///\n" + + " \\\\\\ ///\n" + + " \\\\\\ ///\n", loadingf: ( desc, path ) => - `Loading ${desc} from ${path}...`, + ( desc ) + ? `Loading ${desc} from ${path}...` + : `Loading ${path}...`, loadedf: ( path, len ) => `Loaded ${path} (len=${len}).`, prebirthDesc: () => - "\n" + - "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.\n", + "+ 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 ) => - `[prebirth] Compilation complete (len=${len}).`, + `Birth compilation complete (len=${len}).`, birthCompiled: () => - "\n" + - "Birth has been compiled with Prebirth. Since Birth is\n" + - "a re-implementation of Prebirth, it can now be used\n" + - "to compile itself.\n", + "+ 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: () => - "[birth] Self-compiling Birth...", + "Self-compiling Birth...", birthVerify: () => - "[birth] Verifying self-compilation output...", + "Verifying self-compilation output...", birthVerifyFail: () => "\n" + @@ -340,7 +498,29 @@ Bootstrap._strmap = { "Prebirth and/or Birth. Please report a bug!", birthVerifyOk: () => - "[birth] Birth output matches that of Prebirth.", + "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" + @@ -348,13 +528,24 @@ Bootstrap._strmap = { "See the console for a stack trace.\n\n", ok: () => - "Bootstrap successful!", + "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;