Bootstrap: Integrate Rebirth

This bootstraps up to the final Rebirth generation.  I had been doing this
manually from the command line for far too long.

This also includes an ASCII Ulambda logo.

* README.md: Add Ulabmda logo
* build-aux/bootstrap/Bootstrap.js:
  (bootstrap): Extract majority of existing code into separate methods.
    Add invocation of `_rebirth'.
  (_birth): Extracted from `bootstrap'.  Extracted compiler function
    creation into separate method `_makeCompiler'.  Extract self-
    verification into `_verifyBirthOutput'.
  (_verifyBirthOutput, _makeCompiler): Extracted from `_birth'/`bootstrap'.
  (_rebirth, _compileRebirth): New methods.
  (_loadPath)[desc]: Default param value to empty string.
  (_strmap)[header]: Change logo.
    [loadingf]: Account for optional empty `desc'.
    [prebirthDesc, birthCompiled, birthVerifyOk]: Prefix with "+".
    [prebirthComplete]: Remove prefix "[prebirth]".
    [birthSelfCompiling, birthVerify]: Remove prefix "[birth]".
    [rebirthCompiling, rebirthCompiled, rebirthDone]: New keys.
    [ok]: Update to indicate that bootstrapping isn't complete yet.
  (_supmap): New field.
master
Mike Gerwitz 2018-08-10 23:22:14 -04:00
parent beb561eeff
commit b9f8e24ec2
Signed by: mikegerwitz
GPG Key ID: 8C917B7F5DC51BA2
2 changed files with 255 additions and 60 deletions

View File

@ -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. There's a lot more to be said, but that story will evolve over time.
``` ```
\/ Ulambda \\ // \\\
\ Scheme \\ // \\\
\\// Ulambda \\\
\\\ Scheme ///
\\\ ///
\\\ ///
``` ```

View File

@ -74,7 +74,7 @@ class Bootstrap
* *
* This compiles each of the phases of Ulambda Scheme beginning with * This compiles each of the phases of Ulambda Scheme beginning with
* Prebirth. This will evolve in complexity as we continue to move * 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 * There is currently no final result from this method other than
* log output and an indication of success or failure; that'll change as * log output and an indication of success or failure; that'll change as
@ -86,7 +86,36 @@ class Bootstrap
{ {
this._strout( 'header' ); 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<function(string):string>} Birth compiler
*/
_birth()
{
return this._loadPaths( [
[ "birth.scm", "Birth" ], [ "birth.scm", "Birth" ],
[ "libprebirth.js", "libprebirth" ], [ "libprebirth.js", "libprebirth" ],
] ) ] )
@ -94,11 +123,9 @@ class Bootstrap
{ {
this._strout( 'prebirthDesc' ); this._strout( 'prebirthDesc' );
return [ const preout = this._prebirth.compile( scm, lib );
this._prebirth.compile( scm, lib ),
scm, return [ preout, scm, lib ];
lib
];
} ) } )
.then( ( [ birthjs, scm, lib ] ) => .then( ( [ birthjs, scm, lib ] ) =>
{ {
@ -106,21 +133,32 @@ class Bootstrap
this._strout( 'birthCompiled' ); this._strout( 'birthCompiled' );
this._strout( 'birthSelfCompiling' ); this._strout( 'birthSelfCompiling' );
const birth = new Function( const birthf = this._makeCompiler( birthjs, {
'let __fsinit = this.__fsinit;' + "libprebirth.js": lib
'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 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 === '' ) { if ( birthout === '' ) {
throw Error( "Self-compilation yielded no output" ); throw Error( "Self-compilation yielded no output" );
} }
@ -136,12 +174,131 @@ class Bootstrap
} }
this._strout( 'birthVerifyOk' ); this._strout( 'birthVerifyOk' );
}
return birthout;
/**
* 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<function(string):string>} 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,
} ) } )
.catch( e => this._error( e ) ) );
.then( status => }
this._log( "=> " + this._doneMessage( status ) )
/**
* 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<function(string):string>} 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
) ) )
); );
} }
@ -175,11 +332,11 @@ class Bootstrap
* must return a Promise. * must return a Promise.
* *
* @param {string} path file path * @param {string} path file path
* @param {string} desc file description for logging * @param {string=} desc file description for logging
* *
* @return {Promise} promise of string file contents * @return {Promise} promise of string file contents
*/ */
_loadPath( path, desc ) _loadPath( path, desc = "" )
{ {
this._strout( 'loadingf', desc, path ); this._strout( 'loadingf', desc, path );
@ -297,38 +454,39 @@ class Bootstrap
*/ */
Bootstrap._strmap = { Bootstrap._strmap = {
header: () => header: () =>
" _ _ \n" + "\\\\ // \\\\\\\n" +
" ,--0---0--. \n" + " \\\\ // \\\\\\\n" +
"| |\n" + " \\\\// Ulambda \\\\\\\n" +
"| |\n" + " \\\\\\ Scheme ///\n" +
" `---------' \n", " \\\\\\ ///\n" +
" \\\\\\ ///\n",
loadingf: ( desc, path ) => loadingf: ( desc, path ) =>
`Loading ${desc} from ${path}...`, ( desc )
? `Loading ${desc} from ${path}...`
: `Loading ${path}...`,
loadedf: ( path, len ) => loadedf: ( path, len ) =>
`Loaded ${path} (len=${len}).`, `Loaded ${path} (len=${len}).`,
prebirthDesc: () => prebirthDesc: () =>
"\n" + "+ Prebirth is a very basic Lisp dialect with a compiler\n" +
"Prebirth is a very basic Lisp dialect with a compiler\n" + "+ implemented in ECMAScript. Birth is the same\n" +
"implemented in ECMAScript. Birth is the same\n" + "+ compiler, but re-implemented in Prebirth Lisp.",
"compiler, but re-implemented in Prebirth Lisp.\n",
prebirthComplete: ( len ) => prebirthComplete: ( len ) =>
`[prebirth] Compilation complete (len=${len}).`, `Birth compilation complete (len=${len}).`,
birthCompiled: () => birthCompiled: () =>
"\n" + "+ Birth has been compiled with Prebirth. Since Birth is\n" +
"Birth has been compiled with Prebirth. Since Birth is\n" + "+ a re-implementation of Prebirth, it can now be used\n" +
"a re-implementation of Prebirth, it can now be used\n" + "+ to compile itself.",
"to compile itself.\n",
birthSelfCompiling: () => birthSelfCompiling: () =>
"[birth] Self-compiling Birth...", "Self-compiling Birth...",
birthVerify: () => birthVerify: () =>
"[birth] Verifying self-compilation output...", "Verifying self-compilation output...",
birthVerifyFail: () => birthVerifyFail: () =>
"\n" + "\n" +
@ -340,7 +498,29 @@ Bootstrap._strmap = {
"Prebirth and/or Birth. Please report a bug!", "Prebirth and/or Birth. Please report a bug!",
birthVerifyOk: () => 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 ) => fatal: ( e ) =>
"\n\n!!! " + e.toString() + "\n\n" + "\n\n!!! " + e.toString() + "\n\n" +
@ -348,13 +528,24 @@ Bootstrap._strmap = {
"See the console for a stack trace.\n\n", "See the console for a stack trace.\n\n",
ok: () => ok: () =>
"Bootstrap successful!", "Bootstrap successful (but not yet complete)!",
fail: () => fail: () =>
"Bootstrap failed.", "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 // for use in a CommonJS (e.g. Node.js) environment
if ( typeof module !== 'undefined' ) { if ( typeof module !== 'undefined' ) {
module.exports = Bootstrap; module.exports = Bootstrap;