Skip to content

Commit

Permalink
Refactor core parser
Browse files Browse the repository at this point in the history
  • Loading branch information
DrSensor committed Aug 24, 2019
1 parent 31757d0 commit 1264873
Show file tree
Hide file tree
Showing 10 changed files with 96 additions and 84 deletions.
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
[![License](https://img.shields.io/github/license/drsensor/scdlang.svg)](./LICENSE)
[![Chats](https://img.shields.io/badge/community-grey.svg?logo=matrix)](https://matrix.to/#/+statecharts:matrix.org)

> 🚧 Still **Work in Progress** 🏗️
> 🚧 Slowly **Work in Progress** 🏗️
## About
Scdlang (pronounced `/ˈesˌsi:ˈdi:ˈlæŋ/`) is a description language for describing Statecharts that later can be used to generate code or just transpile it into another format. This project is more focus on how to describe Statecharts universally that can be used in another language/platform rather than drawing a Statecharts diagram. For drawing, see [State Machine Cat][].
Expand All @@ -35,11 +35,12 @@ Scdlang (pronounced `/ˈesˌsi:ˈdi:ˈlæŋ/`) is a description language for des
- [ ] [WaveDrom](https://observablehq.com/@drom/wavedrom)
- Compile into other formats (hopefully, no promise):
- [ ] WebAssembly (using [parity-wasm](https://github.com/paritytech/parity-wasm))
- Code generation 🤔
- [ ] Julia via [`@generated`](https://docs.julialang.org/en/v1/manual/metaprogramming/#Generated-functions-1) implemented as [parametric](https://docs.julialang.org/en/v1/manual/methods/#Parametric-Methods-1) [multiple-dispatch](https://en.wikipedia.org/wiki/Multiple_dispatch#Julia) [functors](https://docs.julialang.org/en/v1/manual/methods/#Function-like-objects-1)
- [ ] Rust via [`#[proc_macro_attribute]`](https://doc.rust-lang.org/reference/procedural-macros.html#attribute-macros) implemented as [typestate programming](https://rust-embedded.github.io/book/static-guarantees/typestate-programming.html)? (I'm still afraid if it will conflict with another crates)
- [ ] Elixir via [`use`](https://elixir-lang.org/getting-started/alias-require-and-import.html#use) macro which desugar into [gen_statem](https://andrealeopardi.com/posts/connection-managers-with-gen_statem/) 💪
- [ ] Flutter via [`builder_factories`](https://github.com/flutter/flutter/wiki/Code-generation-in-Flutter) (waiting for the [FFI](https://github.com/dart-lang/sdk/issues/34452) to be stable)
- Code generation (all of them can be non-embedded and it will be the priority) 🤔
- [ ] Julia via [`@generated`](https://docs.julialang.org/en/v1/manual/metaprogramming/#Generated-functions-1) implemented as [parametric](https://docs.julialang.org/en/v1/manual/methods/#Parametric-Methods-1) [multiple-dispatch](https://en.wikipedia.org/wiki/Multiple_dispatch#Julia) [functors](https://docs.julialang.org/en/v1/manual/methods/#Function-like-objects-1) [non-embedded until there is a project like PyO3 but for Julia]
- [ ] Rust via [`#[proc_macro_attribute]`](https://doc.rust-lang.org/reference/procedural-macros.html#attribute-macros) implemented as [typestate programming](https://rust-embedded.github.io/book/static-guarantees/typestate-programming.html)? (Need to figure out how to support async, non-async, and no-std in single abstraction) [embedded]
- [ ] Elixir via [`use`](https://elixir-lang.org/getting-started/alias-require-and-import.html#use) macro and [rustler](https://github.com/rusterlium/rustler) which desugar into [gen_statem](https://andrealeopardi.com/posts/connection-managers-with-gen_statem/) [embedded]
- [ ] Flutter via [`builder_factories`](https://github.com/flutter/flutter/wiki/Code-generation-in-Flutter) (waiting for the [FFI](https://github.com/dart-lang/sdk/issues/34452) to be stable) [embedded]
- [ ] Typescript or **AssemblyScript** implemented as [typestate interface or abstract-class](https://spectrum.chat/statecharts/general/typestate-guard~d1ec4eb1-6db7-45bb-8b79-836c9a22cd5d) (usefull for building smart contract) [non-embedded]

> For more info, see the changelog in the [release page][]
Expand Down
7 changes: 4 additions & 3 deletions packages/core/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ use std::{collections::HashMap, sync::*};
// 🤔 or is there any better way?
// pub static mut TRANSITION: Option<HashMap<Transition, &str>> = None; // doesn't work!
// type LazyMut<T> = Mutex<Option<T>>;
static TRANSITION: Lazy<Mutex<MapTransition>> = Lazy::new(|| Mutex::new(HashMap::new()));
static TRANSITION: Lazy<Mutex<TransitionMap>> = Lazy::new(|| Mutex::new(HashMap::new()));
static WARNING: Lazy<RwLock<String>> = Lazy::new(|| RwLock::new(String::new()));
/*reserved for another caches*/

/// Access cached transition safely
pub fn transition<'a>() -> Result<MutexGuard<'a, MapTransition>, Error> {
pub fn transition<'a>() -> Result<MutexGuard<'a, TransitionMap>, Error> {
TRANSITION.lock().map_err(|_| Error::Deadlock)
}

Expand Down Expand Up @@ -55,7 +55,8 @@ impl Shrink {
}

// TODO: 🤔 consider using this approach http://idubrov.name/rust/2018/06/01/tricking-the-hashmap.html
pub(crate) type MapTransition = HashMap<CurrentState, HashMap<Trigger, NextState>>;
pub(crate) type TransitionMap = HashMap<CurrentState, HashMap<Trigger, NextState>>;
// pub(crate) type WarningSet = HashSet<CurrentState>;
pub(crate) type CurrentState = String;
pub(crate) type NextState = String;
pub(crate) type Trigger = Option<String>;
3 changes: 1 addition & 2 deletions packages/core/src/core/builder.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{cache, error::Error, external::Builder};
use pest_derive::Parser;
use std::collections::HashMap;
use std::collections::*;

#[derive(Debug, Parser, Default, Clone)] // 🤔 is it wise to derive from Copy&Clone ?
#[grammar = "grammar.pest"]
Expand Down Expand Up @@ -34,7 +34,6 @@ pub struct Scdlang<'g> {

pub(super) clear_cache: bool, //-|in case for program that need to disable…|
pub semantic_error: bool, //-|…then enable semantic error at runtime|
pub warnings: &'g [&'g str],

derive_config: Option<HashMap<&'static str, &'g str>>,
}
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/external.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,11 @@ pub trait Builder<'t> {
/// Set the line_of_code offset of the error essages.
fn with_err_line(&mut self, line: usize) -> &mut dyn Builder<'t>;

// Set custom config. Used on derived Parser.
// WARNING: `Any` is not supported because trait object can't have generic methods

/// Set custom config. Used on derived Parser.
fn set(&mut self, key: &'static str, value: &'t str) -> &mut dyn Builder<'t>;
// Get custom config. Used on derived Parser.
/// Get custom config. Used on derived Parser.
fn get(&self, key: &'t str) -> Option<&'t str>;
}

Expand Down
71 changes: 3 additions & 68 deletions packages/core/src/semantics/transition/analyze.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::helper::prelude::*;
use super::helper::{prelude::*, transform_key::*};
use crate::{cache, semantics, utils::naming::sanitize, Error};
use semantics::{analyze::*, Event, Kind, Transition};
use semantics::{analyze::*, Kind, Transition};

impl SemanticCheck for Transition<'_> {
fn check_error(&self) -> Result<Option<String>, Error> {
Expand Down Expand Up @@ -52,63 +52,6 @@ impl SemanticCheck for Transition<'_> {
}
}

// WARNING: not performant because of using concatenated String as a key which cause filtering
impl From<&Event<'_>> for String {
fn from(event: &Event<'_>) -> Self {
format!("{}?{}", event.name.unwrap_or(""), event.guard.unwrap_or(""))
}
}

impl<'i> EventKey<'i> for &'i Option<String> {}
trait EventKey<'i>: Into<Option<&'i String>> {
fn has_trigger(self) -> bool {
self.into().filter(|e| is_empty(e.rsplit('?'))).is_some()
}
fn has_guard(self) -> bool {
self.into().filter(|e| is_empty(e.split('?'))).is_some()
}
fn get_guard(self) -> Option<&'i str> {
self.into().and_then(|e| none_empty(e.split('?')))
}
fn get_trigger(self) -> Option<&'i str> {
self.into().and_then(|e| none_empty(e.rsplit('?')))
}
fn guards_with_same_trigger(self, trigger: Option<&'i str>) -> Option<&'i str> {
self.into()
.filter(|e| none_empty(e.rsplit('?')) == trigger)
.and_then(|e| none_empty(e.split('?')))
}
fn triggers_with_same_guard(self, guard: Option<&'i str>) -> Option<&'i str> {
self.into()
.filter(|e| none_empty(e.split('?')) == guard)
.and_then(|e| none_empty(e.rsplit('?')))
}
fn as_expression(self) -> String {
self.into().map(String::as_str).as_expression()
}
}

impl<'o> Trigger<'o> for &'o Option<&'o str> {}
trait Trigger<'o>: Into<Option<&'o &'o str>> {
fn as_expression(self) -> String {
self.into()
.map(|s| {
format!(
" @ {trigger}{guard}",
trigger = none_empty(s.rsplit('?')).unwrap_or_default(),
guard = none_empty(s.split('?'))
.filter(|_| s.contains('?'))
.map(|g| format!("[{}]", g))
.unwrap_or_default(),
)
})
.unwrap_or_default()
}
fn as_key(self, guard: &str) -> Option<String> {
Some(format!("{}?{}", self.into().unwrap_or(&""), guard))
}
}

impl<'t> SemanticAnalyze<'t> for Transition<'t> {
fn analyze_error(&self, span: Span<'t>, options: &'t Scdlang) -> Result<(), Error> {
let make_error = |message| options.err_from_span(span, message).into();
Expand Down Expand Up @@ -139,17 +82,9 @@ impl<'t> SemanticAnalyze<'t> for Transition<'t> {
}
}

fn is_empty<'a>(split: impl Iterator<Item = &'a str>) -> bool {
none_empty(split).is_some()
}

fn none_empty<'a>(split: impl Iterator<Item = &'a str>) -> Option<&'a str> {
split.last().filter(|s| !s.is_empty())
}

use std::collections::HashMap;
type CacheMap = HashMap<Option<String>, String>;
type CachedTransition<'state> = MutexGuard<'state, cache::MapTransition>;
type CachedTransition<'state> = MutexGuard<'state, cache::TransitionMap>;

impl<'t> Transition<'t> {
fn cache_current_state<'a>(&self, cache: &'t mut CachedTransition<'a>) -> &'t mut CacheMap {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! Code for desugaring expression into multiple transition
use crate::semantics;
use semantics::{Transition, TransitionType};
use std::iter::FromIterator;
Expand All @@ -8,7 +10,6 @@ impl<'i> IntoIterator for Transition<'i> {

fn into_iter(mut self) -> Self::IntoIter {
TransitionIterator(match self.kind {
/*FIXME: iterator for internal transition*/
TransitionType::Normal | TransitionType::Internal => vec![self],
TransitionType::Toggle => {
self.kind = TransitionType::Normal;
Expand Down
70 changes: 70 additions & 0 deletions packages/core/src/semantics/transition/helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,73 @@ pub(super) mod get {
Event { name: event, guard }
}
}

/// analyze.rs helpers for transforming key for caches
pub(super) mod transform_key {
use crate::semantics::*;

// WARNING: not performant because of using concatenated String as a key which cause filtering
impl From<&Event<'_>> for String {
fn from(event: &Event<'_>) -> Self {
format!("{}?{}", event.name.unwrap_or(""), event.guard.unwrap_or(""))
}
}

impl<'i> EventKey<'i> for &'i Option<String> {}
pub trait EventKey<'i>: Into<Option<&'i String>> {
fn has_trigger(self) -> bool {
self.into().filter(|e| is_empty(e.rsplit('?'))).is_some()
}
fn has_guard(self) -> bool {
self.into().filter(|e| is_empty(e.split('?'))).is_some()
}
fn get_guard(self) -> Option<&'i str> {
self.into().and_then(|e| none_empty(e.split('?')))
}
fn get_trigger(self) -> Option<&'i str> {
self.into().and_then(|e| none_empty(e.rsplit('?')))
}
fn guards_with_same_trigger(self, trigger: Option<&'i str>) -> Option<&'i str> {
self.into()
.filter(|e| none_empty(e.rsplit('?')) == trigger)
.and_then(|e| none_empty(e.split('?')))
}
fn triggers_with_same_guard(self, guard: Option<&'i str>) -> Option<&'i str> {
self.into()
.filter(|e| none_empty(e.split('?')) == guard)
.and_then(|e| none_empty(e.rsplit('?')))
}
fn as_expression(self) -> String {
self.into().map(String::as_str).as_expression()
}
}

impl<'o> Trigger<'o> for &'o Option<&'o str> {}
pub trait Trigger<'o>: Into<Option<&'o &'o str>> {
fn as_expression(self) -> String {
self.into()
.map(|s| {
format!(
" @ {trigger}{guard}",
trigger = none_empty(s.rsplit('?')).unwrap_or_default(),
guard = none_empty(s.split('?'))
.filter(|_| s.contains('?'))
.map(|g| format!("[{}]", g))
.unwrap_or_default(),
)
})
.unwrap_or_default()
}
fn as_key(self, guard: &str) -> Option<String> {
Some(format!("{}?{}", self.into().unwrap_or(&""), guard))
}
}

fn is_empty<'a>(split: impl Iterator<Item = &'a str>) -> bool {
none_empty(split).is_some()
}

fn none_empty<'a>(split: impl Iterator<Item = &'a str>) -> Option<&'a str> {
split.last().filter(|s| !s.is_empty())
}
}
4 changes: 3 additions & 1 deletion packages/core/src/semantics/transition/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
//! parse -> convert -> desugar -> analyze -> consume
mod analyze;
mod convert;
mod desugar;
mod helper;
mod iter;

use crate::{
semantics::{analyze::*, Check, Expression, Found, Kind, Transition},
Expand Down
1 change: 1 addition & 0 deletions packages/transpiler/smcat/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ mod test {
"from": "A",
"to": "B",
"color": "red",
// FIXME: 👇 should be tested using regex
"note": ["duplicate transient-transition: A -> B,C"]
}]
}),
Expand Down
2 changes: 1 addition & 1 deletion packages/transpiler/xstate/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ scdlang = { path = "../../core", version = "0.2.1" }
serde_json = "1"
serde = { version = "1", features = ["derive"] }
serde_with = { version = "1", features = ["json"] }
voca_rs = "1"
voca_rs = "1" # helper to convert Scdlang naming convention into DavidKPiano naming convention

[dev-dependencies]
assert-json-diff = "1"

0 comments on commit 1264873

Please sign in to comment.