/**
* 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. Each step of the process is self-verifying.
*
* 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' );
this._loadPaths( [
[ "birth.scm", "Birth" ],
[ "libprebirth.js", "libprebirth" ],
] )
.then( ( [ scm, lib ] ) =>
{
this._strout( 'prebirthDesc' );
return [
this._prebirth.compile( scm, lib ),
scm,
lib
];
} )
.then( ( [ birthjs, scm, lib ] ) =>
{
this._strout( 'prebirthComplete', birthjs.length );
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,
},
} );
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' );
return birthout;
} )
.catch( e => this._error( e ) )
.then( status =>
this._log( "=> " + this._doneMessage( status ) )
);
}
/**
* 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
*
* @return {boolean} false
*/
_error( e )
{
const str = this._str( 'fatal', e );
this._logf( str, e );
return false;
}
/**
* 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" +
" ,--0---0--. \n" +
"| |\n" +
"| |\n" +
" `---------' \n",
loadingf: ( desc, path ) =>
`Loading ${desc} from ${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",
prebirthComplete: ( len ) =>
`[prebirth] 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",
birthSelfCompiling: () =>
"[birth] Self-compiling Birth...",
birthVerify: () =>
"[birth] 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] Birth output matches that of Prebirth.",
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!",
fail: () =>
"Bootstrap failed.",
};
// for use in a CommonJS (e.g. Node.js) environment
if ( typeof module !== 'undefined' ) {
module.exports = Bootstrap;
}