Prechádzať zdrojové kódy

Rust: implemented history

Joeri Exelmans 4 rokov pred
rodič
commit
117f0af01c

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 2 - 0
notes.txt


+ 6 - 0
src/sccd/cd/codegen/sccdlib.rs

@@ -5,10 +5,16 @@ use std::cmp::Ordering;
 type Timestamp = usize; // unsigned integer, platform's word size
 
 pub trait State<OutputCallback> {
+  // Execute enter actions of only this state
   fn enter_actions(output: &mut OutputCallback);
+  // Execute exit actions of only this state
   fn exit_actions(output: &mut OutputCallback);
+  // Execute enter actions of this state and its 'default' child(ren), recursively
   fn enter_default(output: &mut OutputCallback);
 
+  // Execute enter actions as if the configuration recorded in this state is being entered
+  fn enter_current(&self, output: &mut OutputCallback);
+  // Execute exit actions as if the configuration recorded in this state is being exited
   fn exit_current(&self, output: &mut OutputCallback);
 }
 

+ 7 - 4
src/sccd/statechart/codegen/code_generation.txt

@@ -32,16 +32,19 @@ Roadmap
     - no event parameters
 
 
-  Milestone 2: Minimal support for semantic variability:
+  (DONE) Milestone 2: Minimal support for semantic variability:
 
     - (DONE) Priority: Child-first
-    - Big-Step Maximality: Take Many
+    - (DONE) Big-Step Maximality: Take Many
 
-    - goal: following tests should pass:
-        (DONE) semantics/priority/test_source_child.xml
+    - (DONE) goal: following tests should pass:
+        semantics/priority/test_source_child.xml
         semantics/big_step_maximality/test_flat_takemany.xml
         semantics/big_step_maximality/test_ortho_takemany.xml
 
+
+  Milestone 3: Support history
+
 Insights
 ========
 

+ 150 - 75
src/sccd/statechart/codegen/rust.py

@@ -43,6 +43,9 @@ def ident_arena_label(state: State) -> str:
     else:
         return "arena" + snake_case(state)
 
+def ident_history_field(state: HistoryState) -> str:
+    return "history" + snake_case(state.parent) # A history state records history value for its parent
+
 # Name of the output callback parameter, everywhere
 IDENT_OC = "_output" # underscore to keep Rust from warning us for unused variable
 
@@ -52,7 +55,7 @@ def compile_actions(actions: List[Action], w: IndentingWriter):
             # TODO: evaluate event parameters
             w.writeln("%s(\"%s\", \"%s\");" % (IDENT_OC, a.outport, a.name))
         else:
-            w.writeln("println!(\"UNIMPLEMENTED: %s\");" % a.render())
+            w.writeln("panic!(\"Unimplemented action %s (%s)\");" % (type(a), a.render()))
 
 def compile_statechart(sc: Statechart, globals: Globals, w: IndentingWriter):
 
@@ -77,18 +80,21 @@ def compile_statechart(sc: Statechart, globals: Globals, w: IndentingWriter):
         if isinstance(state, HistoryState):
             return None # we got no time for pseudo-states!
 
+        w.writeln("#[allow(non_camel_case_types)]")
+        w.writeln("#[derive(Copy, Clone)]")
+
         def as_struct():
-            w.writeln("#[allow(non_camel_case_types)]")
             w.writeln("struct %s {" % ident_type(state))
             for child in children:
-                w.writeln("  %s: %s," % (ident_field(child), ident_type(child)))
+                if child is not None:
+                    w.writeln("  %s: %s," % (ident_field(child), ident_type(child)))
             w.writeln("}")
 
         def as_enum():
-            w.writeln("#[allow(non_camel_case_types)]")
             w.writeln("enum %s {" % ident_type(state))
             for child in children:
-                w.writeln("  %s(%s)," % (ident_enum_variant(child), ident_type(child)))
+                if child is not None:
+                    w.writeln("  %s(%s)," % (ident_enum_variant(child), ident_type(child)))
             w.writeln("}")
 
         if isinstance(state, ParallelState):
@@ -116,8 +122,6 @@ def compile_statechart(sc: Statechart, globals: Globals, w: IndentingWriter):
         return state
 
 
-    # Write "enter/exit state" functions
-
     # Write "default" constructor
 
     def write_default(state: State, children: List[State]):
