hdl-js
Version:
Hardware definition language (HDL) and Hardware simulator
600 lines (487 loc) • 16.3 kB
JavaScript
/**
* The MIT License (MIT)
* Copyright (c) 2017-present Dmitry Soshnikov <dmitry.soshnikov@gmail.com>
*/
'use strict';
var colors = require('colors');
var fs = require('fs');
var hdl = require('../../index');
var path = require('path');
var TablePrinter = require('../table-printer');
var vm = require('vm');
var _hdl$emulator = hdl.emulator,
BuiltInGate = _hdl$emulator.BuiltInGate,
HDLClassFactory = _hdl$emulator.HDLClassFactory,
SystemClock = _hdl$emulator.Clock.SystemClock,
ScriptInterpreter = _hdl$emulator.ScriptInterpreter;
var _require = require('../util/numbers'),
int16 = _require.int16;
var _require2 = require('../util/string-util'),
centerString = _require2.centerString;
function enforceUnique(v) {
return Array.isArray(v) ? v[v.length - 1] : v;
}
var options = require('yargs').usage('Usage: $0 [options]').options({
gate: {
alias: 'g',
describe: 'Name of a built-in gate or path to an HDL file',
requiresArg: true,
coerce: enforceUnique
},
parse: {
alias: 'p',
describe: 'Parse the HDL file, and print AST'
},
list: {
alias: 'l',
describe: 'List supported built-in gates'
},
describe: {
alias: 'd',
describe: "Prints gate's specification"
},
'exec-on-data': {
alias: 'e',
describe: "Evaluates gate's logic on passed data; " + 'validates outputs if passed',
requiresArg: true,
coerce: enforceUnique
},
format: {
alias: 'f',
describe: 'Values format (binary, hexadecimal, decimal)',
nargs: 1,
choices: ['bin', 'hex', 'dec'],
coerce: enforceUnique
},
run: {
alias: 'r',
describe: 'Runs sequentially the rows from --exec-on-data table'
},
'clock-rate': {
describe: 'Rate (number of cycles per second) for the System clock',
requiresArg: true,
coerce: enforceUnique
},
columns: {
alias: 'c',
describe: 'Whitelist of columns (comma-separated) to show in the table',
requiresArg: true,
coerce: enforceUnique
},
script: {
alias: 's',
describe: 'Run testing script, which automatically loads a gate, ' + 'tests the logic, and compares the results.',
requiresArg: true,
coerce: enforceUnique
}
}).alias('help', 'h').alias('version', 'v').argv;
/**
* Directory with all built-in gates.
*/
var BUILTINS_DIR = __dirname + '/../emulator/hardware/builtin-gates';
/**
* Format to radix.
*/
var FORMAT_VALUES = {
bin: { radix: 2, pad: 16 }, // 0000000000001111
hex: { radix: 16, pad: 4 }, // 000F
dec: { radix: 10, pad: 0 } // no padding
};
/**
* Loads data in the "Extended JSON" format.
*
* The `data` is either the actual data-string, or a filename.
*/
function loadAndProcessData(data, formatRadix) {
var fullPath = path.resolve(data);
if (fs.existsSync(fullPath)) {
data = fs.readFileSync(fullPath, 'utf-8');
}
return parseInputData(data, formatRadix);
}
/**
* Parse input data.
*/
function parseInputData(data, formatRadix) {
var parsed = vm.runInNewContext('(' + data + ')');
parsed.forEach(function (row) {
for (var prop in row) {
row[prop] = processInputValue(row[prop], formatRadix);
}
});
return parsed;
}
/**
* Process input data.
*/
function processInputValue(value, formatRadix) {
// Strings are converted to numbers according to the `formatRadix`.
if (typeof value === 'string') {
value = Number.parseInt(value, formatRadix);
}
// Truncate numbers to 16-bits.
if (typeof value === 'number') {
return int16(value);
}
return value;
}
/**
* Lists built-in gates.
*/
function listBuiltInGates() {
var builtinGates = fs.readdirSync(BUILTINS_DIR).filter(function (file) {
return (/^[A-Z]/.test(file)
);
}).map(function (file) {
return ' - ' + path.basename(file, '.js');
});
console.info('');
console.info(colors.bold('Built-in gates:'));
console.info('');
console.info(builtinGates.join('\n'), '\n');
}
/**
* Loads a gate class.
*/
function loadGate(gate) {
try {
// Custom gate from HDL.
if (fs.existsSync(gate)) {
return HDLClassFactory.fromHDLFile(gate);
}
// Built-in gate.
return require(BUILTINS_DIR + '/' + gate);
} catch (_e) {
console.error(colors.red('\nUnknown gate: "' + gate + '".'));
listBuiltInGates();
process.exit(1);
}
}
/**
* Prints specification and truth table of a gate.
*/
function describeGate(gate, formatRadix, formatStringLengh, columns) {
var run = options.run;
var GateClass = loadGate(gate);
var isBuiltIn = Object.getPrototypeOf(GateClass) === BuiltInGate;
var isKeyboard = isBuiltIn && GateClass.name === 'Keyboard';
var spec = GateClass.Spec;
console.info('');
console.info((isBuiltIn ? 'BuiltIn ' : 'Custom ') + colors.bold('"' + GateClass.name + '"') + ' gate:');
var toFullName = function toFullName(name) {
var isSimple = typeof name === 'string' || name.size === 1;
return name = isSimple ? ' - ' + (name.name || name) : ' - ' + name.name + '[' + name.size + ']';
};
// Description:
var description = spec.description.split('\n').map(function (line) {
return ' ' + line;
}).join('\n');
console.info('\n' + colors.bold('Description:\n\n') + description);
// Input pins:
var inputPins = spec.inputPins.length > 0 ? spec.inputPins.map(function (input) {
return toFullName(input);
}).join('\n') : ' None';
if (isKeyboard) {
inputPins = ' Keyboard input';
}
console.info('\n' + colors.bold('Inputs:\n\n') + inputPins);
// Internal pins:
if (spec.internalPins && spec.internalPins.length > 0) {
var internalPins = spec.internalPins.map(function (internal) {
return toFullName(internal);
}).join('\n');
console.info('\n' + colors.bold('Internal pins:\n\n') + internalPins);
}
// Output pins:
var outputPins = spec.outputPins.map(function (output) {
return toFullName(output);
}).join('\n');
console.info('\n' + colors.bold('Outputs:\n\n') + outputPins);
console.info('');
// Special truth table for Keyboard.
if (isKeyboard) {
var keyboard = GateClass.defaultFromSpec().listen();
console.info(colors.bold('Truth table:') + ' press any key...\n');
var printKeyboardTable = function printKeyboardTable(ch, code) {
var printer = new TablePrinter({
head: ['char', 'out']
});
printer.push([{ content: ch.trim() || ' ', hAlign: 'center' }, { content: code, hAlign: 'center' }]);
console.info(printer.toString() + '\n');
console.info('Ctrl-c to exit...\n');
};
printKeyboardTable('?', '?');
keyboard.getPin('out').on('change', function (value) {
clearPreviousLines(8);
printKeyboardTable(String.fromCharCode(value), value);
});
return;
}
var truthTable = spec.truthTable;
var isCustomTable = truthTable.length === 0;
// Compiled gates from HDL don't provide static canonical
// truth table, so we calculate it for 5 rows on random data.
if (isCustomTable) {
truthTable = GateClass.defaultFromSpec().generateTruthTable();
} else {
console.info(colors.bold('Truth table:'), '\n');
}
var printTable = function printTable(table) {
GateClass.printTruthTable({
table: table,
columns: columns,
formatRadix: formatRadix,
formatStringLengh: formatStringLengh
});
};
// Sequential table run.
if (run) {
console.info(colors.bold('\nCurrent results for pins:'), '\n');
runSlice(truthTable, 0, printTable);
return;
}
// Truth table:
if (isCustomTable) {
var isSimple = spec.inputPins.every(function (input) {
return typeof input === 'string' || input.size === 1;
});
if (isSimple) {
console.info(colors.bold('Truth table:'), '\n');
} else {
console.info(colors.bold('Truth table:'), '(generated, random 5 rows)\n');
}
}
printTable(truthTable);
}
/**
* Runs a slice of calculation, printing a table
* one row at a time.
*/
function runSlice(data, index, action) {
var slice = data.slice(index, index + 1);
action(slice);
if (index === data.length - 1) {
// Reset and run forever in a loop.
index = -1;
}
setTimeout(function () {
// Clear 6 previous lines, which take a previous table row.
clearPreviousLines(6);
runSlice(data, index + 1, action);
}, 1000 / SystemClock.getRate());
}
/**
* Clears previous lines in the terminal.
*/
function clearPreviousLines(count) {
process.stdout.write('\r\x1B[K\r\x1B[1A'.repeat(count));
}
function execScript(script) {
var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
_ref$verbose = _ref.verbose,
verbose = _ref$verbose === undefined ? false : _ref$verbose;
try {
new ScriptInterpreter({
file: script,
workingDirectory: path.dirname(script)
}).exec();
if (verbose) {
console.info(colors.green('\n\u2713 Script executed successfully!\n'));
} else {
console.info(colors.green('[PASS]'), path.basename(script));
}
} catch (e) {
if (!verbose) {
console.info(colors.red('[FAIL]'), path.basename(script));
return;
}
if (!e.errorData) {
console.info('Script error:', e);
return;
}
var _e$errorData = e.errorData,
header = _e$errorData.header,
errorList = _e$errorData.errorList,
compareTo = _e$errorData.compareTo;
console.info(colors.red('\nError executing the script:\n'));
var pad2 = ' ';
var pad4 = ' ';
// Find Max line number
var maxLineLen = errorList[errorList.length - 1].line.toString().length;
var firstColumnLength = header.slice(1).indexOf('|');
var firstLinePad = '1'.padStart(maxLineLen, ' ');
var maxLinePad = ' '.repeat(maxLineLen);
var prevLine = 1;
var actualLines = [];
var expectedLines = [];
var lines = [];
errorList.forEach(function (_ref2) {
var actual = _ref2.actual,
expected = _ref2.expected,
line = _ref2.line;
lines.push(colors.bold(line));
var expectedDiffLine = expected.split('').map(function (symbol, idx) {
if (actual[idx] !== symbol) {
return colors.green(symbol);
}
return symbol;
}).join('');
if (prevLine !== line - 1) {
expectedLines.push(pad4 + maxLinePad + ' |' + centerString('...', firstColumnLength));
}
expectedLines.push(pad4 + line.toString().padStart(maxLineLen, ' ') + ' ' + expectedDiffLine);
var actualDiffLine = actual.split('').map(function (symbol, idx) {
if (expected[idx] !== symbol) {
return colors.bold(colors.red(symbol));
}
return symbol;
}).join('');
if (prevLine !== line - 1) {
actualLines.push(pad4 + maxLinePad + ' |' + centerString('...', firstColumnLength));
}
actualLines.push(pad4 + line.toString().padStart(maxLineLen, ' ') + ' ' + actualDiffLine);
prevLine = line;
});
if (lines.length > 1) {
lines[lines.length - 1] = 'and ' + lines[lines.length - 1];
}
var linesInfo = lines.join(', ');
console.info(pad2 + colors.bold('Expected'), 'on line' + (lines.length > 1 ? 's' : ''), linesInfo, 'of', colors.dim(path.dirname(compareTo) + '/') + colors.bold(path.basename(compareTo)) + ':\n');
console.info(pad4 + firstLinePad, colors.green(header));
console.info(expectedLines.join('\n'));
console.info('\n' + pad2 + colors.bold('Received:\n'));
console.info(pad4 + firstLinePad, colors.red(header));
console.info(actualLines.join('\n'), '\n');
}
}
function main() {
var clockRate = options.clockRate,
describe = options.describe,
execOnData = options.execOnData,
_options$format = options.format,
format = _options$format === undefined ? 'bin' : _options$format,
gate = options.gate,
list = options.list,
parse = options.parse,
run = options.run,
script = options.script;
// ------------------------------------------------------
// Script execution.
if (script) {
if (fs.lstatSync(script).isDirectory()) {
// Run the whole directory:
fs.readdirSync(script).forEach(function (file) {
if (path.extname(file) !== '.tst') {
return;
}
execScript(script + '/' + file, { verbose: false });
});
} else {
// Single script in the verbose mode:
execScript(script, { verbose: true });
}
}
// Whitelist of columns, empty list -- all columns.
var columns = options.columns ? options.columns.split(',') : [];
if (clockRate) {
SystemClock.setRate(clockRate);
}
var formatRadix = FORMAT_VALUES[format].radix;
var formatStringLengh = FORMAT_VALUES[format].pad;
if (gate && !describe && !execOnData) {
console.info('\nHint: pass ' + colors.bold('--describe') + ' option to see ' + (colors.bold('"' + gate + '"') + ' gate specification.\n'));
}
if (describe && !gate) {
console.info('\nHint: pass ' + colors.bold('--gate') + ' option to see ' + 'the specification of a built-in or custom gate.\n');
}
// HDL file to be parsed.
var hdlFile = void 0;
// ------------------------------------------------------
// Handle gate (built-in or custom).
if (gate && fs.existsSync(gate)) {
hdlFile = fs.readFileSync(gate, 'utf-8');
}
// ------------------------------------------------------
// List built-in gates.
if (list) {
listBuiltInGates();
}
// ------------------------------------------------------
// Describes a gate (built-in or composite).
if (gate && describe) {
describeGate(gate, formatRadix, formatStringLengh, columns);
}
// ------------------------------------------------------
// Exec on data.
if (execOnData) {
if (!gate) {
console.info('\nHint: pass ' + colors.bold('--gate') + ' option to execute ' + 'gate logic on the passed data.\n');
return;
}
var GateClass = loadGate(gate);
var gateInstance = GateClass.defaultFromSpec();
var data = loadAndProcessData(execOnData, formatRadix);
var conflictingRows = {};
/**
* Prints truth table from the result.
*/
var printTable = function printTable(table) {
GateClass.printTruthTable({
table: table,
columns: columns,
formatRadix: formatRadix,
formatStringLengh: formatStringLengh,
transformValue: function transformValue(value, row, column) {
if (conflictingRows[row] && conflictingRows[row].hasOwnProperty(column)) {
var pinInfo = GateClass.getPinInfo(column);
var expected = (data[row][column] >>> 0).toString(formatRadix).padStart(formatRadix !== 10 ? pinInfo.size : 0, '0').toUpperCase();
if (expected.length > formatStringLengh) {
expected = expected.slice(-formatStringLengh);
}
return colors.red(expected) + ' / ' + colors.green(value);
}
return value;
}
});
};
if (run) {
console.info(colors.bold('\nCurrent results for pins:'), '\n');
runSlice(data, 0, printTable);
return;
}
var _gateInstance$execOnD = gateInstance.execOnData(data),
result = _gateInstance$execOnD.result,
conflicts = _gateInstance$execOnD.conflicts;
if (conflicts.length) {
console.info(colors.red(colors.bold('\nFound ' + conflicts.length + ' conflicts in:\n')));
conflicts.forEach(function (conflict) {
var row = conflict.row,
pins = conflict.pins;
var pinNames = Object.keys(pins);
conflictingRows[row] = pins;
console.info(' - row: ' + row + ', pins: ' + pinNames.join(', '));
});
console.info('');
} else {
// No conflicts.
console.info(colors.bold('\nTruth table for data:'), '\n');
}
// Always print correct table eventually,
// showing conflicting values in red.
printTable(result);
}
// ------------------------------------------------------
// Parser.
if (parse && hdlFile) {
var parsed = hdl.parse(hdlFile);
console.info('');
console.info(colors.bold('Parsed:'));
console.info('');
console.info(JSON.stringify(parsed, null, 2), '\n');
return;
}
}
module.exports = main;
if (require.main === module) {
main();
}