Skip to content

Commit

Permalink
feat: add noir_wasm-based compiler
Browse files Browse the repository at this point in the history
  • Loading branch information
alexghr committed Oct 8, 2023
1 parent 4881414 commit ac2921d
Show file tree
Hide file tree
Showing 10 changed files with 485 additions and 3 deletions.
4 changes: 4 additions & 0 deletions yarn-project/noir-compiler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
},
"dependencies": {
"@aztec/foundation": "workspace:^",
"@noir-lang/noir_wasm": "^0.16.0",
"@noir-lang/source-resolver": "^0.16.0",
"base64-js": "^1.5.1",
"commander": "^9.0.0",
"fs-extra": "^11.1.1",
Expand All @@ -47,6 +49,7 @@
"lodash.times": "^4.3.2",
"lodash.upperfirst": "^4.3.1",
"pako": "^2.1.0",
"tar": "^6.2.0",
"toml": "^3.0.0",
"tslib": "^2.4.0"
},
Expand All @@ -62,6 +65,7 @@
"@types/lodash.upperfirst": "^4.3.7",
"@types/node": "^18.7.23",
"@types/pako": "^2.0.0",
"@types/tar": "^6.1.6",
"jest": "^29.5.0",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1",
Expand Down
11 changes: 9 additions & 2 deletions yarn-project/noir-compiler/src/cli/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import { writeFileSync } from 'fs';
import { mkdirpSync } from 'fs-extra';
import path, { resolve } from 'path';

import { compileUsingNargo, generateNoirContractInterface, generateTypescriptContractInterface } from '../index.js';
import {
compileUsingNargo,
compileUsingNoirWasm,
generateNoirContractInterface,
generateTypescriptContractInterface,
} from '../index.js';

