Source: vcd-parser.js

/**
 * A namespace.
 * @namespace VCDParser
 */

const async = require('async');
const { extend } = require('./utils');

/**
 * Parse VCD text content and generate a valid JSON representation.
 * The function returns a promise unless a callback is provided.
 * @name VCDParser~parse
 * @function
 * @param {string} content - The text content of the VCD file
 * @param {VCDParser~Options} [opts = {}] - Optional configuration to customize the parsing process
 * @param {VCDParser~ParseCallback} [cb = null] - Optional callback if you don't prefer to use promises
 * @return {Promise<VCDParser~ParsedData>} Promise object that resolves with the parsed data
 * @example
 * const VCDParser = require('vcd-parser');
 * VCDParser.parse(`
	$date
	Tue Feb 12 14:01:15 2019
	$end
	$version
	Icarus Verilog
	$end
	$timescale
	1ns
	$end
	$scope module test_tb $end
	$var reg 1 ! clk $end
	$var wire 1 " rst $end
	$upscope $end
	$enddefinitions $end
	#0
	$dumpvars
	0"
	0!
	$end
	#15
	1"
	#20
	1!
	#40
	0!
	#60
	1!
	#80
	0!
	#100
	1!
	#115
`).then(parsedData => {
 *	console.log(parsedData);
// {
// 	"date": "Tue Feb 12 14:01:15 2019",
// 	"version": "Icarus Verilog",
// 	"timescale": "1ns",
// 	"endtime": "115",
// 	"scale": "1ns",
// 	"signal": [
// 		{
// 			"type": "reg",
// 			"size": 1,
// 			"refName": "!",
// 			"signalName": "clk",
// 			"module": "test_tb",
// 			"name": "test_tb.clk",
// 			"wave": [
// 				[
// 					"0",
// 					"0"
// 				],
// 				[
// 					"20",
// 					"1"
// 				],
// 				[
// 					"40",
// 					"0"
// 				],
// 				[
// 					"60",
// 					"1"
// 				],
// 				[
// 					"80",
// 					"0"
// 				],
// 				[
// 					"100",
// 					"1"
// 				]
// 			]
// 		},
// 		{
// 			"type": "wire",
// 			"size": 1,
// 			"refName": "\"",
// 			"signalName": "rst",
// 			"module": "test_tb",
// 			"name": "test_tb.rst",
// 			"wave": [
// 				[
// 					"0",
// 					"0"
// 				],
// 				[
// 					"15",
// 					"1"
// 				]
// 			]
// 		}
// 	]
// }
 * }).catch(err => {
 *	console.error(err);
 * }).
 */
