Skip to content

Commit

Permalink
feat: minimal wasm compiler interface
Browse files Browse the repository at this point in the history
  • Loading branch information
ebastien committed Nov 9, 2023
1 parent 7306f05 commit 5d31159
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 44 deletions.
9 changes: 8 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
]
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
CARGO_BIN := $(shell which cargo)
WASM_PACK := $(shell which wasm-pack)

ifeq ($(CARGO_BIN),)
$(error cargo not found)
Expand Down Expand Up @@ -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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
49 changes: 6 additions & 43 deletions oal-client/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -108,62 +107,26 @@ impl<'a> Loader<anyhow::Error> 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))
}
}

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
}
}
45 changes: 45 additions & 0 deletions oal-model/src/span.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
1 change: 1 addition & 0 deletions oal-wasm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pkg/
32 changes: 32 additions & 0 deletions oal-wasm/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[package]
name = "oal-wasm"
version = "0.1.0"
edition = "2021"
authors = ["Emmanuel Bastien <[email protected]>"]
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"
147 changes: 147 additions & 0 deletions oal-wasm/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<anyhow::Error> for WebLoader<'a> {
fn is_valid(&mut self, loc: &Locator) -> bool {
loc.url().as_str() == INPUT
}

fn load(&mut self, loc: &Locator) -> anyhow::Result<String> {
assert_eq!(loc.url().as_str(), INPUT);
Ok(self.0.to_owned())
}

fn parse(&mut self, loc: Locator, input: String) -> anyhow::Result<Tree> {
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<String> {
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<M: ToString>(input: &str, span: Span, msg: M) -> anyhow::Result<String> {
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());
}

0 comments on commit 5d31159

Please sign in to comment.