diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml index 995b2accef..e5dcd69d1d 100644 --- a/.github/workflows/wasm.yml +++ b/.github/workflows/wasm.yml @@ -1,4 +1,4 @@ -name: WASM Tests +name: WASM on: pull_request: push: @@ -14,14 +14,14 @@ env: jobs: test-wasm-parser: - name: Test WASM on ${{ matrix.os }} + name: Test on ${{ matrix.os }} runs-on: ${{ matrix.os }} defaults: run: working-directory: wasm strategy: matrix: - os: [macOS-latest, windows-latest, ubuntu-latest] + os: [windows-latest] steps: - name: Checkout uses: actions/checkout@v2 diff --git a/Cargo.lock b/Cargo.lock index afde4b3ff5..0af4ad8d82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1459,6 +1459,7 @@ dependencies = [ "leo-errors", "leo-parser", "serde", + "serde_json", "wasm-bindgen", ] diff --git a/errors/src/common/backtraced.rs b/errors/src/common/backtraced.rs index 4d28bfe1b9..67c0fab0f1 100644 --- a/errors/src/common/backtraced.rs +++ b/errors/src/common/backtraced.rs @@ -84,15 +84,23 @@ impl BacktracedError { code } + + /// Gets a unique error identifier. + pub fn error_code(&self) -> String { + format!( + "E{error_type}{code_identifier:0>3}{exit_code:0>4}", + error_type = self.error_type, + code_identifier = self.code_identifier, + exit_code = self.exit_code, + ) + } } impl fmt::Display for BacktracedError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let error_message = format!( - "Error [E{error_type}{code_identifier:0>3}{exit_code:0>4}]: {message}", - error_type = self.error_type, - code_identifier = self.code_identifier, - exit_code = self.exit_code, + "Error [{error_code}]: {message}", + error_code = self.error_code(), message = self.message, ); diff --git a/errors/src/common/formatted.rs b/errors/src/common/formatted.rs index f4262e0de4..5987f19fe7 100644 --- a/errors/src/common/formatted.rs +++ b/errors/src/common/formatted.rs @@ -70,6 +70,11 @@ impl FormattedError { pub fn exit_code(&self) -> i32 { self.backtrace.exit_code() } + + /// Returns an error identifier. + pub fn error_code(&self) -> String { + self.backtrace.error_code() + } } impl fmt::Display for FormattedError { @@ -96,10 +101,8 @@ impl fmt::Display for FormattedError { let underlined = underline(self.span.col_start, self.span.col_stop); let error_message = format!( - "Error [E{error_type}{code_identifier:0>3}{exit_code:0>4}]: {message}", - error_type = self.backtrace.error_type, - code_identifier = self.backtrace.code_identifier, - exit_code = self.backtrace.exit_code, + "Error [{error_code}]: {message}", + error_code = self.error_code(), message = self.backtrace.message, ); diff --git a/errors/src/common/macros.rs b/errors/src/common/macros.rs index 9dfa00f67c..51ae254070 100644 --- a/errors/src/common/macros.rs +++ b/errors/src/common/macros.rs @@ -42,14 +42,17 @@ macro_rules! create_errors { #[inline(always)] fn exit_code(&self) -> i32 { match self { - Self::FormattedError(formatted) => { - formatted.exit_code() - }, - Self::BacktracedError(backtraced) => { - backtraced.exit_code() - } + Self::FormattedError(formatted) => formatted.exit_code(), + Self::BacktracedError(backtraced) => backtraced.exit_code() } + } + #[inline(always)] + fn error_code(&self) -> String { + match self { + Self::FormattedError(formatted) => formatted.error_code(), + Self::BacktracedError(backtraced) => backtraced.error_code() + } } #[inline(always)] diff --git a/errors/src/common/traits.rs b/errors/src/common/traits.rs index b171cb5bfc..a1bbfc3dbf 100644 --- a/errors/src/common/traits.rs +++ b/errors/src/common/traits.rs @@ -19,6 +19,9 @@ pub trait LeoErrorCode: Sized { /// Returns the error's exit code for the program. fn exit_code(&self) -> i32; + /// Returns the prefixed error identifier. + fn error_code(&self) -> String; + /// Returns the error's exit code mask, as to avoid conflicts. fn exit_code_mask() -> i32; diff --git a/errors/src/lib.rs b/errors/src/lib.rs index 5b7aa7a56d..be8dea5aaa 100644 --- a/errors/src/lib.rs +++ b/errors/src/lib.rs @@ -109,6 +109,24 @@ pub enum LeoError { } impl LeoError { + /// Implement error code for each type of Error. For the unsupported use a default value. + pub fn error_code(&self) -> String { + use LeoError::*; + + match self { + AsgError(error) => error.error_code(), + AstError(error) => error.error_code(), + CliError(error) => error.error_code(), + CompilerError(error) => error.error_code(), + ImportError(error) => error.error_code(), + InputError(_error) => Default::default(), // TODO migrate me, or not cause we want inputs to have 0 deps. + PackageError(error) => error.error_code(), + ParserError(error) => error.error_code(), + SnarkVMError(_error) => Default::default(), // TODO update once snarkvm implments a global top level error similar to LeoError. + StateError(error) => error.error_code(), + } + } + /// Implment exit code for each type of Error, even the ones that don't have one. pub fn exit_code(&self) -> i32 { use LeoError::*; diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml index 2ae13de171..3427f09d3e 100644 --- a/wasm/Cargo.toml +++ b/wasm/Cargo.toml @@ -45,8 +45,12 @@ path = "../ast-passes" version = "1.0" features = [ "derive" ] +[dependencies.serde_json] +version = "1.0" + [dependencies.wasm-bindgen] version = "0.2" +features = [ "serde-serialize" ] # Crate metadata diff --git a/wasm/README.md b/wasm/README.md new file mode 100644 index 0000000000..03ad4c6512 --- /dev/null +++ b/wasm/README.md @@ -0,0 +1,34 @@ +# Leo WASM + + +[![Authors](https://img.shields.io/badge/authors-Aleo-orange.svg)](../AUTHORS) +[![License](https://img.shields.io/badge/License-GPLv3-blue.svg)](./LICENSE.md) + +This directory contains WASM bindings for the Leo compiler. + +## Limitations + +Currently, WASM target of the compiler supports parsing and canonicalization stages. + +## API + +This is a list of the supported methods and their signatures. + +### leo.parse + +Method takes in a Leo program as string and returns JSON string with the resulting AST or throws a LeoError. + +```ts +export interface LeoError { + text: string, // Full error text (including span) + code: string, // Leo error identifier (e.g. "EPAR0370005") + exitCode: number // Exit code for an error (e.g. 370005) +} + +/** + * @param {String} program Leo program text to parse and produce AST + * @return {String} Resulting AST as a JSON string. + * @throws {LeoError} When program contains invalid Leo code. + */ +export function parse(program: string): string; +``` diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index 9b05e00ae2..5cfd73bde6 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -18,12 +18,21 @@ // which is not wasm compatible. All compiler passes (such as TypeInference) use leo_ast::AstPass; +use leo_errors::LeoError; +use serde_json::json; use wasm_bindgen::prelude::*; +#[wasm_bindgen(typescript_custom_section)] +const TS_APPEND_CONTENT: &'static str = r#" +export interface LeoError { text: string, code: string, exitCode: number } +"#; + +/// Publicly accessible method. +/// Parse the code and return an AST as JSON or an error object. asd #[wasm_bindgen(method, catch)] pub fn parse(program: &str) -> Result { - Ok(parse_program(program).map_err(|err| JsValue::from_str(&err.to_string()))?) + Ok(parse_program(program).map_err(error_to_value)?) } /// Parse the program and pass the Canonicalization phase; @@ -35,3 +44,13 @@ fn parse_program(program: &str) -> leo_errors::Result { Ok(ast) } + +/// Make a pretty-print JS object for the thrown error. +fn error_to_value(err: LeoError) -> JsValue { + JsValue::from_serde(&json!({ + "error": err.to_string(), + "code": err.error_code(), + "exitCode": err.exit_code() + })) + .expect("Unable to create an error object from JSON") +} diff --git a/wasm/tests/package-lock.json b/wasm/tests/package-lock.json index 065afe7326..e1561325bd 100644 --- a/wasm/tests/package-lock.json +++ b/wasm/tests/package-lock.json @@ -1,59 +1,8 @@ { "name": "parser-wasm-tests", "version": "0.1.0", - "lockfileVersion": 2, + "lockfileVersion": 1, "requires": true, - "packages": { - "": { - "name": "parser-wasm-tests", - "version": "0.1.0", - "license": "GNU", - "dependencies": { - "@types/js-yaml": "^4.0.3", - "@types/node": "^16.9.4", - "js-yaml": "^4.1.0", - "typescript": "^3.9.10" - } - }, - "node_modules/@types/js-yaml": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.3.tgz", - "integrity": "sha512-5t9BhoORasuF5uCPr+d5/hdB++zRFUTMIZOzbNkr+jZh3yQht4HYbRDyj9fY8n2TZT30iW9huzav73x4NikqWg==" - }, - "node_modules/@types/node": { - "version": "16.9.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.4.tgz", - "integrity": "sha512-KDazLNYAGIuJugdbULwFZULF9qQ13yNWEBFnfVpqlpgAAo6H/qnM9RjBgh0A0kmHf3XxAKLdN5mTIng9iUvVLA==" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/typescript": { - "version": "3.9.10", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", - "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - } - }, "dependencies": { "@types/js-yaml": { "version": "4.0.3", diff --git a/wasm/tests/src/index.ts b/wasm/tests/src/index.ts index a5e125fb36..fe7708e3b0 100644 --- a/wasm/tests/src/index.ts +++ b/wasm/tests/src/index.ts @@ -16,7 +16,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import * as parser from '../../pkg/leo_wasm'; +import * as leo from '../../pkg/leo_wasm'; import * as yaml from 'js-yaml'; // Path to the parser tests folder @@ -100,7 +100,7 @@ function test(filePath: string, outFile: string|null): TestResult[] { // and collect outputs to later compare to saved .out expectations. for (const sample of samples) { try { - outputs.push(JSON.parse(parser.parse(sample))); // Parser outputs JSON + outputs.push(JSON.parse(leo.parse(sample))); // Parser outputs JSON if (expectation === Expectation.Fail) { // If expectation was Fail and it passed mismatches.push({ filePath, @@ -110,6 +110,10 @@ function test(filePath: string, outFile: string|null): TestResult[] { }); } } catch (error) { + + console.log(error.code); + console.log(error.exitCode); + outputs.push(error.toString()); if (expectation === Expectation.Pass) { // If expectation was Pass and it failed mismatches.push({