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 {
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);
}
}

@ -51,14 +51,16 @@ export class NoirPluginClient extends PluginClient {
async compile(path: string): Promise<void> {
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<void> {
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<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