@@ -133,7 +137,8 @@ def compile_statechart(sc: Statechart, globals: Globals, w: IndentingWriter):
         if isinstance(state, ParallelState):
             w.writeln("    Self {")
             for child in children:
-                w.writeln("      %s: Default::default()," % (ident_field(child)))
+                if child is not None:
+                    w.writeln("      %s: Default::default()," % (ident_field(child)))
             w.writeln("    }")
         elif isinstance(state, State):
             if state.default_state is not None:
@@ -148,6 +153,8 @@ def compile_statechart(sc: Statechart, globals: Globals, w: IndentingWriter):
         w.writeln()
         return state
 
+    # Write "enter/exit state" functions
+
     def write_enter_exit(state: State, children: List[State]):
         if isinstance(state, HistoryState):
             return None # we got no time for pseudo-states!
@@ -172,21 +179,44 @@ def compile_statechart(sc: Statechart, globals: Globals, w: IndentingWriter):
         w.writeln("    %s::enter_actions(%s);" % (ident_type(state), IDENT_OC))
         if isinstance(state, ParallelState):
             for child in children:
-                w.writeln("    %s::enter_default(%s);" % (ident_type(child), IDENT_OC))
+                if child is not None:
+                    w.writeln("    %s::enter_default(%s);" % (ident_type(child), IDENT_OC))
         else:
             if state.default_state is not None:
                 w.writeln("    %s::enter_default(%s);" % (ident_type(state.default_state), IDENT_OC))
         w.writeln("  }")
 
         w.writeln("  fn exit_current(&self, %s: &mut OutputCallback) {" % IDENT_OC)
+        # Children's exit actions
+        if isinstance(state, ParallelState):
+            for child in children:
+                if child is not None:
+                    w.writeln("    self.%s.exit_current(%s);" % (ident_field(child), IDENT_OC))
+        else:
+            if len(children) > 0:
+                w.writeln("    match self {")
+                for child in children:
+                    if child is not None:
+                        w.writeln("      Self::%s(s) => { s.exit_current(%s); }," % (ident_enum_variant(child), IDENT_OC))
+                w.writeln("    }")
+        # Our own exit actions
+        w.writeln("    %s::exit_actions(%s);" % (ident_type(state), IDENT_OC))
+        w.writeln("  }")
+
+        w.writeln("  fn enter_current(&self, %s: &mut OutputCallback) {" % IDENT_OC)
+        # Children's enter actions
+        w.writeln("    %s::enter_actions(%s);" % (ident_type(state), IDENT_OC))
+        # Our own enter actions
         if isinstance(state, ParallelState):
             for child in children:
-                w.writeln("    self.%s.exit_current(%s);" % (ident_field(child), IDENT_OC))
+                if child is not None:
+                    w.writeln("    self.%s.enter_current(%s);" % (ident_field(child), IDENT_OC))
         else:
             if len(children) > 0:
                 w.writeln("    match self {")
                 for child in children:
-                    w.writeln("      Self::%s(s) => { s.exit_current(%s); }," % (ident_enum_variant(child), IDENT_OC))
+                    if child is not None:
+                        w.writeln("      Self::%s(s) => { s.enter_current(%s); }," % (ident_enum_variant(child), IDENT_OC))
                 w.writeln("    }")
         w.writeln("  }")
 
@@ -214,18 +244,22 @@ def compile_statechart(sc: Statechart, globals: Globals, w: IndentingWriter):
     # Write statechart type
     w.writeln("pub struct Statechart {")
     w.writeln("  current_state: %s," % ident_type(tree.root))
-    w.writeln("  // TODO: history values")
+    if len(tree.history_states) > 0:
+        # w.writeln("  // History values")
+        for h in tree.history_states:
+            w.writeln("  %s: %s," % (ident_history_field(h), ident_type(h.parent)))
     w.writeln("  // TODO: timers")
     w.writeln("}")
     w.writeln()
 
     w.writeln("impl Default for Statechart {")
     w.writeln("  fn default() -> Self {")
-    w.writeln("    return Self{")
+    w.writeln("    Self {")
     w.writeln("      current_state: Default::default(),")
-    # w.writeln("      history: Default::default(),")
+    for h in tree.history_states:
+        w.writeln("      %s: Default::default()," % (ident_history_field(h)))
     # w.writeln("      timers: Default::default(),")
-    w.writeln("    };")
+    w.writeln("    }")
     w.writeln("  }")
     w.writeln("}")
     w.writeln()
