diff --git a/apps/circuit-compiler/src/app/components/actions.tsx b/apps/circuit-compiler/src/app/components/actions.tsx deleted file mode 100644 index b555b5304fe..00000000000 --- a/apps/circuit-compiler/src/app/components/actions.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { CompileBtn } from "./compileBtn" - -export function CircuitActions () { - return ( -
- -
- ) -} \ No newline at end of file diff --git a/apps/circuit-compiler/src/app/components/compileBtn.tsx b/apps/circuit-compiler/src/app/components/compileBtn.tsx deleted file mode 100644 index 25dc686eb16..00000000000 --- a/apps/circuit-compiler/src/app/components/compileBtn.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { CustomTooltip, RenderIf, RenderIfNot, extractNameFromKey } from "@remix-ui/helper"; -import { useContext } from "react"; -import { CircuitAppContext } from "../contexts"; -import { FormattedMessage } from "react-intl"; -import { compileCircuit } from "../actions"; - -export function CompileBtn () { - const { plugin, appState } = useContext(CircuitAppContext) - - return ( - -
- Ctrl+S to compile {appState.filePath} -
- - } - > - -
- ) -} \ No newline at end of file diff --git a/apps/circuit-compiler/src/app/components/container.tsx b/apps/circuit-compiler/src/app/components/container.tsx index c1034576f70..7d3c092c64b 100644 --- a/apps/circuit-compiler/src/app/components/container.tsx +++ b/apps/circuit-compiler/src/app/components/container.tsx @@ -1,17 +1,17 @@ import { useContext } from 'react' -import { CustomTooltip, RenderIf } from '@remix-ui/helper' +import { CompileBtn, CustomTooltip, RenderIf } from '@remix-ui/helper' import { FormattedMessage } from 'react-intl' import { CircuitAppContext } from '../contexts' -import { CompileOptions } from './options' +import { CompileOptions, CompilerReport } from '@remix-ui/helper' import { VersionList } from './versions' import { Toggler } from './toggler' import { Configurations } from './configurations' -import { CircuitActions } from './actions' import { WitnessSection } from './witness' -import { CompilerFeedback } from './feedback' -import { CompilerReport, PrimeValue } from '../types' +import { CompilerFeedback } from '@remix-ui/helper' +import { PrimeValue } from '../types' import { SetupExports } from './setupExports' import { GenerateProof } from './generateProof' +import { compileCircuit } from '../actions' export function Container () { const circuitApp = useContext(CircuitAppContext) @@ -34,15 +34,19 @@ export function Container () { circuitApp.dispatch({ type: 'SET_COMPILER_VERSION', payload: version }) } - const handleOpenErrorLocation = async (location: string, startRange: string) => { - if (location) { - const fullPathLocation = await circuitApp.plugin.resolveReportPath(location) + const handleOpenErrorLocation = async (report: CompilerReport) => { + if (report.labels.length > 0) { + const location = circuitApp.appState.filePathToId[report.labels[0].file_id] + const startRange = report.labels[0].range.start + if (location) { + const fullPathLocation = await circuitApp.plugin.resolveReportPath(location) - await circuitApp.plugin.call('fileManager', 'open', fullPathLocation) - // @ts-ignore - const startPosition: { lineNumber: number; column: number } = await circuitApp.plugin.call('editor', 'getPositionAt', startRange) - // @ts-ignore - await circuitApp.plugin.call('editor', 'gotoLine', startPosition.lineNumber - 1, startPosition.column) + await circuitApp.plugin.call('fileManager', 'open', fullPathLocation) + // @ts-ignore + const startPosition: { lineNumber: number; column: number } = await circuitApp.plugin.call('editor', 'getPositionAt', startRange) + // @ts-ignore + await circuitApp.plugin.call('editor', 'gotoLine', startPosition.lineNumber - 1, startPosition.column) + } } } @@ -102,6 +106,10 @@ export function Container () { } } + const handleCompileClick = () => { + compileCircuit(circuitApp.plugin, circuitApp.appState) + } + return (
@@ -123,7 +131,9 @@ export function Container () { - +
+ +
diff --git a/apps/circuit-compiler/src/app/components/options.tsx b/apps/circuit-compiler/src/app/components/options.tsx deleted file mode 100644 index 85161c1b3fc..00000000000 --- a/apps/circuit-compiler/src/app/components/options.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import {FormattedMessage} from 'react-intl' -import { CompileOptionsProps } from '../types' - -export function CompileOptions ({autoCompile, hideWarnings, setCircuitAutoCompile, setCircuitHideWarnings}: CompileOptionsProps) { - - return ( -
-
- setCircuitAutoCompile(e.target.checked)} - title="Auto compile" - checked={autoCompile} - id="autoCompileCircuit" - /> - -
-
- setCircuitHideWarnings(e.target.checked)} - id="hideCircuitWarnings" - type="checkbox" - title="Hide warnings" - checked={hideWarnings} - /> - -
-
- ) -} \ No newline at end of file diff --git a/apps/circuit-compiler/src/app/services/circomPluginClient.ts b/apps/circuit-compiler/src/app/services/circomPluginClient.ts index ff3170f26e0..daee5c96067 100644 --- a/apps/circuit-compiler/src/app/services/circomPluginClient.ts +++ b/apps/circuit-compiler/src/app/services/circomPluginClient.ts @@ -7,8 +7,8 @@ import * as compilerV218 from 'circom_wasm/v2.1.8' import * as compilerV217 from 'circom_wasm/v2.1.7' import * as compilerV216 from 'circom_wasm/v2.1.6' import * as compilerV215 from 'circom_wasm/v2.1.5' -import { extractNameFromKey, extractParentFromKey } from '@remix-ui/helper' -import { CompilationConfig, CompilerReport, PrimeValue } from '../types' +import { extractNameFromKey, extractParentFromKey, CompilerReport } from '@remix-ui/helper' +import { CompilationConfig, PrimeValue } from '../types' import isElectron from 'is-electron' export class CircomPluginClient extends PluginClient { diff --git a/apps/circuit-compiler/src/app/types/index.ts b/apps/circuit-compiler/src/app/types/index.ts index bd770bbec17..33c2420cb9a 100644 --- a/apps/circuit-compiler/src/app/types/index.ts +++ b/apps/circuit-compiler/src/app/types/index.ts @@ -1,6 +1,7 @@ import { compiler_list } from 'circom_wasm' import { Dispatch } from 'react' import type { CircomPluginClient } from '../services/circomPluginClient' +import { CompilerReport } from '@remix-ui/helper' export type CompilerStatus = "compiling" | "computing" | "idle" | "errored" | "warning" | "exporting" | "proving" @@ -87,34 +88,6 @@ export type CompilationConfig = { export type PrimeValue = "bn128" | "bls12381" | "goldilocks" | "grumpkin" | "pallas" | "vesta" -export type CompilerFeedbackProps = { - feedback: string | CompilerReport[], - filePathToId: Record, - openErrorLocation: (location: string, startRange: string) => void, - hideWarnings: boolean, - askGPT: (report: CompilerReport) => void -} - -export type CompilerReport = { - type: "Error" | "Bug" | "Help" | "Note" | "Warning" | "Unknown", - message: string, - labels: { - style: "Primary" | "Secondary" | "Unknown", - file_id: string, - range: { - start: string, - end: string - }, - message: string - }[], - notes: string[] -} - -export type FeedbackAlertProps = { - message: string, - askGPT: () => void -} - export type ConfigurationsProps = { setPrimeValue: (prime: PrimeValue) => void, primeValue: PrimeValue, diff --git a/apps/noir-compiler/src/app/actions/constants.ts b/apps/noir-compiler/src/app/actions/constants.ts new file mode 100644 index 00000000000..de94fbe570e --- /dev/null +++ b/apps/noir-compiler/src/app/actions/constants.ts @@ -0,0 +1,8 @@ +export const DEFAULT_TOML_CONFIG = `[package] +name = "test" +authors = [""] +compiler_version = ">=0.18.0" +type = "bin" + +[dependencies] +` \ No newline at end of file diff --git a/apps/noir-compiler/src/app/actions/index.ts b/apps/noir-compiler/src/app/actions/index.ts new file mode 100644 index 00000000000..7b5564abe71 --- /dev/null +++ b/apps/noir-compiler/src/app/actions/index.ts @@ -0,0 +1,10 @@ +import { NoirPluginClient } from "../services/noirPluginClient" +import { AppState } from "../types" + +export const compileNoirCircuit = async (plugin: NoirPluginClient, appState: AppState) => { + if (appState.status !== "compiling") { + await plugin.compile(appState.filePath) + } else { + console.log('Existing noir compilation in progress') + } +} \ No newline at end of file diff --git a/apps/noir-compiler/src/app/app.tsx b/apps/noir-compiler/src/app/app.tsx new file mode 100644 index 00000000000..4c5d396f85f --- /dev/null +++ b/apps/noir-compiler/src/app/app.tsx @@ -0,0 +1,103 @@ +import { useEffect, useReducer, useState } from "react" +import { NoirPluginClient } from "./services/noirPluginClient" +import { RenderIf } from '@remix-ui/helper' +import { IntlProvider } from 'react-intl' +import { Container } from "./components/container" +import { NoirAppContext } from "./contexts" +import { appInitialState, appReducer } from "./reducers/state" +import { compileNoirCircuit } from "./actions" + +const plugin = new NoirPluginClient() + +function App() { + const [appState, dispatch] = useReducer(appReducer, appInitialState) + const [locale, setLocale] = useState<{code: string; messages: any}>({ + code: 'en', + messages: null + }) + const [isContentChanged, setIsContentChanged] = useState(false) + const [isPluginActivated, setIsPluginActivated] = useState(false) + + useEffect(() => { + plugin.internalEvents.on('noir_activated', () => { + // @ts-ignore + plugin.on('locale', 'localeChanged', (locale: any) => { + setLocale(locale) + }) + plugin.on('fileManager', 'currentFileChanged', (filePath) => { + if (filePath.endsWith('.nr')) { + dispatch({ type: 'SET_FILE_PATH', payload: filePath }) + plugin.parse(filePath) + } + }) + // @ts-ignore + plugin.on('editor', 'contentChanged', async (path: string, content: string) => { + if (path.endsWith('.nr')) { + setIsContentChanged(true) + plugin.parse(path, content) + } + }) + // noir compiling events + plugin.internalEvents.on('noir_compiling_start', () => dispatch({ type: 'SET_COMPILER_STATUS', payload: 'compiling' })) + plugin.internalEvents.on('noir_compiling_done', () => { + dispatch({ type: 'SET_COMPILER_STATUS', payload: 'idle' }) + dispatch({ type: 'SET_COMPILER_FEEDBACK', payload: null }) + }) + plugin.internalEvents.on('noir_compiling_errored', noirCompilerErrored) + setIsPluginActivated(true) + }) + }, []) + + useEffect(() => { + if (isPluginActivated) { + setCurrentLocale() + } + }, [isPluginActivated]) + + useEffect(() => { + if (isContentChanged) { + (async () => { + if (appState.autoCompile) await compileNoirCircuit(plugin, appState) + })() + setIsContentChanged(false) + } + }, [appState.autoCompile, isContentChanged]) + + const noirCompilerErrored = (err: ErrorEvent) => { + dispatch({ type: 'SET_COMPILER_STATUS', payload: 'errored' }) + try { + const report = JSON.parse(err.message) + + dispatch({ type: 'SET_COMPILER_FEEDBACK', payload: report }) + } catch (e) { + dispatch({ type: 'SET_COMPILER_FEEDBACK', payload: err.message }) + } + } + + const setCurrentLocale = async () => { + // @ts-ignore + const currentLocale = await plugin.call('locale', 'currentLocale') + + setLocale(currentLocale) + } + + const value = { + plugin, + dispatch, + appState + } + + return ( +
+ + + + + + + +
+ ) +} + +export default App diff --git a/apps/noir-compiler/src/app/components/container.tsx b/apps/noir-compiler/src/app/components/container.tsx new file mode 100644 index 00000000000..062149bb598 --- /dev/null +++ b/apps/noir-compiler/src/app/components/container.tsx @@ -0,0 +1,68 @@ +import { useContext } from 'react' +import { CompileBtn, CompilerFeedback, CompilerReport, CustomTooltip, RenderIf } from '@remix-ui/helper' +import { FormattedMessage } from 'react-intl' +import { NoirAppContext } from '../contexts' +import { CompileOptions } from '@remix-ui/helper' +import { compileNoirCircuit } from '../actions' + +export function Container () { + const noirApp = useContext(NoirAppContext) + + const showCompilerLicense = async (message = 'License not available') => { + try { + const response = await fetch('https://raw.githubusercontent.com/noir-lang/noir/master/LICENSE-APACHE') + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`) + const content = await response.text() + // @ts-ignore + noirApp.plugin.call('notification', 'modal', { id: 'modal_noir_compiler_license', title: 'Compiler License', message: content }) + } catch (e) { + // @ts-ignore + noirApp.plugin.call('notification', 'modal', { id: 'modal_noir_compiler_license', title: 'Compiler License', message }) + } + } + + const handleOpenErrorLocation = async (report: CompilerReport) => {} + + const handleCircuitAutoCompile = (value: boolean) => { + noirApp.dispatch({ type: 'SET_AUTO_COMPILE', payload: value }) + } + + const handleCircuitHideWarnings = (value: boolean) => { + noirApp.dispatch({ type: 'SET_HIDE_WARNINGS', payload: value }) + } + + const askGPT = async (report: CompilerReport) => {} + + const handleCompileClick = () => { + compileNoirCircuit(noirApp.plugin, noirApp.appState) + } + + return ( +
+
+
+
+ + + showCompilerLicense()}> + + +
+ +
+ + + +
+
+
+
+ ) +} \ No newline at end of file diff --git a/apps/noir-compiler/src/app/contexts/index.ts b/apps/noir-compiler/src/app/contexts/index.ts new file mode 100644 index 00000000000..02b7537756f --- /dev/null +++ b/apps/noir-compiler/src/app/contexts/index.ts @@ -0,0 +1,4 @@ +import { createContext } from 'react' +import { INoirAppContext } from '../types' + +export const NoirAppContext = createContext({} as INoirAppContext) diff --git a/apps/noir-compiler/src/app/reducers/state.ts b/apps/noir-compiler/src/app/reducers/state.ts new file mode 100644 index 00000000000..7fe068a42ab --- /dev/null +++ b/apps/noir-compiler/src/app/reducers/state.ts @@ -0,0 +1,48 @@ +import { Actions, AppState } from '../types' + +export const appInitialState: AppState = { + filePath: '', + filePathToId: {}, + autoCompile: false, + hideWarnings: false, + status: 'idle', + compilerFeedback: '' +} + +export const appReducer = (state = appInitialState, action: Actions): AppState => { + switch (action.type) { + + case 'SET_AUTO_COMPILE': + return { + ...state, + autoCompile: action.payload + } + + case 'SET_HIDE_WARNINGS': + return { + ...state, + hideWarnings: action.payload + } + + case 'SET_FILE_PATH': + return { + ...state, + filePath: action.payload + } + + case 'SET_COMPILER_FEEDBACK': + return { + ...state, + compilerFeedback: action.payload + } + + case 'SET_COMPILER_STATUS': + return { + ...state, + status: action.payload + } + + default: + throw new Error() + } +} diff --git a/apps/noir-compiler/src/app/services/noirParser.ts b/apps/noir-compiler/src/app/services/noirParser.ts new file mode 100644 index 00000000000..5b5ebcf0d24 --- /dev/null +++ b/apps/noir-compiler/src/app/services/noirParser.ts @@ -0,0 +1,243 @@ +class NoirParser { + 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 = []; + const lines = code.split('\n'); + const functions = this.analyzeFunctions(lines); + + for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) { + const line = lines[lineIdx]; + const trimmedLine = line.trim(); + + 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); + + if (currentFunction) { + if (lineIdx === currentFunction.startLine) this.checkFunctionReturnType(trimmedLine, lineIdx, line); + else this.checkFunctionBodyStatement(trimmedLine, lineIdx, line, currentFunction, lines); + } + + 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_]*)/); + + 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 + }; + } + + 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); + + 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)]; + + if (semicolonMatches.length > 1) { + this.addError({ + message: 'Multiple semicolons in a single statement', + type: 'syntax', + position: this.calculatePosition( + lineIdx, + semicolonMatches[1].index, + originalLine.length + ) + }); + } + } + + 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) { + // Handle visibility modifiers (pub/priv) and extract base type + const typeParts = type.split(/\s+/); + const baseType = typeParts[typeParts.length - 1]; // Get last part after any modifiers + + if (this.noirTypes.includes(baseType)) return true; + if (baseType.includes('[') && baseType.includes(']')) { + const innerTypeMatch = baseType.match(/\[(.*?);/); + if (innerTypeMatch) { + const innerType = innerTypeMatch[1].trim(); + return this.noirTypes.includes(innerType); + } + return false; + } + return false; + } + + extractReturnType(line) { + const returnMatch = line.match(/->\s*((?:pub\s+)?[a-zA-Z_][a-zA-Z0-9_:<>, ]*)/); + return returnMatch ? returnMatch[1].trim() : null; + } + + calculatePosition(line, startColumn, endColumn) { + return { + start: { + line: line + 1, + column: startColumn + 1 + }, + end: { + line: line + 1, + column: endColumn + 1 + } + }; + } + + addError(error) { + this.errors.push(error); + } +} + +export default NoirParser; \ No newline at end of file diff --git a/apps/noir-compiler/src/app/services/noirPluginClient.ts b/apps/noir-compiler/src/app/services/noirPluginClient.ts index e2872172100..f15d70fdf1b 100644 --- a/apps/noir-compiler/src/app/services/noirPluginClient.ts +++ b/apps/noir-compiler/src/app/services/noirPluginClient.ts @@ -1,15 +1,24 @@ import { PluginClient } from '@remixproject/plugin' import { createClient } from '@remixproject/plugin-webview' import EventManager from 'events' - +// @ts-ignore +import { compile_program, createFileManager } from '@noir-lang/noir_wasm/default' +import type { FileManager } from '@noir-lang/noir_wasm/dist/node/main' +import pathModule from 'path' +import { DEFAULT_TOML_CONFIG } from '../actions/constants' +import NoirParser from './noirParser' export class NoirPluginClient extends PluginClient { public internalEvents: EventManager + public fm: FileManager + public parser: NoirParser constructor() { super() - this.methods = ['init'] + this.methods = ['init', 'parse', 'compile'] createClient(this) this.internalEvents = new EventManager() + this.fm = createFileManager('/') + this.parser = new NoirParser() this.onload() } @@ -19,5 +28,136 @@ export class NoirPluginClient extends PluginClient { onActivation(): void { this.internalEvents.emit('noir_activated') + this.setup() + } + + async setup(): Promise { + // @ts-ignore + const nargoTomlExists = await this.call('fileManager', 'exists', 'Nargo.toml') + + if (!nargoTomlExists) { + await this.call('fileManager', 'writeFile', 'Nargo.toml', DEFAULT_TOML_CONFIG) + const fileBytes = new TextEncoder().encode(DEFAULT_TOML_CONFIG) + + this.fm.writeFile('Nargo.toml', new Blob([fileBytes]).stream()) + } else { + const nargoToml = await this.call('fileManager', 'readFile', 'Nargo.toml') + const fileBytes = new TextEncoder().encode(nargoToml) + + this.fm.writeFile('Nargo.toml', new Blob([fileBytes]).stream()) + } + } + + async compile(path: string): Promise { + try { + this.internalEvents.emit('noir_compiling_start') + 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, null, this.logFn.bind(this), this.debugFn.bind(this)) + + this.call('fileManager', 'writeFile', path.replace('.nr', '.json'), JSON.stringify(program, null, 2)) + 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) { + const regex = /^\s*(\/[^:]+):(\d+):/gm; + + Array.from(e.message.matchAll(regex), (match) => { + const errorPath = match[1] + const line = parseInt(match[2]) + // @ts-ignore + this.call('editor', 'addErrorMarker', [{ + message: e.message, + severity: 'error', + position: { + start: { line, column: 1 }, + end: { line, column: 1 } + }, + file: errorPath.slice(1) + }]) + }) + this.emit('statusChanged', { key: 'error', title: e.message, type: 'error' }) + this.internalEvents.emit('noir_compiling_errored', e) + console.error(e) + } + } + + async parse(path: string, content?: string): Promise { + if (!content) content = await this.call('fileManager', 'readFile', path) + const result = this.parser.parseNoirCode(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) + + await 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 { + const imports = Array.from(fileContent.matchAll(/mod\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*(=\s*["'](.*?)["'])?\s*;/g), match => match[3] || match[1]); + + for (let dep of imports) { + if (!dep.endsWith('.nr')) dep += '.nr' + if (visited[filePath] && visited[filePath].includes(parentPath)) return console.log('circular dependency detected') + let dependencyContent = '' + let path = dep.replace(/(\.\.\/)+/g, '') + + // @ts-ignore + const pathExists = await this.call('fileManager', 'exists', path) + + if (pathExists) { + dependencyContent = await this.call('fileManager', 'readFile', path) + } else { + let relativePath = pathModule.resolve(filePath.slice(0, filePath.lastIndexOf('/')), dep) + + if (relativePath.indexOf('/') === 0) relativePath = relativePath.slice(1) + // @ts-ignore + const relativePathExists = await this.call('fileManager', 'exists', relativePath) + + if (relativePathExists) { + path = relativePath + dependencyContent = await this.call('fileManager', 'readFile', relativePath) + visited[filePath] = visited[filePath] ? [...visited[filePath], path] : [path] + // extract all mod imports from the dependency content + const depImports = Array.from(fileContent.matchAll(/mod\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*(=\s*["'](.*?)["'])?\s*;/g), match => match[3] || match[1]) + + if (depImports.length > 0 && dependencyContent.length > 0) { + const fileBytes = new TextEncoder().encode(dependencyContent) + const writePath = parentPath ? `${filePath.replace('.nr', '')}/${dep}` : path + + this.fm.writeFile(writePath, new Blob([fileBytes]).stream()) + await this.resolveDependencies(path, dependencyContent, filePath, visited) + } + } else { + throw new Error(`Dependency ${dep} not found in Remix file system`) + } + } + } + } + + logFn(log) { + this.call('terminal', 'log', { type: 'error', value: log }) + } + + debugFn(log) { + this.call('terminal', 'log', { type: 'log', value: log }) } } diff --git a/apps/noir-compiler/src/app/types/index.ts b/apps/noir-compiler/src/app/types/index.ts new file mode 100644 index 00000000000..42753b3f050 --- /dev/null +++ b/apps/noir-compiler/src/app/types/index.ts @@ -0,0 +1,33 @@ +import { compiler_list } from 'circom_wasm' +import { Dispatch } from 'react' +import type { NoirPluginClient } from '../services/noirPluginClient' + +export type CompilerStatus = "compiling" | "idle" | "errored" | "warning" +export interface INoirAppContext { + appState: AppState + dispatch: Dispatch, + plugin: NoirPluginClient +} + +export interface AppState { + filePath: string, + filePathToId: Record, + autoCompile: boolean, + hideWarnings: boolean, + status: CompilerStatus, + compilerFeedback: string +} + +export interface ActionPayloadTypes { + SET_AUTO_COMPILE: boolean, + SET_HIDE_WARNINGS: boolean, + SET_FILE_PATH: string, + SET_COMPILER_FEEDBACK: string, + SET_COMPILER_STATUS: CompilerStatus +} +export interface Action { + type: T + payload: ActionPayloadTypes[T] +} + +export type Actions = {[A in keyof ActionPayloadTypes]: Action}[keyof ActionPayloadTypes] diff --git a/apps/noir-compiler/src/css/app.css b/apps/noir-compiler/src/css/app.css new file mode 100644 index 00000000000..5d68203a992 --- /dev/null +++ b/apps/noir-compiler/src/css/app.css @@ -0,0 +1,101 @@ +body { + font-size : .8rem; +} +.noir_section { + padding: 12px 24px 16px; +} +.noir_label { + margin-bottom: 2px; + font-size: 11px; + line-height: 12px; + text-transform: uppercase; +} +.noir_warnings_box { + display: flex; + align-items: center; +} +.noir_warnings_box label { + margin: 0; +} +.noir_config_section:hover { + cursor: pointer; +} +.noir_config_section { + font-size: 1rem; +} +.noir_config { + display: flex; + align-items: center; +} +.noir_config label { + margin: 0; +} +.noir_inner_label { + margin-bottom: 2px; + font-size: 11px; + line-height: 12px; + text-transform: uppercase; +} +.circuit_errors_box { + word-break: break-word; +} +.circuit_feedback.success, +.circuit_feedback.error, +.circuit_feedback.warning { + white-space: pre-line; + word-wrap: break-word; + cursor: pointer; + position: relative; + margin: 0.5em 0 1em 0; + border-radius: 5px; + line-height: 20px; + padding: 8px 15px; +} + +.circuit_feedback.success pre, +.circuit_feedback.error pre, +.circuit_feedback.warning pre { + white-space: pre-line; + overflow-y: hidden; + background-color: transparent; + margin: 0; + font-size: 12px; + border: 0 none; + padding: 0; + border-radius: 0; +} + +.circuit_feedback.success .close, +.circuit_feedback.error .close, +.circuit_feedback.warning .close { + visibility: hidden; + white-space: pre-line; + font-weight: bold; + position: absolute; + color: hsl(0, 0%, 0%); /* black in style-guide.js */ + top: 0; + right: 0; + padding: 0.5em; +} + +.circuit_feedback.success a, +.circuit_feedback.error a, +.circuit_feedback.warning a { + bottom: 0; + right: 0; +} +.custom-dropdown-items { + padding: 0.25rem 0.25rem; + border-radius: .25rem; + background: var(--custom-select); +} +.custom-dropdown-items a { + border-radius: .25rem; + text-transform: none; + text-decoration: none; + font-weight: normal; + font-size: 0.875rem; + padding: 0.25rem 0.25rem; + width: auto; + color: var(--text); +} diff --git a/apps/noir-compiler/src/main.tsx b/apps/noir-compiler/src/main.tsx index 7c379bc3869..502a6c93a3d 100644 --- a/apps/noir-compiler/src/main.tsx +++ b/apps/noir-compiler/src/main.tsx @@ -1,8 +1,9 @@ import React from 'react' import { createRoot } from 'react-dom/client' +import App from './app/app' const container = document.getElementById('root') if (container) { - createRoot(container).render(<>) + createRoot(container).render() } \ No newline at end of file diff --git a/apps/noir-compiler/src/profile.json b/apps/noir-compiler/src/profile.json index e617ef74d66..dcf409d212a 100644 --- a/apps/noir-compiler/src/profile.json +++ b/apps/noir-compiler/src/profile.json @@ -4,11 +4,11 @@ "displayName": "Noir Compiler", "events": [], "version": "2.0.0", - "methods": ["init", "parse"], + "methods": ["init", "parse", "compile"], "canActivate": [], "url": "", "description": "Enables support for noir circuit compilation", - "icon": "assets/img/circom-icon-bw-800b.webp", + "icon": "assets/img/noir-icon.webp", "location": "sidePanel", "documentation": "", "repo": "https://github.com/ethereum/remix-project/tree/master/apps/noir-compiler", diff --git a/apps/noir-compiler/webpack.config.js b/apps/noir-compiler/webpack.config.js index a4aa984df96..48f51d27a88 100644 --- a/apps/noir-compiler/webpack.config.js +++ b/apps/noir-compiler/webpack.config.js @@ -2,11 +2,18 @@ const { composePlugins, withNx } = require('@nrwl/webpack') const webpack = require('webpack') const TerserPlugin = require("terser-webpack-plugin") const CssMinimizerPlugin = require("css-minimizer-webpack-plugin") +const fs = require('fs') +const path = require('path') // Nx plugins for webpack. module.exports = composePlugins(withNx(), (config) => { - // Update the webpack config as needed here. - // e.g. `config.plugins.push(new MyPlugin())` + // use the web build for noir-wasm + let pkgNoirWasm = fs.readFileSync(path.resolve(__dirname, '../../node_modules/@noir-lang/noir_wasm/package.json'), 'utf8') + let typeCount = 0 + + pkgNoirWasm = pkgNoirWasm.replace(/"node"/, '"./node"').replace(/"import"/, '"./import"').replace(/"require"/, '"./require"').replace(/"types"/g, match => ++typeCount === 2 ? '"./types"' : match).replace(/"default"/, '"./default"') + fs.writeFileSync(path.resolve(__dirname, '../../node_modules/@noir-lang/noir_wasm/package.json'), pkgNoirWasm) + // add fallback for node modules config.resolve.fallback = { ...config.resolve.fallback, diff --git a/apps/remix-ide/project.json b/apps/remix-ide/project.json index a0f749c399c..ed4eb4d8510 100644 --- a/apps/remix-ide/project.json +++ b/apps/remix-ide/project.json @@ -3,7 +3,7 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "apps/remix-ide/src", "projectType": "application", - "implicitDependencies": ["doc-gen", "doc-viewer", "contract-verification", "vyper", "solhint", "walletconnect", "circuit-compiler", "learneth", "quick-dapp", "remix-dapp"], + "implicitDependencies": ["doc-gen", "doc-viewer", "contract-verification", "vyper", "solhint", "walletconnect", "circuit-compiler", "learneth", "quick-dapp", "remix-dapp", "noir-compiler"], "targets": { "build": { "executor": "@nrwl/webpack:webpack", diff --git a/apps/remix-ide/src/app/editor/editor.js b/apps/remix-ide/src/app/editor/editor.js index 04259079aa5..6afcda16ecb 100644 --- a/apps/remix-ide/src/app/editor/editor.js +++ b/apps/remix-ide/src/app/editor/editor.js @@ -512,6 +512,7 @@ export default class Editor extends Plugin { // error markers async addErrorMarker (error){ + console.log('called addErrorMarker ', error) const { from } = this.currentRequest this.api.addErrorMarker(error, from) } diff --git a/apps/remix-ide/src/app/plugins/matomo.ts b/apps/remix-ide/src/app/plugins/matomo.ts index 0421ec6a499..40c61e718e7 100644 --- a/apps/remix-ide/src/app/plugins/matomo.ts +++ b/apps/remix-ide/src/app/plugins/matomo.ts @@ -11,7 +11,7 @@ const profile = { version: '1.0.0' } -const allowedPlugins = ['LearnEth', 'etherscan', 'vyper', 'circuit-compiler', 'doc-gen', 'doc-viewer', 'solhint', 'walletconnect', 'scriptRunner', 'scriptRunnerBridge', 'dgit', 'contract-verification'] +const allowedPlugins = ['LearnEth', 'etherscan', 'vyper', 'circuit-compiler', 'doc-gen', 'doc-viewer', 'solhint', 'walletconnect', 'scriptRunner', 'scriptRunnerBridge', 'dgit', 'contract-verification', 'noir-compiler'] export class Matomo extends Plugin { diff --git a/apps/remix-ide/src/app/tabs/locales/en/noir.json b/apps/remix-ide/src/app/tabs/locales/en/noir.json new file mode 100644 index 00000000000..37a7ad7c6ed --- /dev/null +++ b/apps/remix-ide/src/app/tabs/locales/en/noir.json @@ -0,0 +1,27 @@ +{ + "noir.compiler": "Compiler", + "noir.autoCompile": "Auto compile", + "noir.hideWarnings": "Hide warnings", + "noir.advancedConfigurations": "Advanced Configurations", + "noir.compilerConfiguration": "Compiler configuration", + "noir.prime": "Prime", + "noir.useConfigurationFile": "Use configuration file", + "noir.compile": "Compile", + "noir.noFileSelected": "no file selected", + "noir.generateR1cs": "Generate R1CS", + "noir.computeWitness": "Compute Witness", + "noir.generateProof": "Generate Proof", + "noir.signalInput": "Signal Input", + "noir.compute": "Compute", + "noir.setupExports": "Setup and Exports", + "noir.provingScheme": "Proving Scheme", + "noir.ptau": "POWER OF TAU (PTAU)", + "noir.randomText": "Ceremony: Random Text", + "noir.randomBeacon": "Ceremony: Random Beacon", + "noir.exportVerifierContract": "Export verifier contract", + "noir.exportVerificationKey": "Export verification key", + "noir.exportVerifierCalldata": "Export verifier calldata", + "noir.exportWtnsJson": "Export witness as JSON", + "noir.runSetup": "Run setup" + } + \ No newline at end of file diff --git a/apps/remix-ide/src/assets/img/noir-icon.webp b/apps/remix-ide/src/assets/img/noir-icon.webp new file mode 100644 index 00000000000..1fab6574b8b Binary files /dev/null and b/apps/remix-ide/src/assets/img/noir-icon.webp differ diff --git a/apps/remix-ide/src/remixAppManager.js b/apps/remix-ide/src/remixAppManager.js index 04179529601..c8df53a0580 100644 --- a/apps/remix-ide/src/remixAppManager.js +++ b/apps/remix-ide/src/remixAppManager.js @@ -95,7 +95,7 @@ let requiredModules = [ // dependentModules shouldn't be manually activated (e.g hardhat is activated by remixd) const dependentModules = ['foundry', 'hardhat', 'truffle', 'slither'] -const loadLocalPlugins = ['doc-gen', 'doc-viewer', 'contract-verification', 'vyper', 'solhint', 'walletconnect', 'circuit-compiler', 'learneth', 'quick-dapp'] +const loadLocalPlugins = ['doc-gen', 'doc-viewer', 'contract-verification', 'vyper', 'solhint', 'walletconnect', 'circuit-compiler', 'learneth', 'quick-dapp', 'noir-compiler'] const partnerPlugins = ['cookbookdev'] @@ -151,6 +151,7 @@ export function isNative(name) { 'contract-verification', 'popupPanel', 'LearnEth', + 'noir-compiler' ] return nativePlugins.includes(name) || requiredModules.includes(name) || isInjectedProvider(name) || isVM(name) || isScriptRunner(name) } diff --git a/apps/remix-ide/src/remixEngine.js b/apps/remix-ide/src/remixEngine.js index b883655e55e..ba26ef02470 100644 --- a/apps/remix-ide/src/remixEngine.js +++ b/apps/remix-ide/src/remixEngine.js @@ -31,6 +31,7 @@ export class RemixEngine extends Engine { if (name === 'cookbookdev') return { queueTimeout: 60000 * 3 } if (name === 'contentImport') return { queueTimeout: 60000 * 3 } if (name === 'circom') return { queueTimeout: 60000 * 4 } + if (name === 'noir-compiler') return { queueTimeout: 60000 * 4 } return { queueTimeout: 10000 } } diff --git a/libs/remix-ui/helper/src/index.ts b/libs/remix-ui/helper/src/index.ts index 94dab78f43e..5156d0a3da2 100644 --- a/libs/remix-ui/helper/src/index.ts +++ b/libs/remix-ui/helper/src/index.ts @@ -3,4 +3,6 @@ export * from './lib/bleach' export * from './lib/helper-components' export * from './lib/components/PluginViewWrapper' export * from './lib/components/custom-dropdown' -export * from './lib/components/custom-tooltip' \ No newline at end of file +export * from './lib/components/custom-tooltip' +export * from './lib/components/feedback' +export type { CompilerReport } from './types/compilerTypes' \ No newline at end of file diff --git a/apps/circuit-compiler/src/app/components/feedback.tsx b/libs/remix-ui/helper/src/lib/components/feedback.tsx similarity index 81% rename from apps/circuit-compiler/src/app/components/feedback.tsx rename to libs/remix-ui/helper/src/lib/components/feedback.tsx index af42b20d862..2c8ebabdcff 100644 --- a/apps/circuit-compiler/src/app/components/feedback.tsx +++ b/libs/remix-ui/helper/src/lib/components/feedback.tsx @@ -1,5 +1,5 @@ -import { useState } from 'react' -import { CompilerFeedbackProps, CompilerReport } from '../types' +import React, { useState } from 'react' +import { CompilerFeedbackProps, CompilerReport } from '../../types/compilerTypes' import { RenderIf } from '@remix-ui/helper' import { CopyToClipboard } from '@remix-ui/clipboard' import { FeedbackAlert } from './feedbackAlert' @@ -11,16 +11,6 @@ export function CompilerFeedback ({ feedback, filePathToId, hideWarnings, openEr setShowException(false) } - const handleOpenError = (report: CompilerReport) => { - if (report.labels.length > 0) { - openErrorLocation(filePathToId[report.labels[0].file_id], report.labels[0].range.start) - } - } - - const handleAskGPT = (report: CompilerReport) => { - askGPT(report) - } - return (
{ @@ -43,19 +33,19 @@ export function CompilerFeedback ({ feedback, filePathToId, hideWarnings, openEr <> { Array.isArray(feedback) && feedback.map((response, index) => ( -
handleOpenError(response)}> +
openErrorLocation(response)}>
handleAskGPT(response) } /> + askGPT={ () => askGPT(response) } />
{ handleAskGPT(response) }} /> + askGPT={() => { askGPT(response) }} />
diff --git a/apps/circuit-compiler/src/app/components/feedbackAlert.tsx b/libs/remix-ui/helper/src/lib/components/feedbackAlert.tsx similarity index 79% rename from apps/circuit-compiler/src/app/components/feedbackAlert.tsx rename to libs/remix-ui/helper/src/lib/components/feedbackAlert.tsx index c6328dd510a..cf7137ab20e 100644 --- a/apps/circuit-compiler/src/app/components/feedbackAlert.tsx +++ b/libs/remix-ui/helper/src/lib/components/feedbackAlert.tsx @@ -1,10 +1,10 @@ -import { useState } from 'react' -import { FeedbackAlertProps } from '../types' +import React, { useState } from 'react' +import { FeedbackAlertProps } from '../../types/compilerTypes' import { RenderIf } from '@remix-ui/helper' -import {CopyToClipboard} from '@remix-ui/clipboard' +import { CopyToClipboard } from '@remix-ui/clipboard' export function FeedbackAlert ({ message, askGPT }: FeedbackAlertProps) { - const [ showAlert, setShowAlert] = useState(true) + const [showAlert, setShowAlert] = useState(true) const handleCloseAlert = () => { setShowAlert(false) diff --git a/libs/remix-ui/helper/src/lib/helper-components.tsx b/libs/remix-ui/helper/src/lib/helper-components.tsx index 663955414e3..81d7aa9d5e3 100644 --- a/libs/remix-ui/helper/src/lib/helper-components.tsx +++ b/libs/remix-ui/helper/src/lib/helper-components.tsx @@ -1,5 +1,9 @@ import { LayoutCompatibilityReport } from '@openzeppelin/upgrades-core/dist/storage/report' import React from 'react' +import { FormattedMessage } from 'react-intl' +import { CompileOptionsProps } from '../types/compilerTypes' +import { CustomTooltip } from './components/custom-tooltip' +import { extractNameFromKey } from './remix-ui-helper' export const fileChangedToastMsg = (from: string, path: string) => (
@@ -142,3 +146,75 @@ export function RenderIf({ condition, children }: { condition: boolean, children export function RenderIfNot({ condition, children }: { condition: boolean, children: JSX.Element }) { return condition ? null : children } + +export const CompileOptions = ({ autoCompile, hideWarnings, setCircuitAutoCompile, setCircuitHideWarnings }: CompileOptionsProps) => ( + +
+
+ setCircuitAutoCompile(e.target.checked)} + title="Auto compile" + checked={autoCompile} + id="autoCompileCircuit" + /> + +
+
+ setCircuitHideWarnings(e.target.checked)} + id="hideCircuitWarnings" + type="checkbox" + title="Hide warnings" + checked={hideWarnings} + /> + +
+
+) + +export const CompileBtn = ({ plugin, appState, id, compileAction }: { plugin: any, appState: { status, filePath }, id: string, compileAction: () => void }) => ( + +
+ Ctrl+S to compile {appState.filePath} +
+
+ } + > + + +) diff --git a/libs/remix-ui/helper/src/lib/remix-ui-helper.ts b/libs/remix-ui/helper/src/lib/remix-ui-helper.ts index aecc3ebc690..32e3947b822 100644 --- a/libs/remix-ui/helper/src/lib/remix-ui-helper.ts +++ b/libs/remix-ui/helper/src/lib/remix-ui-helper.ts @@ -82,7 +82,8 @@ export const getPathIcon = (path: string) => { ? 'fad fa-brackets-curly' : path.endsWith('.cairo') ? 'small fa-kit fa-cairo' : path.endsWith('.circom') ? 'fa-kit fa-circom' : path.endsWith('.nr') - ? 'fa-duotone fa-regular fa-diamond' : 'far fa-file' + ? 'fa-kit fa-noir' : path.endsWith('.toml') + ? 'fad fa-cog': 'far fa-file' } export const isNumeric = (value) => { diff --git a/libs/remix-ui/helper/src/types/compilerTypes.ts b/libs/remix-ui/helper/src/types/compilerTypes.ts new file mode 100644 index 00000000000..17b96cd48e5 --- /dev/null +++ b/libs/remix-ui/helper/src/types/compilerTypes.ts @@ -0,0 +1,34 @@ +export type CompileOptionsProps = { + setCircuitAutoCompile: (value: boolean) => void, + setCircuitHideWarnings: (value: boolean) => void, + autoCompile: boolean, + hideWarnings: boolean + } + +export type FeedbackAlertProps = { + message: string, + askGPT: () => void + } + +export type CompilerFeedbackProps = { + feedback: string | CompilerReport[], + filePathToId: Record, + openErrorLocation: (report: CompilerReport) => void, + hideWarnings: boolean, + askGPT: (report: CompilerReport) => void + } + +export type CompilerReport = { + type: "Error" | "Bug" | "Help" | "Note" | "Warning" | "Unknown", + message: string, + labels: { + style: "Primary" | "Secondary" | "Unknown", + file_id: string, + range: { + start: string, + end: string + }, + message: string + }[], + notes: string[] + } \ No newline at end of file diff --git a/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx b/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx index 0d279961a33..b8d1ce97346 100644 --- a/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx +++ b/libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx @@ -66,6 +66,7 @@ const tabsReducer = (state: ITabsState, action: ITabsAction) => { return state } } +const PlayExtList = ['js', 'ts', 'sol', 'circom', 'vy', 'nr'] export const TabsUI = (props: TabsUIProps) => { const [tabsState, dispatch] = useReducer(tabsReducer, initialTabsState) @@ -208,23 +209,22 @@ export const TabsUI = (props: TabsUIProps) => {