Skip to content

Commit 84c4706

Browse files
committed
Improve noir circuit parsing
1 parent 344e9fa commit 84c4706

File tree

2 files changed

+237
-74
lines changed

2 files changed

+237
-74
lines changed

apps/noir-compiler/src/app/services/noirParser.ts

Lines changed: 205 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,240 @@
1-
// Noir Circuit Program Parser
2-
// Detects syntax errors and warnings in .nr files
3-
41
class NoirParser {
5-
errors: any;
6-
currentLine: any;
2+
errors: {
3+
message: string;
4+
type: string;
5+
position: {
6+
start: { line: number; column: number };
7+
end: { line: number; column: number };
8+
};
9+
}[];
10+
currentLine: number;
711
currentColumn: number;
12+
noirTypes: string[];
13+
814
constructor() {
915
this.errors = [];
1016
this.currentLine = 1;
1117
this.currentColumn = 1;
18+
this.noirTypes = ['Field', 'bool', 'u8', 'u16', 'u32', 'u64', 'i8', 'i16', 'i32', 'i64'];
1219
}
1320

1421
parseNoirCode(code) {
1522
this.errors = [];
16-
this.currentLine = 1;
17-
this.currentColumn = 1;
18-
1923
const lines = code.split('\n');
20-
let inFunctionBody = false;
24+
const functions = this.analyzeFunctions(lines);
2125

2226
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
23-
const line = lines[lineIdx].trim();
27+
const line = lines[lineIdx];
28+
const trimmedLine = line.trim();
2429

25-
// Skip empty lines or comments
26-
if (line === '' || line.startsWith('//')) {
27-
this.currentLine++;
30+
if (trimmedLine === '' || trimmedLine.startsWith('//')) continue;
31+
if (trimmedLine.startsWith('mod ')) {
32+
this.checkModuleImport(trimmedLine, lineIdx, line);
2833
continue;
2934
}
35+
const currentFunction = functions.find(f => lineIdx >= f.startLine && lineIdx <= f.endLine);
3036

31-
// Track function body
32-
if (line.includes('{')) {
33-
inFunctionBody = true;
34-
} else if (line.includes('}')) {
35-
inFunctionBody = false;
37+
if (currentFunction) {
38+
if (lineIdx === currentFunction.startLine) this.checkFunctionReturnType(trimmedLine, lineIdx, line);
39+
else this.checkFunctionBodyStatement(trimmedLine, lineIdx, line, currentFunction, lines);
3640
}
3741

38-
// Check for multiple semicolons
39-
const semicolonMatches = [...line.matchAll(/;/g)];
40-
if (semicolonMatches.length > 1) {
41-
this.addError(
42-
'Multiple semicolons in a single statement',
43-
lineIdx + 1,
44-
semicolonMatches[1].index + 1,
45-
[lineIdx + 1, line.length]
46-
);
42+
if (/[ \t]$/.test(line)) {
43+
this.addError({
44+
message: 'Trailing whitespace detected',
45+
type: 'style',
46+
position: this.calculatePosition(lineIdx, line.length - 1, line.length)
47+
});
4748
}
49+
}
50+
51+
return this.errors;
52+
}
53+
54+
analyzeFunctions(lines) {
55+
const functions = [];
56+
let currentFunction = null;
57+
let bracketCount = 0;
58+
59+
for (let i = 0; i < lines.length; i++) {
60+
const line = lines[i];
61+
const codePart = line.split('//')[0].trim();
62+
63+
if (codePart.startsWith('fn ')) {
64+
if (currentFunction !== null) {
65+
this.addError({
66+
message: 'Nested function definition not allowed',
67+
type: 'syntax',
68+
position: this.calculatePosition(i, 0, line.length)
69+
});
70+
}
71+
const fnMatch = codePart.match(/fn\s+([a-zA-Z_][a-zA-Z0-9_]*)/);
4872

49-
// Check module imports
50-
if (line.startsWith('mod ')) {
51-
const modulePattern = /^mod\s+[a-zA-Z_][a-zA-Z0-9_]*\s*;?$/;
52-
if (!modulePattern.test(line)) {
53-
this.addError(
54-
'Invalid module import syntax',
55-
lineIdx + 1,
56-
1,
57-
[lineIdx + 1, line.length]
58-
);
73+
if (!fnMatch) {
74+
this.addError({
75+
message: 'Invalid function name',
76+
type: 'syntax',
77+
position: this.calculatePosition(i, 0, line.length)
78+
});
79+
continue;
5980
}
81+
currentFunction = {
82+
startLine: i,
83+
name: fnMatch[1],
84+
returnType: this.extractReturnType(codePart),
85+
bracketCount: 0
86+
};
6087
}
6188

62-
// Check statement semicolons
63-
if (inFunctionBody &&
64-
!line.endsWith('{') &&
65-
!line.endsWith('}') &&
66-
!line.startsWith('fn ') &&
67-
!line.startsWith('//') &&
68-
!line.endsWith(';') &&
69-
line.length > 0) {
70-
this.addError(
71-
'Missing semicolon at statement end',
72-
lineIdx + 1,
73-
line.length,
74-
[lineIdx + 1, line.length]
75-
);
89+
if (currentFunction) {
90+
const open = (codePart.match(/{/g) || []).length;
91+
const close = (codePart.match(/}/g) || []).length;
92+
93+
bracketCount += open - close;
94+
if (bracketCount === 0) {
95+
currentFunction.endLine = i;
96+
functions.push({ ...currentFunction });
97+
currentFunction = null;
98+
}
7699
}
100+
}
101+
102+
return functions;
103+
}
104+
105+
checkFunctionBodyStatement(line, lineIdx, originalLine, currentFunction, allLines) {
106+
if (line === '' || line.startsWith('//') || line === '{' || line === '}') return;
107+
const codePart = line.split('//')[0].trimEnd();
108+
const isLastStatement = this.isLastStatementInFunction(lineIdx, currentFunction, allLines);
77109

78-
// Check for trailing whitespace
79-
if (lines[lineIdx].endsWith(' ')) {
80-
this.addError(
81-
'Trailing whitespace',
82-
lineIdx + 1,
83-
lines[lineIdx].length,
84-
[lineIdx + 1, lines[lineIdx].length]
85-
);
110+
if (!isLastStatement && !codePart.endsWith(';') && !codePart.endsWith('{')) {
111+
const nextNonEmptyLine = this.findNextNonEmptyLine(lineIdx + 1, allLines);
112+
if (nextNonEmptyLine && !nextNonEmptyLine.trim().startsWith('//')) {
113+
this.addError({
114+
message: 'Missing semicolon at statement end',
115+
type: 'syntax',
116+
position: this.calculatePosition(
117+
lineIdx,
118+
originalLine.length,
119+
originalLine.length
120+
)
121+
});
86122
}
123+
}
124+
const semicolonMatches = [...codePart.matchAll(/;/g)];
87125

88-
this.currentLine++;
126+
if (semicolonMatches.length > 1) {
127+
this.addError({
128+
message: 'Multiple semicolons in a single statement',
129+
type: 'syntax',
130+
position: this.calculatePosition(
131+
lineIdx,
132+
semicolonMatches[1].index,
133+
originalLine.length
134+
)
135+
});
89136
}
137+
}
90138

91-
return this.errors;
139+
extractReturnType(line) {
140+
const returnMatch = line.match(/->\s*([a-zA-Z_][a-zA-Z0-9_:<>, ]*)/);
141+
142+
return returnMatch ? returnMatch[1].trim() : null;
143+
}
144+
145+
checkFunctionReturnType(line, lineIdx, originalLine) {
146+
const returnMatch = line.match(/->\s*([a-zA-Z_][a-zA-Z0-9_:<>, ]*)/);
147+
148+
if (returnMatch) {
149+
const returnType = returnMatch[1].trim();
150+
151+
// Check if it's a valid Noir type or a custom type
152+
if (!this.isValidNoirType(returnType)) {
153+
this.addError({
154+
message: `Potentially invalid return type: ${returnType}`,
155+
type: 'warning',
156+
position: this.calculatePosition(
157+
lineIdx,
158+
originalLine.indexOf(returnType),
159+
originalLine.indexOf(returnType) + returnType.length
160+
)
161+
});
162+
}
163+
}
164+
}
165+
166+
isLastStatementInFunction(currentLine, currentFunction, lines) {
167+
for (let i = currentLine + 1; i <= currentFunction.endLine; i++) {
168+
const line = lines[i].trim();
169+
if (line && !line.startsWith('//') && line !== '}') {
170+
return false;
171+
}
172+
}
173+
return true;
174+
}
175+
176+
findNextNonEmptyLine(startIndex, lines) {
177+
for (let i = startIndex; i < lines.length; i++) {
178+
const line = lines[i].trim();
179+
if (line && !line.startsWith('//')) {
180+
return line;
181+
}
182+
}
183+
return null;
184+
}
185+
186+
checkModuleImport(line, lineIdx, originalLine) {
187+
const modulePattern = /^mod\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*;?$/;
188+
const match = line.match(modulePattern);
189+
190+
if (!match) {
191+
this.addError({
192+
message: 'Invalid module import syntax',
193+
type: 'syntax',
194+
position: this.calculatePosition(lineIdx, 0, originalLine.length)
195+
});
196+
} else if (!line.endsWith(';')) {
197+
this.addError({
198+
message: 'Missing semicolon after module import',
199+
type: 'syntax',
200+
position: this.calculatePosition(
201+
lineIdx,
202+
originalLine.length,
203+
originalLine.length
204+
)
205+
});
206+
}
207+
}
208+
209+
isValidNoirType(type) {
210+
// Basic types
211+
if (this.noirTypes.includes(type)) return true;
212+
213+
// Array types
214+
if (type.includes('[') && type.includes(']')) {
215+
const baseType = type.match(/\[(.*?);/)?.[1];
216+
return baseType && this.noirTypes.includes(baseType);
217+
}
218+
219+
// Generic types or custom types (not supported for now)
220+
return false;
221+
}
222+
223+
calculatePosition(line, startColumn, endColumn) {
224+
return {
225+
start: {
226+
line: line + 1,
227+
column: startColumn + 1
228+
},
229+
end: {
230+
line: line + 1,
231+
column: endColumn + 1
232+
}
233+
};
92234
}
93235

94-
addError(message, line, column, range) {
95-
this.errors.push({
96-
message,
97-
line,
98-
column,
99-
range: range || [line, column]
100-
});
236+
addError(error) {
237+
this.errors.push(error);
101238
}
102239
}
103240

apps/noir-compiler/src/app/services/noirPluginClient.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,16 @@ export class NoirPluginClient extends PluginClient {
5151
async compile(path: string): Promise<void> {
5252
try {
5353
this.internalEvents.emit('noir_compiling_start')
54-
this.emit('statusChanged', { key: 'loading', title: 'Compiling Noir Circuit...', type: 'info' })
54+
this.emit('statusChanged', { key: 'loading', title: 'Compiling Noir Program...', type: 'info' })
5555
// @ts-ignore
5656
this.call('terminal', 'log', { type: 'log', value: 'Compiling ' + path })
57-
const program = await compile_program(this.fm)
57+
const program = await compile_program(this.fm, null, this.logFn.bind(this), this.debugFn.bind(this))
5858

5959
console.log('program: ', program)
6060
this.internalEvents.emit('noir_compiling_done')
6161
this.emit('statusChanged', { key: 'succeed', title: 'Noir circuit compiled successfully', type: 'success' })
62+
// @ts-ignore
63+
this.call('terminal', 'log', { type: 'log', value: 'Compiled successfully' })
6264
} catch (e) {
6365
this.emit('statusChanged', { key: 'error', title: e.message, type: 'error' })
6466
this.internalEvents.emit('noir_compiling_errored', e)
@@ -68,13 +70,29 @@ export class NoirPluginClient extends PluginClient {
6870

6971
async parse(path: string, content?: string): Promise<void> {
7072
if (!content) content = await this.call('fileManager', 'readFile', path)
71-
await this.resolveDependencies(path, content)
7273
const result = this.parser.parseNoirCode(content)
7374

74-
console.log('result: ', result)
75-
const fileBytes = new TextEncoder().encode(content)
75+
if (result.length > 0) {
76+
const markers = []
77+
78+
for (const error of result) {
79+
markers.push({
80+
message: error.message,
81+
severity: 'error',
82+
position: error.position,
83+
file: path,
84+
})
85+
}
86+
// @ts-ignore
87+
await this.call('editor', 'addErrorMarker', markers)
88+
} else {
89+
await this.resolveDependencies(path, content)
90+
const fileBytes = new TextEncoder().encode(content)
7691

77-
this.fm.writeFile(`${path}`, new Blob([fileBytes]).stream())
92+
this.fm.writeFile(`${path}`, new Blob([fileBytes]).stream())
93+
// @ts-ignore
94+
await this.call('editor', 'clearErrorMarkers', [path])
95+
}
7896
}
7997

8098
async resolveDependencies (filePath: string, fileContent: string, parentPath: string = '', visited: Record<string, string[]> = {}): Promise<void> {
@@ -118,4 +136,12 @@ export class NoirPluginClient extends PluginClient {
118136
}
119137
}
120138
}
139+
140+
logFn(log) {
141+
this.call('terminal', 'log', { type: 'error', value: log })
142+
}
143+
144+
debugFn(log) {
145+
this.call('terminal', 'log', { type: 'log', value: log })
146+
}
121147
}

0 commit comments

Comments
 (0)