@@ -237,6 +271,7 @@ def compile_statechart(sc: Statechart, globals: Globals, w: IndentingWriter):
     w.writeln("  fn fair_step(&mut self, _event: Option<Event>, %s: &mut OutputCallback) -> bool {" % IDENT_OC)
     w.writeln("    #![allow(non_snake_case)]")
     w.writeln("    #![allow(unused_labels)]")
+    w.writeln("    #![allow(unused_variables)]")
     w.writeln("    println!(\"fair step\");")
     w.writeln("    let mut fired = false;")
     w.writeln("    let %s = &mut self.current_state;" % ident_var(tree.root))
@@ -261,79 +296,121 @@ def compile_statechart(sc: Statechart, globals: Globals, w: IndentingWriter):
         #    +--> S1 also on exit path
         #
         # (2) The descendants of S, if S is the transition target
+        #
+        # The same applies to entering states.
+
+        # Writes statements that perform exit actions
+        # in the correct order (children (last to first), then parent) for given 'exit path'.
         def write_exit(exit_path: List[State]):
+            # w.writeln("println!(\"exit path = %s\");" % str(exit_path).replace('"', "'"))
             if len(exit_path) > 0:
-                s = exit_path[0]
-                if isinstance(s, HistoryState):
-                    raise Exception("Can't deal with history yet!")
-                elif isinstance(s, ParallelState):
-                    for c in reversed(s.children):
-                        if exit_path[1] is c:
-                            write_exit(exit_path[1:]) # continue recursively
-                        else:
-                            w.writeln("%s.exit_current(%s);" % (ident_var(c), IDENT_OC))
-                elif isinstance(s, State):
-                    if s.default_state is not None:
-                        # Or-state
-                        write_exit(exit_path[1:]) # continue recursively with the next child on the exit path
-                w.writeln("%s::exit_actions(%s);" % (ident_type(s), IDENT_OC))
+                s = exit_path[0] # state to exit
 
-        def write_new_configuration(enter_path: List[State]):
-            if len(enter_path) > 0:
-                s = enter_path[0]
-                if len(enter_path) == 1:
-                    # Construct target state.
-                    # Whatever the type of parent (And/Or/Basic), just construct the default value:
-                    w.writeln("let new_%s: %s = Default::default();" % (ident_var(s), ident_type(s)))
+                if len(exit_path) == 1:
+                    # Exit s:
+                    w.writeln("%s.exit_current(%s);" % (ident_var(s), IDENT_OC))
                 else:
+                    # Exit children:
                     if isinstance(s, ParallelState):
-                        for c in s.children:
-                            if enter_path[1] is c:
-                                write_new_configuration(enter_path[1:]) # recurse
+                        for c in reversed(s.children):
+                            if exit_path[1] is c:
+                                write_exit(exit_path[1:]) # continue recursively
                             else:
-                                # Other children's default states are constructed
-                                w.writeln("let new_%s: %s = Default::default();" % (ident_var(c), ident_type(c)))
-                        # Construct struct
-                        w.writeln("let new_%s = %s{%s:%s, ..Default::default()};" % (ident_var(s), ident_type(s), ident_field(enter_path[1]), ident_var(enter_path[1])))
-
+                                w.writeln("%s.exit_current(%s);" % (ident_var(c), IDENT_OC))
                     elif isinstance(s, State):
-                        if len(s.children) > 0:
+                        if s.default_state is not None:
                             # Or-state
-                            write_new_configuration(enter_path[1:]) # recurse
-                            w.writeln("let new_%s = %s::%s(new_%s);" % (ident_var(s), ident_type(s), ident_enum_variant(enter_path[1]), ident_var(enter_path[1])))
-                        else:
-                            # The following should never occur
-                            # The parser should have rejected the model before we even get here
-                            raise Exception("Basic state in the middle of enter path")
+                            write_exit(exit_path[1:]) # continue recursively with the next child on the exit path
+
+                    # Exit s:
+                    w.writeln("%s::exit_actions(%s);" % (ident_type(s), IDENT_OC))
 