/**
* Registers a 'contract' command on the given commander program that compiles an Aztec.nr contract project.
Expand All @@ -20,6 +25,7 @@ export function compileContract(program: Command, name = 'contract', log: LogFn
.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('--compiler <compiler>', 'Compiler to use (nargo|wasm)', 'nargo')
.description('Compiles the contracts in the target project')

.action(
Expand All @@ -30,14 +36,15 @@ export function compileContract(program: Command, name = 'contract', log: LogFn
outdir: string;
typescript: string | undefined;
interface: string | undefined;
compiler: 'nargo' | 'wasm';
},
/* eslint-enable jsdoc/require-jsdoc */
) => {
const { outdir, typescript, interface: noirInterface } = options;
if (typeof projectPath !== 'string') throw new Error(`Missing project path argument`);
const currentDir = process.cwd();

const compile = compileUsingNargo;
const compile = options.compiler === 'nargo' ? compileUsingNargo : compileUsingNoirWasm;
log(`Compiling contracts...`);
const result = await compile(projectPath, { log });

Expand Down
140 changes: 140 additions & 0 deletions yarn-project/noir-compiler/src/compile/noir/dependency-resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { LogFn, createDebugOnlyLogger } from '@aztec/foundation/log';

import { join, resolve, sep } from 'node:path';
import { Readable } from 'node:stream';
import { finished } from 'node:stream/promises';
import { ReadableStream } from 'node:stream/web';
// eslint-disable-next-line import/no-extraneous-dependencies
import { Parse } from 'tar';

import { Filemanager } from './filemanager.js';
import { NoirGitDependencyConfig, NoirLocalDependencyConfig } from './package-config.js';

/**
* Noir Dependency Resolver
*/
export class NoirDependencyResolver {
#libs = new Map<string, string>();
#fm: Filemanager;
#log: LogFn;

constructor(fm: Filemanager) {
this.#fm = fm;
this.#log = createDebugOnlyLogger('noir:dependency-resolver');
}

/**
* Resolves a dependency.
* @param pkgLocation - Location of the package
* @param name - Name of the dependency
* @param dependency - Dependency to resolve
*/
public async add(
pkgLocation: string,
name: string,
dependency: NoirGitDependencyConfig | NoirLocalDependencyConfig,
): Promise<void> {
const path =
'git' in dependency ? await this.#fetchRemoteDependency(dependency) : resolve(pkgLocation, dependency.path);
this.#libs.set(name, path);
}

/**
* Gets the names of the crates in this dependency list
*/
public getCrateNames() {
return [...this.#libs.keys()];
}

/**
* Looks up a dependency
* @param sourceId - The source being resolved
* @returns The path to the resolved file
*/
public resolve(sourceId: string): string | null {
const [lib, ...path] = sourceId.split('/').filter(x => x);
if (this.#libs.has(lib)) {
return join(this.#libs.get(lib)!, 'src', ...path);
} else {
return null;
}
}

async #fetchRemoteDependency(dependency: NoirGitDependencyConfig): Promise<string> {
const archivePath = await this.#fetchTarFromGithub(dependency);
const libPath = await this.#extractTar(dependency, archivePath);
return libPath;
}

async #extractTar(dependency: NoirGitDependencyConfig, archivePath: string): Promise<string> {
const gitUrl = new URL(dependency.git);
const extractLocation = join('libs', gitUrl.pathname.replaceAll('/', '_') + '@' + (dependency.tag ?? 'HEAD'));
const packagePath = join(extractLocation, dependency.directory ?? '');

// TODO check contents before reusing old results
if (await this.#fm.hasEntry(packagePath)) {
return packagePath;
}

const filter = dependency.directory
? (path: string) => {
const pathWithoutArchiveName = stripSegments(path, 1);
return pathWithoutArchiveName.startsWith(dependency.directory!);
}
: () => true;

const tarParser = new Parse({
filter: filter,
onentry: async entry => {
// just save files
if (entry.type === 'File') {
this.#log(`inflating file ${entry.path}`);
// there's no `strip: 1` with the parser
await this.#fm.writeFile(join(extractLocation, stripSegments(entry.path, 1)), Readable.from(entry));
} else {
entry.resume();
}
},
});

const archive = this.#fm.readFileSync(archivePath, 'binary');
await finished(Readable.from(archive).pipe(tarParser));

return packagePath;
}

async #fetchTarFromGithub(dependency: Pick<NoirGitDependencyConfig, 'git' | 'tag'>): Promise<string> {
// TODO support actual git hosts
if (!dependency.git.startsWith('https://github.com')) {
throw new Error('Only github dependencies are supported');
}

const url = new URL(`${dependency.git}/archive/${dependency.tag ?? 'HEAD'}.tar.gz`);
const localArchivePath = join('archives', url.pathname.replaceAll('/', '_'));

// TODO should check signature before accepting any file
if (await this.#fm.hasEntry(localArchivePath)) {
this.#log('using cached archive', { url: url.href, path: localArchivePath });
return localArchivePath;
}

const response = await fetch(url, {
method: 'GET',
});

if (!response.ok || !response.body) {
throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
}

await this.#fm.writeFile(localArchivePath, Readable.fromWeb(response.body as ReadableStream));
return localArchivePath;
}
}

/**
* Strips the first n segments from a path
*/
function stripSegments(path: string, count: number): string {
const segments = path.split(sep).filter(Boolean);
return segments.slice(count).join(sep);
}
73 changes: 73 additions & 0 deletions yarn-project/noir-compiler/src/compile/noir/filemanager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { mkdirp } from 'fs-extra';
import { createWriteStream, readFileSync } from 'node:fs';
import { access } from 'node:fs/promises';
import { dirname, isAbsolute, join } from 'node:path';
import { Readable } from 'node:stream';
import { finished } from 'node:stream/promises';

