UNPKG

hdl-js

Version:

Hardware definition language (HDL) and Hardware simulator

600 lines (487 loc) 16.3 kB
/** * 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(); }