-        # Comment of 'write_exit' applies here as well
+                # Store history
+                if s.opt.deep_history:
+                    _, _, h = s.opt.deep_history
+                    w.writeln("self.%s = *%s; // Store deep history" % (ident_history_field(h), ident_var(s)))
+                if s.opt.shallow_history:
+                    _, h = s.opt.shallow_history
+                    if isinstance(s, ParallelState):
+                        raise Exception("Shallow history makes no sense for And-state!")
+                    w.writeln("self.%s = match %s { // Store shallow history" % (ident_history_field(h), ident_var(s)))
+                    for c in s.children:
+                        if not isinstance(c, HistoryState):
+                            w.writeln("  %s::%s(_) => %s::%s(%s::default())," % (ident_type(s), ident_enum_variant(c), ident_type(s), ident_enum_variant(c), ident_type(c)))
+                    w.writeln("};")
+                    # w.writeln("println!(\"recorded history\");")
+
+        # Writes statements that perform enter actions
+        # in the correct order (parent, children (first to last)) for given 'enter path'.
         def write_enter(enter_path: List[State]):
             if len(enter_path) > 0:
-                s = enter_path[0]
+                s = enter_path[0] # state to enter
                 if len(enter_path) == 1:
                     # Target state.
-                    w.writeln("%s::enter_default(%s);" % (ident_type(s), IDENT_OC))
+                    if isinstance(s, HistoryState):
+                        w.writeln("self.%s.enter_current(%s); // Enter actions for history state" %(ident_history_field(s), IDENT_OC))
+                    else:
+                        w.writeln("%s::enter_default(%s);" % (ident_type(s), IDENT_OC))
                 else:
+                    # Enter s:
+                    w.writeln("%s::enter_actions(%s);" % (ident_type(s), IDENT_OC))
+                    # Enter children:
                     if isinstance(s, ParallelState):
                         for c in s.children:
                             if enter_path[1] is c:
-                                w.writeln("%s::enter_actions(%s);" % (ident_type(c), IDENT_OC))
+                                # if not isinstance(c, HistoryState):
+                                #     w.writeln("%s::enter_actions(%s);" % (ident_type(c), IDENT_OC))
                                 write_enter(enter_path[1:]) # continue recursively
                             else:
                                 w.writeln("%s::enter_default(%s);" % (ident_type(c), IDENT_OC))
                     elif isinstance(s, State):
                         if len(s.children) > 0:
-                            # Or-state
-                            w.writeln("%s::enter_actions(%s);" % (ident_type(s), IDENT_OC))
                             write_enter(enter_path[1:]) # continue recursively with the next child on the enter path
                         else:
-                            # The following should never occur
-                            # The parser should have rejected the model before we even get here
+                            # If the following occurs, there's a bug in this source file
                             raise Exception("Basic state in the middle of enter path")
 
+        # The 'state' of a state is just a value in our compiled code.
+        # When executing a transition, the value of the transition's arena changes.
+        # This function writes statements that build a new value that can be assigned to the arena.
+        def write_new_configuration(enter_path: List[State]):
+            if len(enter_path) > 0:
+                s = enter_path[0]
+                if len(enter_path) == 1:
+                    # Construct target state.
+                    # And/Or/Basic state: Just construct the default value:
+                    w.writeln("let new_%s: %s = Default::default();" % (ident_var(s), ident_type(s)))
+                else:
+                    next_child = enter_path[1]
+                    if isinstance(next_child, HistoryState):
+                        # No recursion
+                        w.writeln("let new_%s = self.%s; // Restore history value" % (ident_var(s), ident_history_field(next_child)))
+                    else:
+                        if isinstance(s, ParallelState):
+                            for c in s.children:
+                                if next_child is c:
+                                    write_new_configuration(enter_path[1:]) # recurse
+                                else:
+                                    # Other children's default states are constructed
+                                    w.writeln("let new_%s: %s = Default::default();" % (ident_var(c), ident_type(c)))
+                            # Construct struct
+                            # if isinstance(next_child, HistoryState):
+                            #     w.writeln("let new_%s = %s{%s:%s, ..Default::default()};" % (ident_var(s), ident_type(s), ident_field(next_child), ident_var(next_child)))
+                            # else:
+                            w.writeln("let new_%s = %s{%s:%s, ..Default::default()};" % (ident_var(s), ident_type(s), ident_field(next_child), ident_var(next_child)))
+                        elif isinstance(s, State):
+                            if len(s.children) > 0:
+                                # Or-state
+                                write_new_configuration(enter_path[1:]) # recurse
+                                # Construct enum value
+                                w.writeln("let new_%s = %s::%s(new_%s);" % (ident_var(s), ident_type(s), ident_enum_variant(next_child), ident_var(next_child)))
+                            else:
+                                # If the following occurs, there's a bug in this source file
+                                raise Exception("Basic state in the middle of enter path")
+
         def parent():
