Selaa lähdekoodia

Update test framework to use Rust library. Fix TimerId error in generated code.

Joeri Exelmans 5 vuotta sitten
vanhempi
commit
57e1cb0e29

+ 3 - 5
README.md

@@ -16,7 +16,7 @@ Tip: Users of the Nix package manager can get a usable development environment t
 
 ### Optional
 
-* [Rust compiler](https://www.rust-lang.org/) to test compilation to Rust.
+* [Rust compiler](https://www.rust-lang.org/) and [Cargo](https://doc.rust-lang.org/cargo/index.html) to test compilation to Rust.
 * [state-machine-cat](https://github.com/sverweij/state-machine-cat) to render statecharts as SVG images. Runs on NodeJS, installable from NPM.
 * [Graphviz dot](https://graphviz.org/) to render the priorities between a statechart's transitions as a graph.
 
@@ -38,9 +38,7 @@ It will recursively visit the directory tree of `test_files` and look for XML fi
 
 ### Code generation with Rust
 
-The test framework can also generate Rust code for each test, and then invokes the Rust compiler (must be in your PATH as `rustc`) to compile to native code for your machine, which is put in a temporary directory. The native code is then run (the main-function of the generated code executes the test).
-
-Other than the `rustc` command, there are (currently) no dependencies.
+The test framework can also generate a Rust crate for each test, and then invokes Cargo (must be in your PATH as `cargo`) to compile to native code for your machine. The created crates and compilation artifacts are put in a temporary directory. The native code is then run (the main-function of the generated code executes the test).
 
 Add the `--rust` flag to the test command to try it:
 
@@ -48,7 +46,7 @@ Add the `--rust` flag to the test command to try it:
 python -m sccd.test.cmd.run --rust test/test_files
 ```
 
-Rust code generation is a work-in-progress. Tests that contain unsupported features will be skipped.
+Rust code generation is a work-in-progress. Some tests may fail, or be skipped.
 
 ## Runtime environment variables
 

src/rust/.gitignore → rust/.gitignore


+ 1 - 1
src/rust/Cargo.toml

@@ -8,4 +8,4 @@ edition = "2018"
 
 [lib]
 name = "sccd"
-path = "lib.rs"
+path = "src/lib.rs"

+ 5 - 5
src/rust/action_lang.rs

@@ -41,10 +41,10 @@ pub struct Empty{}
 #[macro_export]
 macro_rules! call_closure {
   ($closure: expr, $($param: expr),*  $(,)?) => {
-  (||{
-    let scope = &mut $closure.0;
-    let function = &mut $closure.1;
-    return function($($param),* scope);
-  })()
+    (||{
+      let scope = &mut $closure.0;
+      let function = &mut $closure.1;
+      return function($($param),* scope);
+    })()
   };
 }

src/rust/controller.rs → rust/src/controller.rs


src/rust/lib.rs → rust/src/lib.rs


+ 2 - 2
src/rust/statechart.rs

@@ -90,6 +90,6 @@ pub trait SC<InEvent, TimerId, Sched: Scheduler<InEvent, TimerId>, OutputCallbac
 // TODO: Does not belong in "common", this should become a statechart-specific enum-type.
 #[derive(Debug, Eq, PartialEq)]
 pub struct OutEvent {
-  port: &'static str,
-  event: &'static str,
+  pub port: &'static str,
+  pub event: &'static str,
 }

+ 3 - 3
src/sccd/action_lang/codegen/rust.py

@@ -45,7 +45,7 @@ class ScopeHelper():
     
     def type(self, scope, end):
         if end == 0:
-            return "Empty"
+            return "action_lang::Empty"
         else:
             return self.basename(scope) + "_l" + str(end)
 
@@ -56,7 +56,7 @@ class ScopeHelper():
 
         if start != end  and  end > 0:
             if start == 0:
-                supertype_name = "Empty"
+                supertype_name = "action_lang::Empty"
             else:
                 supertype_name = self.scope_structs[self.current().scope][start].type_name
 
@@ -141,7 +141,7 @@ class ActionLangRustGenerator(Visitor):
 
             self.w.wnoln(" {")
             self.w.indent()
-            self.w.writeln("let scope = Empty{};")
+            self.w.writeln("let scope = action_lang::Empty{};")
 
             self.scope.push(function.scope)
             # Parameters are part of function's scope

+ 0 - 0
src/sccd/cd/cmd/gen_rust.py


+ 7 - 3
src/sccd/cd/codegen/rust.py

@@ -1,6 +1,10 @@
-from sccd.statechart.codegen.rust import *
 
-class ClassDiagramRustGenerator(StatechartRustGenerator):
+class ClassDiagramRustGenerator:
+
+    def __init__(self, writer):
+        self.w = writer
 
     def visit_SingleInstanceCD(self, cd):
-        cd.statechart.accept(self)
+        from sccd.statechart.codegen.rust import StatechartRustGenerator
+        gen = StatechartRustGenerator(self.w, cd.globals)
+        cd.statechart.accept(gen)

+ 1 - 1
src/sccd/cd/parser/xml.py

@@ -21,7 +21,7 @@ def cd_parser_rules(statechart_parser_rules, default_delta = duration(100, Micro
 
     return ([("delta?", parse_delta), ("statechart", sc_rules)], finish_single_instance_cd)
 
-  return [("single_instance_cd?", parse_single_instance_cd)]
+  return parse_single_instance_cd
 
 # This is usually how you would want to load a class diagram:
 def load_cd(src):

+ 0 - 59
src/sccd/statechart/cmd/gen_rust.py

@@ -1,59 +0,0 @@
-import argparse
-import sys
-import os
-
-# Output can be piped to Rust compiler as follows:
-#
-# For statecharts, class diagrams (build library):
-#  python -m sccd.statechart.cmd.gen_rust <path/to/statechart.xml> | rustc --crate-type=lib -
-#
-# For tests (build executable):
-#  python -m sccd.statechart.cmd.gen_rust <path/to/test.xml> | rustc -
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(
-        description="Generate Rust code. Rust code is written to stdout.")
-    parser.add_argument('path', metavar='PATH', type=str, help="A SCCD statechart or test XML file. A statechart can be compiled to a Rust library. A test can be compiled to a Rust executable (the main-function runs the test).")
-    args = parser.parse_args()
-    src = args.path
-    path = os.path.dirname(src)
-
-    from sccd.statechart.parser.xml import *
-    from sccd.test.parser.xml import *
-    from sccd.util.indenting_writer import *
-    from functools import partial
-
-    globals = Globals()
-
-    sc_parser_rules = partial(statechart_parser_rules, path=path, load_external=True)
-
-    rules = {
-        "statechart": sc_parser_rules(globals),
-        "single_instance_cd": cd_parser_rules(sc_parser_rules),
-        "test": test_parser_rules(sc_parser_rules),
-    }
-
-    parsed = parse_f(src, rules)
-
-    sys.stderr.write("Parsing finished.\n")
-
-    w = IndentingWriter()
-
-    if isinstance(parsed, Statechart):
-        
-        from sccd.statechart.codegen.rust import StatechartRustGenerator
-
-        gen = StatechartRustGenerator(w, globals)
-        gen.accept(parsed)
-
-    elif isinstance(parsed, AbstractCD):
-        from sccd.cd.codegen.rust import ClassDiagramRustGenerator
-
-        gen = ClassDiagramRustGenerator(w, globals)
-        gen.accept(parsed)
-
-    elif isinstance(parsed, list) and reduce(lambda x,y: x and y, (isinstance(test, TestVariant) for test in parsed)):
-        sys.stderr.write("Loaded test.\n")
-
-        from sccd.test.codegen.rust import compile_test
-        compile_test(parsed, w)

+ 0 - 204
src/sccd/statechart/codegen/common.rs

@@ -1,204 +0,0 @@
-use std::collections::BinaryHeap;
-use std::cmp::Ordering;
-use std::cmp::Reverse;
-
-
-#[derive(Default)]
-pub struct SameRoundLifeline<InternalType> {
-  current: InternalType,
-}
-
-impl<InternalType: Default> SameRoundLifeline<InternalType> {
-  fn current(&self) -> &InternalType {
-    &self.current
-  }
-  fn raise(&mut self) -> &mut InternalType {
-    &mut self.current
-  }
-  fn cycle(&mut self) {
-    self.current = Default::default()
-  }
-}
-
-#[derive(Default)]
-pub struct NextRoundLifeline<InternalType> {
-  one: InternalType,
-  two: InternalType,
-
-  one_is_current: bool,
-}
-
-impl<InternalType: Default> NextRoundLifeline<InternalType> {
-  fn current(&self) -> &InternalType {
-    if self.one_is_current { &self.one } else { &self.two }
-  }
-  fn raise(&mut self) -> &mut InternalType {
-    if self.one_is_current { &mut self.two } else { &mut self.one }
-  }
-  fn cycle(&mut self) {
-    if self.one_is_current {
-      self.one = Default::default();
-    } else {
-      self.two = Default::default();
-    }
-    self.one_is_current = ! self.one_is_current
-  }
-}
-
-pub trait Scheduler<EventType> {
-  fn set_timeout(&mut self, delay: Timestamp, event: EventType) -> EntryId;
-  fn unset_timeout(&mut self, id: EntryId);
-}
-
-pub trait SC<EventType, Sched: Scheduler<EventType>, OutputCallback> {
-  fn init(&mut self, sched: &mut Sched, output: &mut OutputCallback);
-  fn big_step(&mut self, event: Option<EventType>, sched: &mut Sched, output: &mut OutputCallback);
-}
-
-pub type Timestamp = u32;
-pub type TimerId = u16;
-
-#[derive(Default, Copy, Clone, Ord, PartialOrd, PartialEq, Eq)]
-pub struct EntryId {
-  timestamp: Timestamp,
-  n: TimerId,
-}
-
-pub struct QueueEntry<EventType> {
-  id: EntryId,
-  event: EventType,
-}
-impl<EventType> Ord for QueueEntry<EventType> {
-  fn cmp(&self, other: &Self) -> Ordering {
-    self.id.cmp(&other.id)
-  }
-}
-impl<EventType> PartialOrd for QueueEntry<EventType> {
-    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
-        Some(self.cmp(other))
-    }
-}
-impl<EventType> PartialEq for QueueEntry<EventType> {
-    fn eq(&self, other: &Self) -> bool {
-        self.id == other.id
-    }
-}
-impl<EventType> Eq for QueueEntry<EventType> {}
-
-#[derive(Debug, Eq, PartialEq)]
-pub struct OutEvent {
-  port: &'static str,
-  event: &'static str,
-}
-
-pub struct Controller<EventType> {
-  simtime: Timestamp,
-  next_id: TimerId,
-  queue: BinaryHeap<Reverse<QueueEntry<EventType>>>,
-  removed: BinaryHeap<Reverse<EntryId>>,
-}
-
-impl<EventType> Scheduler<EventType> for Controller<EventType> {
-  fn set_timeout(&mut self, delay: Timestamp, event: EventType) -> EntryId {
-    let id = EntryId{ timestamp: self.simtime + delay, n: self.next_id };
-    let entry = QueueEntry::<EventType>{ id, event };
-    self.queue.push(Reverse(entry));
-    self.next_id += 1; // TODO: will overflow eventually :(
-    return id
-  }
-  fn unset_timeout(&mut self, id: EntryId) {
-    self.removed.push(Reverse(id));
-  }
-}
-
-pub enum Until {
-  Timestamp(Timestamp),
-  Eternity,
-}
-
-impl<EventType: Copy>
-Controller<EventType> {
-  fn new() -> Self {
-    Self {
-      simtime: 0,
-      next_id: 0,
-      queue: BinaryHeap::with_capacity(8),
-      removed: BinaryHeap::with_capacity(4),
-    }
-  }
-  fn run_until<StatechartType: SC<EventType, Controller<EventType>, OutputCallback>, OutputCallback: FnMut(OutEvent)>(&mut self, sc: &mut StatechartType, until: Until, output: &mut OutputCallback) {
-    'running: loop {
-      if let Some(Reverse(entry)) = self.queue.peek() {
-        // Check if event was removed
-        if let Some(Reverse(removed)) = self.removed.peek() {
-          if entry.id == *removed {
-            self.queue.pop();
-            self.removed.pop();
-            continue;
-          }
-        }
-        // Check if event too far in the future
-        if let Until::Timestamp(t) = until {
-          if entry.id.timestamp > t {
-            println!("break, timestamp {}, t {}", entry.id.timestamp, t);
-            break 'running;
-          }
-        }
-        // OK, handle event
-        self.simtime = entry.id.timestamp;
-        // eprintln!("time is now {}", self.simtime);
-        sc.big_step(Some(entry.event), self, output);
-        self.queue.pop();
-      }
-      else {
-        break 'running;
-      }
-    }
-  }
-}
-
-use std::ops::Deref;
-use std::ops::DerefMut;
-
-// This macro lets a struct "inherit" the data members of another struct
-// The inherited struct is added as a struct member and the Deref and DerefMut
-// traits are implemented to return a reference to the base struct
-#[macro_export]
-macro_rules! inherit_struct {
-    ($name: ident ($base: ty) { $($element: ident: $ty: ty),* $(,)? } ) => {
-        #[derive(Copy, Clone)]
-        struct $name {
-            _base: $base,
-            $($element: $ty),*
-        }
-        impl Deref for $name {
-            type Target = $base;
-            fn deref(&self) -> &$base {
-                &self._base
-            }
-        }
-        impl DerefMut for $name {
-            fn deref_mut(&mut self) -> &mut $base {
-                &mut self._base
-            }
-        }
-    }
-}
-
-// "Base struct" for all scopes
-#[derive(Copy, Clone)]
-pub struct Empty{}
-
-// A closure object is a pair of a functions first argument and that function.
-// The call may be part of an larger expression, and therefore we cannot just write 'let' statements to assign the pair's elements to identifiers which we need for the call.
-// This macro does exactly that, in an anonymous Rust closure, which is immediately called.
-#[macro_export]
-macro_rules! call_closure {
-  ($closure: expr, $($param: expr),*  $(,)?) => {
-    (||{
-      let scope = &mut $closure.0;
-      let function = &mut $closure.1;
-      return function($($param),* scope);
-    })()
-  };
-}

+ 27 - 17
src/sccd/statechart/codegen/rust.py

@@ -74,7 +74,7 @@ class StatechartRustGenerator(ActionLangRustGenerator):
 
     def visit_RaiseOutputEvent(self, a):
         # TODO: evaluate event parameters
-        self.w.writeln("(output)(OutEvent{port:\"%s\", event:\"%s\"});" % (a.outport, a.name))
+        self.w.writeln("(output)(statechart::OutEvent{port:\"%s\", event:\"%s\"});" % (a.outport, a.name))
 
     def visit_RaiseInternalEvent(self, a):
         self.w.writeln("internal.get_mut().%s = Some(%s{});" % (ident_event_field(a.name), (ident_event_type(a.name))))
@@ -118,7 +118,7 @@ class StatechartRustGenerator(ActionLangRustGenerator):
         self.w.writeln("impl %s {" % ident_type(state))
 
         # Enter actions: Executes enter actions of only this state
-        self.w.writeln("  fn enter_actions<Sched: Scheduler<InEvent>, OutputCallback: FnMut(OutEvent)>(timers: &mut Timers, data: &mut DataModel, internal: &mut InternalLifeline, sched: &mut Sched, output: &mut OutputCallback) {")
+        self.w.writeln("  fn enter_actions<TimerId: Copy, Sched: statechart::Scheduler<InEvent, TimerId>, OutputCallback: FnMut(statechart::OutEvent)>(timers: &mut Timers<TimerId>, data: &mut DataModel, internal: &mut InternalLifeline, sched: &mut Sched, output: &mut OutputCallback) {")
         if DEBUG:
             self.w.writeln("    eprintln!(\"enter %s\");" % state.full_name);
         self.w.writeln("    let scope = data;")
@@ -131,7 +131,7 @@ class StatechartRustGenerator(ActionLangRustGenerator):
         self.w.writeln("  }")
 
         # Enter actions: Executes exit actions of only this state
-        self.w.writeln("  fn exit_actions<Sched: Scheduler<InEvent>, OutputCallback: FnMut(OutEvent)>(timers: &mut Timers, data: &mut DataModel, internal: &mut InternalLifeline, sched: &mut Sched, output: &mut OutputCallback) {")
+        self.w.writeln("  fn exit_actions<TimerId: Copy, Sched: statechart::Scheduler<InEvent, TimerId>, OutputCallback: FnMut(statechart::OutEvent)>(timers: &mut Timers<TimerId>, data: &mut DataModel, internal: &mut InternalLifeline, sched: &mut Sched, output: &mut OutputCallback) {")
         self.w.writeln("    let scope = data;")
         for a in state.after_triggers:
             self.w.writeln("    sched.unset_timeout(timers[%d]);" % (a.after_id))
@@ -144,7 +144,7 @@ class StatechartRustGenerator(ActionLangRustGenerator):
         self.w.writeln("  }")
 
         # Enter default: Executes enter actions of entering this state and its default substates, recursively
-        self.w.writeln("  fn enter_default<Sched: Scheduler<InEvent>, OutputCallback: FnMut(OutEvent)>(timers: &mut Timers, data: &mut DataModel, internal: &mut InternalLifeline, sched: &mut Sched, output: &mut OutputCallback) {")
+        self.w.writeln("  fn enter_default<TimerId: Copy, Sched: statechart::Scheduler<InEvent, TimerId>, OutputCallback: FnMut(statechart::OutEvent)>(timers: &mut Timers<TimerId>, data: &mut DataModel, internal: &mut InternalLifeline, sched: &mut Sched, output: &mut OutputCallback) {")
         self.w.writeln("    %s::enter_actions(timers, data, internal, sched, output);" % (ident_type(state)))
         if isinstance(state.type, AndState):
             for child in state.real_children:
@@ -154,7 +154,7 @@ class StatechartRustGenerator(ActionLangRustGenerator):
         self.w.writeln("  }")
 
         # Exit current: Executes exit actions of this state and current children, recursively
-        self.w.writeln("  fn exit_current<Sched: Scheduler<InEvent>, OutputCallback: FnMut(OutEvent)>(&self, timers: &mut Timers, data: &mut DataModel, internal: &mut InternalLifeline, sched: &mut Sched, output: &mut OutputCallback) {")
+        self.w.writeln("  fn exit_current<TimerId: Copy, Sched: statechart::Scheduler<InEvent, TimerId>, OutputCallback: FnMut(statechart::OutEvent)>(&self, timers: &mut Timers<TimerId>, data: &mut DataModel, internal: &mut InternalLifeline, sched: &mut Sched, output: &mut OutputCallback) {")
         # first, children (recursion):
         if isinstance(state.type, AndState):
             for child in state.real_children:
@@ -169,7 +169,7 @@ class StatechartRustGenerator(ActionLangRustGenerator):
         self.w.writeln("  }")
 
         # Exit current: Executes enter actions of this state and current children, recursively
-        self.w.writeln("  fn enter_current<Sched: Scheduler<InEvent>, OutputCallback: FnMut(OutEvent)>(&self, timers: &mut Timers, data: &mut DataModel, internal: &mut InternalLifeline, sched: &mut Sched, output: &mut OutputCallback) {")
+        self.w.writeln("  fn enter_current<TimerId: Copy, Sched: statechart::Scheduler<InEvent, TimerId>, OutputCallback: FnMut(statechart::OutEvent)>(&self, timers: &mut Timers<TimerId>, data: &mut DataModel, internal: &mut InternalLifeline, sched: &mut Sched, output: &mut OutputCallback) {")
         # first, parent:
         self.w.writeln("    %s::enter_actions(timers, data, internal, sched, output);" % (ident_type(state)))
         # then, children (recursion):
@@ -189,6 +189,16 @@ class StatechartRustGenerator(ActionLangRustGenerator):
     def visit_Statechart(self, sc):
         self.scope.push(sc.scope)
 
+        self.w.writeln("use std::ops::Deref;")
+        self.w.writeln("use std::ops::DerefMut;")
+        self.w.writeln();
+        self.w.writeln("use sccd::action_lang;")
+        self.w.writeln("use sccd::inherit_struct;")
+        self.w.writeln("use sccd::call_closure;")
+        self.w.writeln("use sccd::statechart;")
+        self.w.writeln("use sccd::statechart::EventLifeline;")
+        self.w.writeln();
+
         if sc.semantics.concurrency == Concurrency.MANY:
             raise UnsupportedFeature("concurrency")
 
@@ -196,7 +206,7 @@ class StatechartRustGenerator(ActionLangRustGenerator):
 
         tree = sc.tree
 
-        self.w.writeln("type Timers = [EntryId; %d];" % tree.timer_count)
+        self.w.writeln("type Timers<TimerId> = [TimerId; %d];" % tree.timer_count)
         self.w.writeln()
 
         # Write event types
@@ -236,9 +246,9 @@ class StatechartRustGenerator(ActionLangRustGenerator):
             self.w.writeln("}")
 
             if internal_same_round:
-                self.w.writeln("type InternalLifeline = SameRoundLifeline<Internal>;")
+                self.w.writeln("type InternalLifeline = statechart::SameRoundLifeline<Internal>;")
             else:
-                self.w.writeln("type InternalLifeline = NextRoundLifeline<Internal>;")
+                self.w.writeln("type InternalLifeline = statechart::NextRoundLifeline<Internal>;")
         elif internal_type == "queue":
             pass
             # self.w.writeln("#[derive(Copy, Clone)]")
@@ -281,11 +291,11 @@ class StatechartRustGenerator(ActionLangRustGenerator):
         self.w.writeln()
 
         # Write statechart type
-        self.w.writeln("impl Default for Statechart {")
+        self.w.writeln("impl<TimerId: Default> Default for Statechart<TimerId> {")
         self.w.writeln("  fn default() -> Self {")
         self.w.writeln("    // Initialize data model")
         self.w.indent(); self.w.indent();
-        self.w.writeln("    let scope = Empty{};")
+        self.w.writeln("    let scope = action_lang::Empty{};")
         if sc.datamodel is not None:
             sc.datamodel.accept(self)
         datamodel_type = self.scope.commit(sc.scope.size(), self.w)
@@ -300,13 +310,13 @@ class StatechartRustGenerator(ActionLangRustGenerator):
         self.w.writeln("  }")
         self.w.writeln("}")
         self.w.writeln("type DataModel = %s;" % datamodel_type)
-        self.w.writeln("pub struct Statechart {")
+        self.w.writeln("pub struct Statechart<TimerId> {")
         self.w.writeln("  current_state: %s," % ident_type(tree.root))
         # We always store a history value as 'deep' (also for shallow history).
         # TODO: We may save a tiny bit of space in some rare cases by storing shallow history as only the exited child of the Or-state.
         for h in tree.history_states:
             self.w.writeln("  %s: %s," % (ident_history_field(h), ident_type(h.parent)))
-        self.w.writeln("  timers: Timers,")
+        self.w.writeln("  timers: Timers<TimerId>,")
         self.w.writeln("  data: DataModel,")
         self.w.writeln("}")
         self.w.writeln()
@@ -314,7 +324,7 @@ class StatechartRustGenerator(ActionLangRustGenerator):
         self.write_decls()
 
         # Function fair_step: a single "Take One" Maximality 'round' (= nonoverlapping arenas allowed to fire 1 transition)
-        self.w.writeln("fn fair_step<Sched: Scheduler<InEvent>, OutputCallback: FnMut(OutEvent)>(sc: &mut Statechart, input: Option<InEvent>, internal: &mut InternalLifeline, sched: &mut Sched, output: &mut OutputCallback, dirty: Arenas) -> Arenas {")
+        self.w.writeln("fn fair_step<TimerId: Copy, Sched: statechart::Scheduler<InEvent, TimerId>, OutputCallback: FnMut(statechart::OutEvent)>(sc: &mut Statechart<TimerId>, input: Option<InEvent>, internal: &mut InternalLifeline, sched: &mut Sched, output: &mut OutputCallback, dirty: Arenas) -> Arenas {")
         self.w.writeln("  let mut fired: Arenas = ARENA_NONE;")
         self.w.writeln("  let mut scope = &mut sc.data;")
         self.w.writeln("  let %s = &mut sc.current_state;" % ident_var(tree.root))
@@ -596,7 +606,7 @@ class StatechartRustGenerator(ActionLangRustGenerator):
 
         # Write combo step and big step function
         def write_stepping_function(name: str, title: str, maximality: Maximality, substep: str, cycle_input: bool, cycle_internal: bool):
-            self.w.writeln("fn %s<Sched: Scheduler<InEvent>, OutputCallback: FnMut(OutEvent)>(sc: &mut Statechart, input: Option<InEvent>, internal: &mut InternalLifeline, sched: &mut Sched, output: &mut OutputCallback, dirty: Arenas) -> Arenas {" % (name))
+            self.w.writeln("fn %s<TimerId: Copy, Sched: statechart::Scheduler<InEvent, TimerId>, OutputCallback: FnMut(statechart::OutEvent)>(sc: &mut Statechart<TimerId>, input: Option<InEvent>, internal: &mut InternalLifeline, sched: &mut Sched, output: &mut OutputCallback, dirty: Arenas) -> Arenas {" % (name))
             self.w.writeln("  // %s Maximality: %s" % (title, maximality))
             if maximality == Maximality.TAKE_ONE:
                 self.w.writeln("  %s(sc, input, internal, sched, output, dirty)" % (substep))
@@ -640,7 +650,7 @@ class StatechartRustGenerator(ActionLangRustGenerator):
         self.w.writeln()
 
         # Implement 'SC' trait
-        self.w.writeln("impl<Sched: Scheduler<InEvent>, OutputCallback: FnMut(OutEvent)> SC<InEvent, Sched, OutputCallback> for Statechart {")
+        self.w.writeln("impl<TimerId: Copy, Sched: statechart::Scheduler<InEvent, TimerId>, OutputCallback: FnMut(statechart::OutEvent)> statechart::SC<InEvent, TimerId, Sched, OutputCallback> for Statechart<TimerId> {")
         self.w.writeln("  fn init(&mut self, sched: &mut Sched, output: &mut OutputCallback) {")
         self.w.writeln("    %s::enter_default(&mut self.timers, &mut self.data, &mut Default::default(), sched, output)" % (ident_type(tree.root)))
         self.w.writeln("  }")
@@ -667,7 +677,7 @@ class StatechartRustGenerator(ActionLangRustGenerator):
                     write_state_size(child)
             write_state_size(tree.root)
             self.w.writeln("  eprintln!(\"info: InEvent: {} bytes\", size_of::<InEvent>());")
-            self.w.writeln("  eprintln!(\"info: OutEvent: {} bytes\", size_of::<OutEvent>());")
+            self.w.writeln("  eprintln!(\"info: statechart::OutEvent: {} bytes\", size_of::<statechart::OutEvent>());")
             self.w.writeln("  eprintln!(\"info: Arenas: {} bytes\", size_of::<Arenas>());")
             self.w.writeln("  eprintln!(\"------------------------\");")
             self.w.writeln("}")

+ 9 - 11
src/sccd/test/cmd/run.py

@@ -22,20 +22,19 @@ class Test(unittest.TestCase):
 
     # assume external statechart files in same directory as test
     
-    path = os.path.dirname(self.src)
-    sc_rules = functools.partial(statechart_parser_rules, path=path)
-    test_rules = test_parser_rules(sc_rules)
-    try:
-      with timer.Context("parse test"):
-        test_variants = parse_f(self.src, {"test" :test_rules})
-
 
+    try:
       if self.enable_rust:
-        from sccd.test.dynamic.test_rust import run_variants
-        run_variants(test_variants, self)
+        from sccd.test.dynamic.test_rust import run_rust_test
+        run_rust_test(self.src, self)
       else:
+        path = os.path.dirname(self.src)
+        sc_rules = functools.partial(statechart_parser_rules, path=path)
+        test_rules = test_parser_rules(sc_rules)
+        with timer.Context("parse test"):
+          test = parse_f(self.src, {"test" :test_rules})
         from sccd.test.dynamic.test_interpreter import run_variant
-        for v in test_variants:
+        for v in test.variants:
           run_variant(v, self)
 
     except Exception as e:
@@ -53,7 +52,6 @@ if __name__ == '__main__':
         description="Run SCCD tests.",
         epilog="Set environment variable SCCDDEBUG=1 to display debug information about the inner workings of the runtime.")
     argparser.add_argument('path', metavar='PATH', type=str, nargs='*', help="Tests to run. Can be a XML file or a directory. If a directory, it will be recursively scanned for XML files.")
-    argparser.add_argument('--build-dir', metavar='BUILD_DIR', type=str, default='build', help="Directory for built tests. Defaults to 'build'")
     argparser.add_argument('--rust', action='store_true', help="Instead of testing the interpreter, generate Rust code from test and run it. Depends on the 'rustc' command in your environment's PATH. Does not depend on Cargo.")
     args = argparser.parse_args()
 

+ 14 - 0
src/sccd/test/cmd/to_rust.py

@@ -0,0 +1,14 @@
+import argparse
+import sys
+import os
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser(
+        description="Generate Rust code. Rust code is written to stdout.")
+    parser.add_argument('--output', metavar='DIRNAME', type=str, default="codegen", help="Name of directory (Rust crate) to create.")
+    parser.add_argument('path', metavar='PATH', type=str, help="A SCCD statechart or test XML file. A statechart can be compiled to a Rust library. A test can be compiled to a Rust executable (the main-function runs the test).")
+    args = parser.parse_args()
+
+    from sccd.test.codegen.write_crate import write_crate
+    write_crate(args.path, args.output)
+    sys.stderr.write("Wrote rust crate '%s'.\n" % args.output)

+ 46 - 59
src/sccd/test/codegen/rust.py

@@ -1,71 +1,58 @@
 from sccd.test.static.syntax import *
-from sccd.statechart.codegen.rust import *
 from sccd.util.indenting_writer import *
+from sccd.cd.codegen.rust import ClassDiagramRustGenerator
+from sccd.statechart.codegen.rust import ident_event_type
 
-import os
-import sccd.statechart.codegen
-rustlib = os.path.dirname(sccd.statechart.codegen.__file__) + "/common.rs"
+class TestRustGenerator(ClassDiagramRustGenerator):
+    def __init__(self, w):
+        super().__init__(w)
 
-# class TestRustGenerator(StatechartRustGenerator):
-#     def visit_TestVariant(self, variant):
+    def visit_TestVariant(self, variant):
+        variant.cd.accept(self)
 
+        self.w.writeln("pub fn run() {")
+        self.w.indent()
 
-def compile_test(variants: List[TestVariant], w: IndentingWriter):
+        self.w.writeln("use sccd::controller;")
+        self.w.writeln("use sccd::statechart;")
+        self.w.writeln("use sccd::statechart::SC;")
+        self.w.writeln("use sccd::statechart::Scheduler;")
+        self.w.writeln();
 
-    # Note: The reason for these is that we cannot convert the casing of our state's names:
-    # SCCD allows any combination of upper and lower case symbols, and
-    # converting to, say, camelcase, as Rust likes it for type names,
-    # could cause naming collisions.
-    # Rust may output a ton of warnings for this. We disable these types of warnings,
-    # so that other, more interesting types of warnings don't go unnoticed.
-    w.writeln("#![allow(non_camel_case_types)]")
-    w.writeln("#![allow(non_snake_case)]")
-    w.writeln("#![allow(unused_labels)]")
-    w.writeln("#![allow(unused_variables)]")
-    w.writeln("#![allow(dead_code)]")
-    w.writeln("#![allow(unused_parens)]")
-    w.writeln("#![allow(unused_macros)]")
-    w.writeln("#![allow(non_upper_case_globals)]")
-    w.writeln("#![allow(unused_mut)]")
-    w.writeln("#![allow(unused_imports)]")
-
-    with open(rustlib, 'r') as file:
-        data = file.read()
-        w.writeln(data)
-
-    if len(variants) > 0:
-        cd = variants[0].cd
-        gen = StatechartRustGenerator(w, cd.globals)
-        cd.get_default_class().accept(gen)
-        # compile_statechart(cd.get_default_class(), cd.globals, w)
-
-
-    w.writeln("fn main() {")
-    w.indent()
-    if DEBUG:
-        w.writeln("debug_print_sizes();")
-
-    for n, v in enumerate(variants):
-        w.writeln("// Test variant %d" % n)
-        w.writeln("let mut raised = Vec::<OutEvent>::new();")
-        w.writeln("let mut output = |out: OutEvent| {")
+        self.w.writeln("let mut raised = Vec::<statechart::OutEvent>::new();")
+        self.w.writeln("let mut output = |out: statechart::OutEvent| {")
         if DEBUG:
-            w.writeln("  eprintln!(\"^{}:{}\", out.port, out.event);")
-        w.writeln("  raised.push(out);")
-        w.writeln("};")
-        w.writeln("let mut controller = Controller::<InEvent>::new();")
-        w.writeln("let mut sc: Statechart = Default::default();")
-        w.writeln("sc.init(&mut controller, &mut output);")
-        for i in v.input:
+            self.w.writeln("  eprintln!(\"^{}:{}\", out.port, out.event);")
+        self.w.writeln("  raised.push(out);")
+        self.w.writeln("};")
+        self.w.writeln("let mut controller = controller::Controller::<InEvent>::new();")
+        self.w.writeln("let mut sc: Statechart::<controller::TimerId> = Default::default();")
+        self.w.writeln("sc.init(&mut controller, &mut output);")
+        for i in variant.input:
             if len(i.events) > 1:
                 raise UnsupportedFeature("Multiple simultaneous input events not supported")
             elif len(i.events) == 0:
                 raise UnsupportedFeature("Test declares empty bag of input events")
-            w.writeln("controller.set_timeout(%d, InEvent::%s);" % (i.timestamp.opt, ident_event_type(i.events[0].name)))
-
-        w.writeln("controller.run_until(&mut sc, Until::Eternity, &mut output);")
-        w.writeln("assert_eq!(raised, [%s]);" % ", ".join('OutEvent{port:"%s", event:"%s"}' % (e.port, e.name) for o in v.output for e in o))
-        w.writeln("eprintln!(\"Test variant %d passed\");" % n)
-
-    w.dedent()
-    w.writeln("}")
+            self.w.writeln("controller.set_timeout(%d, InEvent::%s);" % (i.timestamp.opt, ident_event_type(i.events[0].name)))
+
+        self.w.writeln("controller.run_until(&mut sc, controller::Until::Eternity, &mut output);")
+        self.w.writeln("assert_eq!(raised, [%s]);" % ", ".join('statechart::OutEvent{port:"%s", event:"%s"}' % (e.port, e.name) for o in variant.output for e in o))
+
+        self.w.dedent()
+        self.w.writeln("}")
+
+    def visit_Test(self, test):
+        for i, v in enumerate(test.variants):
+            self.w.writeln("mod variant%d {" % i)
+            self.w.indent()
+            v.accept(self)
+            self.w.dedent()
+            self.w.writeln("}")
+            self.w.writeln()
+
+        self.w.writeln("fn main() {")
+        for i, v in enumerate(test.variants):
+            self.w.writeln("  variant%d::run();" % i)
+            self.w.writeln("  eprintln!(\"Test variant %d passed\");" % i)
+        self.w.writeln("}")
+        self.w.writeln()

+ 80 - 0
src/sccd/test/codegen/write_crate.py

@@ -0,0 +1,80 @@
+from sccd.cd.parser.xml import *
+from sccd.test.parser.xml import *
+from sccd.util.indenting_writer import *
+from functools import partial
+
+import sccd
+RUST_DIR = os.path.dirname(sccd.__file__) + "/../../rust"
+
+# High-level function, that takes a ...
+#  - statechart-xml
+#  - class-diagram-xml
+#  - test-xml
+# ... file and generates from it a Rust crate, which is written to filesystem as a directory.
+def write_crate(src, target):
+    path = os.path.dirname(src)
+
+
+    globals = Globals()
+
+    sc_parser_rules = partial(statechart_parser_rules, path=path, load_external=True)
+
+    rules = {
+        "statechart": sc_parser_rules(globals),
+        "single_instance_cd": cd_parser_rules(sc_parser_rules),
+        "test": test_parser_rules(sc_parser_rules),
+    }
+
+    parsed = parse_f(src, rules)
+
+    if not os.path.isdir(target):
+        os.mkdir(target)
+
+    with open(target+"/statechartgen.rs", 'w') as file:
+        w = IndentingWriter(out=file)
+
+        w.writeln("#![allow(non_camel_case_types)]")
+        w.writeln("#![allow(non_snake_case)]")
+        w.writeln("#![allow(unused_labels)]")
+        w.writeln("#![allow(unused_variables)]")
+        w.writeln("#![allow(dead_code)]")
+        w.writeln("#![allow(unused_parens)]")
+        w.writeln("#![allow(unused_macros)]")
+        w.writeln("#![allow(non_upper_case_globals)]")
+        w.writeln("#![allow(unused_mut)]")
+        w.writeln("#![allow(unused_imports)]")
+        w.writeln()
+
+        if isinstance(parsed, Statechart):
+            from sccd.statechart.codegen.rust import StatechartRustGenerator
+
+            gen = StatechartRustGenerator(w, globals)
+            parsed.accept(gen)
+
+        else:
+            from sccd.test.codegen.rust import TestRustGenerator
+
+            # can parse Class Diagrams and Tests:
+            gen = TestRustGenerator(w)
+            parsed.accept(gen)
+
+    with open(target+"/Cargo.toml", 'w') as file:
+        w = IndentingWriter(out=file)
+
+        w.writeln("[package]")
+        w.writeln("name = \"statechartgen\"")
+        w.writeln("version = \"0.1.0\"")
+        w.writeln("edition = \"2018\"")
+        w.writeln()
+        w.writeln("[dependencies]")
+        w.writeln("sccd = { path = \"%s\" }" % RUST_DIR)
+        w.writeln()
+        if isinstance(parsed, Test):
+            # Tests are compiled to binaries
+            w.writeln("[[bin]]")
+        else:
+            # Everything else becomes a library
+            w.writeln("[lib]")
+        w.writeln("name = \"statechartgen\"")
+        w.writeln("path = \"statechartgen.rs\"")
+        w.writeln()

+ 21 - 42
src/sccd/test/dynamic/test_rust.py

@@ -6,13 +6,17 @@ from unittest import SkipTest
 from typing import *
 from sccd.test.static.syntax import TestVariant
 from sccd.statechart.codegen.rust import UnsupportedFeature
-from sccd.test.codegen.rust import compile_test
+from sccd.test.codegen.write_crate import write_crate
 from sccd.util.indenting_writer import IndentingWriter
 from sccd.util.debug import *
 
+import os
+import sccd
+RUST_DIR = os.path.dirname(sccd.__file__) + "/../../rust"
+
 # Generate Rust code from the test case. This Rust code is piped to a Rust compiler (rustc) process, which reads from stdin. The Rust compiler outputs a binary in a temp dir. We then run the created binary as a subprocess.
 # If the result code of either the Rust compiler or the created binary is not 0 ("success"), the 'unittest' fails.
-def run_variants(variants: List[TestVariant], unittest):
+def run_rust_test(path: str, unittest):
     if DEBUG:
         stdout = None
         stderr = None
@@ -20,54 +24,29 @@ def run_variants(variants: List[TestVariant], unittest):
         stdout = subprocess.DEVNULL
         stderr = subprocess.STDOUT
 
-    output_file = os.path.join(tempfile.gettempdir(), "sccd_rust_out")
-    print_debug("Writing binary to " + output_file)
-
-    with subprocess.Popen(["rustc", "-o", output_file, "-"],
-        stdin=subprocess.PIPE,
-        stdout=stdout,
-        stderr=subprocess.PIPE) as pipe:
-
-        class PipeWriter:
-            def __init__(self, pipe):
-                self.pipe = pipe
-            def write(self, s):
-                self.pipe.stdin.write(s.encode(encoding='UTF-8'))
-
-        w = IndentingWriter(out=PipeWriter(pipe))
-
-        try:
-            compile_test(variants, w)
-        except UnsupportedFeature as e:
-            raise SkipTest("unsupported feature: " + str(e))
+    from hashlib import sha1
 
-        pipe.stdin.close()
+    output = tempfile.gettempdir() + "/sccd_test_crate"
 
-        print_debug("Generated Rust code.")
+    print_debug("Writing crate to " + output)
 
-        ruststderr = pipe.stderr.read().decode('UTF-8')
+    try:
+        write_crate(path, output)
+    except UnsupportedFeature as e:
+        raise SkipTest("unsupported feature: " + str(e))
 
-        status = pipe.wait()
+    print_debug("Done. Running crate...")
 
-        if DEBUG:
-            print(ruststderr)
-
-        if status != 0:
-            # This does not indicate a test failure, but an error in our code generator
-            raise Exception("Rust compiler status %d. Sterr:\n%s" % (status, ruststderr))
-
-    print_debug("Generated binary. Running...")
-
-    with subprocess.Popen([output_file],
+    with subprocess.Popen(["cargo", "run"],
+        cwd=output,
         stdout=stdout,
-        stderr=subprocess.PIPE) as binary:
-
-        binarystderr = binary.stderr.read().decode('UTF-8')
+        stderr=subprocess.PIPE) as cargo:
 
-        status = binary.wait()
+        cargostderr = cargo.stderr.read().decode('UTF-8')
+        status = cargo.wait()
 
         if DEBUG:
-            print(binarystderr)
+            print(cargostderr)
 
         if status != 0:
-            unittest.fail("Test status %d. Stderr:\n%s" % (status, binarystderr))
+            unittest.fail("Test status %d. Stderr:\n%s" % (status, cargostderr))

+ 2 - 2
src/sccd/test/parser/xml.py

@@ -92,12 +92,12 @@ def test_parser_rules(statechart_parser_rules):
         text += str(variant)
         return text
 
-      return [TestVariant(
+      return Test(variants=[TestVariant(
         name=variant_description(i, variant.semantics),
         cd=SingleInstanceCD(globals=globals, statechart=variant),
         input=input,
         output=output)
-      for i, variant in enumerate(variants)]
+      for i, variant in enumerate(variants)])
 
     sc_rules = statechart_parser_rules(globals)
     return ([("statechart", sc_rules), ("input?", parse_input), ("output?", parse_output)], finish_test)

+ 5 - 1
src/sccd/test/static/syntax.py

@@ -10,8 +10,12 @@ class TestInputBag:
   timestamp: Expression
 
 @dataclass
-class TestVariant:
+class TestVariant(Visitable):
   name: str
   cd: AbstractCD
   input: List[TestInputBag]
   output: List[List[OutputEvent]]
+
+@dataclass
+class Test(Visitable):
+  variants: List[TestVariant]