diff --git a/apps/noir-compiler/src/app/services/noirParser.ts b/apps/noir-compiler/src/app/services/noirParser.ts index 323f86124a..daf8190663 100644 --- a/apps/noir-compiler/src/app/services/noirParser.ts +++ b/apps/noir-compiler/src/app/services/noirParser.ts @@ -1,103 +1,240 @@ -// Noir Circuit Program Parser -// Detects syntax errors and warnings in .nr files - class NoirParser { - errors: any; - currentLine: any; + errors: { + message: string; + type: string; + position: { + start: { line: number; column: number }; + end: { line: number; column: number }; + }; + }[]; + currentLine: number; currentColumn: number; + noirTypes: string[]; + constructor() { this.errors = []; this.currentLine = 1; this.currentColumn = 1; + this.noirTypes = ['Field', 'bool', 'u8', 'u16', 'u32', 'u64', 'i8', 'i16', 'i32', 'i64']; } parseNoirCode(code) { this.errors = []; - this.currentLine = 1; - this.currentColumn = 1; - const lines = code.split('\n'); - let inFunctionBody = false; + const functions = this.analyzeFunctions(lines); for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) { - const line = lines[lineIdx].trim(); + const line = lines[lineIdx]; + const trimmedLine = line.trim(); - // Skip empty lines or comments - if (line === '' || line.startsWith('//')) { - this.currentLine++; + if (trimmedLine === '' || trimmedLine.startsWith('//')) continue; + if (trimmedLine.startsWith('mod ')) { + this.checkModuleImport(trimmedLine, lineIdx, line); continue; } + const currentFunction = functions.find(f => lineIdx >= f.startLine && lineIdx <= f.endLine); - // Track function body - if (line.includes('{')) { - inFunctionBody = true; - } else if (line.includes('}')) { - inFunctionBody = false; + if (currentFunction) { + if (lineIdx === currentFunction.startLine) this.checkFunctionReturnType(trimmedLine, lineIdx, line); + else this.checkFunctionBodyStatement(trimmedLine, lineIdx, line, currentFunction, lines); } - // Check for multiple semicolons - const semicolonMatches = [...line.matchAll(/;/g)]; - if (semicolonMatches.length > 1) { - this.addError( - 'Multiple semicolons in a single statement', - lineIdx + 1, - semicolonMatches[1].index + 1, - [lineIdx + 1, line.length] - ); + if (/[ \t]$/.test(line)) { + this.addError({ + message: 'Trailing whitespace detected', + type: 'style', + position: this.calculatePosition(lineIdx, line.length - 1, line.length) + }); } + } + + return this.errors; + } + + analyzeFunctions(lines) { + const functions = []; + let currentFunction = null; + let bracketCount = 0; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const codePart = line.split('//')[0].trim(); + + if (codePart.startsWith('fn ')) { + if (currentFunction !== null) { + this.addError({ + message: 'Nested function definition not allowed', + type: 'syntax', + position: this.calculatePosition(i, 0, line.length) + }); + } + const fnMatch = codePart.match(/fn\s+([a-zA-Z_][a-zA-Z0-9_]*)/); - // Check module imports - if (line.startsWith('mod ')) { - const modulePattern = /^mod\s+[a-zA-Z_][a-zA-Z0-9_]*\s*;?$/; - if (!modulePattern.test(line)) { - this.addError( - 'Invalid module import syntax', - lineIdx + 1, - 1, - [lineIdx + 1, line.length] - ); + if (!fnMatch) { + this.addError({ + message: 'Invalid function name', + type: 'syntax', + position: this.calculatePosition(i, 0, line.length) + }); + continue; } + currentFunction = { + startLine: i, + name: fnMatch[1], + returnType: this.extractReturnType(codePart), + bracketCount: 0 + }; } - // Check statement semicolons - if (inFunctionBody && - !line.endsWith('{') && - !line.endsWith('}') && - !line.startsWith('fn ') && - !line.startsWith('//') && - !line.endsWith(';') && - line.length > 0) { - this.addError( - 'Missing semicolon at statement end', - lineIdx + 1, - line.length, - [lineIdx + 1, line.length] - ); + if (currentFunction) { + const open = (codePart.match(/{/g) || []).length; + const close = (codePart.match(/}/g) || []).length; + + bracketCount += open - close; + if (bracketCount === 0) { + currentFunction.endLine = i; + functions.push({ ...currentFunction }); + currentFunction = null; + } } + } + + return functions; + } + + checkFunctionBodyStatement(line, lineIdx, originalLine, currentFunction, allLines) { + if (line === '' || line.startsWith('//') || line === '{' || line === '}') return; + const codePart = line.split('//')[0].trimEnd(); + const isLastStatement = this.isLastStatementInFunction(lineIdx, currentFunction, allLines); - // Check for trailing whitespace - if (lines[lineIdx].endsWith(' ')) { - this.addError( - 'Trailing whitespace', - lineIdx + 1, - lines[lineIdx].length, - [lineIdx + 1, lines[lineIdx].length] - ); + if (!isLastStatement && !codePart.endsWith(';') && !codePart.endsWith('{')) { + const nextNonEmptyLine = this.findNextNonEmptyLine(lineIdx + 1, allLines); + if (nextNonEmptyLine && !nextNonEmptyLine.trim().startsWith('//')) { + this.addError({ + message: 'Missing semicolon at statement end', + type: 'syntax', + position: this.calculatePosition( + lineIdx, + originalLine.length, + originalLine.length + ) + }); } + } + const semicolonMatches = [...codePart.matchAll(/;/g)]; - this.currentLine++; + if (semicolonMatches.length > 1) { + this.addError({ + message: 'Multiple semicolons in a single statement', + type: 'syntax', + position: this.calculatePosition( + lineIdx, + semicolonMatches[1].index, + originalLine.length + ) + }); } + } - return this.errors; + extractReturnType(line) { + const returnMatch = line.match(/->\s*([a-zA-Z_][a-zA-Z0-9_:<>, ]*)/); + + return returnMatch ? returnMatch[1].trim() : null; + } + + checkFunctionReturnType(line, lineIdx, originalLine) { + const returnMatch = line.match(/->\s*([a-zA-Z_][a-zA-Z0-9_:<>, ]*)/); + + if (returnMatch) { + const returnType = returnMatch[1].trim(); + + // Check if it's a valid Noir type or a custom type + if (!this.isValidNoirType(returnType)) { + this.addError({ + message: `Potentially invalid return type: ${returnType}`, + type: 'warning', + position: this.calculatePosition( + lineIdx, + originalLine.indexOf(returnType), + originalLine.indexOf(returnType) + returnType.length + ) + }); + } + } + } + + isLastStatementInFunction(currentLine, currentFunction, lines) { + for (let i = currentLine + 1; i <= currentFunction.endLine; i++) { + const line = lines[i].trim(); + if (line && !line.startsWith('//') && line !== '}') { + return false; + } + } + return true; + } + + findNextNonEmptyLine(startIndex, lines) { + for (let i = startIndex; i < lines.length; i++) { + const line = lines[i].trim(); + if (line && !line.startsWith('//')) { + return line; + } + } + return null; + } + + checkModuleImport(line, lineIdx, originalLine) { + const modulePattern = /^mod\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*;?$/; + const match = line.match(modulePattern); + + if (!match) { + this.addError({ + message: 'Invalid module import syntax', + type: 'syntax', + position: this.calculatePosition(lineIdx, 0, originalLine.length) + }); + } else if (!line.endsWith(';')) { + this.addError({ + message: 'Missing semicolon after module import', + type: 'syntax', + position: this.calculatePosition( + lineIdx, + originalLine.length, + originalLine.length + ) + }); + } + } + + isValidNoirType(type) { + // Basic types + if (this.noirTypes.includes(type)) return true; + + // Array types + if (type.includes('[') && type.includes(']')) { + const baseType = type.match(/\[(.*?);/)?.[1]; + return baseType && this.noirTypes.includes(baseType); + } + + // Generic types or custom types (not supported for now) + return false; + } + + calculatePosition(line, startColumn, endColumn) { + return { + start: { + line: line + 1, + column: startColumn + 1 + }, + end: { + line: line + 1, + column: endColumn + 1 + } + }; } - addError(message, line, column, range) { - this.errors.push({ - message, - line, - column, - range: range || [line, column] - }); + addError(error) { + this.errors.push(error); } } diff --git a/apps/noir-compiler/src/app/services/noirPluginClient.ts b/apps/noir-compiler/src/app/services/noirPluginClient.ts index e27154d23e..90d7fe94ee 100644 --- a/apps/noir-compiler/src/app/services/noirPluginClient.ts +++ b/apps/noir-compiler/src/app/services/noirPluginClient.ts @@ -51,14 +51,16 @@ export class NoirPluginClient extends PluginClient { async compile(path: string): Promise { try { this.internalEvents.emit('noir_compiling_start') - this.emit('statusChanged', { key: 'loading', title: 'Compiling Noir Circuit...', type: 'info' }) + this.emit('statusChanged', { key: 'loading', title: 'Compiling Noir Program...', type: 'info' }) // @ts-ignore this.call('terminal', 'log', { type: 'log', value: 'Compiling ' + path }) - const program = await compile_program(this.fm) + const program = await compile_program(this.fm, null, this.logFn.bind(this), this.debugFn.bind(this)) console.log('program: ', program) this.internalEvents.emit('noir_compiling_done') this.emit('statusChanged', { key: 'succeed', title: 'Noir circuit compiled successfully', type: 'success' }) + // @ts-ignore + this.call('terminal', 'log', { type: 'log', value: 'Compiled successfully' }) } catch (e) { this.emit('statusChanged', { key: 'error', title: e.message, type: 'error' }) this.internalEvents.emit('noir_compiling_errored', e) @@ -68,13 +70,29 @@ export class NoirPluginClient extends PluginClient { async parse(path: string, content?: string): Promise { if (!content) content = await this.call('fileManager', 'readFile', path) - await this.resolveDependencies(path, content) const result = this.parser.parseNoirCode(content) - console.log('result: ', result) - const fileBytes = new TextEncoder().encode(content) + if (result.length > 0) { + const markers = [] + + for (const error of result) { + markers.push({ + message: error.message, + severity: 'error', + position: error.position, + file: path, + }) + } + // @ts-ignore + await this.call('editor', 'addErrorMarker', markers) + } else { + await this.resolveDependencies(path, content) + const fileBytes = new TextEncoder().encode(content) - this.fm.writeFile(`${path}`, new Blob([fileBytes]).stream()) + this.fm.writeFile(`${path}`, new Blob([fileBytes]).stream()) + // @ts-ignore + await this.call('editor', 'clearErrorMarkers', [path]) + } } async resolveDependencies (filePath: string, fileContent: string, parentPath: string = '', visited: Record = {}): Promise { @@ -118,4 +136,12 @@ export class NoirPluginClient extends PluginClient { } } } + + logFn(log) { + this.call('terminal', 'log', { type: 'error', value: log }) + } + + debugFn(log) { + this.call('terminal', 'log', { type: 'log', value: log }) + } }