-
Notifications
You must be signed in to change notification settings - Fork 613
feat: noir_wasm compilation of noir programs #3272
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 40 commits
41413c5
b2c5741
9abd0ff
b51c19a
e44955d
fa43b73
27c62b2
ae7ef52
94d6399
1473663
3a9bd7e
d62887e
cde221e
d26d6ac
d1b1d4e
c34bb69
c79086e
d1f1ecb
e655751
7ebbf5d
452b858
c635a05
e2c70da
eae79ec
cf82db1
1f38e39
71ba52f
0911592
955417e
8efa75a
47b8c2b
af5832f
eb6f46e
d7abdb5
c2659eb
857101e
d3a51cc
2dde02d
d2af58d
d5ada69
ac66988
b0be295
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| import { ContractArtifact } from '@aztec/foundation/abi'; | ||
| import { LogFn } from '@aztec/foundation/log'; | ||
|
|
||
| import { Command } from 'commander'; | ||
| import { mkdirSync, writeFileSync } from 'fs'; | ||
| import { mkdirpSync } from 'fs-extra'; | ||
| import path, { resolve } from 'path'; | ||
|
|
||
| import { | ||
| ProgramArtifact, | ||
| compileUsingNargo, | ||
| compileUsingNoirWasm, | ||
| generateNoirContractInterface, | ||
| generateTypescriptContractInterface, | ||
| generateTypescriptProgramInterface, | ||
| } from '../index.js'; | ||
|
|
||
| /** | ||
| * CLI options for configuring behavior | ||
| */ | ||
| interface options { | ||
| // eslint-disable-next-line jsdoc/require-jsdoc | ||
| outdir: string; | ||
| // eslint-disable-next-line jsdoc/require-jsdoc | ||
| typescript: string | undefined; | ||
| // eslint-disable-next-line jsdoc/require-jsdoc | ||
| interface: string | undefined; | ||
| // eslint-disable-next-line jsdoc/require-jsdoc | ||
| compiler: string | undefined; | ||
| } | ||
| /** | ||
| * Registers a 'contract' command on the given commander program that compiles an Aztec.nr contract project. | ||
| * @param program - Commander program. | ||
| * @param log - Optional logging function. | ||
| * @returns The program with the command registered. | ||
| */ | ||
| export function compileNoir(program: Command, name = 'compile', log: LogFn = () => {}): Command { | ||
| return program | ||
| .command(name) | ||
| .argument('<project-path>', 'Path to the bin or Aztec.nr project to compile') | ||
| .option('-o, --outdir <path>', 'Output folder for the binary artifacts, relative to the project path', 'target') | ||
| .option('-ts, --typescript <path>', 'Optional output folder for generating typescript wrappers', undefined) | ||
| .option('-i, --interface <path>', 'Optional output folder for generating an Aztec.nr contract interface', undefined) | ||
| .option('-c --compiler <string>', 'Which compiler to use. Either nargo or wasm. Defaults to nargo', 'wasm') | ||
| .description('Compiles the Noir Source in the target project') | ||
|
|
||
| .action(async (projectPath: string, options: options) => { | ||
| const { compiler } = options; | ||
| if (typeof projectPath !== 'string') {throw new Error(`Missing project path argument`);} | ||
| if (compiler !== 'nargo' && compiler !== 'wasm') {throw new Error(`Invalid compiler: ${compiler}`);} | ||
|
|
||
| const compile = compiler === 'wasm' ? compileUsingNoirWasm : compileUsingNargo; | ||
| log(`Compiling ${projectPath} with ${compiler} backend...`); | ||
| const results = await compile(projectPath, { log }); | ||
| for (const result of results) { | ||
| generateOutput(projectPath, result, options, log); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * | ||
| * @param contract - output from compiler, to serialize locally. branch based on Contract vs Program | ||
| */ | ||
| function generateOutput( | ||
| projectPath: string, | ||
| _result: ContractArtifact | ProgramArtifact, | ||
| options: options, | ||
| log: LogFn, | ||
| ) { | ||
| const contract = _result as ContractArtifact; | ||
| if (contract.name) { | ||
|
Comment on lines
+75
to
+76
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this (and the if statement after it) could use the helper method
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ahh i just tried, the types are actually slightly different... got bogged down converting |
||
| return generateContractOutput(projectPath, contract, options, log); | ||
| } else { | ||
| const program = _result as ProgramArtifact; | ||
| if (program.abi) { | ||
| return generateProgramOutput(projectPath, program, options, log); | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * | ||
| * @param program - output from compiler, to serialize locally | ||
| */ | ||
| function generateProgramOutput(projectPath: string, program: ProgramArtifact, options: options, log: LogFn) { | ||
| const currentDir = process.cwd(); | ||
| const { outdir, typescript, interface: noirInterface } = options; | ||
| const artifactPath = resolve(projectPath, outdir, `${program.name ? program.name : 'main'}.json`); | ||
| log(`Writing ${program.name} artifact to ${path.relative(currentDir, artifactPath)}`); | ||
| mkdirSync(path.dirname(artifactPath), { recursive: true }); | ||
| writeFileSync(artifactPath, JSON.stringify(program, null, 2)); | ||
|
|
||
| if (noirInterface) { | ||
| log(`noirInterface generation not implemented for programs`); | ||
| // not implemented | ||
| } | ||
|
|
||
| if (typescript) { | ||
| // just need type definitions, since a lib has just one entry point | ||
| const tsPath = resolve(projectPath, typescript, `../types/${program.name}_types.ts`); | ||
| log(`Writing ${program.name} typescript types to ${path.relative(currentDir, tsPath)}`); | ||
| const tsWrapper = generateTypescriptProgramInterface(program.abi); | ||
| mkdirpSync(path.dirname(tsPath)); | ||
| writeFileSync(tsPath, tsWrapper); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * | ||
| * @param contract - output from compiler, to serialize locally | ||
| */ | ||
| function generateContractOutput(projectPath: string, contract: ContractArtifact, options: options, log: LogFn) { | ||
| const currentDir = process.cwd(); | ||
| const { outdir, typescript, interface: noirInterface } = options; | ||
| const artifactPath = resolve(projectPath, outdir, `${contract.name}.json`); | ||
| log(`Writing ${contract.name} artifact to ${path.relative(currentDir, artifactPath)}`); | ||
| mkdirSync(path.dirname(artifactPath), { recursive: true }); | ||
| writeFileSync(artifactPath, JSON.stringify(contract, null, 2)); | ||
|
|
||
| if (noirInterface) { | ||
| const noirInterfacePath = resolve(projectPath, noirInterface, `${contract.name}_interface.nr`); | ||
| log(`Writing ${contract.name} Aztec.nr external interface to ${path.relative(currentDir, noirInterfacePath)}`); | ||
| const noirWrapper = generateNoirContractInterface(contract); | ||
| mkdirpSync(path.dirname(noirInterfacePath)); | ||
| writeFileSync(noirInterfacePath, noirWrapper); | ||
| } | ||
|
|
||
| if (typescript) { | ||
| const tsPath = resolve(projectPath, typescript, `${contract.name}.ts`); | ||
| log(`Writing ${contract.name} typescript interface to ${path.relative(currentDir, tsPath)}`); | ||
| let relativeArtifactPath = path.relative(path.dirname(tsPath), artifactPath); | ||
| if (relativeArtifactPath === `${contract.name}.json`) { | ||
| // relative path edge case, prepending ./ for local import - the above logic just does | ||
| // `${contract.name}.json`, which is not a valid import for a file in the same directory | ||
| relativeArtifactPath = `./${contract.name}.json`; | ||
| } | ||
| const tsWrapper = generateTypescriptContractInterface(contract, relativeArtifactPath); | ||
| mkdirpSync(path.dirname(tsPath)); | ||
| writeFileSync(tsPath, tsWrapper); | ||
| } | ||
| } | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,3 @@ | ||
| export { compileContract } from './contract.js'; | ||
| export { compileNoir } from './compileNoir.js'; | ||
| export { generateNoirInterface } from './noir-interface.js'; | ||
| export { generateTypescriptInterface } from './typescript.js'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,7 @@ import { LogFn, createDebugLogger } from '@aztec/foundation/log'; | |
| import { CompileError, compile } from '@noir-lang/noir_wasm'; | ||
| import { isAbsolute } from 'node:path'; | ||
|
|
||
| import { NoirCompilationArtifacts } from '../../noir_artifact.js'; | ||
| import { NoirCompilationResult, NoirProgramCompilationArtifacts } from '../../noir_artifact.js'; | ||
| import { NoirDependencyManager } from './dependencies/dependency-manager.js'; | ||
| import { GithubDependencyResolver as GithubCodeArchiveDependencyResolver } from './dependencies/github-dependency-resolver.js'; | ||
| import { LocalDependencyResolver } from './dependencies/local-dependency-resolver.js'; | ||
|
|
@@ -54,9 +54,6 @@ export class NoirWasmContractCompiler { | |
| } | ||
|
|
||
| const noirPackage = NoirPackage.open(projectPath, fileManager); | ||
| if (noirPackage.getType() !== 'contract') { | ||
| throw new Error('This is not a contract project'); | ||
| } | ||
|
|
||
| const dependencyManager = new NoirDependencyManager( | ||
| [ | ||
|
|
@@ -80,22 +77,73 @@ export class NoirWasmContractCompiler { | |
| } | ||
|
|
||
| /** | ||
| * Compiles the project. | ||
| * Compile EntryPoint | ||
| */ | ||
| public async compile(): Promise<NoirCompilationArtifacts[]> { | ||
| const isContract = this.#package.getType() === 'contract'; | ||
| // limit to contracts-only because the rest of the pipeline only supports processing contracts | ||
| if (!isContract) { | ||
| throw new Error('Noir project is not a contract'); | ||
| public async compile(): Promise<NoirCompilationResult[]> { | ||
| if (this.#package.getType() === 'contract') { | ||
| this.#debugLog(`Compiling Contract at ${this.#package.getEntryPointPath()}`); | ||
| return await this.compileContract(); | ||
| } else if (this.#package.getType() === 'bin') { | ||
| this.#debugLog(`Compiling Program at ${this.#package.getEntryPointPath()}`); | ||
| return await this.compileProgram(); | ||
| } else { | ||
| this.#log( | ||
| `Compile skipped - only supports compiling "contract" and "bin" package types (${this.#package.getType()})`, | ||
| ); | ||
| return []; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Compiles the Program. | ||
| */ | ||
| public async compileProgram(): Promise<NoirProgramCompilationArtifacts[]> { | ||
| await this.#dependencyManager.resolveDependencies(); | ||
| this.#debugLog(`Dependencies: ${this.#dependencyManager.getPackageNames().join(', ')}`); | ||
|
|
||
| initializeResolver(this.#resolveFile); | ||
|
|
||
| try { | ||
| const isContract: boolean = false; | ||
| const result = compile(this.#package.getEntryPointPath(), isContract, { | ||
| /* eslint-disable camelcase */ | ||
| root_dependencies: this.#dependencyManager.getEntrypointDependencies(), | ||
| library_dependencies: this.#dependencyManager.getLibraryDependencies(), | ||
| /* eslint-enable camelcase */ | ||
| }); | ||
|
|
||
| if (!('program' in result)) { | ||
| throw new Error('No program found in compilation result'); | ||
| } | ||
|
|
||
| return [{ name: this.#package.getNoirPackageConfig().package.name, ...result }]; | ||
| } catch (err) { | ||
| if (err instanceof Error && err.name === 'CompileError') { | ||
| this.#processCompileError(err as CompileError); | ||
| } | ||
|
|
||
| throw err; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Compiles the Contract. | ||
| */ | ||
| public async compileContract(): Promise<NoirCompilationResult[]> { | ||
| if (!(this.#package.getType() === 'contract' || this.#package.getType() === 'bin')) { | ||
| this.#log( | ||
| `Compile skipped - only supports compiling "contract" and "bin" package types (${this.#package.getType()})`, | ||
| ); | ||
| return []; | ||
| } | ||
| this.#debugLog(`Compiling contract at ${this.#package.getEntryPointPath()}`); | ||
| await this.#dependencyManager.resolveDependencies(); | ||
| this.#debugLog(`Dependencies: ${this.#dependencyManager.getPackageNames().join(', ')}`); | ||
|
|
||
| initializeResolver(this.#resolveFile); | ||
|
|
||
| try { | ||
| const isContract: boolean = true; // this.#package.getType() === 'contract'; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WDYT about removing the commented out code at the end? I like the explicit name for the parameter (here and in
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oops yeah will remove! |
||
| const result = compile(this.#package.getEntryPointPath(), isContract, { | ||
| /* eslint-disable camelcase */ | ||
| root_dependencies: this.#dependencyManager.getEntrypointDependencies(), | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the convention is to use pascal case for interface names. Potentially we could simplify the lint rules too