-            for t in state.transitions:
-                w.writeln("// Outgoing transition")
+            for i, t in enumerate(state.transitions):
+                w.writeln("// Outgoing transition %d" % i)
 
                 if t.trigger is not EMPTY_TRIGGER:
                     if len(t.trigger.enabling) > 1:
@@ -382,17 +459,21 @@ def compile_statechart(sc: Statechart, globals: Globals, w: IndentingWriter):
                     w.writeln("}")
 
         def child():
+            # Here is were we recurse and write the transition code for the children of our 'state'.
             if isinstance(state, ParallelState):
                 for child in state.children:
-                    w.writeln("// Orthogonal region")
-                    w.writeln("let %s = &mut %s.%s;" % (ident_var(child), ident_var(state), ident_field(child)))
-                    write_transitions(child)
+                    if not isinstance(child, HistoryState):
+                        w.writeln("// Orthogonal region")
+                        w.writeln("let %s = &mut %s.%s;" % (ident_var(child), ident_var(state), ident_field(child)))
+                        write_transitions(child)
             elif isinstance(state, State):
                 if state.default_state is not None:
                     w.writeln("'%s: loop {" % ident_arena_label(state))
                     w.indent()
                     w.writeln("match %s {" % ident_var(state))
                     for child in state.children:
+                        if isinstance(child, HistoryState):
+                            continue
                         w.indent()
                         w.writeln("%s::%s(%s) => {" % (ident_type(state), ident_enum_variant(child), ident_var(child)))
                         w.indent()
@@ -434,11 +515,15 @@ def compile_statechart(sc: Statechart, globals: Globals, w: IndentingWriter):
         w.writeln("    self.fair_step(event, %s);" % IDENT_OC)
     elif sc.semantics.big_step_maximality == Maximality.TAKE_MANY:
         w.writeln("    // Big-Step Maximality: Take Many")
+        w.writeln("    let mut e = event;")
         w.writeln("    loop {")
-        w.writeln("      let fired = self.fair_step(event, %s);" % IDENT_OC)
+        w.writeln("      let fired = self.fair_step(e, %s);" % IDENT_OC)
         w.writeln("      if !fired {")
         w.writeln("        break;")
         w.writeln("      }")
+        if sc.semantics.input_event_lifeline != InputEventLifeline.WHOLE:
+            w.writeln("      // Input Event Lifeline: %s" % sc.semantics.input_event_lifeline)
+            w.writeln("      e = None;")
         w.writeln("    }")
     else:
         raise Exception("Unsupported semantics %s" % sc.semantics.big_step_maximality)
@@ -446,13 +531,3 @@ def compile_statechart(sc: Statechart, globals: Globals, w: IndentingWriter):
 
     w.writeln("}")
     w.writeln()
-
-
-    # # See if it works
-    # w.writeln("fn main() {")
-    # w.writeln("  let mut sc: Statechart = Default::default();")
-    # w.writeln("  Root::enter_default();")
-    # w.writeln("  sc.fair_step(None);")
-    # w.writeln("  sc.fair_step(None);")
-    # w.writeln("}")
-    # w.writeln()

+ 2 - 2
src/sccd/statechart/dynamic/statechart_execution.py

@@ -72,10 +72,10 @@ class StatechartExecution:
                         print_debug(termcolor.colored('  EXIT %s' % s.opt.full_name, 'green'))
                         if s.opt.deep_history is not None:
                             # s has a deep-history child:
-                            history_id, history_mask = s.opt.deep_history
+                            history_id, history_mask, _ = s.opt.deep_history
                             self.history_values[history_id] = exit_ids & history_mask
                         if s.opt.shallow_history is not None:
-                            history_id = s.opt.shallow_history
+                            history_id, _ = s.opt.shallow_history
                             self.history_values[history_id] = just_exited.opt.effective_targets
                         self._cancel_timers(s.opt.after_triggers)
                         _perform_actions(ctx, s.exit)

+ 9 - 7
src/sccd/statechart/static/tree.py

@@ -213,11 +213,11 @@ class StateStatic(Freezable):
 
         self.effective_targets: Bitmap = Bitmap()
 
