From 5d31159b4e97f12ecd79fccd9c52b67b49eda048 Mon Sep 17 00:00:00 2001 From: Emmanuel Bastien Date: Wed, 1 Nov 2023 16:34:28 +0100 Subject: [PATCH] feat: minimal wasm compiler interface --- Cargo.toml | 9 ++- Makefile | 10 +++ README.md | 7 ++ oal-client/src/cli/mod.rs | 49 ++----------- oal-model/src/span.rs | 45 ++++++++++++ oal-wasm/.gitignore | 1 + oal-wasm/Cargo.toml | 32 +++++++++ oal-wasm/src/lib.rs | 147 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 256 insertions(+), 44 deletions(-) create mode 100644 oal-wasm/.gitignore create mode 100644 oal-wasm/Cargo.toml create mode 100644 oal-wasm/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 5d06b1f..c515554 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,10 @@ [workspace] resolver = "2" -members = [ "oal-model", "oal-client", "oal-openapi", "oal-syntax", "oal-compiler" ] +members = [ + "oal-model", + "oal-client", + "oal-openapi", + "oal-syntax", + "oal-compiler", + "oal-wasm" +] diff --git a/Makefile b/Makefile index 0be0d06..7669b1f 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ CARGO_BIN := $(shell which cargo) +WASM_PACK := $(shell which wasm-pack) ifeq ($(CARGO_BIN),) $(error cargo not found) @@ -26,4 +27,13 @@ test: install: $(CARGO_BIN) install --path oal-client +.PHONY: wasm +ifeq ($(WASM_PACK),) +wasm: + @echo "wasm-pack not found" && exit 1 +else +wasm: + $(WASM_PACK) build oal-wasm +endif + all: fmt lint build test install diff --git a/README.md b/README.md index e457200..f65cb98 100644 --- a/README.md +++ b/README.md @@ -36,3 +36,10 @@ OPTIONS: ``` oal-cli --conf examples/oal.toml ``` + +## Experimental: WebAssembly support +Release to WebAssembly requires the installation of [`wasm-pack`](https://rustwasm.github.io/wasm-pack/installer/). + +``` +make wasm +``` \ No newline at end of file diff --git a/oal-client/src/cli/mod.rs b/oal-client/src/cli/mod.rs index 9314cd3..153e51a 100644 --- a/oal-client/src/cli/mod.rs +++ b/oal-client/src/cli/mod.rs @@ -6,7 +6,6 @@ use oal_compiler::spec::Spec; use oal_compiler::tree::Tree; use oal_model::locator::Locator; use oal_model::span::Span; -use std::fmt::{Display, Formatter}; #[derive(Default)] /// The CLI compilation processor. @@ -108,41 +107,11 @@ impl<'a> Loader for ProcLoader<'a> { } } -/// A span of Unicode code points. -pub struct CharSpan { - start: usize, - end: usize, - loc: Locator, -} - -fn utf8_to_char_index(input: &str, index: usize) -> usize { - let mut char_index = 0; - for (utf8_index, _) in input.char_indices() { - if utf8_index >= index { - return char_index; - } - char_index += 1; - } - char_index -} - -#[test] -fn test_utf8_to_char_index() { - let input = "some😉text!"; - assert_eq!(input.len(), 13); - assert_eq!(input.chars().count(), 10); - assert_eq!(utf8_to_char_index(input, 0), 0); - assert_eq!(utf8_to_char_index(input, 8), 5); - assert_eq!(utf8_to_char_index(input, 42), 10); -} +struct CharSpan(oal_model::span::CharSpan); impl CharSpan { - fn from(input: &str, span: oal_model::span::Span) -> Self { - CharSpan { - start: utf8_to_char_index(input, span.start()), - end: utf8_to_char_index(input, span.end()), - loc: span.locator().clone(), - } + pub fn from(input: &str, span: Span) -> Self { + CharSpan(oal_model::span::CharSpan::from(input, span)) } } @@ -150,20 +119,14 @@ impl ariadne::Span for CharSpan { type SourceId = Locator; fn source(&self) -> &Self::SourceId { - &self.loc + &self.0.loc } fn start(&self) -> usize { - self.start + self.0.start } fn end(&self) -> usize { - self.end - } -} - -impl Display for CharSpan { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}#{}..{}", self.loc, self.start, self.end) + self.0.end } } diff --git a/oal-model/src/span.rs b/oal-model/src/span.rs index 7370660..3097849 100644 --- a/oal-model/src/span.rs +++ b/oal-model/src/span.rs @@ -41,3 +41,48 @@ impl Display for Span { write!(f, "{}#{}..{}", self.loc, self.start, self.end) } } + +/// A span of Unicode code points. +pub struct CharSpan { + pub start: usize, + pub end: usize, + pub loc: Locator, +} + +/// Converts a UTF-8 index to a Unicode code point index. +fn utf8_to_char_index(input: &str, index: usize) -> usize { + let mut char_index = 0; + for (utf8_index, _) in input.char_indices() { + if utf8_index >= index { + return char_index; + } + char_index += 1; + } + char_index +} + +#[test] +fn test_utf8_to_char_index() { + let input = "some😉text!"; + assert_eq!(input.len(), 13); + assert_eq!(input.chars().count(), 10); + assert_eq!(utf8_to_char_index(input, 0), 0); + assert_eq!(utf8_to_char_index(input, 8), 5); + assert_eq!(utf8_to_char_index(input, 42), 10); +} + +impl CharSpan { + pub fn from(input: &str, span: Span) -> Self { + CharSpan { + start: utf8_to_char_index(input, span.start()), + end: utf8_to_char_index(input, span.end()), + loc: span.locator().clone(), + } + } +} + +impl Display for CharSpan { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}#{}..{}", self.loc, self.start, self.end) + } +} diff --git a/oal-wasm/.gitignore b/oal-wasm/.gitignore new file mode 100644 index 0000000..b2bcbe8 --- /dev/null +++ b/oal-wasm/.gitignore @@ -0,0 +1 @@ +pkg/ \ No newline at end of file diff --git a/oal-wasm/Cargo.toml b/oal-wasm/Cargo.toml new file mode 100644 index 0000000..7231b93 --- /dev/null +++ b/oal-wasm/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "oal-wasm" +version = "0.1.0" +edition = "2021" +authors = ["Emmanuel Bastien "] +license = "Apache-2.0" +description = "A high-level functional programming language for designing OpenAPI definitions" +readme = "../README.md" +homepage = "https://www.oxlip-lang.org" +repository = "https://github.com/oxlip-lang/oal" +keywords = ["api"] +categories = ["compilers"] + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["console_error_panic_hook"] + +[dependencies] +oal-model = { path = "../oal-model" } +oal-syntax = { path = "../oal-syntax" } +oal-compiler = { path = "../oal-compiler" } +oal-openapi = { path = "../oal-openapi" } +ariadne = "0.3" +serde_yaml = "0.9" +anyhow = "1.0" +wasm-bindgen = "0.2" +console_error_panic_hook = { version = "0.1", optional = true } + +[dev-dependencies] +wasm-bindgen-test = "0.3" diff --git a/oal-wasm/src/lib.rs b/oal-wasm/src/lib.rs new file mode 100644 index 0000000..d0e3c16 --- /dev/null +++ b/oal-wasm/src/lib.rs @@ -0,0 +1,147 @@ +use anyhow::anyhow; +use ariadne::{Config, Label, Report, ReportKind, Source}; +use oal_compiler::module::{Loader, ModuleSet}; +use oal_compiler::tree::Tree; +use oal_model::locator::Locator; +use oal_model::span::Span; +use wasm_bindgen::prelude::*; + +/// The identifier for the unique source. +const INPUT: &str = "file:///main.oal"; + +/// The default error message if something goes very wrong. +const INTERNAL_ERRROR: &str = "internal error"; + +/// The result of a compilation for interfacing with JavaScript. +#[wasm_bindgen(getter_with_clone)] +pub struct CompilationResult { + pub api: String, + pub error: String, +} + +/// The compiler interface with JavaScript. +#[wasm_bindgen] +pub fn compile(input: &str) -> CompilationResult { + match process(input) { + Ok(api) => CompilationResult { + api, + error: String::default(), + }, + Err(err) => CompilationResult { + api: String::default(), + error: err.to_string(), + }, + } +} + +/// The web loader type for a unique source and no I/O. +struct WebLoader<'a>(&'a str); + +impl<'a> Loader for WebLoader<'a> { + fn is_valid(&mut self, loc: &Locator) -> bool { + loc.url().as_str() == INPUT + } + + fn load(&mut self, loc: &Locator) -> anyhow::Result { + assert_eq!(loc.url().as_str(), INPUT); + Ok(self.0.to_owned()) + } + + fn parse(&mut self, loc: Locator, input: String) -> anyhow::Result { + let (tree, mut errs) = oal_syntax::parse(loc.clone(), &input); + if let Some(err) = errs.pop() { + let span = match err { + oal_syntax::errors::Error::Grammar(ref err) => err.span(), + oal_syntax::errors::Error::Lexicon(ref err) => err.span(), + _ => Span::new(loc, 0..0), + }; + let err = report(&input, span, err).unwrap_or(INTERNAL_ERRROR.to_owned()); + Err(anyhow!(err)) + } else { + Ok(tree.unwrap()) + } + } + + fn compile(&mut self, mods: &ModuleSet, loc: &Locator) -> anyhow::Result<()> { + if let Err(err) = oal_compiler::compile::compile(mods, loc) { + let span = match err.span() { + Some(s) => s.clone(), + None => Span::new(loc.clone(), 0..0), + }; + let err = report(self.0, span, err).unwrap_or(INTERNAL_ERRROR.to_owned()); + Err(anyhow!(err)) + } else { + Ok(()) + } + } +} + +/// Runs the end-to-end compilation process on a single input. +fn process(input: &str) -> anyhow::Result { + let loader = &mut WebLoader(input); + let main = Locator::try_from(INPUT).unwrap(); + let mods = oal_compiler::module::load(loader, &main)?; + let spec = oal_compiler::eval::eval(&mods)?; + let builder = oal_openapi::Builder::new(spec); + let api = builder.into_openapi(); + let api_yaml = serde_yaml::to_string(&api)?; + Ok(api_yaml) +} + +/// Generates an error report. +fn report(input: &str, span: Span, msg: M) -> anyhow::Result { + let mut builder = Report::build(ReportKind::Error, INPUT, span.start()) + .with_config(Config::default().with_color(false)) + .with_message(msg); + if !span.range().is_empty() { + let s = CharSpan::from(input, span); + builder.add_label(Label::new(s)) + } + let mut buf = Vec::new(); + builder + .finish() + .write((INPUT, Source::from(input)), &mut buf)?; + let out = String::from_utf8(buf)?; + Ok(out) +} + +/// A span of Unicode code points within the unique source. +struct CharSpan(oal_model::span::CharSpan); + +impl CharSpan { + pub fn from(input: &str, span: Span) -> Self { + CharSpan(oal_model::span::CharSpan::from(input, span)) + } +} + +impl ariadne::Span for CharSpan { + type SourceId = &'static str; + + fn source(&self) -> &Self::SourceId { + &INPUT + } + + fn start(&self) -> usize { + self.0.start + } + + fn end(&self) -> usize { + self.0.end + } +} + +#[test] +fn test_compile() { + let res = compile("res / on get -> {};"); + assert!(res.error.is_empty()); + assert!(res.api.starts_with("openapi")); +} + +#[test] +fn test_compile_error() { + let res = compile("res a on get -> {};"); + assert!(res + .error + .starts_with("Error: not in scope: variable is not defined")); + assert!(res.api.is_empty()); +}