diff --git a/package.json b/package.json index 47c77ffa945..0e86b100b7c 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "tooling/noir_js_types", "tooling/noirc_abi_wasm", "tooling/noir_js", + "tooling/noir_codegen", "tooling/noir_js_backend_barretenberg", "acvm-repo/acvm_js", "release-tests", diff --git a/release-please-config.json b/release-please-config.json index ddf6ff7b3a9..afc0bfd420d 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -24,6 +24,11 @@ "path": "compiler/wasm/package.json", "jsonpath": "$.version" }, + { + "type": "json", + "path": "tooling/noir_codegen/package.json", + "jsonpath": "$.version" + }, { "type": "json", "path": "tooling/noir_js/package.json", @@ -45,8 +50,8 @@ "jsonpath": "$.version" } ] - }, - "acvm-repo" : { + }, + "acvm-repo": { "release-type": "simple", "package-name": "acvm", "component": "acvm", diff --git a/tooling/noir_codegen/.eslintignore b/tooling/noir_codegen/.eslintignore new file mode 100644 index 00000000000..491fc35975b --- /dev/null +++ b/tooling/noir_codegen/.eslintignore @@ -0,0 +1,2 @@ +node_modules +lib diff --git a/tooling/noir_codegen/.eslintrc.cjs b/tooling/noir_codegen/.eslintrc.cjs new file mode 100644 index 00000000000..33335c2a877 --- /dev/null +++ b/tooling/noir_codegen/.eslintrc.cjs @@ -0,0 +1,3 @@ +module.exports = { + extends: ["../../.eslintrc.js"], +}; diff --git a/tooling/noir_codegen/.gitignore b/tooling/noir_codegen/.gitignore new file mode 100644 index 00000000000..721d05448d6 --- /dev/null +++ b/tooling/noir_codegen/.gitignore @@ -0,0 +1,4 @@ +crs +lib + +!test/*/target diff --git a/tooling/noir_codegen/.mocharc.json b/tooling/noir_codegen/.mocharc.json new file mode 100644 index 00000000000..82855d2ddf4 --- /dev/null +++ b/tooling/noir_codegen/.mocharc.json @@ -0,0 +1,11 @@ +{ + "require": "ts-node/register", + "loader": "ts-node/esm", + "extensions": [ + "ts", + "cjs" + ], + "spec": [ + "test/**/*.test.ts*" + ] +} \ No newline at end of file diff --git a/tooling/noir_codegen/README.md b/tooling/noir_codegen/README.md new file mode 100644 index 00000000000..c24c12ff987 --- /dev/null +++ b/tooling/noir_codegen/README.md @@ -0,0 +1,4 @@ + +## Acknowledgements + +`noir-codegen` repurposes the CLI code from https://github.com/dethcrypto/TypeChain, used under the MIT license. \ No newline at end of file diff --git a/tooling/noir_codegen/package.json b/tooling/noir_codegen/package.json new file mode 100644 index 00000000000..9930ac2d603 --- /dev/null +++ b/tooling/noir_codegen/package.json @@ -0,0 +1,53 @@ +{ + "name": "@noir-lang/noir_codegen", + "collaborators": [ + "The Noir Team " + ], + "version": "0.18.0", + "packageManager": "yarn@3.5.1", + "license": "(MIT OR Apache-2.0)", + "type": "module", + "dependencies": { + "@noir-lang/types": "workspace:*", + "glob": "^10.3.10", + "lodash": "^4.17.21", + "ts-command-line-args": "^2.5.1" + }, + "files": [ + "lib", + "package.json" + ], + "source": "src/index.ts", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "bin": { + "noir-codegen": "lib/main.js" + }, + "scripts": { + "dev": "tsc-multi --watch", + "build": "tsc", + "test": "ts-node --esm src/main.ts ./test/assert_lt/target/** --out-dir ./test/codegen && yarn test:node && rm -rf ./test/codegen", + "test:node": "mocha --timeout 25000 --exit --config ./.mocharc.json", + "prettier": "prettier 'src/**/*.ts'", + "prettier:fix": "prettier --write 'src/**/*.ts' 'test/**/*.ts'", + "lint": "NODE_NO_WARNINGS=1 eslint . --ext .ts --ignore-path ./.eslintignore --max-warnings 0", + "nightly:version": "jq --arg new_version \"-$(git rev-parse --short HEAD)$1\" '.version = .version + $new_version' package.json > package-tmp.json && mv package-tmp.json package.json", + "publish": "echo 📡 publishing `$npm_package_name` && yarn npm publish", + "clean": "rm -rf ./lib" + }, + "devDependencies": { + "@noir-lang/noir_js": "workspace:*", + "@types/chai": "^4", + "@types/lodash": "^4", + "@types/mocha": "^10.0.1", + "@types/node": "^20.6.2", + "@types/prettier": "^3", + "chai": "^4.3.8", + "eslint": "^8.50.0", + "eslint-plugin-prettier": "^5.0.0", + "mocha": "^10.2.0", + "prettier": "3.0.3", + "ts-node": "^10.9.1", + "typescript": "^5.2.2" + } +} diff --git a/tooling/noir_codegen/src/index.ts b/tooling/noir_codegen/src/index.ts new file mode 100644 index 00000000000..1f8d2d183d4 --- /dev/null +++ b/tooling/noir_codegen/src/index.ts @@ -0,0 +1,28 @@ +import { CompiledCircuit } from '@noir-lang/types'; + +const codegenImports = `import { InputMap, InputValue } from "@noir-lang/noirc_abi" +import { Noir } from "@noir-lang/noir_js"`; + +const codegenFunction = ( + name: string, + compiled_program: CompiledCircuit, +) => `export async function ${name}(args: InputMap): Promise { + const program = new Noir(${JSON.stringify(compiled_program)}); + const { returnValue } = await program.execute(args); + return returnValue; +}`; + +export const codegen = (programs: [string, CompiledCircuit][]): string => { + const results = [codegenImports]; + for (const [name, program] of programs) { + results.push(codegenFunction(name, stripUnwantedFields(program))); + } + + return results.join('\n\n'); +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function stripUnwantedFields(value: any): CompiledCircuit { + const { abi, bytecode } = value; + return { abi, bytecode }; +} diff --git a/tooling/noir_codegen/src/main.ts b/tooling/noir_codegen/src/main.ts new file mode 100644 index 00000000000..591e7420dba --- /dev/null +++ b/tooling/noir_codegen/src/main.ts @@ -0,0 +1,47 @@ +#! /usr/bin/env node + +import { CompiledCircuit } from '@noir-lang/types'; +import fs from 'fs'; +import path from 'path'; +import { parseArgs } from './parseArgs.js'; +import { glob } from './utils/glob.js'; +import { codegen } from './index.js'; + +function main() { + const cliConfig = parseArgs(); + const cwd = process.cwd(); + + const files = getFilesToProcess(cwd, cliConfig.files); + if (files.length === 0) { + throw new Error('No files passed.' + '\n' + `\`${cliConfig.files}\` didn't match any input files in ${cwd}`); + } + + const programs = files.map((file_path): [string, CompiledCircuit] => { + const program_name = path.parse(file_path).name; + const file_contents = fs.readFileSync(file_path).toString(); + const { abi, bytecode } = JSON.parse(file_contents); + + return [program_name, { abi, bytecode }]; + }); + + const result = codegen(programs); + + const outputDir = path.resolve(cliConfig.outDir ?? './codegen'); + const outputFile = path.join(outputDir, 'index.ts'); + if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir); + fs.writeFileSync(outputFile, result); +} + +function getFilesToProcess(cwd: string, filesOrPattern: string[]) { + let res = glob(cwd, filesOrPattern); + + if (res.length === 0) { + // If there are no files found, but first parameter is surrounded with single quotes, we try again without quotes + const match = filesOrPattern[0].match(/'([\s\S]*)'/)?.[1]; + if (match) res = glob(cwd, [match]); + } + + return res; +} + +main(); diff --git a/tooling/noir_codegen/src/parseArgs.ts b/tooling/noir_codegen/src/parseArgs.ts new file mode 100644 index 00000000000..58468c1b8f8 --- /dev/null +++ b/tooling/noir_codegen/src/parseArgs.ts @@ -0,0 +1,64 @@ +import { parse as commandLineArgs } from 'ts-command-line-args'; + +const DEFAULT_GLOB_PATTERN = './target/**/*.json'; + +export interface ParsedArgs { + files: string[]; + outDir?: string | undefined; + inputDir?: string | undefined; +} + +export function parseArgs(): ParsedArgs { + const rawOptions = commandLineArgs( + { + glob: { + type: String, + defaultOption: true, + multiple: true, + defaultValue: [DEFAULT_GLOB_PATTERN], + description: + 'Pattern that will be used to find program artifacts. Remember about adding quotes: noir-codegen "**/*.json".', + }, + 'out-dir': { type: String, optional: true, description: 'Output directory for generated files.' }, + 'input-dir': { + type: String, + optional: true, + description: + 'Directory containing program artifact files. Inferred as lowest common path of all files if not specified.', + }, + help: { type: Boolean, defaultValue: false, alias: 'h', description: 'Prints this message.' }, + }, + { + helpArg: 'help', + headerContentSections: [ + { + content: `\ + noir-codegen generates TypeScript wrappers for Noir programs to simplify replicating your Noir logic in JS.`, + }, + ], + footerContentSections: [ + { + header: 'Example Usage', + content: `\ + noir-codegen --out-dir app/noir_programs './target/*.json' + + + You can read more about noir-codegen at {underline https://github.com/noir-lang/noir}.`, + }, + ], + }, + ); + + return { + files: rawOptions.glob, + outDir: rawOptions['out-dir'], + inputDir: rawOptions['input-dir'], + }; +} + +interface CommandLineArgs { + glob: string[]; + 'out-dir'?: string; + 'input-dir'?: string; + help: boolean; +} diff --git a/tooling/noir_codegen/src/utils/glob.ts b/tooling/noir_codegen/src/utils/glob.ts new file mode 100644 index 00000000000..15deaf72e44 --- /dev/null +++ b/tooling/noir_codegen/src/utils/glob.ts @@ -0,0 +1,9 @@ +import { sync as globSync } from 'glob'; +import _ from 'lodash'; +const { flatten, uniq } = _; + +export function glob(cwd: string, patternsOrFiles: string[]): string[] { + const matches = patternsOrFiles.map((p) => globSync(p, { ignore: 'node_modules/**', absolute: true, cwd })); + + return uniq(flatten(matches)); +} diff --git a/tooling/noir_codegen/test/assert_lt/Nargo.toml b/tooling/noir_codegen/test/assert_lt/Nargo.toml new file mode 100644 index 00000000000..f32ec18cae7 --- /dev/null +++ b/tooling/noir_codegen/test/assert_lt/Nargo.toml @@ -0,0 +1,5 @@ +[package] +name = "assert_lt" +type = "bin" +authors = [""] +[dependencies] diff --git a/tooling/noir_codegen/test/assert_lt/src/main.nr b/tooling/noir_codegen/test/assert_lt/src/main.nr new file mode 100644 index 00000000000..67b8c84bdcd --- /dev/null +++ b/tooling/noir_codegen/test/assert_lt/src/main.nr @@ -0,0 +1,4 @@ +fn main(x : u64, y : pub u64) -> pub u64 { + assert(x < y); + x + y +} diff --git a/tooling/noir_codegen/test/assert_lt/target/assert_lt.json b/tooling/noir_codegen/test/assert_lt/target/assert_lt.json new file mode 100644 index 00000000000..3b2b1b2c5a1 --- /dev/null +++ b/tooling/noir_codegen/test/assert_lt/target/assert_lt.json @@ -0,0 +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=="} \ No newline at end of file diff --git a/tooling/noir_codegen/test/index.test.ts b/tooling/noir_codegen/test/index.test.ts new file mode 100644 index 00000000000..702ba1f9cbb --- /dev/null +++ b/tooling/noir_codegen/test/index.test.ts @@ -0,0 +1,11 @@ +import { expect } from 'chai'; +import { assert_lt } from './codegen/index.js'; + +it('codegens a callable function', async () => { + const result = await assert_lt({ + x: '2', + y: '3', + }); + + expect(result).to.be.eq('0x05'); +}); diff --git a/tooling/noir_codegen/tsconfig.json b/tooling/noir_codegen/tsconfig.json new file mode 100644 index 00000000000..30dd2a7ee5f --- /dev/null +++ b/tooling/noir_codegen/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "esnext", + "declaration": true, + "emitDeclarationOnly": false, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./lib", + "esModuleInterop": true, + "resolveJsonModule": true, + "strict": true, + "noImplicitAny": false, + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/yarn.lock b/yarn.lock index 2efc4eabfec..f423ba8b384 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3446,6 +3446,32 @@ __metadata: languageName: unknown linkType: soft +"@noir-lang/noir_codegen@workspace:tooling/noir_codegen": + version: 0.0.0-use.local + resolution: "@noir-lang/noir_codegen@workspace:tooling/noir_codegen" + dependencies: + "@noir-lang/noir_js": "workspace:*" + "@noir-lang/types": "workspace:*" + "@types/chai": ^4 + "@types/lodash": ^4 + "@types/mocha": ^10.0.1 + "@types/node": ^20.6.2 + "@types/prettier": ^3 + chai: ^4.3.8 + eslint: ^8.50.0 + eslint-plugin-prettier: ^5.0.0 + glob: ^10.3.10 + lodash: ^4.17.21 + mocha: ^10.2.0 + prettier: 3.0.3 + ts-command-line-args: ^2.5.1 + ts-node: ^10.9.1 + typescript: ^5.2.2 + bin: + noir-codegen: lib/main.js + languageName: unknown + linkType: soft + "@noir-lang/noir_js@workspace:*, @noir-lang/noir_js@workspace:tooling/noir_js": version: 0.0.0-use.local resolution: "@noir-lang/noir_js@workspace:tooling/noir_js" @@ -4640,6 +4666,13 @@ __metadata: languageName: node linkType: hard +"@types/lodash@npm:^4": + version: 4.14.200 + resolution: "@types/lodash@npm:4.14.200" + checksum: 6471f8bb5da692a6ecf03a8da4935bfbc341e67ee9bcb4f5730bfacff0c367232548f0a01e8ac5ea18c6fe78fb085d502494e33ccb47a7ee87cbdee03b47d00d + languageName: node + linkType: hard + "@types/lru-cache@npm:^5.1.0": version: 5.1.1 resolution: "@types/lru-cache@npm:5.1.1" @@ -5971,6 +6004,13 @@ __metadata: languageName: node linkType: hard +"array-back@npm:^4.0.1, array-back@npm:^4.0.2": + version: 4.0.2 + resolution: "array-back@npm:4.0.2" + checksum: f30603270771eeb54e5aad5f54604c62b3577a18b6db212a7272b2b6c32049121b49431f656654790ed1469411e45f387e7627c0de8fd0515995cc40df9b9294 + languageName: node + linkType: hard + "array-back@npm:^6.2.2": version: 6.2.2 resolution: "array-back@npm:6.2.2" @@ -7271,6 +7311,18 @@ __metadata: languageName: node linkType: hard +"command-line-usage@npm:^6.1.0": + version: 6.1.3 + resolution: "command-line-usage@npm:6.1.3" + dependencies: + array-back: ^4.0.2 + chalk: ^2.4.2 + table-layout: ^1.0.2 + typical: ^5.2.0 + checksum: 8261d4e5536eb0bcddee0ec5e89c05bb2abd18e5760785c8078ede5020bc1c612cbe28eb6586f5ed4a3660689748e5aaad4a72f21566f4ef39393694e2fa1a0b + languageName: node + linkType: hard + "command-line-usage@npm:^7.0.0, command-line-usage@npm:^7.0.1": version: 7.0.1 resolution: "command-line-usage@npm:7.0.1" @@ -7966,7 +8018,7 @@ __metadata: languageName: node linkType: hard -"deep-extend@npm:^0.6.0": +"deep-extend@npm:^0.6.0, deep-extend@npm:~0.6.0": version: 0.6.0 resolution: "deep-extend@npm:0.6.0" checksum: 7be7e5a8d468d6b10e6a67c3de828f55001b6eb515d014f7aeb9066ce36bd5717161eb47d6a0f7bed8a9083935b465bc163ee2581c8b128d29bf61092fdf57a7 @@ -9818,6 +9870,21 @@ __metadata: languageName: node linkType: hard +"glob@npm:^10.3.10": + version: 10.3.10 + resolution: "glob@npm:10.3.10" + dependencies: + foreground-child: ^3.1.0 + jackspeak: ^2.3.5 + minimatch: ^9.0.1 + minipass: ^5.0.0 || ^6.0.2 || ^7.0.0 + path-scurry: ^1.10.1 + bin: + glob: dist/esm/bin.mjs + checksum: 4f2fe2511e157b5a3f525a54092169a5f92405f24d2aed3142f4411df328baca13059f4182f1db1bf933e2c69c0bd89e57ae87edd8950cba8c7ccbe84f721cf3 + languageName: node + linkType: hard + "glob@npm:^7.0.0, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6": version: 7.2.3 resolution: "glob@npm:7.2.3" @@ -11200,6 +11267,19 @@ __metadata: languageName: node linkType: hard +"jackspeak@npm:^2.3.5": + version: 2.3.6 + resolution: "jackspeak@npm:2.3.6" + dependencies: + "@isaacs/cliui": ^8.0.2 + "@pkgjs/parseargs": ^0.11.0 + dependenciesMeta: + "@pkgjs/parseargs": + optional: true + checksum: 57d43ad11eadc98cdfe7496612f6bbb5255ea69fe51ea431162db302c2a11011642f50cfad57288bd0aea78384a0612b16e131944ad8ecd09d619041c8531b54 + languageName: node + linkType: hard + "jest-util@npm:^29.7.0": version: 29.7.0 resolution: "jest-util@npm:29.7.0" @@ -14518,6 +14598,13 @@ __metadata: languageName: node linkType: hard +"reduce-flatten@npm:^2.0.0": + version: 2.0.0 + resolution: "reduce-flatten@npm:2.0.0" + checksum: 64393ef99a16b20692acfd60982d7fdbd7ff8d9f8f185c6023466444c6dd2abb929d67717a83cec7f7f8fb5f46a25d515b3b2bf2238fdbfcdbfd01d2a9e73cb8 + languageName: node + linkType: hard + "regenerate-unicode-properties@npm:^10.1.0": version: 10.1.1 resolution: "regenerate-unicode-properties@npm:10.1.1" @@ -15714,6 +15801,13 @@ __metadata: languageName: node linkType: hard +"string-format@npm:^2.0.0": + version: 2.0.0 + resolution: "string-format@npm:2.0.0" + checksum: dada2ef95f6d36c66562c673d95315f80457fa7dce2f3609a2e75d1190b98c88319028cf0a5b6c043d01c18d581b2641579f79480584ba030d6ac6fceb30bc55 + languageName: node + linkType: hard + "string-to-stream@npm:^3.0.1": version: 3.0.1 resolution: "string-to-stream@npm:3.0.1" @@ -15944,6 +16038,18 @@ __metadata: languageName: node linkType: hard +"table-layout@npm:^1.0.2": + version: 1.0.2 + resolution: "table-layout@npm:1.0.2" + dependencies: + array-back: ^4.0.1 + deep-extend: ~0.6.0 + typical: ^5.2.0 + wordwrapjs: ^4.0.0 + checksum: 8f41b5671f101a5195747ec1727b1d35ea2cd5bf85addda11cc2f4b36892db9696ce3c2c7334b5b8a122505b34d19135fede50e25678df71b0439e0704fd953f + languageName: node + linkType: hard + "table-layout@npm:^3.0.0": version: 3.0.2 resolution: "table-layout@npm:3.0.2" @@ -16205,6 +16311,20 @@ __metadata: languageName: node linkType: hard +"ts-command-line-args@npm:^2.5.1": + version: 2.5.1 + resolution: "ts-command-line-args@npm:2.5.1" + dependencies: + chalk: ^4.1.0 + command-line-args: ^5.1.1 + command-line-usage: ^6.1.0 + string-format: ^2.0.0 + bin: + write-markdown: dist/write-markdown.js + checksum: 7c0a7582e94f1d2160e3dd379851ec4f1758bc673ccd71bae07f839f83051b6b83e0ae14325c2d04ea728e5bde7b7eacfd2ab060b8fd4b8ab29e0bbf77f6c51e + languageName: node + linkType: hard + "ts-node@npm:^10.9.1": version: 10.9.1 resolution: "ts-node@npm:10.9.1" @@ -16458,6 +16578,13 @@ __metadata: languageName: node linkType: hard +"typical@npm:^5.2.0": + version: 5.2.0 + resolution: "typical@npm:5.2.0" + checksum: ccaeb151a9a556291b495571ca44c4660f736fb49c29314bbf773c90fad92e9485d3cc2b074c933866c1595abbbc962f2b8bfc6e0f52a8c6b0cdd205442036ac + languageName: node + linkType: hard + "typical@npm:^7.1.1": version: 7.1.1 resolution: "typical@npm:7.1.1" @@ -17247,6 +17374,16 @@ __metadata: languageName: node linkType: hard +"wordwrapjs@npm:^4.0.0": + version: 4.0.1 + resolution: "wordwrapjs@npm:4.0.1" + dependencies: + reduce-flatten: ^2.0.0 + typical: ^5.2.0 + checksum: 3d927f3c95d0ad990968da54c0ad8cde2801d8e91006cd7474c26e6b742cc8557250ce495c9732b2f9db1f903601cb74ec282e0f122ee0d02d7abe81e150eea8 + languageName: node + linkType: hard + "wordwrapjs@npm:^5.1.0": version: 5.1.0 resolution: "wordwrapjs@npm:5.1.0"