bootstrap: Formalize (both command-line and browser)

This allows bootstrapping in either a development environment (Node.js) or
simply using the intended runtime environment: the user's browser.

* bootstrap.html: Add file (browser).
* bootstrap.js: Add file (command-line).
* bootstrap/Bootstrap.js: Add class.  Formalize bootstrap process.
* bootstrap/libprebirth.js
  (fsdata): Add variable to serve as filesystem stub.
  (fs): Always throw error when `fs' module is unavailable.
  ($$js$file$_$$g$string): Consider `fsdata'.
* bootstrap/prebirth.js: Export as CommonJS module if in proper
    environment.  Abort automatic processing via stdin if root CommonJS
    module.
master
Mike Gerwitz 2017-11-11 23:59:45 -05:00
parent 2e5b536a5d
commit 6138731304
Signed by: mikegerwitz
GPG Key ID: 8C917B7F5DC51BA2
5 changed files with 545 additions and 6 deletions

View File

@ -0,0 +1,104 @@
<!DOCTYPE html>
<!--
Copyright (C) 2017 Mike Gerwitz
This file is part of Gibble.
Gibble 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 <http://www.gnu.org/licenses/>.
-->
<html>
<head>
<meta charset="utf-8" />
<title>Gibble Lisp Browserstrap</title>
<style type="text/css">
* {
font-family: monospace;
}
button#start {
display: inline-block;
border: 0px solid black;
background-color: transparent;
margin-left: 10ex;
}
</style>
</head>
<body>
<h1>Gibble Lisp Bootstrap</h1>
<pre id="log">
This is the in-browser version of the bootstrap process; it operates
no differently than when invoked from the command-line, and has the
benefit of being able to be run without any further dependencies
(CLI requires Node.js, which is a hefty dependency.)
You do need a browser that supports ECMAScript 2015 or later, though.
Seeing as how it is rather rude to begin hammering your CPU when you
first load a page (as virtually every site you visit does now-a-days),
I'll wait for you to click the button below to get started.
</pre>
<button id="start">| ... just a sec ... |</button>
<noscript>
<p>
<strong>Bootstrapping requires JavaScript.</strong>
All of Gibble requires JavaScript, actually.
</p>
</noscript>
<script type="text/javascript" src="bootstrap/prebirth.js"></script>
<script type="text/javascript" src="bootstrap/Bootstrap.js"></script>
<script type="text/javascript">
const loge = document.getElementById( 'log' );
const logf = ( str, e ) =>
{
loge.innerText += str + "\n";
console.log( str );
if ( e instanceof Error ) {
console.error( e );
}
};
const getf = path => new Promise( ( resolve, reject ) =>
{
const xhr = new XMLHttpRequest();
xhr.responseType = 'text';
xhr.open( 'GET', `bootstrap/${path}`, true );
xhr.onload = result => resolve( result.target.responseText );
xhr.onerror = e => reject( e );
xhr.send();
} );
const strap = new Bootstrap( getf, logf, new Prebirth() );
const start = document.getElementById( 'start' );
start.addEventListener( 'click', ev =>
{
ev.target.parentElement.removeChild( ev.target );
strap.bootstrap();
} );
start.innerText = '| Start Bootstrapping! |';
</script>
</body>
</html>

View File

@ -0,0 +1,53 @@
/**
* Command-line bootstrap procedure for Gibble Lisp
*
* Copyright (C) 2017 Mike Gerwitz
*
* This file is part of Gibble.
*
* Gibble 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 <http://www.gnu.org/licenses/>.
*
* This script is intended to be run from the command line and requires
* Node.js; it is more suitable to development and automated processes.
* If you are a user or non-developer without such an environment set up,
* then you might rather bootstrap using your web browser instead; you can
* do so by directing your web browser to `bootstrap.html', located in the
* same directory as this file.
*
* To perform the entire bootstrapping process, simply invoke this script:
* $ node bootstrap.js
*
* For more information on each aspect of the process, see the individual
* files in `./bootstrap/'.
*/
const Bootstrap = require( './bootstrap/Bootstrap' );
const Prebirth = require( './bootstrap/prebirth' );
const logf = ( str, e ) =>
{
console.log( str );
if ( e instanceof Error ) {
console.error( e );
}
};
const getf = path => Promise.resolve(
require( 'fs' ).readFileSync( `./bootstrap/${path}` ).toString()
);
const strap = new Bootstrap( getf, logf, new Prebirth() );
strap.bootstrap();

View File

@ -0,0 +1,361 @@
/**
* Bootstrap procedure for Gibble Lisp
*
* Copyright (C) 2017 Mike Gerwitz
*
* This file is part of Gibble.
*
* Gibble 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 <http://www.gnu.org/licenses/>.
*
* Ideally, the user should be able to bootstrap Gibble Lisp with nothing
* more than what they already have installed on their computer, in the
* environment that Gibble 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 Gibble Lisp
*
* 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 Gibble 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<string>} 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;
}

View File

@ -144,13 +144,27 @@ const $$js$match = ( r, s ) => s.match( r ) || false;
const $$js$replace = ( r, repl, s ) => s.replace( r, repl );
// the variable __fsinit, if defined, can be used to stub the filesystem
const fsdata = ( typeof __fsinit !== 'undefined' ) ? __fsinit : {};
const fs = ( typeof require !== 'undefined' )
? require( 'fs' )
: { readFileSync: path => window.fsdata[ path ] };
: {
readFileSync( path )
{
throw Error( `Cannot load ${path} (no fs module)` );
},
}
// stdin->string
// file->string
const $$js$file$_$$g$string = ( path ) =>
fs.readFileSync( path ).toString();
{
if ( fsdata[ path ] !== undefined ) {
return fsdata[ path ];
}
return fsdata[ path ] = fs.readFileSync( path ).toString();
};
/** =============== end of libprebirth =============== **/

View File

@ -688,6 +688,13 @@ class Prebirth
}
/* If we're running in a CommonJS environment (e.g. Node.js), export our
/* entrypoint.
*/
if ( typeof module !== 'undefined' ) {
module.exports = Prebirth;
}
/*
* Prebirth was originally intended to be run via the command line using
@ -696,9 +703,9 @@ class Prebirth
*/
( function ()
{
if ( typeof process === 'undefined' ) {
return;
}
if ( ( typeof process === 'undefined' )
|| ( ( typeof module !== 'undefined' ) && ( module.id !== '.' ) ) )
return;
const fs = require( 'fs' );
const src = fs.readFileSync( '/dev/stdin' ).toString();