Rework the Rust entry point API for the interpreter

Switch to the stateful `ComponentCompiler` concept that we discussed for the C++ API.
This commit is contained in:
Simon Hausmann 2021-03-18 18:20:50 +01:00
parent 6de1c4e3b5
commit 4a9cd954b8
9 changed files with 169 additions and 119 deletions

View file

@ -62,14 +62,11 @@ fn load(mut cx: FunctionContext) -> JsResult<JsValue> {
}
None => vec![],
};
let compiler_config =
sixtyfps_interpreter::CompilerConfiguration::new().with_include_paths(include_paths);
let (c, diags) = spin_on::spin_on(sixtyfps_interpreter::ComponentDefinition::from_path(
path,
compiler_config,
));
let mut compiler = sixtyfps_interpreter::ComponentCompiler::new();
compiler.set_include_paths(include_paths);
let c = spin_on::spin_on(compiler.build_from_path(path));
sixtyfps_interpreter::print_diagnostics(&diags);
sixtyfps_interpreter::print_diagnostics(&compiler.diagnostics());
let c = if let Some(c) = c { c } else { return cx.throw_error("Compilation error") };

View file

@ -32,7 +32,7 @@ pub async fn compile_from_string(
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
let mut config = sixtyfps_interpreter::CompilerConfiguration::new();
let mut compiler = sixtyfps_interpreter::ComponentCompiler::new();
if let Some(load_callback) = optional_import_callback {
let open_import_fallback = move |file_name: &Path| -> core::pin::Pin<
@ -55,12 +55,10 @@ pub async fn compile_from_string(
}
})
};
config = config.with_file_loader(open_import_fallback);
compiler.set_file_loader(open_import_fallback);
}
let (c, diags) =
sixtyfps_interpreter::ComponentDefinition::from_source(source, base_url.into(), config)
.await;
let c = compiler.build_from_source(source, base_url.into()).await;
match c {
Some(c) => Ok(WrappedCompiledComp(c)),
@ -72,7 +70,7 @@ pub async fn compile_from_string(
let level_key = JsValue::from_str("level");
let mut error_as_string = String::new();
let array = js_sys::Array::new();
for d in diags.into_iter() {
for d in compiler.diagnostics().into_iter() {
let filename = d
.source_file()
.as_ref()

View file

@ -66,6 +66,7 @@ mod passes {
}
/// CompilationConfiguration allows configuring different aspects of the compiler.
#[derive(Clone)]
pub struct CompilerConfiguration {
/// Indicate whether to embed resources such as images in the generated output or whether
/// to retain references to the resources on the file system.
@ -80,7 +81,7 @@ pub struct CompilerConfiguration {
/// The callback should open the file specified by the given file name and
/// return an future that provides the text content of the file as output.
pub open_import_fallback:
Option<Box<dyn Fn(String) -> Pin<Box<dyn Future<Output = std::io::Result<String>>>>>>,
Option<Rc<dyn Fn(String) -> Pin<Box<dyn Future<Output = std::io::Result<String>>>>>>,
}
impl CompilerConfiguration {

View file

@ -481,7 +481,7 @@ fn test_load_from_callback_ok() {
let mut compiler_config =
CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
compiler_config.style = Some("ugly".into());
compiler_config.open_import_fallback = Some(Box::new(move |path| {
compiler_config.open_import_fallback = Some(Rc::new(move |path| {
let ok_ = ok_.clone();
Box::pin(async move {
assert_eq!(path, "../FooBar.60");

View file

@ -334,7 +334,124 @@ impl TryInto<Vec<Value>> for Value {
}
}
/// ComponentDescription is a representation of a compiled component from .60
/// ComponentCompiler is the entry point to the SixtyFPS interpreter that can be used
/// to load .60 files or compile them on-the-fly from a string.
pub struct ComponentCompiler {
config: sixtyfps_compilerlib::CompilerConfiguration,
diagnostics: Vec<Diagnostic>,
}
impl ComponentCompiler {
/// Returns a new ComponentCompiler
pub fn new() -> Self {
Self {
config: sixtyfps_compilerlib::CompilerConfiguration::new(
sixtyfps_compilerlib::generator::OutputFormat::Interpreter,
),
diagnostics: vec![],
}
}
/// Create a new configuration that includes sets the include paths used for looking up
/// `.60` imports to the specified vector of paths.
pub fn set_include_paths(&mut self, include_paths: Vec<std::path::PathBuf>) {
self.config.include_paths = include_paths;
}
/// Create a new configuration that selects the style to be used for widgets.
pub fn set_style(&mut self, style: String) {
self.config.style = Some(style);
}
/// Create a new configuration that will use the provided callback for loading.
pub fn set_file_loader(
&mut self,
file_loader_fallback: impl Fn(
&Path,
)
-> core::pin::Pin<Box<dyn core::future::Future<Output = std::io::Result<String>>>>
+ 'static,
) {
self.config.open_import_fallback =
Some(Rc::new(move |path| file_loader_fallback(Path::new(path.as_str()))));
}
/// Returns the diagnostics that were produced in the last call to [[build_from_path]] or [[build_from_source]].
pub fn diagnostics(&self) -> &Vec<Diagnostic> {
&self.diagnostics
}
/// Compile a .60 file into a ComponentDefinition
///
/// Returns the compiled `ComponentDefinition` if there was no errors.
///
/// Any diagnostics produced during the compilation, such as warnigns or errors, are collected
/// in this ComponentCompiler and can be retrieved after the call using the [[diagnostics()]]
/// function. The [`print_diagnostics`] function can be used to display the diagnostics
/// to the users.
///
/// Diagnostics from previous calls are cleared when calling this function.
///
/// This function is `async` but in practice, this is only asynchronious if
/// [`set_file_loader`] was called and its future is actually asynchronious.
/// If that is not used, then it is fine to use a very simple executor, such as the one
/// provided by the `spin_on` crate
pub async fn build_from_path<P: AsRef<Path>>(
&mut self,
path: P,
) -> Option<ComponentDefinition> {
let path = path.as_ref();
let source = match sixtyfps_compilerlib::diagnostics::load_from_path(path) {
Ok(s) => s,
Err(d) => {
self.diagnostics = vec![d];
return None;
}
};
// We create here a 'static guard. That's alright because we make sure
// in this module that we only use erased component
let guard = unsafe { generativity::Guard::new(generativity::Id::new()) };
let (c, diag) =
crate::dynamic_component::load(source, path.into(), self.config.clone(), guard).await;
self.diagnostics = diag.into_iter().collect();
c.ok().map(|inner| ComponentDefinition { inner })
}
/// Compile some .60 code into a ComponentDefinition
///
/// The `path` argument will be used for diagnostics and to compute relative
/// path while importing
///
/// Any diagnostics produced during the compilation, such as warnigns or errors, are collected
/// in this ComponentCompiler and can be retrieved after the call using the [[diagnostics()]]
/// function. The [`print_diagnostics`] function can be used to display the diagnostics
/// to the users.
///
/// Diagnostics from previous calls are cleared when calling this function.
///
/// This function is `async` but in practice, this is only asynchronious if
/// [`set_file_loader`] is set and its future is actually asynchronious.
/// If that is not used, then it is fine to use a very simple executor, such as the one
/// provided by the `spin_on` crate
pub async fn build_from_source(
&mut self,
source_code: String,
path: PathBuf,
) -> Option<ComponentDefinition> {
// We create here a 'static guard. That's alright because we make sure
// in this module that we only use erased component
let guard = unsafe { generativity::Guard::new(generativity::Id::new()) };
let (c, diag) =
crate::dynamic_component::load(source_code, path, self.config.clone(), guard).await;
self.diagnostics = diag.into_iter().collect();
c.ok().map(|inner| ComponentDefinition { inner })
}
}
/// ComponentDefinition is a representation of a compiled component from .60
///
/// It can be constructed from a .60 file using the [`Self::from_path`] or [`Self::from_source`] functions.
/// And then it can be instentiated with the [`Self::create`] function
@ -344,66 +461,6 @@ pub struct ComponentDefinition {
}
impl ComponentDefinition {
/// Compile a .60 file into a ComponentDefinition
///
/// The first element of the returned tuple is going to be the compiled
/// `ComponentDefinition` if there was no errors. This function also return
/// a vector if diagnostics with errors and/or warnings.
/// The [`print_diagnostics`] function can be used to display the diagnostics
/// to the users.
///
/// This function is `async` but in practice, this is only asynchronious if
/// [`CompilerConfiguration::with_file_loader`] is set and its future is actually asynchronious.
/// If that is not used, then it is fine to use a very simple executor, such as the one
/// provided by the `spin_on` crate
pub async fn from_path<P: AsRef<Path>>(
path: P,
config: CompilerConfiguration,
) -> (Option<Self>, Vec<Diagnostic>) {
let path = path.as_ref();
let source = match sixtyfps_compilerlib::diagnostics::load_from_path(path) {
Ok(s) => s,
Err(d) => return (None, vec![d]),
};
// We create here a 'static guard. That's alright because we make sure
// in this module that we only use erased component
let guard = unsafe { generativity::Guard::new(generativity::Id::new()) };
let (c, diag) =
crate::dynamic_component::load(source, path.into(), config.config, guard).await;
(c.ok().map(|inner| Self { inner }), diag.into_iter().collect())
}
/// Compile some .60 code into a ComponentDefinition
///
/// The `path` argument will be used for diagnostics and to compute relative
/// path while importing
///
/// The first element of the returned tuple is going to be the compiled
/// `ComponentDefinition` if there was no errors. This function also return
/// a vector if diagnostics with errors and/or warnings.
/// The [`print_diagnostics`] function can be used to display the diagnostics
/// to the users.
///
/// This function is `async` but in practice, this is only asynchronious if
/// [`CompilerConfiguration::with_file_loader`] is set and its future is actually asynchronious.
/// If that is not used, then it is fine to use a very simple executor, such as the one
/// provided by the `spin_on` crate
pub async fn from_source(
source_code: String,
path: PathBuf,
config: CompilerConfiguration,
) -> (Option<Self>, Vec<Diagnostic>) {
// We create here a 'static guard. That's alright because we make sure
// in this module that we only use erased component
let guard = unsafe { generativity::Guard::new(generativity::Id::new()) };
let (c, diag) =
crate::dynamic_component::load(source_code, path, config.config, guard).await;
(c.ok().map(|inner| Self { inner }), diag.into_iter().collect())
}
/// Instantiate the component
pub fn create(&self) -> ComponentInstance {
ComponentInstance {
@ -478,15 +535,16 @@ impl ComponentInstance {
/// ## Examples
///
/// ```
/// use sixtyfps_interpreter::{ComponentDefinition, CompilerConfiguration, Value, SharedString};
/// use sixtyfps_interpreter::{ComponentDefinition, ComponentCompiler, Value, SharedString};
/// let code = r#"
/// MyWin := Window {
/// property <int> my_property: 42;
/// }
/// "#;
/// let (definition, diagnostics) = spin_on::spin_on(
/// ComponentDefinition::from_source(code.into(), Default::default(), CompilerConfiguration::new()));
/// assert!(diagnostics.is_empty(), "{:?}", diagnostics);
/// let mut compiler = ComponentCompiler::new();
/// let definition = spin_on::spin_on(
/// compiler.build_from_source(code.into(), Default::default()));
/// assert!(compiler.diagnostics().is_empty(), "{:?}", compiler.diagnostics());
/// let instance = definition.unwrap().create();
/// assert_eq!(instance.get_property("my_property").unwrap(), Value::from(42));
/// ```
@ -517,7 +575,7 @@ impl ComponentInstance {
/// ## Examples
///
/// ```
/// use sixtyfps_interpreter::{ComponentDefinition, CompilerConfiguration, Value, SharedString};
/// use sixtyfps_interpreter::{ComponentDefinition, ComponentCompiler, Value, SharedString};
/// use core::convert::TryInto;
/// let code = r#"
/// MyWin := Window {
@ -525,8 +583,8 @@ impl ComponentInstance {
/// property <int> my_prop: 12;
/// }
/// "#;
/// let (definition, _) = spin_on::spin_on(
/// ComponentDefinition::from_source(code.into(), Default::default(), CompilerConfiguration::new()));
/// let definition = spin_on::spin_on(
/// ComponentCompiler::new().build_from_source(code.into(), Default::default()));
/// let instance = definition.unwrap().create();
///
/// let instance_weak = instance.as_weak();
@ -704,7 +762,7 @@ impl CompilerConfiguration {
) -> Self {
let mut config = self.config;
config.open_import_fallback =
Some(Box::new(move |path| file_loader_fallback(Path::new(path.as_str()))));
Some(Rc::new(move |path| file_loader_fallback(Path::new(path.as_str()))));
Self { config }
}
}

View file

@ -30,12 +30,13 @@ executor, such as the one provided by the `spin_on` crate
This example load a `.60` dynamically from a path and show error if any
```rust
use sixtyfps_interpreter::{ComponentDefinition, CompilerConfiguration};
use sixtyfps_interpreter::{ComponentDefinition, ComponentCompiler, CompilerConfiguration};
let (definition, diagnostics) =
spin_on::spin_on(ComponentDefinition::from_path("hello.60", CompilerConfiguration::new()));
let mut compiler = ComponentCompiler::new();
let definition =
spin_on::spin_on(compiler.build_from_path("hello.60"));
# #[cfg(feature="print_diagnostics")]
sixtyfps_interpreter::print_diagnostics(&diagnostics);
sixtyfps_interpreter::print_diagnostics(&compiler.diagnostics());
if let Some(definition) = definition {
let instance = definition.create();
instance.run();
@ -45,7 +46,7 @@ if let Some(definition) = definition {
This example load a `.60` from a string and set some properties
```rust
use sixtyfps_interpreter::{ComponentDefinition, CompilerConfiguration, Value, SharedString};
use sixtyfps_interpreter::{ComponentDefinition, ComponentCompiler, Value, SharedString};
let code = r#"
MyWin := Window {
@ -56,9 +57,10 @@ let code = r#"
}
"#;
let (definition, diagnostics) =
spin_on::spin_on(ComponentDefinition::from_source(code.into(), Default::default(), CompilerConfiguration::new()));
assert!(diagnostics.is_empty());
let mut compiler = ComponentCompiler::new();
let definition =
spin_on::spin_on(compiler.build_from_source(code.into(), Default::default()));
assert!(compiler.diagnostics().is_empty());
let instance = definition.unwrap().create();
instance.set_property("my_name", Value::from(SharedString::from("World"))).unwrap();
# return; // we don't want to call run in the tests

View file

@ -10,18 +10,14 @@ LICENSE END */
#[cfg(test)]
fn do_test(snippet: &str) -> Result<(), Box<dyn std::error::Error>> {
let config = sixtyfps_interpreter::CompilerConfiguration::new();
let (component, diagnostics) =
spin_on::spin_on(sixtyfps_interpreter::ComponentDefinition::from_source(
snippet.into(),
Default::default(),
config,
));
let mut compiler = sixtyfps_interpreter::ComponentCompiler::new();
let component =
spin_on::spin_on(compiler.build_from_source(snippet.into(), Default::default()));
#[cfg(feature = "display-diagnostics")]
sixtyfps_interpreter::print_diagnostics(&diagnostics);
sixtyfps_interpreter::print_diagnostics(&compiler.diagnostics());
for d in diagnostics {
for d in compiler.diagnostics() {
if d.level() == sixtyfps_interpreter::DiagnosticLevel::Error {
return Err(d.message().to_owned().into());
}

View file

@ -15,20 +15,21 @@ pub fn test(testcase: &test_driver_lib::TestCase) -> Result<(), Box<dyn Error>>
let include_paths = test_driver_lib::extract_include_paths(&source)
.map(std::path::PathBuf::from)
.collect::<Vec<_>>();
let config =
sixtyfps_interpreter::CompilerConfiguration::new().with_include_paths(include_paths);
let mut compiler = sixtyfps_interpreter::ComponentCompiler::new();
compiler.set_include_paths(include_paths);
let (component, diags) =
spin_on::spin_on(sixtyfps_interpreter::ComponentDefinition::from_source(
source,
testcase.absolute_path.clone(),
config,
));
let component =
spin_on::spin_on(compiler.build_from_source(source, testcase.absolute_path.clone()));
let component = match component {
None => {
sixtyfps_interpreter::print_diagnostics(&diags);
return Err(diags.into_iter().map(|d| d.to_string()).join("\n").into());
sixtyfps_interpreter::print_diagnostics(&compiler.diagnostics());
return Err(compiler
.diagnostics()
.into_iter()
.map(|d| d.to_string())
.join("\n")
.into());
}
Some(c) => c,
};

View file

@ -37,17 +37,14 @@ fn main() -> std::io::Result<()> {
});
});
let mut compiler_config =
sixtyfps_interpreter::CompilerConfiguration::new().with_include_paths(args.include_paths);
let mut compiler = sixtyfps_interpreter::ComponentCompiler::new();
compiler.set_include_paths(args.include_paths);
if !args.style.is_empty() {
compiler_config = compiler_config.with_style(args.style);
compiler.set_style(args.style);
}
let (c, diags) = spin_on::spin_on(sixtyfps_interpreter::ComponentDefinition::from_path(
args.path,
compiler_config,
));
sixtyfps_interpreter::print_diagnostics(&diags);
let c = spin_on::spin_on(compiler.build_from_path(args.path));
sixtyfps_interpreter::print_diagnostics(&compiler.diagnostics());
let c = match c {
Some(c) => c,