From 24ee1809bd05006cdfd8ad0a2739148ee559d6e6 Mon Sep 17 00:00:00 2001 From: Carlos Venegas Date: Sat, 18 Jan 2025 22:37:22 +0100 Subject: [PATCH] Verilog Code Importer: Support multiple module in files and header comments per module --- app/scripts/services/forms.js | 64 +++++---- app/scripts/services/utils.js | 248 +++++++++++++++++++++++++--------- 2 files changed, 228 insertions(+), 84 deletions(-) diff --git a/app/scripts/services/forms.js b/app/scripts/services/forms.js index ba5b292b1..0cf65039e 100644 --- a/app/scripts/services/forms.js +++ b/app/scripts/services/forms.js @@ -1737,32 +1737,50 @@ angular.module('icestudio') //-- Field 5: Import code let field5 = new ButtonField( gettextCatalog.getString('Import code'), //-- Button text - function(){ //-- Callback + function(){ //-- Callback //-- Open the file Dialog //-- The selector is passed as a parameter //-- The html element is located in the menu.html file - utils.openDialog("#input-import-verilog", function (filepath) { - nodeFs.readFile(filepath, 'utf8', (err, content) => { - if (err) { - alertify.error(gettextCatalog.getString('Error reading the file: {{error}}', { error: err.message })); - } else { - const result = utils.parseVerilog(content); - portsIn = result.inputs; - portsOut = result.outputs; - paramsIn = result.parameters; - field0.write(result.inputs); - field1.write(result.outputs); - field2.write(result.parameters); - if(result.inouts.length>0){ - if(field3){ field3.write(result.inouts);} - else{ - - alertify.error(gettextCatalog.getString('Import incomplete, this design contains tristate IO, you need to enable tristate IO in Edit->Preferences->Advanced features->Enable tri-state connections and reimport the block'),20000); - } - } - self.code = result.moduleBody; - } - }); + utils.openDialog("#input-import-verilog", async function (filepath) { + try{ + + + // Leer el archivo de forma asíncrona + const content = await nodeFs.promises.readFile(filepath, 'utf8'); + + // Parsear el contenido del archivo + const result = await utils.parseVerilog(content); + + console.log('RESULT', result); + + // Actualizar los campos con los resultados del parseo + portsIn = result.inputs; + portsOut = result.outputs; + paramsIn = result.parameters; + field0.write(result.inputs); + field1.write(result.outputs); + field2.write(result.parameters); + + if (result.inouts.length > 0) { + if (field3) { + field3.write(result.inouts); + } else { + alertify.error( + gettextCatalog.getString( + 'Import incomplete, this design contains tristate IO, you need to enable tristate IO in Edit->Preferences->Advanced features->Enable tri-state connections and reimport the block' + ), + 20000 + ); + } + } + + // Almacenar el cuerpo del módulo + self.code = result.moduleBody; + + }catch(err){ + alertify.error(gettextCatalog.getString('Error reading the file: {{error}}', { error: err.message })); + } + }); }, diff --git a/app/scripts/services/utils.js b/app/scripts/services/utils.js index dd3555dfe..be56f586f 100644 --- a/app/scripts/services/utils.js +++ b/app/scripts/services/utils.js @@ -918,7 +918,7 @@ angular.module('icestudio') //-- * callback(filepath): It is called when the user has pressed the //-- ok button. The chosen file is passed as a parameter //----------------------------------------------------------------------- - this.openDialog = function (inputID, callback) { + this.openDialog = function (inputID, callback) { //-- Get the file chooser element (from the DOM) let chooser = $(inputID); @@ -1692,68 +1692,194 @@ angular.module('icestudio') function processSignals(regex, block) { - return [...block.matchAll(regex)].map(match => { - const range = match[2]?.trim() || ''; - const names = match[3].split(',').map(name => name.trim()); - return names.map(name => (range ? `${name}${range}` : name)); // icestudio swap range - }).flat(); } - -this.parseVerilog = function (code) { - - - const inputRegex = /\binput\s+(wire\s+|signed\s+|wire signed\s+)?(\[[^\]]+\]\s+)?([\w\s,]+?)(?=,?\s*\boutput\b|,?\s*\binput\b|$)/gm; - const outputRegex = /\boutput\s+(reg\s+|wire\s+|signed\s+|wire signed\s+|reg signed\s+)?(\[[^\]]+\]\s+)?([\w\s,]+?)(?=,?\s*\binput\b|,?\s*$)/gm; - const moduleRegex = /module\s+(\w+)\s*(#\([\s\S]*?\))?\s*\(([\s\S]*?)\)\s*;\s*([\s\S]*?)\s*endmodule/; - const inoutRegex = /\binout\s+(reg\s+|wire\s+|signed\s+|wire signed\s+|reg signed\s+)?(\[[^\]]+\]\s+)?([\w\s,]+?)(?=,?\s*\binput\b|,?\s*\boutput\b|,?\s*\binout\b|,?\s*$)/gm; - - const paramRegex = /parameter\s+(\w+)(?:\s*=\s*([^,;]+))?;?/g; - - const metaBlock = { - moduleName: "", - inputs: "", - outputs: "", - inouts: "", - parameters: "", - moduleBody: "", - }; - - const moduleMatch = code.match(moduleRegex); - - if (moduleMatch) { - metaBlock.moduleName = moduleMatch[1]; - metaBlock.moduleBody = moduleMatch[4]?.trim() || ""; - - const paramBlock = moduleMatch[2] || ""; - const headerParameters = [...paramBlock.matchAll(paramRegex)].map((match) => ({ - name: match[1].trim(), - value: match[2]?.trim() || null, - })); - - const bodyParameters = [...metaBlock.moduleBody.matchAll(paramRegex)].map((match) => ({ - name: match[1].trim(), - value: match[2]?.trim() || null, - })); - // - Body parameters should be cleaned because Icestudio generate the code always in they - // module header - metaBlock.moduleBody = metaBlock.moduleBody.replace(paramRegex, "").trim(); - - const allParameters = [...headerParameters, ...bodyParameters]; - - const ioBlock = moduleMatch[3] || ""; - const inputs = processSignals(inputRegex, ioBlock); - const outputs = processSignals(outputRegex, ioBlock); - const inouts = processSignals(inoutRegex, ioBlock); - - if (inputs.length > 0) metaBlock.inputs = inputs.join(", "); - if (outputs.length > 0) metaBlock.outputs = outputs.join(", "); - if (inouts.length > 0) metaBlock.inouts = inouts.join(", "); - if (allParameters.length > 0) { - metaBlock.parameters = allParameters.map((param) => param.name).join(", "); + return [...block.matchAll(regex)].map(match => { + const range = match[2]?.trim() || ''; + const names = match[3].split(',').map(name => name.trim()); + return names.map(name => (range ? `${name}${range}` : name)); // icestudio swap range + }).flat(); } + + function processModule(moduleCode, headerComments, moduleHeaderComments) { + const inputRegex = /\binput\s+(wire\s+|signed\s+|wire signed\s+)?(\[[^\]]+\]\s+)?([\w\s,]+?)(?=,?\s*\boutput\b|,?\s*\binput\b|$)/gm; + const outputRegex = /\boutput\s+(reg\s+|wire\s+|signed\s+|wire signed\s+|reg signed\s+)?(\[[^\]]+\]\s+)?([\w\s,]+?)(?=,?\s*\binput\b|,?\s*\s*$)/gm; + const inoutRegex = /\binout\s+(reg\s+|wire\s+|signed\s+|wire signed\s+|reg signed\s+)?(\[[^\]]+\]\s+)?([\w\s,]+?)(?=,?\s*\binput\b|,?\s*\boutput\b|,?\s*\binout\b|,?\s*$)/gm; + const paramRegex = /parameter\s+(\w+)(?:\s*=\s*([^,;]+))?;?/g; + + const headerParamRegex = /#\(\s*parameter\s+(\w+)\s*=\s*([^,;]+)\s*\)/g; + + const metaBlock = { + moduleName: "", + inputs: "", + outputs: "", + inouts: "", + parameters: "", + moduleBody: "", + headerComments: headerComments + }; + + const moduleRegex = /module\s+(\w+)\s*(#\([\s\S]*?\))?\s*\(([\s\S]*?)\)\s*;\s*([\s\S]*?)\s*endmodule/; + const moduleMatch = moduleCode.match(moduleRegex); + + if (moduleMatch) { + metaBlock.moduleName = moduleMatch[1]; + metaBlock.moduleBody = moduleMatch[4]?.trim() || ""; + + const headerParamBlock = moduleMatch[2] || ""; + const headerParameters = [...headerParamBlock.matchAll(headerParamRegex)].map((match) => ({ + name: match[1].trim() + })); + + const bodyParameters = [...metaBlock.moduleBody.matchAll(paramRegex)].map((match) => ({ + name: match[1].trim() + })); + + metaBlock.moduleBody = metaBlock.moduleBody.replace(paramRegex, "").trim(); + + const allParameters = [...headerParameters, ...bodyParameters]; + + const ioBlock = moduleMatch[3] || ""; + const inputs = processSignals(inputRegex, ioBlock); + const outputs = processSignals(outputRegex, ioBlock); + const inouts = processSignals(inoutRegex, ioBlock); + + if (inputs.length > 0) {metaBlock.inputs = inputs.join(", ");} + if (outputs.length > 0) {metaBlock.outputs = outputs.join(", ");} + if (inouts.length > 0) {metaBlock.inouts = inouts.join(", ");} + if (allParameters.length > 0) { + metaBlock.parameters = allParameters.map((param) => param.name).join(", "); + } + + let fullModuleBody = headerComments; + if (moduleHeaderComments) { + fullModuleBody += "\n\n" + moduleHeaderComments; + } + fullModuleBody += "\n\n" + metaBlock.moduleBody; + metaBlock.moduleBody = fullModuleBody; + + } + + return metaBlock; } - } - return metaBlock; -}; + this.parseVerilog = async function (code) { + const headerCommentsRegex = /^(\/\/.*$|\/\*[\s\S]*?\*\/)(?:\r?\n(\/\/.*$|\/\*[\s\S]*?\*\/))*/gm; + const moduleRegex = /module\s+(\w+)\s*(#\([\s\S]*?\))?\s*\(([\s\S]*?)\)\s*;\s*([\s\S]*?)\s*endmodule/gm; + + const metaBlock = { + moduleName: "", + inputs: "", + outputs: "", + inouts: "", + parameters: "", + moduleBody: "", + headerComments: "" + }; + + const allHeaderMatches = code.matchAll(headerCommentsRegex); + const headerCommentsMatch = allHeaderMatches.next().value; + + if (headerCommentsMatch) { + metaBlock.headerComments = headerCommentsMatch[0].trim(); + code = code.replace(headerCommentsMatch[0], "").trim(); + } + + let moduleMatches; + const modules = []; + while ((moduleMatches = moduleRegex.exec(code)) !== null) { + const moduleName = moduleMatches[1]; + + const startOfModule = moduleMatches.index; + const endOfModule = code.indexOf('endmodule', startOfModule) + 'endmodule'.length; + const moduleContent = code.substring(startOfModule, endOfModule); + + const preModuleContent = code.substring(0, startOfModule).trim(); + const moduleHeaderCommentsRegex = /((?:\/\/.*(?:\r?\n|$))+|\/\*[\s\S]*?\*\/)\s*$/gm; + const moduleHeaderCommentsMatch = [...preModuleContent.matchAll(moduleHeaderCommentsRegex)]; + const moduleHeaderComments = (moduleHeaderCommentsMatch.length > 0) ? moduleHeaderCommentsMatch[moduleHeaderCommentsMatch.length - 1][0].trim() : ""; + + modules.push({ + name: moduleName, + headerComments: moduleHeaderComments, + content: moduleContent + }); + } + + if (modules.length > 1) { + const selectedModule = await showModuleSelectionModal(modules); + const headerComments = metaBlock.headerComments; //+ "\n\n" + selectedModule.headerComments; + + const parsedModule = processModule(selectedModule.content, headerComments, selectedModule.headerComments); + + return parsedModule; + + } else if (modules.length === 1) { + const selectedModule = modules[0]; + + const headerComments = metaBlock.headerComments; // + "\n\n" + selectedModule.headerComments; + + const parsedModule = processModule(selectedModule.content, headerComments, selectedModule.headerComments); + + return parsedModule; + } + + return metaBlock; + }; + + + + async function showModuleSelectionModal(modules) { + return new Promise((resolve) => { + const modalDiv = document.createElement('div'); + modalDiv.id = 'moduleSelectorModal'; + modalDiv.style.position = 'fixed'; + modalDiv.style.top = '0'; + modalDiv.style.left = '0'; + modalDiv.style.width = '100%'; + modalDiv.style.height = '100%'; + modalDiv.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; + modalDiv.style.zIndex = '999999999999'; + modalDiv.style.display = 'flex'; + modalDiv.style.justifyContent = 'center'; + modalDiv.style.alignItems = 'center'; + + const modalContent = document.createElement('div'); + modalContent.style.backgroundColor = '#fff'; + modalContent.style.padding = '20px'; + modalContent.style.borderRadius = '8px'; + modalContent.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)'; + modalContent.style.maxWidth = '500px'; + modalContent.style.width = '100%'; + + const title = document.createElement('h3'); + title.innerText = 'Select a Module to Import'; + modalContent.appendChild(title); + + const moduleList = document.createElement('ul'); + moduleList.style.listStyleType = 'none'; + moduleList.style.padding = '0'; + moduleList.style.margin = '0'; + + modules.forEach((module, index) => { + const listItem = document.createElement('li'); + listItem.style.padding = '10px'; + listItem.style.cursor = 'pointer'; + listItem.style.borderBottom = '1px solid #ddd'; + listItem.style.transition = 'background-color 0.3s'; + + listItem.innerText = module.name; + + listItem.addEventListener('click', function () { + resolve(modules[index]); + document.body.removeChild(modalDiv); + }); + + moduleList.appendChild(listItem); + }); + + modalContent.appendChild(moduleList); + modalDiv.appendChild(modalContent); + document.body.appendChild(modalDiv); + }); + } });