-        # If a direct child of this state is a deep history state, then "deep history" needs to be recorded when exiting this state. This value contains a tuple, with the (history-id, history_mask) of that child state.
-        self.deep_history: Optional[Tuple[int, Bitmap]] = None
+        # If a direct child of this state is a deep history state, then "deep history" needs to be recorded when exiting this state. This value contains a tuple, with the (history-id, history_mask, history state) of that child state.
+        self.deep_history: Optional[Tuple[int, Bitmap, DeepHistoryState]] = None
 
         # If a direct child of this state is a shallow history state, then "shallow history" needs to be recorded when exiting this state. This value is the history-id of that child state
-        self.shallow_history: Optional[int] = None
+        self.shallow_history: Optional[Tuple[int, ShallowHistoryState]] = None
 
         # Triggers of outgoing transitions that are AfterTrigger.
         self.after_triggers: List[AfterTrigger] = []
@@ -242,7 +242,7 @@ class TransitionStatic(Freezable):
 
 
 class StateTree(Freezable):
-    __slots__ = ["root", "transition_list", "state_list", "state_dict", "timer_count", "initial_history_values", "initial_states"]
+    __slots__ = ["root", "transition_list", "state_list", "state_dict", "timer_count", "history_states", "initial_history_values", "initial_states"]
 
     def __init__(self, root: State):
         super().__init__()
@@ -252,7 +252,8 @@ class StateTree(Freezable):
         self.state_list = []
         self.transition_list = []
         self.timer_count = 0 # number of after-transitions in the statechart
-        self.initial_history_values = []
+        self.history_states: List[HistoryState] = []
+        self.initial_history_values: List[Bitmap] = []
 
         with timer.Context("optimize tree"):
 
@@ -311,12 +312,13 @@ class StateTree(Freezable):
             def deal_with_history(state: State, children_history):
                 for h in children_history:
                     if isinstance(h, DeepHistoryState):
-                        state.opt.deep_history = (history_ids[h], h.parent.opt.descendants)
+                        state.opt.deep_history = (history_ids[h], h.parent.opt.descendants, h)
                     elif isinstance(h, ShallowHistoryState):
-                        state.opt.shallow_history = history_ids[h]
+                        state.opt.shallow_history = (history_ids[h], h)
 
                 if isinstance(state, HistoryState):
                     history_ids[state] = len(self.initial_history_values) # generate history ID
+                    self.history_states.append(state)
                     self.initial_history_values.append(state.parent.opt.effective_targets)
                     return state
 

+ 1 - 1
src/sccd/test/cmd/run.py

@@ -53,7 +53,7 @@ if __name__ == '__main__':
         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.")
+    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()
 
     src_files = get_files(args.path,

+ 10 - 9
src/sccd/test/dynamic/test_rust.py

@@ -13,7 +13,6 @@ from sccd.util.debug import *
 def run_variants(variants: List[TestVariant], unittest):
     if DEBUG:
         stdout = None
-        # stderr = subprocess.STDOUT
         stderr = None
     else:
         stdout = subprocess.DEVNULL
@@ -25,7 +24,7 @@ def run_variants(variants: List[TestVariant], unittest):
     with subprocess.Popen(["rustc", "-o", output_file, "-"],
         stdin=subprocess.PIPE,
         stdout=stdout,
-        stderr=stderr) as pipe:
+        stderr=subprocess.PIPE) as pipe:
 
         class PipeWriter:
             def __init__(self, pipe):
@@ -39,19 +38,21 @@ def run_variants(variants: List[TestVariant], unittest):
 
         pipe.stdin.close()
 
-        print_debug("Done generating Rust code")
+        print_debug("Generated Rust code.")
 
-        pipe.wait()
+        status = pipe.wait()
 
-    print_debug("Done generating binary")
+        if status != 0:
+            # This does not indicate a test failure, but an error in our code generator
+            raise Exception("Rust compiler status %d. Sterr:" % (status, pipe.stderr.read().decode('UTF-8')))
+
+    print_debug("Generated binary. Running...")
 
     with subprocess.Popen([output_file],
-        stdout=subprocess.PIPE,
+        stdout=stdout,
         stderr=subprocess.PIPE) as binary:
 
         status = binary.wait()
 
         if status != 0:
-            unittest.fail("Status code %d:\n%s%s" % (status, binary.stdout.read().decode('UTF-8'), binary.stderr.read().decode('UTF-8')))
-
-    print_debug("Done running binary")
+            unittest.fail("Test status %d. Stderr:\n%s" % (status, binary.stderr.read().decode('UTF-8')))