/**
* A file manager that writes file to a specific directory but reads globally.
*/
export class Filemanager {
dataDir: string;

public constructor(dataDir: string) {
this.dataDir = dataDir;
}

/**
* Saves a file to the data directory.
* @param name - File to save
* @param stream - File contents
*/
public async writeFile(name: string, stream: Readable): Promise<void> {
if (isAbsolute(name)) {
throw new Error("can't check absolute path");
}

const path = this.#getPath(name);
await mkdirp(dirname(path));
await finished(stream.pipe(createWriteStream(path)));
}

/**
* Reads a file from the disk and returns a buffer
* @param name - File to read
* @param encoding - Binary encoding
*/
public readFileSync(name: string, encoding: 'binary'): Buffer;

/**
* Reads a file from the disk and returns a string
* @param name - File to read
* @param encoding - Encoding to use
*/
public readFileSync(name: string, encoding: 'utf-8'): string;

/**
* Reads a file from the disk
* @param name - File to read
* @param encoding - Encoding to use
*/
public readFileSync(name: string, encoding: 'utf-8' | 'binary'): Buffer | string {
return readFileSync(this.#getPath(name), encoding);
}

/**
* Checks if a file exists and is accessible
* @param name - File to check
*/
public async hasEntry(name: string): Promise<boolean> {
try {
// TODO check access modes?
await access(this.#getPath(name));
return true;
} catch {
return false;
}
}

#getPath(name: string) {
return isAbsolute(name) ? name : join(this.dataDir, name);
}
}
68 changes: 68 additions & 0 deletions yarn-project/noir-compiler/src/compile/noir/noir-wasm-compiler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { compile } from '@noir-lang/noir_wasm';
import { mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';

import { NoirCompilationArtifacts } from '../../noir_artifact.js';
import { NoirDependencyResolver } from './dependency-resolver.js';
import { Filemanager } from './filemanager.js';
import { NoirPackage } from './package.js';
import { initializeResolver } from './source-resolver.cjs';

/**
* Noir Package Compiler
*/
export class NoirWasmContractCompiler {
#projectPath: string;
public constructor(projectPath: string) {
this.#projectPath = projectPath;
}

/**
* Compiles the project.
*/
public async compile(): Promise<NoirCompilationArtifacts[]> {
const noirPackage = await NoirPackage.new(this.#projectPath);

if (noirPackage.getType() !== 'contract') {
throw new Error('This is not a contract project');
}

const cacheRoot = process.env.XDG_CACHE_HOME ?? join(process.env.HOME ?? '', '.cache');
const filemanager = new Filemanager(join(cacheRoot, 'noir_wasm'));
const dependencyResolver = new NoirDependencyResolver(filemanager);

for (const [name, config] of Object.entries(noirPackage.getDependencies())) {
await dependencyResolver.add(noirPackage.getPackagePath(), name, config);
}

initializeResolver((sourceId: any) => {
try {
const libFile = dependencyResolver.resolve(sourceId);
return filemanager.readFileSync(libFile ?? sourceId, 'utf-8');
} catch (err) {
return '';
}
});

/* eslint-disable camelcase */
const res = await compile({
entry_point: noirPackage.getEntryPointPath(),
optional_dependencies_set: dependencyResolver.getCrateNames(),
contracts: true,
});
/* eslint-enable camelcase */

mkdirSync(join(this.#projectPath, 'target'), { recursive: true });
writeFileSync(join(this.#projectPath, 'target', res.name + '.json'), JSON.stringify(res, null, 2));

return [
{
contract: {
backend: '',
functions: res.functions,
name: res.name,
},
},
];
}
}
43 changes: 43 additions & 0 deletions yarn-project/noir-compiler/src/compile/noir/package-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Noir package configuration.
*/
export type NoirPackageConfig = {
/** Package metadata */
package: {
/** Package name */
name: string;
/** Package type */
type: 'lib' | 'contract' | 'binary';
};
/** Package dependencies */
dependencies: Record<string, NoirGitDependencyConfig | NoirLocalDependencyConfig>;
};

/**
* A remote package dependency.
*/
export type NoirGitDependencyConfig = {
/** Git repository URL. */
git: string;
/** Tag to check out */
tag?: string;
/** Where the dependency sits inside the repo */
directory?: string;
};

/**
* A local package dependency.
*/
export type NoirLocalDependencyConfig = {
/** Path to the dependency */
path: string;
};

/**
* Checks that an object is a package configuration.
* @param config - Config to check
*/
export function isPackageConfig(config: any): config is NoirPackageConfig {
// TODO: validate
return true;
}
Loading

0 comments on commit ac2921d

Please sign in to comment.