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
parent
beb561eeff
commit
b9f8e24ec2
|
@ -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 ///
|
||||
\\\ ///
|
||||
\\\ ///
|
||||
```
|
||||
|
|
|
@ -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<function(string):string>} 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<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,
|
||||
} )
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
) ) )
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue