Skip to content
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

chore: Initial code to generate ts code from Noir ABI #2750

Merged
merged 6 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions yarn-project/noir-compiler/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export { generateNoirContractInterface } from './contract-interface-gen/noir.js'
export { generateTypescriptContractInterface } from './contract-interface-gen/typescript.js';
export { generateAztecAbi };

export * from './noir_artifact.js';

/**
* Compile Aztec.nr contracts in project path using a nargo binary available in the shell.
* @param projectPath - Path to project.
Expand Down
18 changes: 17 additions & 1 deletion yarn-project/noir-compiler/src/noir_artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ABIParameter, ABIType, DebugFileMap, DebugInfo } from '@aztec/foundatio
type NoirFunctionType = 'Open' | 'Secret' | 'Unconstrained';

/** The ABI of an Aztec.nr function. */
interface NoirFunctionAbi {
export interface NoirFunctionAbi {
/** The parameters of the function. */
parameters: ABIParameter[];
/** The witness indices of the parameters. Indexed by parameter name. */
Expand Down Expand Up @@ -47,6 +47,22 @@ export interface NoirCompiledContract {
functions: NoirFunctionEntry[];
}

/**
* The compilation result of an Aztec.nr contract.
*/
export interface NoirCompiledCircuit {
/** The hash of the circuit. */
hash: number;
/** Compilation backend. */
backend: string;
/**
* The ABI of the function.
*/
abi: NoirFunctionAbi;
/** The bytecode of the circuit in base64. */
bytecode: string;
}

/**
* The debug metadata of an Aztec.nr contract.
*/
Expand Down
5 changes: 3 additions & 2 deletions yarn-project/noir-private-kernel/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
dist
.yarn
proofs/
Prover.toml
Verifier.toml
3 changes: 2 additions & 1 deletion yarn-project/noir-private-kernel/.prettierignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
src/crates
src/target
src/target
src/types
4 changes: 4 additions & 0 deletions yarn-project/noir-private-kernel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"clean": "rm -rf ./dest .tsbuildinfo",
"formatting": "run -T prettier --check ./src && run -T eslint ./src",
"formatting:fix": "run -T prettier -w ./src",
"noir:build": "cd src && nargo compile",
"noir:types": "yarn ts-node --esm src/scripts/generate_ts_from_abi.ts",
"test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules $(yarn bin jest) --passWithNoTests"
},
"inherits": [
Expand All @@ -26,7 +28,9 @@
"rootDir": "./src"
},
"dependencies": {
"@aztec/circuits.js": "workspace:^",
"@aztec/foundation": "workspace:^",
"@aztec/noir-compiler": "workspace:^",
"@noir-lang/acvm_js": "^0.28.0",
"@noir-lang/backend_barretenberg": "^0.7.10",
"@noir-lang/noir_js": "^0.16.0",
Expand Down
9 changes: 5 additions & 4 deletions yarn-project/noir-private-kernel/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { NoirCompiledCircuit } from '@aztec/noir-compiler';

import PrivateKernelInitJson from './target/private_kernel_init.json' assert { type: 'json' };
import PrivateKernelInnerJson from './target/private_kernel_inner.json' assert { type: 'json' };
import PrivateKernelOrderingJson from './target/private_kernel_ordering.json' assert { type: 'json' };

// TODO add types for noir circuit artifacts
export const PrivateKernelInitArtifact = PrivateKernelInitJson;
export const PrivateKernelInitArtifact = PrivateKernelInitJson as NoirCompiledCircuit;

export const PrivateKernelInnerArtifact = PrivateKernelInnerJson;
export const PrivateKernelInnerArtifact = PrivateKernelInnerJson as NoirCompiledCircuit;

export const PrivateKernelOrderingArtifact = PrivateKernelOrderingJson;
export const PrivateKernelOrderingArtifact = PrivateKernelOrderingJson as NoirCompiledCircuit;
202 changes: 202 additions & 0 deletions yarn-project/noir-private-kernel/src/scripts/generate_ts_from_abi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { ABIType } from '@aztec/foundation/abi';
import { createConsoleLogger } from '@aztec/foundation/log';
import { NoirCompiledCircuit, NoirFunctionAbi } from '@aztec/noir-compiler';

import fs from 'fs/promises';

const log = createConsoleLogger('aztec:noir-contracts');

/**
* Keep track off all of the Noir primitive types that were used.
* Most of these will not have a 1-1 definition in TypeScript,
* so we will need to generate type aliases for them.
*
* We want to generate type aliases
* for specific types that are used in the ABI.
*
* For example:
* - If `Field` is used we want to alias that
* with `number`.
* - If `u32` is used we want to alias that with `number` too.
*/
type PrimitiveTypesUsed = {
/**
* The name of the type alias that we will generate.
*/
aliasName: string;
/**
* The TypeScript type that we will alias to.
*/
tsType: string;
};

const noirPrimitiveTypesToTsTypes = new Map<string, PrimitiveTypesUsed>();

/**
* Typescript does not allow us to check for equality of non-primitive types
* easily, so we create a addIfUnique function that will only add an item
* to the map if it is not already there by using JSON.stringify.
* @param item - The item to add to the map.
*/
function addIfUnique(item: PrimitiveTypesUsed) {
const key = JSON.stringify(item);
if (!noirPrimitiveTypesToTsTypes.has(key)) {
noirPrimitiveTypesToTsTypes.set(key, item);
}
}

/**
* Converts an ABI type to a TypeScript type.
* @param type - The ABI type to convert.
* @returns The typescript code to define the type.
*/
function abiTypeToTs(type: ABIType): string {
switch (type.kind) {
case 'integer': {
let tsIntType = '';
if (type.sign === 'signed') {
tsIntType = `i${type.width}`;
} else {
tsIntType = `u${type.width}`;
}
addIfUnique({ aliasName: tsIntType, tsType: 'number' });
return tsIntType;
}
case 'boolean':
return `boolean`;
case 'array':
return `${abiTypeToTs(type.type)}[]`;
case 'struct':
return getLastComponentOfPath(type.path);
case 'field':
addIfUnique({ aliasName: 'Field', tsType: 'number' });
return 'Field';
default:
throw new Error(`Unknown ABI type ${type}`);
}
}

/**
* Returns the last component of a path, e.g. "foo::bar::baz" -\> "baz"
* Note: that if we have a path such as "Baz", we will return "Baz".
*
* Since these paths corresponds to structs, we can assume that we
* cannot have "foo::bar::".
*
* We also make the assumption that since these paths are coming from
* Noir, then we will not have two paths that look like this:
* - foo::bar::Baz
* - cat::dog::Baz
* ie the last component of the path (struct name) is enough to uniquely identify
* the whole path.
*
* TODO: We should double check this assumption when we use type aliases,
* I expect that `foo::bar::Baz as Dog` would effectively give `foo::bar::Dog`
* @param str - The path to get the last component of.
* @returns The last component of the path.
*/
function getLastComponentOfPath(str: string): string {
const parts = str.split('::');
const lastPart = parts[parts.length - 1];
return lastPart;
}

/**
* Generates TypeScript interfaces for the structs used in the ABI.
* @param type - The ABI type to generate the interface for.
* @param output - The set of structs that we have already generated bindings for.
* @returns The TypeScript code to define the struct.
*/
function generateStructInterfaces(type: ABIType, output: Set<string>): string {
let result = '';

// Edge case to handle the array of structs case.
if (type.kind === 'array' && type.type.kind === 'struct' && !output.has(getLastComponentOfPath(type.type.path))) {
result += generateStructInterfaces(type.type, output);
}
if (type.kind !== 'struct') return result;

// List of structs encountered while viewing this type that we need to generate
// bindings for.
const typesEncountered = new Set<ABIType>();

// Codegen the struct and then its fields, so that the structs fields
// are defined before the struct itself.
let codeGeneratedStruct = '';
let codeGeneratedStructFields = '';

const structName = getLastComponentOfPath(type.path);
if (!output.has(structName)) {
codeGeneratedStruct += `interface ${structName} {\n`;
for (const field of type.fields) {
codeGeneratedStruct += ` ${field.name}: ${abiTypeToTs(field.type)};\n`;
typesEncountered.add(field.type);
}
codeGeneratedStruct += `}\n\n`;
output.add(structName);

// Generate code for the encountered structs in the field above
for (const type of typesEncountered) {
codeGeneratedStructFields += generateStructInterfaces(type, output);
}
}

return codeGeneratedStructFields + '\n' + codeGeneratedStruct;
}

/**
* Generates a TypeScript interface for the ABI.
* @param abiObj - The ABI to generate the interface for.
* @returns The TypeScript code to define the interface.
*/
function generateTsInterface(abiObj: NoirFunctionAbi): string {
let result = ``;
const outputStructs = new Set<string>();

// Define structs for composite types
for (const param of abiObj.parameters) {
result += generateStructInterfaces(param.type, outputStructs);
}

// Generating Return type, if it exists
//
if (abiObj.return_type != null) {
result += generateStructInterfaces(abiObj.return_type, outputStructs);
result += `export interface ReturnType {\n`;
result += ` value: ${abiTypeToTs(abiObj.return_type)};\n`;
result += `}\n\n`;
}

// Generating Input type
result += 'export interface InputType {\n';
for (const param of abiObj.parameters) {
result += ` ${param.name}: ${abiTypeToTs(param.type)};\n`;
}
result += '}';

// Add the primitive Noir types that do not have a 1-1 mapping to TypeScript.
let primitiveTypeAliases = '';
for (const [, value] of noirPrimitiveTypesToTsTypes) {
primitiveTypeAliases += `\ntype ${value.aliasName} = ${value.tsType};`;
}

return `/* Autogenerated file, do not edit! */\n\n/* eslint-disable */\n` + primitiveTypeAliases + '\n' + result;
}

const circuits = ['private_kernel_init', 'private_kernel_inner', 'private_kernel_ordering'];

const main = async () => {
for (const circuit of circuits) {
const rawData = await fs.readFile(`./src/target/${circuit}.json`, 'utf-8');
const abiObj: NoirCompiledCircuit = JSON.parse(rawData);
const generatedInterface = generateTsInterface(abiObj.abi);
await fs.writeFile(`./src/types/${circuit}_types.ts`, generatedInterface);
}
};

try {
await main();
} catch (err: unknown) {
log(`Error generating types ${err}`);
process.exit(1);
}

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Loading