Improve noir circuit parsing

pull/5770/head
ioedeveloper 3 weeks ago committed by Aniket
parent 1bd9907418
commit 6548616df0
  1. 273
      apps/noir-compiler/src/app/services/noirParser.ts
  2. 38
      apps/noir-compiler/src/app/services/noirPluginClient.ts

@ -1,103 +1,240 @@
// Noir Circuit Program Parser
// Detects syntax errors and warnings in .nr files
class NoirParser { class NoirParser {
errors: any; errors: {
currentLine: any; message: string;
type: string;
position: {
start: { line: number; column: number };
end: { line: number; column: number };
};
}[];
currentLine: number;
currentColumn: number; currentColumn: number;
noirTypes: string[];
constructor() { constructor() {
this.errors = []; this.errors = [];
this.currentLine = 1; this.currentLine = 1;
this.currentColumn = 1; this.currentColumn = 1;
this.noirTypes = ['Field', 'bool', 'u8', 'u16', 'u32', 'u64', 'i8', 'i16', 'i32', 'i64'];
} }
parseNoirCode(code) { parseNoirCode(code) {
this.errors = []; this.errors = [];
this.currentLine = 1;
this.currentColumn = 1;
const lines = code.split('\n'); const lines = code.split('\n');
let inFunctionBody = false; const functions = this.analyzeFunctions(lines);
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) { 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 (trimmedLine === '' || trimmedLine.startsWith('//')) continue;
if (line === '' || line.startsWith('//')) { if (trimmedLine.startsWith('mod ')) {
this.currentLine++; this.checkModuleImport(trimmedLine, lineIdx, line);
continue; continue;
} }
const currentFunction = functions.find(f => lineIdx >= f.startLine && lineIdx <= f.endLine);
// Track function body if (currentFunction) {
if (line.includes('{')) { if (lineIdx === currentFunction.startLine) this.checkFunctionReturnType(trimmedLine, lineIdx, line);
inFunctionBody = true; else this.checkFunctionBodyStatement(trimmedLine, lineIdx, line, currentFunction, lines);
} else if (line.includes('}')) {
inFunctionBody = false;
} }
// Check for multiple semicolons if (/[ \t]$/.test(line)) {
const semicolonMatches = [...line.matchAll(/;/g)]; this.addError({
if (semicolonMatches.length > 1) { message: 'Trailing whitespace detected',
this.addError( type: 'style',
'Multiple semicolons in a single statement', position: this.calculatePosition(lineIdx, line.length - 1, line.length)
lineIdx + 1, });
semicolonMatches[1].index + 1,
[lineIdx + 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 (!fnMatch) {
if (line.startsWith('mod ')) { this.addError({
const modulePattern = /^mod\s+[a-zA-Z_][a-zA-Z0-9_]*\s*;?$/; message: 'Invalid function name',
if (!modulePattern.test(line)) { type: 'syntax',
this.addError( position: this.calculatePosition(i, 0, line.length)
'Invalid module import syntax', });
lineIdx + 1, continue;
1,
[lineIdx + 1, line.length]
);
} }
currentFunction = {
startLine: i,
name: fnMatch[1],
returnType: this.extractReturnType(codePart),
bracketCount: 0
};
} }
// Check statement semicolons if (currentFunction) {
if (inFunctionBody && const open = (codePart.match(/{/g) || []).length;
!line.endsWith('{') && const close = (codePart.match(/}/g) || []).length;
!line.endsWith('}') &&
!line.startsWith('fn ') && bracketCount += open - close;
!line.startsWith('//') && if (bracketCount === 0) {
!line.endsWith(';') && currentFunction.endLine = i;
line.length > 0) { functions.push({ ...currentFunction });
this.addError( currentFunction = null;
'Missing semicolon at statement end', }
lineIdx + 1,
line.length,
[lineIdx + 1, line.length]
);
} }
}
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 (!isLastStatement && !codePart.endsWith(';') && !codePart.endsWith('{')) {
if (lines[lineIdx].endsWith(' ')) { const nextNonEmptyLine = this.findNextNonEmptyLine(lineIdx + 1, allLines);
this.addError( if (nextNonEmptyLine && !nextNonEmptyLine.trim().startsWith('//')) {
'Trailing whitespace', this.addError({
lineIdx + 1, message: 'Missing semicolon at statement end',
lines[lineIdx].length, type: 'syntax',
[lineIdx + 1, lines[lineIdx].length] 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) { addError(error) {
this.errors.push({ this.errors.push(error);
message,
line,
column,
range: range || [line, column]
});
} }
} }

@ -51,14 +51,16 @@ export class NoirPluginClient extends PluginClient {
async compile(path: string): Promise<void> { async compile(path: string): Promise<void> {
try { try {
this.internalEvents.emit('noir_compiling_start') 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 // @ts-ignore
this.call('terminal', 'log', { type: 'log', value: 'Compiling ' + path }) 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) console.log('program: ', program)
this.internalEvents.emit('noir_compiling_done') this.internalEvents.emit('noir_compiling_done')
this.emit('statusChanged', { key: 'succeed', title: 'Noir circuit compiled successfully', type: 'success' }) 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) { } catch (e) {
this.emit('statusChanged', { key: 'error', title: e.message, type: 'error' }) this.emit('statusChanged', { key: 'error', title: e.message, type: 'error' })
this.internalEvents.emit('noir_compiling_errored', e) this.internalEvents.emit('noir_compiling_errored', e)
@ -68,13 +70,29 @@ export class NoirPluginClient extends PluginClient {
async parse(path: string, content?: string): Promise<void> { async parse(path: string, content?: string): Promise<void> {
if (!content) content = await this.call('fileManager', 'readFile', path) if (!content) content = await this.call('fileManager', 'readFile', path)
await this.resolveDependencies(path, content)
const result = this.parser.parseNoirCode(content) const result = this.parser.parseNoirCode(content)
console.log('result: ', result) if (result.length > 0) {
const fileBytes = new TextEncoder().encode(content) 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<string, string[]> = {}): Promise<void> { async resolveDependencies (filePath: string, fileContent: string, parentPath: string = '', visited: Record<string, string[]> = {}): Promise<void> {
@ -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 })
}
} }

Loading…
Cancel
Save