const parse = (content, opts = {}, cb) => {
	if (typeof opts === 'function') {
		cb = opts;
		opts = {};
	}
	const wrappedCallback = () => {
		if (typeof cb !== 'function') {
			return null;
		}
		return function() {
			return cb(null, ...arguments);
		};
	};
	return new Promise((resolve, reject) => {
		setTimeout(function() {
			process.nextTick(function() {
				const blocks = content
					.split(/\s*\$end\s/gm)
					.map(line => line.replace(/[\n\t]/gm, ' '));
				const States = {
					Init: 0,
					Meta: 1,
					Def: 2,
					Dump: 3
				};
				let modules = [];
				const meta = {};
				const signals = [];
				const signalMap = {};
				let lastIndex;
				for (let i = 0; i < blocks.length; i++) {
					const block = blocks[i].trim();
					if (block === '') {
						continue;
					}

					const scopeMatches = /\$(\w+?)\b/gm.exec(block);
					if (scopeMatches) {
						const scopeName = scopeMatches[1];
						if (scopeName === 'scope') {
							state = States.Def;
							const scopeDefMatches = /\$(\w+)\s+([\s\S]+)\s+([\s\S]+)/gm.exec(
								block
							);
							if (!scopeDefMatches) {
								return reject({
									error: 'Invalid VCD data'
								});
							}
							const modName = scopeDefMatches[3];
							modules.push(modName);
						} else if (scopeName === 'enddefinitions') {
							state = States.Dump;
							lastIndex = i + 1;
							break;
						} else if (scopeName === 'upscope') {
							modules.pop();
						} else if (scopeName === 'var') {
							const varDefMatches = /\$(\w+)\s+([\s\S]+?)\s+(\d+)\s+([\s\S]+?)\s+([\s\S]+)\s*/gm.exec(
								block
							);
							const signalName = varDefMatches[5].replace(
								/\s+/,
								''
							);
							const refName = varDefMatches[4];
							if (!signalMap[refName]) {
								const signal = {
									type: varDefMatches[2],
									size: parseInt(varDefMatches[3]),
									refName,
									signalName,
									module: modules[modules.length - 1] || '',
									name: modules.concat(signalName).join('.'),
									wave: []
								};
								signals.push(signal);
								signalMap[refName] = signal;
							}
						} else {
							const contentMatches = /\$(\w+)\b\s*([\s\S]+)?\s*/gm.exec(
								block
							);
							if (contentMatches) {
								meta[contentMatches[1]] = contentMatches[2];
							}
						}
					} else {
						return reject({
							error: 'Invalid VCD data'
						});
					}
				}
				if (!lastIndex) {
					return reject({
						error: 'Invalid VCD data'
					});
				}
				let currentTime = 0;
				const rem = content.split(/\s*\$enddefinitions\s*/gm)[1];
				if (!rem) {
					return reject({
						error: 'Invalid VCD data'
					});
				}
				const lines = rem.split(/\s*\n\s*/gm);
				async.eachSeries(
					lines,
					function(line, callback) {
						(function(line) {
							setTimeout(function() {
								const block = line.trim();
								if (block === '') {
									return callback();
								}
								const timingMatches = /^#(\d+)$/gm.exec(block);
								if (timingMatches) {
									const time = parseInt(timingMatches[1]);
									currentTime = time;
								} else if (block === '$dumpvars') {
									return callback();
								} else if (block === '$end') {
									return callback();
								} else {
									if (block.startsWith('x')) {
										const refName = block.substr(1).trim();
										if (!signalMap[refName]) {
											return callback({
												error: 'Invalid VCD data'
											});
										}
										const wave = signalMap[refName].wave;
										if (
											!opts.compress ||
											!wave.length ||
											wave[wave.length - 1][1] !== 'x'
										) {
											signalMap[refName].wave.push([
												currentTime.toString(),
												'x'
											]);
										}
									} else if (block.startsWith('b')) {
										const matches = /b([01xz]+)\s+([\s\S]+)/gm.exec(
											block
										);
										if (!matches) {
											return callback({
												error: 'Invalid VCD data'
											});
										}
										const refName = matches[2];
										if (!signalMap[refName]) {
											return callback({
												error: 'Invalid VCD data'
											});
										}
										let value = matches[1];
										if (!opts.expandAmbigousBus) {
											if (/z/gm.test(value)) {
												value = 'z';
											} else if (/x/gm.test(value)) {
												value = 'x';
											}
										}
										const wave = signalMap[refName].wave;
										if (
											!opts.compress ||
											!wave.length ||
											wave[wave.length - 1][1] !== value
										) {
											signalMap[refName].wave.push([
												currentTime.toString(),
												value
											]);
										}
									} else if (block.startsWith('z')) {
										const refName = block.substr(1).trim();
										if (!signalMap[refName]) {
											return callback({
												error: 'Invalid VCD data'
											});
										}
										const wave = signalMap[refName].wave;
										if (
											!opts.compress ||
											!wave.length ||
											wave[wave.length - 1][1] !== 'z'
										) {
											signalMap[refName].wave.push([
												currentTime.toString(),
												'z'
											]);
										}
									} else if (/^[01]([\s\S]+)/gm.test(block)) {
										const matches = /^([01])([\s\S]+)/gm.exec(
											block
										);
										const refName = matches[2];
										if (!signalMap[refName]) {
											return callback({
												error: 'Invalid VCD data'
											});
										}
										const converted = parseInt(
											matches[1],
											10
										).toString(2);
										const wave = signalMap[refName].wave;
										if (
											!opts.compress ||
											!wave.length ||
											wave[wave.length - 1][1] !==
												converted
										) {
											signalMap[refName].wave.push([
												currentTime.toString(),
												converted
											]);
										}
									} else if (block.startsWith('r')) {
										const matches = /r((\d+\.?\d*)|(nan)|(x+)|(z+))\s+([\s\S]+)/gm.exec(
											block
										);
										if (!matches) {
											return callback({
												error: 'Invalid VCD data'
											});
										}
										let value;
										if (
											matches[1] === 'nan' ||
											matches[1].charAt(0) === 'x'
										) {
											value = 'x';
										} else if (
											matches[1].charAt(0) === 'z'
										) {
											value = 'z';
										} else {
											value = parseFloat(matches[1]);
										}
										const refName = matches[6];
										if (!signalMap[refName]) {
											return callback({
												error: 'Invalid VCD data'
											});
										}
										const wave = signalMap[refName].wave;
										if (
											!opts.compress ||
											!wave.length ||
											wave[wave.length - 1][1] !==
												converted
										) {
											signalMap[refName].wave.push([
												currentTime.toString(),
												isNaN(value)
													? 'x'
													: value.toString()
											]);
										}
									} else {
										return callback({
											error: 'Invalid VCD data'
										});
									}
								}
								return callback();
							}, 0);
						})(line);
					},
					function(err) {
						if (err) {
							return reject(err);
						}
						meta.endtime = currentTime.toString();
						meta.scale = meta.timescale;
						return resolve(
							extend({}, meta, {
								signal: signals
							})
						);
					}
				);
			});
		}, 0);
	})
		.then(wrappedCallback())
		.catch(cb);
};

/**
 * The optional configuration for the VCD parser
 * @typedef {Object} VCDParser~Options
 * @property {boolean} compress - Compress the output wave by ignoring the unchanged values
 * @property {boolean} expandAmbigousBus - If the bus has some ambigous value (z | x), it gets expanded to represent the whole bus signal
 */

/**
 * The value of a signal at a specific point of time, represnted as a tuple [time, value]
 * @typedef {Array<number>} VCDParser~SignalValue
 * @property {number} 0 - The time of the event
 * @property {number} 1 - The value of the signal at that event
 */

/**
 * The object representing one signal data
 * @typedef {Object} VCDParser~Signal
 * @property {string} name - The full name of the signal
 * @property {string} type - The type of the signal, e.g. wire, reg,..etc
 * @property {number} size - The size/width of the signal in bits
 * @property {string} refName - The reference for this signal used inside the VCD file
 * @property {string} module - The name of the top module for which this signal belongs
 * @property {Array<VCDParser~SignalValue>} wave - The values of the signal at different points of time
 */

/**
 * The parsed VCD object generated by the parser
 * @typedef {Object} VCDParser~ParsedData
 * @property {string} [<"meta">] - The values of different initial meta-data, e.g. date, timescale..etc
 * @property {string} endtime - The endtime of the simulation
 * @property {string} scale - The time-scale unit of the simulation
 * @property {Array<VCDParser~Signal>} signal - The signal values of the simulation
 */

/**
 * The callback for the parsing function.
 * @callback VCDParser~ParseCallback
 * @param {error} err - The error generated while parsing
 * @param {ParsedData} parsedJSON - The JSON document generated by the parser
 */

module.exports = { parse };