-
Notifications
You must be signed in to change notification settings - Fork 219
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: codegen typed interfaces for functions in
noir_codegen
(#3533)
- Loading branch information
1 parent
990ac0a
commit 290c463
Showing
10 changed files
with
343 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
name: noir_codegen | ||
|
||
on: | ||
pull_request: | ||
merge_group: | ||
push: | ||
branches: | ||
- master | ||
|
||
concurrency: | ||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref || github.run_id }} | ||
cancel-in-progress: true | ||
|
||
jobs: | ||
build-nargo: | ||
runs-on: ubuntu-22.04 | ||
strategy: | ||
matrix: | ||
target: [x86_64-unknown-linux-gnu] | ||
|
||
steps: | ||
- name: Checkout Noir repo | ||
uses: actions/checkout@v4 | ||
|
||
- name: Setup toolchain | ||
uses: dtolnay/rust-toolchain@1.71.1 | ||
|
||
- uses: Swatinem/rust-cache@v2 | ||
with: | ||
key: ${{ matrix.target }} | ||
cache-on-failure: true | ||
save-if: ${{ github.event_name != 'merge_group' }} | ||
|
||
- name: Build Nargo | ||
run: cargo build --package nargo_cli --release | ||
|
||
- name: Package artifacts | ||
run: | | ||
mkdir dist | ||
cp ./target/release/nargo ./dist/nargo | ||
7z a -ttar -so -an ./dist/* | 7z a -si ./nargo-x86_64-unknown-linux-gnu.tar.gz | ||
- name: Upload artifact | ||
uses: actions/upload-artifact@v3 | ||
with: | ||
name: nargo | ||
path: ./dist/* | ||
retention-days: 3 | ||
|
||
test: | ||
needs: [build-nargo] | ||
name: Test noir_codegen | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Checkout | ||
uses: actions/checkout@v4 | ||
|
||
- name: Install Yarn dependencies | ||
uses: ./.github/actions/setup | ||
|
||
- name: Setup toolchain | ||
uses: dtolnay/rust-toolchain@1.71.1 | ||
with: | ||
targets: wasm32-unknown-unknown | ||
|
||
- uses: Swatinem/rust-cache@v2 | ||
with: | ||
key: wasm32-unknown-unknown-noir-js | ||
cache-on-failure: true | ||
save-if: ${{ github.event_name != 'merge_group' }} | ||
|
||
- name: Install jq | ||
run: sudo apt-get install jq | ||
|
||
- name: Install wasm-bindgen-cli | ||
uses: taiki-e/install-action@v2 | ||
with: | ||
tool: wasm-bindgen-cli@0.2.86 | ||
|
||
- name: Install wasm-opt | ||
run: | | ||
npm i wasm-opt -g | ||
- name: Build acvm_js | ||
run: yarn workspace @noir-lang/acvm_js build | ||
|
||
- name: Build noirc_abi | ||
run: yarn workspace @noir-lang/noirc_abi build | ||
|
||
- name: Build noir_js_types | ||
run: yarn workspace @noir-lang/types build | ||
|
||
- name: Build noir_js | ||
run: yarn workspace @noir-lang/noir_js build | ||
|
||
- name: Run noir_codegen tests | ||
run: yarn workspace @noir-lang/noir_codegen test |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,3 +2,4 @@ crs | |
lib | ||
|
||
!test/*/target | ||
test/codegen |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
import { AbiType, Abi } from '@noir-lang/noirc_abi'; | ||
|
||
/** | ||
* 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. | ||
*/ | ||
export type PrimitiveTypesUsed = { | ||
/** | ||
* The name of the type alias that we will generate. | ||
*/ | ||
aliasName: string; | ||
/** | ||
* The TypeScript type that we will alias to. | ||
*/ | ||
tsType: string; | ||
}; | ||
|
||
/** | ||
* 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(primitiveTypeMap: Map<string, PrimitiveTypesUsed>, item: PrimitiveTypesUsed) { | ||
const key = JSON.stringify(item); | ||
if (!primitiveTypeMap.has(key)) { | ||
primitiveTypeMap.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, primitiveTypeMap: Map<string, PrimitiveTypesUsed>): string { | ||
switch (type.kind) { | ||
case 'field': | ||
addIfUnique(primitiveTypeMap, { aliasName: 'Field', tsType: 'string' }); | ||
return 'Field'; | ||
case 'integer': { | ||
const typeName = type.sign === 'signed' ? `i${type.width}` : `u${type.width}`; | ||
// Javascript cannot safely represent the full range of Noir's integer types as numbers. | ||
// `Number.MAX_SAFE_INTEGER == 2**53 - 1` so we disallow passing numbers to types which may exceed this. | ||
// 52 has been chosen as the cutoff rather than 53 for safety. | ||
const tsType = type.width <= 52 ? `string | number` : `string`; | ||
|
||
addIfUnique(primitiveTypeMap, { aliasName: typeName, tsType }); | ||
return typeName; | ||
} | ||
case 'boolean': | ||
return `boolean`; | ||
case 'array': | ||
// We can't force the usage of fixed length arrays as this currently throws errors in TS. | ||
// The array would need to be `as const` to support this whereas that's unlikely to happen in user code. | ||
// return `FixedLengthArray<${abiTypeToTs(type.type, primitiveTypeMap)}, ${type.length}>`; | ||
return `${abiTypeToTs(type.type, primitiveTypeMap)}[]`; | ||
case 'string': | ||
// We could enforce that literals are the correct length but not generally. | ||
// This would run into similar problems to above. | ||
return `string`; | ||
case 'struct': | ||
return getLastComponentOfPath(type.path); | ||
default: | ||
throw new Error(`Unknown ABI type ${JSON.stringify(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>, | ||
primitiveTypeMap: Map<string, PrimitiveTypesUsed>, | ||
): 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, primitiveTypeMap); | ||
} | ||
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 += `export type ${structName} = {\n`; | ||
for (const field of type.fields) { | ||
codeGeneratedStruct += ` ${field.name}: ${abiTypeToTs(field.type, primitiveTypeMap)};\n`; | ||
typesEncountered.add(field.type); | ||
} | ||
codeGeneratedStruct += `};`; | ||
output.add(structName); | ||
|
||
// Generate code for the encountered structs in the field above | ||
for (const type of typesEncountered) { | ||
codeGeneratedStructFields += generateStructInterfaces(type, output, primitiveTypeMap); | ||
} | ||
} | ||
|
||
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. | ||
*/ | ||
export function generateTsInterface( | ||
abiObj: Abi, | ||
primitiveTypeMap: Map<string, PrimitiveTypesUsed>, | ||
): [string, { inputs: [string, string][]; returnValue: string | null }] { | ||
let result = ``; | ||
const outputStructs = new Set<string>(); | ||
|
||
// Define structs for composite types | ||
for (const param of abiObj.parameters) { | ||
result += generateStructInterfaces(param.type, outputStructs, primitiveTypeMap); | ||
} | ||
|
||
// Generating Return type, if it exists | ||
if (abiObj.return_type != null) { | ||
result += generateStructInterfaces(abiObj.return_type, outputStructs, primitiveTypeMap); | ||
} | ||
|
||
return [result, getTsFunctionSignature(abiObj, primitiveTypeMap)]; | ||
} | ||
|
||
function getTsFunctionSignature( | ||
abi: Abi, | ||
primitiveTypeMap: Map<string, PrimitiveTypesUsed>, | ||
): { inputs: [string, string][]; returnValue: string | null } { | ||
const inputs: [string, string][] = abi.parameters.map((param) => [ | ||
param.name, | ||
abiTypeToTs(param.type, primitiveTypeMap), | ||
]); | ||
const returnValue = abi.return_type ? abiTypeToTs(abi.return_type, primitiveTypeMap) : null; | ||
return { inputs, returnValue }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,13 @@ | ||
fn main(x: u64, y: pub u64) -> pub u64 { | ||
struct MyStruct { | ||
foo: bool, | ||
bar: [str<5>; 3], | ||
} | ||
|
||
fn main(x: u64, y: pub u64, array: [u8; 5], my_struct: MyStruct, string: str<5>) -> pub u64 { | ||
assert(array.len() == 5); | ||
assert(my_struct.foo); | ||
assert(string == "12345"); | ||
|
||
assert(x < y); | ||
x + y | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
{"hash":13834844072603749544,"backend":"acvm-backend-barretenberg","abi":{"parameters":[{"name":"x","type":{"kind":"integer","sign":"unsigned","width":64},"visibility":"private"},{"name":"y","type":{"kind":"integer","sign":"unsigned","width":64},"visibility":"public"}],"param_witnesses":{"x":[1],"y":[2]},"return_type":{"kind":"integer","sign":"unsigned","width":64},"return_witnesses":[12]},"bytecode":"H4sIAAAAAAAA/+1WUW6DMAx1QksZoGr72jUcAiX8VbvJ0Oj9j7ChJpKbtXw0NpvUWkImUXixn53w3gDgHc6mfh7t/ZGMtR9TU96HeYuHtp36ZjLWfGIzjK7DthsPzjjTue6rcdZOrnX9MA49Dqa1kzl1gz3h2bL7sTDCMhmJbylmTDOT8WEhjXfjH/DcB8u8zwVygWifmL/9lTnWzSWKsxHA3QJf00vlveWvERJIUU4x0eb86aEJppljVox9oO+Py8QTV1Jnw6a85t7vSL8pwvN89j7gd88o8q79Gr2wRt3AeSFz4XvRSyokl5MAtSfgGO2ZCewdsDibLRVrDzIXTMxfqiLIGXPeMdY1gb/Fg8+tznJY50eSGmfB2DNrqciCD+tCRc4X5FNFJmIWnkhu3BL+t4qc8y75aySqIkvGOP9CRWKaGQ0ydUrsgUUVWXlfw4OpyAouVWQN66pITDPDqSJfQaZxuVVkxZhzzVgLTv5uHbDwXhN+vwGywklHPBQAAA=="} | ||
{"noir_version":"0.19.2+87bb3f0d789765f2d65a1e7b7554742994da2680","hash":12941906747567599524,"backend":"acvm-backend-barretenberg","abi":{"parameters":[{"name":"x","type":{"kind":"integer","sign":"unsigned","width":64},"visibility":"private"},{"name":"y","type":{"kind":"integer","sign":"unsigned","width":64},"visibility":"public"},{"name":"array","type":{"kind":"array","length":5,"type":{"kind":"integer","sign":"unsigned","width":8}},"visibility":"private"},{"name":"my_struct","type":{"kind":"struct","path":"MyStruct","fields":[{"name":"foo","type":{"kind":"boolean"}},{"name":"bar","type":{"kind":"array","length":3,"type":{"kind":"string","length":5}}}]},"visibility":"private"},{"name":"string","type":{"kind":"string","length":5},"visibility":"private"}],"param_witnesses":{"array":[{"start":3,"end":8}],"my_struct":[{"start":8,"end":24}],"string":[{"start":24,"end":29}],"x":[{"start":1,"end":2}],"y":[{"start":2,"end":3}]},"return_type":{"kind":"integer","sign":"unsigned","width":64},"return_witnesses":[31]},"bytecode":"H4sIAAAAAAAA/82X206DQBCGF+qh9VDP2gO0eKlXuwVauGt8k7Ys0URTY4h9fTvprm4HJVFmEych8FE6/Ay7zP63jLF7tglnvblqPzXYRdxYb02DdxDvIt5DvK9Y35Op/BC8XoimcS8zb8jHUSQnIylCMeOjdJ7EPIrn40QkIk7ibJSEoUyiZJLO0wlPRRRKkcdpmKvETTqNXNehhepygPgQ8RHiY8RtxCeITxGfIT5HfIH4EvEV4mvEN4g7iLuIe4j7iD32NW502Bg/U6IxY1Nnh0CnzCEyqzq7ZDoXuU2dPTqd0qbOPp3OzKZOj07nAvqNy8rhEmt2GN3cd/+uS+AT3zw6WW6zrr7aD9imh+txoa+BPv/AymPGMY5ddY1bcY3zQ56WcU7/v238XvfhS8Uwb06V01eFpF6A+HQaPxcgAyOnjgZxPWxNqrq5AsJ6VtXvlzo50il8wmceEL7XGvWr/MD953lT9Z55vdiaJ7xeCMp5MmT03x2ds2+8c6gnNBhoPGAYtUmEpgDGCMwQGCAwPdAUwNyAoQETA8YFzAoYFDAlYETAfMAiGRagPXUvj203Kn08ZNtN5k7tPbWfFYV8eS2CYhnMsixYPRWPwfJdvuXPy9UHoDK8FUEPAAA="} |
Oops, something went wrong.