@noir-lang/noir_codegen 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.d.ts +2 -0
- package/lib/index.js +47 -0
- package/lib/main.d.ts +2 -0
- package/lib/main.js +37 -0
- package/lib/noir_types.d.ts +40 -0
- package/lib/noir_types.js +134 -0
- package/lib/parseArgs.d.ts +6 -0
- package/lib/parseArgs.js +43 -0
- package/lib/utils/glob.d.ts +1 -0
- package/lib/utils/glob.js +5 -0
- package/package.json +3 -3
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { generateTsInterface, codegenStructDefinitions } from './noir_types.js';
|
|
2
|
+
// TODO: reenable this. See `abiTypeToTs` for reasoning.
|
|
3
|
+
// export type FixedLengthArray<T, L extends number> = L extends 0 ? never[]: T[] & { length: L };
|
|
4
|
+
const codegenPrelude = `/* Autogenerated file, do not edit! */
|
|
5
|
+
|
|
6
|
+
/* eslint-disable */
|
|
7
|
+
|
|
8
|
+
import { Noir, InputMap, CompiledCircuit, ForeignCallHandler } from "@noir-lang/noir_js"
|
|
9
|
+
|
|
10
|
+
export { ForeignCallHandler } from "@noir-lang/noir_js"
|
|
11
|
+
`;
|
|
12
|
+
const codegenFunction = (name, compiled_program, function_signature) => {
|
|
13
|
+
const args = function_signature.inputs.map(([name]) => `${name}`).join(', ');
|
|
14
|
+
const args_with_types = function_signature.inputs.map(([name, type]) => `${name}: ${type}`).join(', ');
|
|
15
|
+
return `export const ${name}_circuit: CompiledCircuit = ${JSON.stringify(compiled_program)};
|
|
16
|
+
|
|
17
|
+
export async function ${name}(${args_with_types}, foreignCallHandler?: ForeignCallHandler): Promise<${function_signature.returnValue}> {
|
|
18
|
+
const program = new Noir(${name}_circuit);
|
|
19
|
+
const args: InputMap = { ${args} };
|
|
20
|
+
const { returnValue } = await program.execute(args, foreignCallHandler);
|
|
21
|
+
return returnValue as ${function_signature.returnValue};
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
};
|
|
25
|
+
export const codegen = (programs) => {
|
|
26
|
+
let results = [codegenPrelude];
|
|
27
|
+
const primitiveTypeMap = new Map();
|
|
28
|
+
const structTypeMap = new Map();
|
|
29
|
+
const functions = [];
|
|
30
|
+
for (const [name, program] of programs) {
|
|
31
|
+
const function_sig = generateTsInterface(program.abi, structTypeMap, primitiveTypeMap);
|
|
32
|
+
functions.push(codegenFunction(name, stripUnwantedFields(program), function_sig));
|
|
33
|
+
}
|
|
34
|
+
const structTypeDefinitions = codegenStructDefinitions(structTypeMap, primitiveTypeMap);
|
|
35
|
+
// Add the primitive Noir types that do not have a 1-1 mapping to TypeScript.
|
|
36
|
+
const primitiveTypeAliases = [];
|
|
37
|
+
for (const value of primitiveTypeMap.values()) {
|
|
38
|
+
primitiveTypeAliases.push(`export type ${value.aliasName} = ${value.tsType};`);
|
|
39
|
+
}
|
|
40
|
+
results = results.concat(...primitiveTypeAliases, '', structTypeDefinitions, ...functions);
|
|
41
|
+
return results.join('\n');
|
|
42
|
+
};
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
44
|
+
function stripUnwantedFields(value) {
|
|
45
|
+
const { abi, bytecode } = value;
|
|
46
|
+
return { abi, bytecode };
|
|
47
|
+
}
|
package/lib/main.d.ts
ADDED
package/lib/main.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#! /usr/bin/env node
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { parseArgs } from './parseArgs.js';
|
|
5
|
+
import { glob } from './utils/glob.js';
|
|
6
|
+
import { codegen } from './index.js';
|
|
7
|
+
function main() {
|
|
8
|
+
const cliConfig = parseArgs();
|
|
9
|
+
const cwd = process.cwd();
|
|
10
|
+
const files = getFilesToProcess(cwd, cliConfig.files);
|
|
11
|
+
if (files.length === 0) {
|
|
12
|
+
throw new Error('No files passed.' + '\n' + `\`${cliConfig.files}\` didn't match any input files in ${cwd}`);
|
|
13
|
+
}
|
|
14
|
+
const programs = files.map((file_path) => {
|
|
15
|
+
const program_name = path.parse(file_path).name;
|
|
16
|
+
const file_contents = fs.readFileSync(file_path).toString();
|
|
17
|
+
const { abi, bytecode } = JSON.parse(file_contents);
|
|
18
|
+
return [program_name, { abi, bytecode }];
|
|
19
|
+
});
|
|
20
|
+
const result = codegen(programs);
|
|
21
|
+
const outputDir = path.resolve(cliConfig.outDir ?? './codegen');
|
|
22
|
+
const outputFile = path.join(outputDir, 'index.ts');
|
|
23
|
+
if (!fs.existsSync(outputDir))
|
|
24
|
+
fs.mkdirSync(outputDir);
|
|
25
|
+
fs.writeFileSync(outputFile, result);
|
|
26
|
+
}
|
|
27
|
+
function getFilesToProcess(cwd, filesOrPattern) {
|
|
28
|
+
let res = glob(cwd, filesOrPattern);
|
|
29
|
+
if (res.length === 0) {
|
|
30
|
+
// If there are no files found, but first parameter is surrounded with single quotes, we try again without quotes
|
|
31
|
+
const match = filesOrPattern[0].match(/'([\s\S]*)'/)?.[1];
|
|
32
|
+
if (match)
|
|
33
|
+
res = glob(cwd, [match]);
|
|
34
|
+
}
|
|
35
|
+
return res;
|
|
36
|
+
}
|
|
37
|
+
main();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { AbiType, Abi } from '@noir-lang/noirc_abi';
|
|
2
|
+
/**
|
|
3
|
+
* Keep track off all of the Noir primitive types that were used.
|
|
4
|
+
* Most of these will not have a 1-1 definition in TypeScript,
|
|
5
|
+
* so we will need to generate type aliases for them.
|
|
6
|
+
*
|
|
7
|
+
* We want to generate type aliases
|
|
8
|
+
* for specific types that are used in the ABI.
|
|
9
|
+
*
|
|
10
|
+
* For example:
|
|
11
|
+
* - If `Field` is used we want to alias that
|
|
12
|
+
* with `number`.
|
|
13
|
+
* - If `u32` is used we want to alias that with `number` too.
|
|
14
|
+
*/
|
|
15
|
+
export type PrimitiveTypesUsed = {
|
|
16
|
+
/**
|
|
17
|
+
* The name of the type alias that we will generate.
|
|
18
|
+
*/
|
|
19
|
+
aliasName: string;
|
|
20
|
+
/**
|
|
21
|
+
* The TypeScript type that we will alias to.
|
|
22
|
+
*/
|
|
23
|
+
tsType: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Generates a TypeScript interface for the ABI.
|
|
27
|
+
* @param abiObj - The ABI to generate the interface for.
|
|
28
|
+
* @returns The TypeScript code to define the interface.
|
|
29
|
+
*/
|
|
30
|
+
export declare function generateTsInterface(abiObj: Abi, structsEncountered: Map<string, {
|
|
31
|
+
name: string;
|
|
32
|
+
type: AbiType;
|
|
33
|
+
}[]>, primitiveTypeMap: Map<string, PrimitiveTypesUsed>): {
|
|
34
|
+
inputs: [string, string][];
|
|
35
|
+
returnValue: string | null;
|
|
36
|
+
};
|
|
37
|
+
export declare function codegenStructDefinitions(structsEncountered: Map<string, {
|
|
38
|
+
name: string;
|
|
39
|
+
type: AbiType;
|
|
40
|
+
}[]>, primitiveTypeMap: Map<string, PrimitiveTypesUsed>): string;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typescript does not allow us to check for equality of non-primitive types
|
|
3
|
+
* easily, so we create a addIfUnique function that will only add an item
|
|
4
|
+
* to the map if it is not already there by using JSON.stringify.
|
|
5
|
+
* @param item - The item to add to the map.
|
|
6
|
+
*/
|
|
7
|
+
function addIfUnique(primitiveTypeMap, item) {
|
|
8
|
+
const key = JSON.stringify(item);
|
|
9
|
+
if (!primitiveTypeMap.has(key)) {
|
|
10
|
+
primitiveTypeMap.set(key, item);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Converts an ABI type to a TypeScript type.
|
|
15
|
+
* @param type - The ABI type to convert.
|
|
16
|
+
* @returns The typescript code to define the type.
|
|
17
|
+
*/
|
|
18
|
+
function abiTypeToTs(type, primitiveTypeMap) {
|
|
19
|
+
switch (type.kind) {
|
|
20
|
+
case 'field':
|
|
21
|
+
addIfUnique(primitiveTypeMap, { aliasName: 'Field', tsType: 'string' });
|
|
22
|
+
return 'Field';
|
|
23
|
+
case 'integer': {
|
|
24
|
+
const typeName = type.sign === 'signed' ? `i${type.width}` : `u${type.width}`;
|
|
25
|
+
// Javascript cannot safely represent the full range of Noir's integer types as numbers.
|
|
26
|
+
// `Number.MAX_SAFE_INTEGER == 2**53 - 1` so we disallow passing numbers to types which may exceed this.
|
|
27
|
+
// 52 has been chosen as the cutoff rather than 53 for safety.
|
|
28
|
+
const tsType = type.width <= 52 ? `string | number` : `string`;
|
|
29
|
+
addIfUnique(primitiveTypeMap, { aliasName: typeName, tsType });
|
|
30
|
+
return typeName;
|
|
31
|
+
}
|
|
32
|
+
case 'boolean':
|
|
33
|
+
return `boolean`;
|
|
34
|
+
case 'array':
|
|
35
|
+
// We can't force the usage of fixed length arrays as this currently throws errors in TS.
|
|
36
|
+
// The array would need to be `as const` to support this whereas that's unlikely to happen in user code.
|
|
37
|
+
// return `FixedLengthArray<${abiTypeToTs(type.type, primitiveTypeMap)}, ${type.length}>`;
|
|
38
|
+
return `${abiTypeToTs(type.type, primitiveTypeMap)}[]`;
|
|
39
|
+
case 'string':
|
|
40
|
+
// We could enforce that literals are the correct length but not generally.
|
|
41
|
+
// This would run into similar problems to above.
|
|
42
|
+
return `string`;
|
|
43
|
+
case 'struct':
|
|
44
|
+
return getLastComponentOfPath(type.path);
|
|
45
|
+
case 'tuple': {
|
|
46
|
+
const field_types = type.fields.map((field) => abiTypeToTs(field, primitiveTypeMap));
|
|
47
|
+
return `[${field_types.join(', ')}]`;
|
|
48
|
+
}
|
|
49
|
+
default:
|
|
50
|
+
throw new Error(`Unknown ABI type ${JSON.stringify(type)}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Returns the last component of a path, e.g. "foo::bar::baz" -\> "baz"
|
|
55
|
+
* Note: that if we have a path such as "Baz", we will return "Baz".
|
|
56
|
+
*
|
|
57
|
+
* Since these paths corresponds to structs, we can assume that we
|
|
58
|
+
* cannot have "foo::bar::".
|
|
59
|
+
*
|
|
60
|
+
* We also make the assumption that since these paths are coming from
|
|
61
|
+
* Noir, then we will not have two paths that look like this:
|
|
62
|
+
* - foo::bar::Baz
|
|
63
|
+
* - cat::dog::Baz
|
|
64
|
+
* ie the last component of the path (struct name) is enough to uniquely identify
|
|
65
|
+
* the whole path.
|
|
66
|
+
*
|
|
67
|
+
* TODO: We should double check this assumption when we use type aliases,
|
|
68
|
+
* I expect that `foo::bar::Baz as Dog` would effectively give `foo::bar::Dog`
|
|
69
|
+
* @param str - The path to get the last component of.
|
|
70
|
+
* @returns The last component of the path.
|
|
71
|
+
*/
|
|
72
|
+
function getLastComponentOfPath(str) {
|
|
73
|
+
const parts = str.split('::');
|
|
74
|
+
const lastPart = parts[parts.length - 1];
|
|
75
|
+
return lastPart;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Generates TypeScript interfaces for the structs used in the ABI.
|
|
79
|
+
* @param type - The ABI type to generate the interface for.
|
|
80
|
+
* @param output - The set of structs that we have already generated bindings for.
|
|
81
|
+
* @returns The TypeScript code to define the struct.
|
|
82
|
+
*/
|
|
83
|
+
function generateStructInterfaces(type, structsEncountered, primitiveTypeMap) {
|
|
84
|
+
// Edge case to handle the array of structs case.
|
|
85
|
+
if (type.kind === 'array' &&
|
|
86
|
+
type.type.kind === 'struct' &&
|
|
87
|
+
!structsEncountered.has(getLastComponentOfPath(type.type.path))) {
|
|
88
|
+
generateStructInterfaces(type.type, structsEncountered, primitiveTypeMap);
|
|
89
|
+
}
|
|
90
|
+
if (type.kind !== 'struct')
|
|
91
|
+
return;
|
|
92
|
+
const structName = getLastComponentOfPath(type.path);
|
|
93
|
+
if (!structsEncountered.has(structName)) {
|
|
94
|
+
for (const field of type.fields) {
|
|
95
|
+
generateStructInterfaces(field.type, structsEncountered, primitiveTypeMap);
|
|
96
|
+
}
|
|
97
|
+
structsEncountered.set(structName, type.fields);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Generates a TypeScript interface for the ABI.
|
|
102
|
+
* @param abiObj - The ABI to generate the interface for.
|
|
103
|
+
* @returns The TypeScript code to define the interface.
|
|
104
|
+
*/
|
|
105
|
+
export function generateTsInterface(abiObj, structsEncountered, primitiveTypeMap) {
|
|
106
|
+
// Define structs for composite types
|
|
107
|
+
for (const param of abiObj.parameters) {
|
|
108
|
+
generateStructInterfaces(param.type, structsEncountered, primitiveTypeMap);
|
|
109
|
+
}
|
|
110
|
+
// Generating Return type, if it exists
|
|
111
|
+
if (abiObj.return_type != null) {
|
|
112
|
+
generateStructInterfaces(abiObj.return_type.abi_type, structsEncountered, primitiveTypeMap);
|
|
113
|
+
}
|
|
114
|
+
return getTsFunctionSignature(abiObj, primitiveTypeMap);
|
|
115
|
+
}
|
|
116
|
+
export function codegenStructDefinitions(structsEncountered, primitiveTypeMap) {
|
|
117
|
+
let codeGeneratedStruct = '';
|
|
118
|
+
for (const [structName, structFields] of structsEncountered) {
|
|
119
|
+
codeGeneratedStruct += `export type ${structName} = {\n`;
|
|
120
|
+
for (const field of structFields) {
|
|
121
|
+
codeGeneratedStruct += ` ${field.name}: ${abiTypeToTs(field.type, primitiveTypeMap)};\n`;
|
|
122
|
+
}
|
|
123
|
+
codeGeneratedStruct += `};\n\n`;
|
|
124
|
+
}
|
|
125
|
+
return codeGeneratedStruct;
|
|
126
|
+
}
|
|
127
|
+
function getTsFunctionSignature(abi, primitiveTypeMap) {
|
|
128
|
+
const inputs = abi.parameters.map((param) => [
|
|
129
|
+
param.name,
|
|
130
|
+
abiTypeToTs(param.type, primitiveTypeMap),
|
|
131
|
+
]);
|
|
132
|
+
const returnValue = abi.return_type ? abiTypeToTs(abi.return_type.abi_type, primitiveTypeMap) : null;
|
|
133
|
+
return { inputs, returnValue };
|
|
134
|
+
}
|
package/lib/parseArgs.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { parse as commandLineArgs } from 'ts-command-line-args';
|
|
2
|
+
const DEFAULT_GLOB_PATTERN = './target/**/*.json';
|
|
3
|
+
export function parseArgs() {
|
|
4
|
+
const rawOptions = commandLineArgs({
|
|
5
|
+
glob: {
|
|
6
|
+
type: String,
|
|
7
|
+
defaultOption: true,
|
|
8
|
+
multiple: true,
|
|
9
|
+
defaultValue: [DEFAULT_GLOB_PATTERN],
|
|
10
|
+
description: 'Pattern that will be used to find program artifacts. Remember about adding quotes: noir-codegen "**/*.json".',
|
|
11
|
+
},
|
|
12
|
+
'out-dir': { type: String, optional: true, description: 'Output directory for generated files.' },
|
|
13
|
+
'input-dir': {
|
|
14
|
+
type: String,
|
|
15
|
+
optional: true,
|
|
16
|
+
description: 'Directory containing program artifact files. Inferred as lowest common path of all files if not specified.',
|
|
17
|
+
},
|
|
18
|
+
help: { type: Boolean, defaultValue: false, alias: 'h', description: 'Prints this message.' },
|
|
19
|
+
}, {
|
|
20
|
+
helpArg: 'help',
|
|
21
|
+
headerContentSections: [
|
|
22
|
+
{
|
|
23
|
+
content: `\
|
|
24
|
+
noir-codegen generates TypeScript wrappers for Noir programs to simplify replicating your Noir logic in JS.`,
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
footerContentSections: [
|
|
28
|
+
{
|
|
29
|
+
header: 'Example Usage',
|
|
30
|
+
content: `\
|
|
31
|
+
noir-codegen --out-dir app/noir_programs './target/*.json'
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
You can read more about noir-codegen at {underline https://github.com/noir-lang/noir}.`,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
files: rawOptions.glob,
|
|
40
|
+
outDir: rawOptions['out-dir'],
|
|
41
|
+
inputDir: rawOptions['input-dir'],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function glob(cwd: string, patternsOrFiles: string[]): string[];
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"contributors": [
|
|
4
4
|
"The Noir Team <team@noir-lang.org>"
|
|
5
5
|
],
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.25.0",
|
|
7
7
|
"packageManager": "yarn@3.5.1",
|
|
8
8
|
"license": "(MIT OR Apache-2.0)",
|
|
9
9
|
"type": "module",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"url": "https://github.com/noir-lang/noir/issues"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@noir-lang/types": "0.
|
|
20
|
+
"@noir-lang/types": "0.25.0",
|
|
21
21
|
"glob": "^10.3.10",
|
|
22
22
|
"ts-command-line-args": "^2.5.1"
|
|
23
23
|
},
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"clean": "rm -rf ./lib"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@noir-lang/noir_js": "0.
|
|
49
|
+
"@noir-lang/noir_js": "0.25.0",
|
|
50
50
|
"@types/chai": "^4",
|
|
51
51
|
"@types/mocha": "^10.0.1",
|
|
52
52
|
"@types/node": "^20.6.2",
|