Просмотр исходного кода

Lots of earlier, uncommitted changes

Joeri Exelmans 5 лет назад
Родитель
Сommit
40300fd648
32 измененных файлов с 1425 добавлено и 248 удалено
  1. 18 0
      examples/chatroom/chatclient.xml
  2. 16 0
      examples/chatroom/chatserver.xml
  3. 32 0
      examples/chatroom/chatwindowGUI.py
  4. 122 0
      examples/chatroom/classes/chatserver.xml
  5. 255 0
      examples/chatroom/classes/model.xml
  6. 216 0
      examples/chatroom/classes/networkclient.xml
  7. 95 0
      examples/chatroom/classes/networkserver.xml
  8. 109 0
      examples/chatroom/classes/networkserversocket.xml
  9. 15 0
      examples/chatroom/classes/port2event.xml
  10. 34 0
      examples/chatroom/run_client.py
  11. 19 0
      examples/chatroom/run_server.py
  12. 52 0
      examples/chatroom/scrollable_frame.py
  13. 85 0
      examples/chatroom/socket2event.py
  14. 2 2
      examples/digitalwatch/run.py
  15. 29 14
      src/sccd/action_lang/cmd/prompt.py
  16. 14 6
      src/sccd/action_lang/parser/action_lang.g
  17. 14 4
      src/sccd/action_lang/parser/text.py
  18. 0 1
      src/sccd/action_lang/static/expression.py
  19. 1 1
      src/sccd/action_lang/static/scope.py
  20. 7 2
      src/sccd/action_lang/static/statement.py
  21. 6 6
      src/sccd/action_lang/static/types.py
  22. 73 57
      src/sccd/statechart/dynamic/round.py
  23. 21 24
      src/sccd/statechart/dynamic/statechart_execution.py
  24. 8 3
      src/sccd/statechart/dynamic/statechart_instance.py
  25. 3 3
      src/sccd/statechart/static/action.py
  26. 135 87
      src/sccd/statechart/static/tree.py
  27. 1 0
      src/sccd/test/run.py
  28. 4 1
      src/sccd/util/bitmap.py
  29. 11 35
      src/sccd/util/visit_tree.py
  30. 3 1
      test/test_files/day_atlee/statechart_fig1_redialer.xml
  31. 24 0
      test/test_files/day_atlee/test_04_counter_many_srcdstortho.xml
  32. 1 1
      test/test_files/features/action_lang/test_nested.xml

+ 18 - 0
examples/chatroom/chatclient.xml

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<diagram author="Yentl Van Tendeloo" name="Chat window - Tkinter version">
+    <description>
+        Tkinter chat client, with some fault-tolerance when the server goes down.
+    </description>
+    <top>
+        import socket
+        import json
+        import httplib
+    </top>
+
+    <inport name="tkinter_input"/>
+    <inport name="socket_in"/>
+    <outport name="socket_out"/>
+
+    <class src="classes/model.xml" default="true"/>
+    <class src="classes/networkclient.xml"/>
+</diagram>

+ 16 - 0
examples/chatroom/chatserver.xml

@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+<diagram author="Yentl Van Tendeloo" name="Chat Server">
+    <description>
+    </description>
+
+    <top>
+        import sys
+    </top>
+
+    <inport name="socket_in"/>
+    <outport name="socket_out"/>
+
+    <class src="classes/chatserver.xml" default="true"/>
+    <class src="classes/networkserver.xml"/>
+    <class src="classes/networkserversocket.xml"/>
+</diagram>

+ 32 - 0
examples/chatroom/chatwindowGUI.py

@@ -0,0 +1,32 @@
+import Tkinter as tk
+import scrollable_frame
+
+class ChatWindowGUI(tk.Tk):
+	def __init__(self, keypress):
+		tk.Tk.__init__(self)
+		self.resizable(width=tk.FALSE, height=tk.FALSE)
+		self.width = 230
+		self.height = 100
+		self.labelwidth = 30
+
+		self.frame = tk.Frame(self)
+		self.frame.focus_set()
+		self.frame.bind('<Key>', keypress)
+		self.chat_field = scrollable_frame.VerticalScrolledFrame(self.frame, bd='2', height=self.height, width=self.width, relief=tk.RIDGE)
+		tk.Label(self.chat_field.interior, text='SCCD Chat Client -- Tk version', justify=tk.LEFT, anchor=tk.NW, width=self.labelwidth).pack()
+		self.tk_buffer = tk.StringVar()
+		input_frame = tk.Frame(self.frame, bd='2', height=100, width=self.width, relief=tk.RIDGE)
+		self.input_text = tk.Label(input_frame, textvar=self.tk_buffer, anchor=tk.NW, justify=tk.LEFT, wraplength=self.width, width=self.labelwidth, background='grey')
+		self.chat_field.pack(anchor=tk.NW)
+		input_frame.pack(anchor=tk.NW, fill=tk.X)
+		self.input_text.pack(anchor=tk.NW, fill=tk.X)
+		self.frame.pack(anchor=tk.NW)
+	
+	def redraw_buffer(self, text):
+		self.tk_buffer.set(text)
+	
+	def setColor(self, color):
+		self.input_text.configure(background=color)
+		
+	def addMessage(self, msg, color):
+		tk.Label(self.chat_field.interior, text=msg, anchor=tk.NW, justify=tk.LEFT, foreground=color, wraplength=230, width=30).pack(anchor=tk.NW)

+ 122 - 0
examples/chatroom/classes/chatserver.xml

@@ -0,0 +1,122 @@
+<class name="ChatServer">
+    <relationships>
+        <association name="server" class="NetworkServer"/>
+    </relationships>
+
+    <constructor>
+        <parameter name="port"/>
+        <body>
+            <![CDATA[
+            self.room2clients = {}
+            self.client2room = {}
+            self.serverinstance = None
+            self.queue = []
+            self.port = port
+
+            self.tmp_sendlist = []
+            self.tmp_data = None
+            ]]>
+        </body>
+    </constructor>
+
+    <scxml>
+        <parallel id="server">
+            <state id="processor" initial="init">
+                <state id="init">
+                    <onentry>
+                        <raise scope="cd" event="create_instance">
+                            <parameter expr="'server'"/>
+                            <parameter expr="'NetworkServer'"/>
+                            <parameter expr="self.port"/>
+                        </raise>
+                    </onentry>
+                    <transition event="instance_created" target="../main">
+                        <parameter name="instancename"/>
+                        <script>
+                            self.serverinstance = instancename
+                        </script>
+                        <raise scope="narrow" event="set_association_name" target="instancename">
+                            <parameter expr="instancename"/>
+                        </raise>
+                        <raise scope="cd" event="start_instance">
+                            <parameter expr="instancename"/>
+                        </raise>
+                    </transition>
+                </state>
+                <state id="main">
+                    <transition cond="len(self.queue) > 0 and self.queue[0][1].startswith('ACK ')" target="."/>
+                    <transition cond="len(self.queue) > 0 and self.queue[0][1].startswith('MSG ')" target="../forwarding">
+                        <script>
+                            socket, data = self.queue.pop(0)
+                            roomnumber = self.client2room[socket]
+                            self.tmp_sendlist = set(self.room2clients[roomnumber])
+                            # Don't send to the original sender
+                            # This also prevents infinite propagation to the other server
+                            self.tmp_sendlist.remove(socket)
+                            self.tmp_sendlist = list(self.tmp_sendlist)
+                            self.tmp_data = data
+                        </script>
+                        <raise scope="broad" event="server_input">
+                            <parameter expr="socket"/>
+                            <parameter expr="'ACK MSG'"/>
+                        </raise>
+                    </transition>
+                    <transition cond="len(self.queue) > 0 and self.queue[0][1].startswith('POLL')" target=".">
+                        <script>
+                            socket, data = self.queue.pop(0)
+                        </script>
+                        <raise scope="broad" event="server_input">
+                            <parameter expr="socket"/>
+                            <parameter expr="'ALIVE'"/>
+                        </raise>
+                    </transition>
+                    <transition cond="len(self.queue) > 0 and self.queue[0][1].startswith('JOIN ')" target=".">
+                        <script>
+                            socket, data = self.queue.pop(0)
+                            roomnumber = int(data[5:])
+                            self.room2clients.setdefault(roomnumber, set()).add(socket)
+                            self.client2room[socket] = roomnumber
+                        </script>
+                        <raise scope="broad" event="server_input">
+                            <parameter expr="socket"/>
+                            <parameter expr="'ACK JOIN %i' % roomnumber"/>
+                        </raise>
+                    </transition>
+                    <transition cond="len(self.queue) > 0 and self.queue[0][1].startswith('LEAVE')" target=".">
+                        <script>
+                            socket, data = self.queue.pop(0)
+                            roomnumber = self.client2room[socket]
+                            self.room2clients[roomnumber].remove(socket)
+                            del self.client2room[socket]
+                        </script>
+                        <raise scope="broad" event="server_input">
+                            <parameter expr="socket"/>
+                            <parameter expr="'ACK LEAVE'"/>
+                        </raise>
+                    </transition>
+                </state>
+                <state id="forwarding">
+                    <transition cond="len(self.tmp_sendlist) > 0" target=".">
+                        <raise scope="broad" event="server_input">
+                            <parameter expr="self.tmp_sendlist.pop()"/>
+                            <parameter expr="self.tmp_data"/>
+                        </raise>
+                    </transition>
+                    <transition cond="len(self.tmp_sendlist) == 0" target="../main"/>
+                </state>
+            </state>
+
+            <state id="receiver">
+                <state id="queue">
+                    <transition event="server_output" target=".">
+                        <parameter name="socket"/>
+                        <parameter name="data"/>
+                        <script>
+                            self.queue.append((socket, data))
+                        </script>
+                    </transition>
+                </state>
+            </state>
+        </parallel>
+    </scxml>
+</class>

+ 255 - 0
examples/chatroom/classes/model.xml

@@ -0,0 +1,255 @@
+<class name="ChatWindow" default="true">
+	<relationships>
+		<association name="client" class="NetworkClient"/>
+	</relationships>
+	<method name="ChatWindow">
+		<parameter name="my_controller"/>
+	<body><![CDATA[
+self.colors = {'service': 'black', 'me': 'red', 'other': 'blue'}
+self.numbers = set([str(c) for c in range(10)])
+self.characters = set([chr(c) for c in range(32,126)])
+self.server_list = [('localhost', 8000), ('localhost', 8001)]
+self.connecting_server = 0
+self.buffer = ''
+self.my_controller = my_controller
+try:
+	data = json.dumps({'text':'CLIENT_BDAPI :: {"func":"_unhighlight","args":{}}'})
+	headers = {'Content-Type': 'text/plain'}
+	conn = httplib.HTTPConnection('127.0.0.1:8124')
+	conn.request('PUT', '/GET/console?wid=5', data, headers)
+	conn.getresponse()
+	conn.close()
+except:
+	pass
+	]]></body>
+	</method>
+	<method name="add_message">
+		<parameter name="msg"/>
+		<parameter name="color"/>
+	<body><![CDATA[
+self.my_controller.addMessage(msg, color)
+	]]></body>
+	</method>
+	<method name="append_to_buffer">
+		<parameter name="character"/>
+	<body><![CDATA[
+self.buffer = ''.join([self.buffer, character])
+self.my_controller.redraw_buffer(self.buffer)
+	]]></body>
+	</method>
+	<method name="remove_last_in_buffer">
+	<body><![CDATA[
+self.buffer = self.buffer[:-1]
+self.my_controller.redraw_buffer(self.buffer)
+	]]></body>
+	</method>
+	<method name="clear_input">
+	<body><![CDATA[
+self.buffer = ''
+self.my_controller.redraw_buffer(self.buffer)
+	]]></body>
+	</method>
+	<method name="input_join">
+	<body><![CDATA[self.my_controller.setColor('green')
+	]]></body>
+	</method>
+	<method name="input_msg">
+	<body><![CDATA[self.my_controller.setColor('white')
+	]]></body>
+	</method>
+	<method name="input_command">
+	<body><![CDATA[self.my_controller.setColor('grey')
+	]]></body>
+	</method>
+	<method name="get_buffer">
+	<body><![CDATA[return self.buffer
+	]]></body>
+	</method>
+<scxml initial="init_network" priority="source_child">
+<state id="connecting">
+<transition event="connected" target="./../connected" >
+</transition>
+<onentry>
+<script><![CDATA[try:
+ data = json.dumps({'text':'CLIENT_BDAPI :: {"func":"_highlightState","args":{"asid":"232","followCrossFormalismLinks":"*"}}'})
+ headers = {'Content-Type': 'text/plain'}
+ conn = httplib.HTTPConnection('127.0.0.1:8124')
+ conn.request('PUT', '/GET/console?wid=5', data, headers)
+ conn.getresponse()
+ conn.close()
+except:
+ pass
+]]></script>
+<raise event="connect" scope="broad">
+<parameter expr="'localhost'"/>
+<parameter expr="8000"/>
+</raise>
+</onentry>
+<onexit>
+<script><![CDATA[try:
+ data = json.dumps({'text':'CLIENT_BDAPI :: {"func":"_unhighlightState","args":{"asid":"232"}}'})
+ headers = {'Content-Type': 'text/plain'}
+ conn = httplib.HTTPConnection('127.0.0.1:8124')
+ conn.request('PUT', '/GET/console?wid=5', data, headers)
+ conn.getresponse()
+ conn.close()
+except:
+ pass
+]]></script>
+</onexit>
+</state>
+<state id="leaving">
+<transition event="left" target="./../left" >
+</transition>
+<onentry>
+<script><![CDATA[try:
+ data = json.dumps({'text':'CLIENT_BDAPI :: {"func":"_highlightState","args":{"asid":"233","followCrossFormalismLinks":"*"}}'})
+ headers = {'Content-Type': 'text/plain'}
+ conn = httplib.HTTPConnection('127.0.0.1:8124')
+ conn.request('PUT', '/GET/console?wid=5', data, headers)
+ conn.getresponse()
+ conn.close()
+except:
+ pass
+self.add_message('leaving', self.colors['service'])]]></script>
+<raise event="leave" scope="broad">
+</raise>
+</onentry>
+<onexit>
+<script><![CDATA[try:
+ data = json.dumps({'text':'CLIENT_BDAPI :: {"func":"_unhighlightState","args":{"asid":"233"}}'})
+ headers = {'Content-Type': 'text/plain'}
+ conn = httplib.HTTPConnection('127.0.0.1:8124')
+ conn.request('PUT', '/GET/console?wid=5', data, headers)
+ conn.getresponse()
+ conn.close()
+except:
+ pass
+]]></script>
+</onexit>
+</state>
+<state id="joined">
+<transition event="input" cond="character == 'k'" port="tkinter_input" target="./../leaving" >
+<parameter name="character"/>
+</transition>
+<transition after="10.0" target="./../leaving" >
+</transition>
+<onentry>
+<script><![CDATA[try:
+ data = json.dumps({'text':'CLIENT_BDAPI :: {"func":"_highlightState","args":{"asid":"228","followCrossFormalismLinks":"*"}}'})
+ headers = {'Content-Type': 'text/plain'}
+ conn = httplib.HTTPConnection('127.0.0.1:8124')
+ conn.request('PUT', '/GET/console?wid=5', data, headers)
+ conn.getresponse()
+ conn.close()
+except:
+ pass
+self.add_message('joined room', self.colors['service'])]]></script>
+</onentry>
+<onexit>
+<script><![CDATA[try:
+ data = json.dumps({'text':'CLIENT_BDAPI :: {"func":"_unhighlightState","args":{"asid":"228"}}'})
+ headers = {'Content-Type': 'text/plain'}
+ conn = httplib.HTTPConnection('127.0.0.1:8124')
+ conn.request('PUT', '/GET/console?wid=5', data, headers)
+ conn.getresponse()
+ conn.close()
+except:
+ pass
+]]></script>
+</onexit>
+</state>
+<state id="left">
+<onentry>
+<script><![CDATA[try:
+ data = json.dumps({'text':'CLIENT_BDAPI :: {"func":"_highlightState","args":{"asid":"229","followCrossFormalismLinks":"*"}}'})
+ headers = {'Content-Type': 'text/plain'}
+ conn = httplib.HTTPConnection('127.0.0.1:8124')
+ conn.request('PUT', '/GET/console?wid=5', data, headers)
+ conn.getresponse()
+ conn.close()
+except:
+ pass
+self.add_message('left room', self.colors['service'])]]></script>
+</onentry>
+<onexit>
+<script><![CDATA[try:
+ data = json.dumps({'text':'CLIENT_BDAPI :: {"func":"_unhighlightState","args":{"asid":"229"}}'})
+ headers = {'Content-Type': 'text/plain'}
+ conn = httplib.HTTPConnection('127.0.0.1:8124')
+ conn.request('PUT', '/GET/console?wid=5', data, headers)
+ conn.getresponse()
+ conn.close()
+except:
+ pass
+]]></script>
+</onexit>
+</state>
+<state id="connected">
+<transition event="joined" target="./../joined" >
+</transition>
+<onentry>
+<script><![CDATA[try:
+ data = json.dumps({'text':'CLIENT_BDAPI :: {"func":"_highlightState","args":{"asid":"230","followCrossFormalismLinks":"*"}}'})
+ headers = {'Content-Type': 'text/plain'}
+ conn = httplib.HTTPConnection('127.0.0.1:8124')
+ conn.request('PUT', '/GET/console?wid=5', data, headers)
+ conn.getresponse()
+ conn.close()
+except:
+ pass
+self.add_message('joining room 1', self.colors['service'])]]></script>
+<raise event="join" scope="broad">
+<parameter expr="1"/>
+</raise>
+</onentry>
+<onexit>
+<script><![CDATA[try:
+ data = json.dumps({'text':'CLIENT_BDAPI :: {"func":"_unhighlightState","args":{"asid":"230"}}'})
+ headers = {'Content-Type': 'text/plain'}
+ conn = httplib.HTTPConnection('127.0.0.1:8124')
+ conn.request('PUT', '/GET/console?wid=5', data, headers)
+ conn.getresponse()
+ conn.close()
+except:
+ pass
+]]></script>
+</onexit>
+</state>
+<state id="init_network">
+<transition event="instance_created" target="./../connecting" >
+<parameter name="association_name"/>
+<raise event="start_instance" scope="cd">
+<parameter expr="association_name"/>
+</raise>
+</transition>
+<onentry>
+<script><![CDATA[try:
+ data = json.dumps({'text':'CLIENT_BDAPI :: {"func":"_highlightState","args":{"asid":"226","followCrossFormalismLinks":"*"}}'})
+ headers = {'Content-Type': 'text/plain'}
+ conn = httplib.HTTPConnection('127.0.0.1:8124')
+ conn.request('PUT', '/GET/console?wid=5', data, headers)
+ conn.getresponse()
+ conn.close()
+except:
+ pass
+]]></script>
+<raise event="create_instance" scope="cd">
+<parameter expr="'client'"/>
+</raise>
+</onentry>
+<onexit>
+<script><![CDATA[try:
+ data = json.dumps({'text':'CLIENT_BDAPI :: {"func":"_unhighlightState","args":{"asid":"226"}}'})
+ headers = {'Content-Type': 'text/plain'}
+ conn = httplib.HTTPConnection('127.0.0.1:8124')
+ conn.request('PUT', '/GET/console?wid=5', data, headers)
+ conn.getresponse()
+ conn.close()
+except:
+ pass
+]]></script>
+</onexit>
+</state>
+</scxml>
+</class>

+ 216 - 0
examples/chatroom/classes/networkclient.xml

@@ -0,0 +1,216 @@
+<class name="NetworkClient">
+    <constructor>
+        <body>
+            self.socket = None
+            self.host = None
+            self.received_data = ""
+            self.send_data = ""
+            self.parsing_data = ""
+            self.queue = []
+        </body>
+    </constructor>
+
+    <scxml>
+        <parallel id="parallel">
+        <state id="client" initial="wait_for_destination">
+            <transition event="connect" cond="self.socket is not None" target="close">
+                <parameter name="hostname"/>
+                <parameter name="port"/>
+                <script>
+                    self.host = (hostname, port)
+                </script>
+            </transition>
+            <transition event="connect" cond="self.socket is None" target="init">
+                <parameter name="hostname"/>
+                <parameter name="port"/>
+                <script>
+                    self.host = (hostname, port)
+                </script>
+            </transition>
+
+            <state id="wait_for_destination"/>
+
+            <state id="init">
+                <onentry>
+                    <raise scope="output" event="create_socket" output="socket_out"/>
+                </onentry>
+                <transition port="socket_in" event="created_socket" target="../connecting">
+                    <parameter name="socket"/>
+                    <script>
+                        self.socket = socket
+                    </script>
+                </transition>
+            </state>
+
+            <state id="connecting">
+                <onentry>
+                    <raise scope="output" event="connect_socket" output="socket_out">
+                        <parameter expr="self.socket"/>
+                        <parameter expr="self.host"/>
+                    </raise>
+                </onentry>
+                <transition event="connected_socket" port="socket_in" target="../connected">
+                    <raise scope="broad" event="connected"/>
+                </transition>
+            </state>
+
+            <parallel id="connected">
+                <!-- We explicitly use the fact that there is no threading -->
+                <state id="processing" initial="processing">
+                    <state id="processing">
+                        <transition cond="'\0' in self.received_data" target="../parse">
+                            <script>
+                                self.parsing_data, self.received_data = self.received_data.split('\0', 1)
+                            </script>
+                        </transition>
+                    </state>
+                    <state id="parse">
+                        <transition cond="self.parsing_data.startswith('MSG ')" target="../processing">
+                            <raise scope="broad" event="receive_message">
+                                <parameter expr="self.parsing_data[4:]"/>
+                            </raise>
+                        </transition>
+                        <transition cond="self.parsing_data.startswith('ACK MSG')" target="../processing"/>
+                        <transition cond="self.parsing_data.startswith('ACK JOIN')" target="../processing">
+                            <raise scope="broad" event="joined">
+                                <parameter expr="self.parsing_data.split(' ')[2]"/>
+                            </raise>
+                        </transition>
+                        <transition cond="self.parsing_data.startswith('ALIVE')" target="../processing">
+                            <raise scope="broad" event="alive"/>
+                        </transition>
+                        <transition cond="self.parsing_data.startswith('ACK LEAVE')" target="../processing">
+                            <raise scope="broad" event="left"/>
+                        </transition>
+                    </state>
+                </state>
+
+                <state id="receiving">
+                    <state id="receive">
+                        <onentry>
+                            <raise scope="output" event="recv_socket" output="socket_out">
+                                <parameter expr="self.socket"/>
+                            </raise>
+                        </onentry>
+                        <transition port="socket_in" event="received_socket" cond="(self.socket == socket) and (data != '')" target=".">
+                            <parameter name="socket"/>
+                            <parameter name="data"/>
+                            <script>
+                                print("Got data " + repr(data))
+                                print("Total data: " + repr(self.received_data))
+                                self.received_data += data
+                            </script>
+                        </transition>
+                        <!-- For the sake of this exercise, wait for timeout instead of directly signaling this close -->
+                        <transition port="socket_in" event="received_socket" cond="(self.socket == socket) and (data == '')" target=".">
+                            <parameter name="socket"/>
+                            <parameter name="data"/>
+                            <raise event="close"/>
+                        </transition>
+                    </state>
+                </state>
+
+                <state id="sending" initial="waiting_for_inputdata">
+                    <state id="waiting_for_inputdata">
+                        <transition cond="len(self.queue) > 0" target="../transmitting">
+                            <script>
+                                self.send_data = self.queue.pop(0) + "\0"
+                            </script>
+                        </transition>
+                    </state>
+
+                    <state id="transmitting">
+                        <onentry>
+                            <raise scope="output" event="send_socket" output="socket_out">
+                                <parameter expr="self.socket"/>
+                                <parameter expr="self.send_data"/>
+                            </raise>
+                        </onentry>
+                        <transition port="socket_in" cond="self.socket == socket and (sent == len(self.send_data))" event="sent_socket" target="../waiting_for_inputdata">
+                            <parameter name="socket"/>
+                            <parameter name="sent"/>
+                            <script>
+                                self.send_data = ""
+                            </script>
+                        </transition>
+                        <transition port="socket_in" cond="self.socket == socket and (sent != len(self.send_data))" event="sent_socket" target=".">
+                            <parameter name="socket"/>
+                            <parameter name="sent"/>
+                            <script>
+                                self.send_data = self.send_data[sent:]
+                            </script>
+                        </transition>
+                    </state>
+                </state>
+            </parallel>
+            <transition event="close" target="error_close"/>
+
+            <state id="error_close">
+                <onentry>
+                    <raise scope="output" event="close_socket" output="socket_out">
+                        <parameter expr="self.socket"/>
+                    </raise>
+                </onentry>
+                <transition event="closed_socket" cond="self.socket == socket" target="../wait_for_destination">
+                    <parameter name="socket"/>
+                    <script>
+                        self.socket = None
+                        self.host = None
+                    </script>
+                </transition>
+            </state>
+
+            <state id="close">
+                <onentry>
+                    <raise scope="output" event="close_socket" output="socket_out">
+                        <parameter expr="self.socket"/>
+                    </raise>
+                </onentry>
+                <transition event="closed_socket" cond="self.socket == socket" target="../init">
+                    <parameter name="socket"/>
+                    <raise scope="broad" event="disconnected"/>
+                </transition>
+            </state>
+
+            <transition event="disconnect" cond="self.socket is not None" target="close">
+                <script>
+                    print("Closing socket")
+                </script>
+            </transition>
+            <transition event="disconnect" cond="self.socket is None" target="wait_for_destination">
+                <script>
+                    print("Signaling disconnectedness")
+                </script>
+                <raise scope="broad" event="disconnected"/>
+            </transition>
+        </state>
+
+        <state id="queue">
+            <state id="queue">
+                <transition event="send_message" target=".">
+                    <parameter name="data"/>
+                    <script>
+                        self.queue.append("MSG %s: %s" % (socket.gethostname(), data))
+                    </script>
+                </transition>
+                <transition event="join" target=".">
+                    <parameter name="data"/>
+                    <script>
+                        self.queue.append("JOIN %s" % data)
+                    </script>
+                </transition>
+                <transition event="leave" target=".">
+                    <script>
+                        self.queue.append("LEAVE")
+                    </script>
+                </transition>
+                <transition event="poll" target=".">
+                    <script>
+                        self.queue.append("POLL")
+                    </script>
+                </transition>
+            </state>
+        </state>
+        </parallel>
+    </scxml>
+</class>

+ 95 - 0
examples/chatroom/classes/networkserver.xml

@@ -0,0 +1,95 @@
+    <class name="NetworkServer" default="true">
+        <relationships>
+            <association name="sockets" class="NetworkServerSocket" />
+        </relationships>
+        <constructor>
+            <parameter name="port"/>
+            <body>
+                <![CDATA[
+                self.socket = None
+                self.association_name = None
+                self.port = port
+                ]]>
+            </body>
+        </constructor>
+        <scxml initial="init">
+            <state id="init">
+                <transition event="set_association_name" target="../main">
+                    <parameter name="association_name"/>
+                    <script>
+                        self.association_name = association_name
+                    </script>
+                </transition>
+            </state>
+            <parallel id="main">
+                <state id="main" initial="init">
+                    <state id="init">
+                        <onentry>
+                            <raise scope="output" event="create_socket" port="socket_out"/>
+                        </onentry>
+                        <transition port="socket_in" event="created_socket" target="../binding">
+                            <parameter name="socket"/>
+                            <script>
+                                self.socket = socket
+                            </script>
+                        </transition>
+                    </state>
+                    <state id="binding">
+                        <onentry>
+                            <raise scope="output" event="bind_socket" port="socket_out">
+                                <parameter expr="self.socket"/>
+                                <parameter expr="('0.0.0.0', self.port)"/>
+                            </raise>
+                        </onentry>
+                        <transition port="socket_in" event="bound_socket" cond="self.socket == socket" target="../listening">
+                            <parameter name="socket"/>
+                        </transition>
+                    </state>
+                    <state id="listening">
+                        <onentry>
+                            <raise scope="output" event="listen_socket" port="socket_out">
+                                <parameter expr="self.socket"/>
+                            </raise>
+                        </onentry>
+                        <transition port="socket_in" event="listened_socket" cond="self.socket == socket" target="../accepting">
+                            <parameter name="socket"/>
+                        </transition>
+                    </state>
+                    <state id="accepting">
+                        <onentry>
+                            <raise scope="output" port="socket_out" event="accept_socket">
+                                <parameter expr="self.socket"/>
+                            </raise>
+                        </onentry>
+                        <transition port="socket_in" event="accepted_socket" cond="self.socket == socket" target=".">
+                            <parameter name="socket"/>
+                            <parameter name="connected_socket"/>
+                            <raise scope="cd" event="create_instance">
+                                <parameter expr="'sockets'" />
+                                <parameter expr="'NetworkServerSocket'" />
+                                <parameter expr="connected_socket" />
+                            </raise>
+                        </transition>
+                        <transition event="instance_created" target=".">
+                            <parameter name="instancename"/>
+                            <raise scope="narrow" event="set_association_name" target="instancename">
+                                <parameter expr="instancename"/>
+                            </raise>
+                            <raise scope="cd" event="start_instance">
+                                <parameter expr="instancename" />
+                            </raise>
+                        </transition>
+                    </state>
+                </state>
+                <state id="close">
+                    <state id="close">
+                        <transition event="close" target=".">
+                            <raise scope="cd" event="delete_instance">
+                                <parameter expr="association_name"/>
+                            </raise>
+                        </transition>
+                    </state>
+                </state>
+            </parallel>
+        </scxml>
+    </class>

+ 109 - 0
examples/chatroom/classes/networkserversocket.xml

@@ -0,0 +1,109 @@
+    <class name="NetworkServerSocket">
+        <constructor>
+            <parameter name="my_socket"/>
+            <body>
+                <![CDATA[
+                self.socket = my_socket
+                self.received_data = ""
+                self.send_data = None
+                self.association_name = None
+                self.queue = []
+                ]]>
+            </body>
+        </constructor>
+        <scxml initial="parallel">
+            <parallel id="parallel">
+                <!-- We explicitly use the fact that there is no threading -->
+                <state id="processing" initial="processing">
+                    <state id="processing">
+                        <transition cond="'\0' in self.received_data" target=".">
+                            <script>
+                                self.parsing_data, self.received_data = self.received_data.split('\0', 1)
+                            </script>
+                            <raise scope="broad" event="server_output">
+                                <parameter expr="self.socket"/>
+                                <parameter expr="self.parsing_data"/>
+                            </raise>
+                        </transition>
+                    </state>
+                </state>
+
+                <state id="receiving">
+                    <state id="receive">
+                        <onentry>
+                            <raise scope="output" event="recv_socket" output="socket_out">
+                                <parameter expr="self.socket"/>
+                            </raise>
+                        </onentry>
+                        <transition port="socket_in" event="received_socket" cond="self.socket == socket and data != ''" target=".">
+                            <parameter name="socket"/>
+                            <parameter name="data"/>
+                            <script>
+                                self.received_data += data
+                            </script>
+                        </transition>
+                        <transition port="socket_in" event="received_socket" cond="self.socket == socket and data == ''" target=".">
+                            <parameter name="socket"/>
+                            <parameter name="data"/>
+                            <raise event="stop"/>
+                        </transition>
+                    </state>
+                </state>
+
+                <state id="sending" initial="waiting_for_inputdata">
+                    <state id="waiting_for_inputdata">
+                        <transition cond="len(self.queue) > 0" target="../transmitting">
+                            <script>
+                                self.send_data = self.queue.pop(0) + "\0"
+                            </script>
+                        </transition>
+                    </state>
+
+                    <state id="transmitting">
+                        <onentry>
+                            <raise scope="output" event="send_socket" output="socket_out">
+                                <parameter expr="self.socket"/>
+                                <parameter expr="self.send_data"/>
+                            </raise>
+                        </onentry>
+                        <transition port="socket_in" cond="self.socket == socket and (sent == len(self.send_data))" event="sent_socket" target="../waiting_for_inputdata">
+                            <parameter name="socket"/>
+                            <parameter name="sent"/>
+                            <script>
+                                self.send_data = ""
+                            </script>
+                        </transition>
+                        <transition port="socket_in" cond="self.socket == socket and (sent != len(self.send_data))" event="sent_socket" target=".">
+                            <parameter name="socket"/>
+                            <parameter name="sent"/>
+                            <script>
+                                self.send_data = self.send_data[sent:]
+                            </script>
+                        </transition>
+                    </state>
+                </state>
+
+                <state id="receiver">
+                    <state id="queue">
+                        <transition event="server_input" cond="self.socket == socket" target=".">
+                            <parameter name="socket"/>
+                            <parameter name="data"/>
+                            <script>
+                                self.queue.append(data)
+                            </script>
+                        </transition>
+                    </state>
+                </state>
+
+                <transition event="stop" target="../close"/>
+            </parallel>
+
+            <state id="close">
+                <onentry>
+                    <raise scope="output" event="close_socket" output="socket_out">
+                        <parameter expr="self.socket"/>
+                    </raise>
+                </onentry>
+            </state>
+        </scxml>
+    </class>

+ 15 - 0
examples/chatroom/classes/port2event.xml

@@ -0,0 +1,15 @@
+<class name="Port2Event">
+	<scxml initial="main">
+		<state id="main">
+			<transition port="tkinter_input" event="input" target=".">
+				<parameter name="character"/>
+				<raise event="tkinter_input" scope="broad">
+					<parameter expr="character"/>
+				</raise>
+				<script>
+					print("RAISING EVENT" + str(character))
+				</script>
+			</transition>
+		</state>
+	</scxml>
+</class>

+ 34 - 0
examples/chatroom/run_client.py

@@ -0,0 +1,34 @@
+"""
+Runner script for the TkInter chat window SCCD model.
+
+Author: Yentl Van Tendeloo
+"""
+
+import Tkinter as tk
+import chatclient
+import socket2event
+from python_runtime.statecharts_core import Event
+from python_runtime.tkinter_eventloop import *
+from chatwindowGUI import ChatWindowGUI
+
+def keypress(key):
+    global controller
+    try:
+        str(key.char)
+        if len(key.char) == 1:
+            controller.addInput(Event("input", "tkinter_input", [key.char]), 0.0)
+        # Don't do anything for empty characters, as these are control characters (e.g. press shift)
+    except UnicodeEncodeError:
+        print("Unicode input is not supported for simplicity")
+
+root = ChatWindowGUI(keypress)
+		
+if __name__ == "__main__":
+    global controller
+    controller = chatclient.Controller(root, TkEventLoop(root))
+    socket2event.boot_translation_service(controller)
+    controller.start()
+    try:
+		root.mainloop()
+    except:
+        controller.stop()

+ 19 - 0
examples/chatroom/run_server.py

@@ -0,0 +1,19 @@
+import chatserver
+import socket2event
+import sys
+
+if len(sys.argv) != 2:
+    print("Usage:")
+    print("  %s port" % sys.argv[0])
+    sys.exit(1)
+
+controller = chatserver.Controller(int(sys.argv[1]))
+socket2event.boot_translation_service(controller)
+controller.start()
+
+try:
+    import time
+    while 1:
+        time.sleep(1)
+finally:
+    controller.stop()

+ 52 - 0
examples/chatroom/scrollable_frame.py

@@ -0,0 +1,52 @@
+## Source: http://tkinter.unpythonic.net/wiki/VerticalScrolledFrame
+
+from Tkinter import *
+
+class VerticalScrolledFrame(Frame):
+    """A pure Tkinter scrollable frame that actually works!
+
+    * Use the 'interior' attribute to place widgets inside the scrollable frame
+    * Construct and pack/place/grid normally
+    * This frame only allows vertical scrolling
+    
+    """
+    def __init__(self, parent, *args, **kw):
+        Frame.__init__(self, parent, *args, **kw)            
+
+        # create a canvas object and a vertical scrollbar for scrolling it
+        vscrollbar = Scrollbar(self, orient=VERTICAL)
+        vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
+        canvas = Canvas(self, bd=0, highlightthickness=0,
+                        yscrollcommand=vscrollbar.set)
+        canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
+        vscrollbar.config(command=canvas.yview)
+
+        # reset the view
+        canvas.xview_moveto(0)
+        canvas.yview_moveto(0)
+
+        # create a frame inside the canvas which will be scrolled with it
+        self.interior = interior = Frame(canvas)
+        interior_id = canvas.create_window(0, 0, window=interior,
+                                           anchor=NW)
+
+        # track changes to the canvas and frame width and sync them,
+        # also updating the scrollbar
+        def _configure_interior(event):
+            # update the scrollbars to match the size of the inner frame
+            size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
+            canvas.config(scrollregion="0 0 %s %s" % size)
+            if interior.winfo_reqwidth() != canvas.winfo_width():
+                # update the canvas's width to fit the inner frame
+                canvas.config(width=interior.winfo_reqwidth())
+            #NOTE my own addition
+            canvas.yview_moveto(1)
+        interior.bind('<Configure>', _configure_interior)
+
+        def _configure_canvas(event):
+            if interior.winfo_reqwidth() != canvas.winfo_width():
+                # update the inner frame's width to fill the canvas
+                canvas.itemconfigure(interior_id, width=canvas.winfo_width())
+        canvas.bind('<Configure>', _configure_canvas)
+
+        return

+ 85 - 0
examples/chatroom/socket2event.py

@@ -0,0 +1,85 @@
+import threading
+from python_runtime.statecharts_core import Event
+import socket
+
+def _recv(controller, sock):
+    data = sock.recv(4096)
+    controller.addInput(Event("received_socket", "socket_in", [sock, data]), 0.0)
+
+def _accept(controller, sock):
+    conn, addr = sock.accept()
+    controller.addInput(Event("accepted_socket", "socket_in", [sock, conn]), 0.0)
+
+def _connect(controller, sock, destination):
+    sock.connect(destination)
+    controller.addInput(Event("connected_socket", "socket_in", [sock]), 0.0)
+
+def _create(controller, _):
+    sock = socket.socket()
+    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    controller.addInput(Event("created_socket", "socket_in", [sock]), 0.0)
+
+def _send(controller, sock, data):
+    sent = sock.send(data)
+    controller.addInput(Event("sent_socket", "socket_in", [sock, sent]), 0.0)
+
+def _close(controller, sock):
+    sock.close()
+    controller.addInput(Event("closed_socket", "socket_in", [sock]), 0.0)
+
+def _bind(controller, sock, addr):
+    sock.bind(addr)
+    controller.addInput(Event("bound_socket", "socket_in", [sock]), 0.0)
+
+def _listen(controller, sock):
+    sock.listen(1)
+    controller.addInput(Event("listened_socket", "socket_in", [sock]), 0.0)
+
+def _wrapper_func(*args):
+    func = args[0]
+    controller = args[1]
+    sock = args[2]
+    try:
+        func(*args[1:])
+    except socket.error as e:
+        print("ERROR " + str(e))
+        controller.addInput(Event("error_socket", "socket_in", [sock, e]), 0.0)
+    except Exception as e:
+        print("UNKNOWN ERROR " + str(e))
+        controller.addInput(Event("unknown_error_socket", "socket_in", [sock, e]), 0.0)
+
+def _start_on_daemon_thread(func, args):
+    new_args = [func]
+    new_args.extend(args)
+    args = new_args
+    thrd = threading.Thread(target=_wrapper_func, args=args)
+    thrd.daemon = True
+    thrd.start()
+
+def boot_translation_service(controller):
+    _start_on_daemon_thread(_poll, [controller, None])
+
+def _poll(controller, _):
+    socket_out = controller.addOutputListener("socket_out")
+    while 1:
+            evt = socket_out.fetch(-1)
+            name, params = evt.getName(), evt.getParameters()
+            print("Got event " + str(evt))
+            if name == "accept_socket":
+                _start_on_daemon_thread(_accept, [controller, params[0]])
+            elif name == "recv_socket":
+                _start_on_daemon_thread(_recv, [controller, params[0]])
+            elif name == "connect_socket":
+                _start_on_daemon_thread(_connect, [controller, params[0], params[1]])
+            elif name == "create_socket":
+                _start_on_daemon_thread(_create, [controller, None])
+            elif name == "close_socket":
+                _start_on_daemon_thread(_close, [controller, params[0]])
+            elif name == "send_socket":
+                _start_on_daemon_thread(_send, [controller, params[0], params[1]])
+            elif name == "bind_socket":
+                _start_on_daemon_thread(_bind, [controller, params[0], params[1]])
+            elif name == "listen_socket":
+                _start_on_daemon_thread(_listen, [controller, params[0]])
+            elif name == "stop":
+                break

+ 2 - 2
examples/digitalwatch/run.py

@@ -31,8 +31,8 @@ def main():
     cd = load_cd("model_digitalwatch.xml")
 
     # from sccd.statechart.static import tree
-    # # tree.concurrency_arena_orthogonal( cd.statechart.tree )
-    # tree.concurrency_src_dst_orthogonal( cd.statechart.tree )
+    # tree.concurrency_arena_orthogonal( cd.statechart.tree )
+    # # tree.concurrency_src_dst_orthogonal( cd.statechart.tree )
     # exit()
 
     controller = Controller(cd, output_callback=on_output)

+ 29 - 14
src/sccd/action_lang/cmd/prompt.py

@@ -9,31 +9,46 @@ if __name__ == "__main__":
   memory = Memory()
   memory.push_frame(scope)
   readline.set_history_length(1000)
+  print("Enter statements or expressions. Most statements end with ';'. Statements will be executed, expressions will be evaluated. Either can have side effects.")
+  print("Examples:")
+  print("  basic stuff:")
+  print("    greeting = \"hello\";")
+  print("    to_whom = \" world\";")
+  print("    greeting + to_whom")
+  print("  more interesting: higher order functions:")
+  print("    apply = func(i: int, f: func(int) -> int) { return f(i); } ;")
+  print("    apply(10, func(i: int) { return i+1; })")
+  print()
 
   while True:
     try:
       line = input("> ")
-      stmt = parse_block(line)
-      stmt.init_stmt(scope)
-      print_debug(termcolor.colored(str(stmt), 'yellow'))
+      try:
+        # Attempt to parse as a statement
+        stmt = parse_block(line) # may raise LarkError
+        stmt.init_stmt(scope)
 
-      # Grow current stack frame if necessary
-      diff = scope.size() - len(memory.current_frame().storage)
-      if diff > 0:
-        memory.current_frame().storage.extend([None]*diff)
+        # Grow current stack frame if necessary
+        diff = scope.size() - len(memory.current_frame().storage)
+        if diff > 0:
+          memory.current_frame().storage.extend([None]*diff)
 
-      if isinstance(stmt, ExpressionStatement):
-        expr_type = stmt.expr.init_expr(scope) # expr already initialized but init_expr should be idempotent
-        val = stmt.expr.eval(memory)
-        print("%s: %s" % (str(val), str(expr_type)))
-      else:
         stmt.exec(memory)
+      except LarkError as e:
+        try:
+          # Attempt to parse as an expression
+          expr = parse_expression(line)
+          expr_type = expr.init_expr(scope)
+          val = expr.eval(memory)
+          print("%s: %s" % (str(val), str(expr_type)))
+        except LarkError:
+          raise e
 
     except (UnexpectedToken, UnexpectedCharacters) as e:
       print(" " + " "*e.column + "^")
-      print(e)
+      print(type(e).__name__+":", e)
     except (LarkError, ModelError, SCCDRuntimeException) as e:
-      print(e)
+      print(type(e).__name__+":", e)
     except (KeyboardInterrupt, EOFError):
       print()
       exit()

+ 14 - 6
src/sccd/action_lang/parser/action_lang.g

@@ -49,9 +49,17 @@ param_list: ( expr ("," expr)* )?  -> params
 
 func_decl: "func" params_decl stmt
 params_decl: ( "(" param_decl ("," param_decl)* ")" )?
-?param_decl: IDENTIFIER ":" TYPE_ANNOT
-TYPE_ANNOT: "int" | "str" | "dur" | "float"
+?param_decl: IDENTIFIER ":" type_annot
+type_annot: TYPE_INT | TYPE_STR | TYPE_DUR | TYPE_FLOAT
+          | "func" param_types? return_type? -> func_type
 
+param_types: "(" type_annot ( "," type_annot )* ")"
+?return_type: "->" type_annot
+
+TYPE_INT: "int"
+TYPE_STR: "str"
+TYPE_DUR: "dur"
+TYPE_FLOAT: "float"
 
 array: "[" (expr ("," expr)*)? "]"
 
@@ -108,11 +116,11 @@ TIME_D: "d" // for zero-duration
 
 // Statement parsing
 
-?block: (stmt ";")*
+?block: (stmt)*
 
-?stmt: assignment
-     | expr -> expression_stmt
-     | "return" expr -> return_stmt
+?stmt: assignment ";"
+     | expr ";" -> expression_stmt
+     | "return" expr ";" -> return_stmt
      | "{" block "}" -> block
      | "if" "(" expr ")" stmt ("else" stmt)? -> if_stmt
 

+ 14 - 4
src/sccd/action_lang/parser/text.py

@@ -89,13 +89,23 @@ class ExpressionTransformer(Transformer):
   params_decl = list
 
   def param_decl(self, node):
-    type = {
+    return ParamDecl(name=node[0].value, formal_type=node[1])
+
+  def type_annot(self, node):
+    return {
       "int": SCCDInt,
       "str": SCCDString,
       "float": SCCDFloat,
-      "dur": SCCDDuration
-    }[node[1]]
-    return ParamDecl(name=node[0].value, formal_type=type)
+      "dur": SCCDDuration,
+    }[node[0]]
+
+  def func_type(self, node):
+    if len(node) > 1:
+      return SCCDFunction(param_types=node[0], return_type=node[1])
+    else:
+      return SCCDFunction(param_types=node[0])
+
+  param_types = list
 
   def func_decl(self, node):
     return FunctionDeclaration(params_decl=node[0], body=node[1])

+ 0 - 1
src/sccd/action_lang/static/expression.py

@@ -327,7 +327,6 @@ class BinaryExpression(Expression):
         return t
 
     def eval(self, memory: MemoryInterface):
-        
         return {
             "and": lambda x,y: x and y.eval(memory),
             "or": lambda x,y: x or y.eval(memory),

+ 1 - 1
src/sccd/action_lang/static/scope.py

@@ -10,7 +10,7 @@ import termcolor
 
 class ScopeError(ModelError):
   def __init__(self, scope, msg):
-    super().__init__(msg + '\n\nCurrent scope:\n' + str(scope))
+    super().__init__(msg + '\n\n' + str(scope))
 
 
 # Stateless stuff we know about a variable existing within a scope.

+ 7 - 2
src/sccd/action_lang/static/statement.py

@@ -1,5 +1,6 @@
 from typing import *
 from sccd.action_lang.static.expression import *
+from sccd.util.debug import *
 
 @dataclass(frozen=True)
 class Return:
@@ -106,7 +107,7 @@ class Assignment(Statement):
         return DontReturn
 
     def render(self) -> str:
-        return self.lhs.render() + ' = ' + self.rhs.render()
+        return self.lhs.render() + ' = ' + self.rhs.render() + ';'
 
 
 
@@ -127,6 +128,8 @@ class Block(Statement):
     def exec(self, memory: MemoryInterface) -> Return:
         ret = DontReturn
         for stmt in self.stmts:
+            if DEBUG:
+                print("    "+termcolor.colored(stmt.render(), 'grey'))
             ret = stmt.exec(memory)
             if ret.ret:
                 break
@@ -160,6 +163,8 @@ class ReturnStatement(Statement):
 
     def init_stmt(self, scope: Scope) -> ReturnBehavior:
         t = self.expr.init_expr(scope)
+        if t is None:
+            raise StaticTypeError("Return statement: Expression does not evaluate to a value.")
         return AlwaysReturns(t)
 
     def exec(self, memory: MemoryInterface) -> Return:
@@ -167,7 +172,7 @@ class ReturnStatement(Statement):
         return DoReturn(val)
 
     def render(self) -> str:
-        return "return " + self.expr.render()
+        return "return " + self.expr.render() + ";"
 
 @dataclass
 class IfStatement(Statement):

+ 6 - 6
src/sccd/action_lang/static/types.py

@@ -43,7 +43,7 @@ class SCCDType(ABC):
     def is_bool_castable(self):
         return False
 
-@dataclass(eq=False)
+@dataclass(eq=False, repr=False)
 class _SCCDSimpleType(SCCDType):
     name: str
     neg: bool = False
@@ -96,26 +96,26 @@ class _SCCDSimpleType(SCCDType):
     def is_bool_castable(self):
         return self.bool_cast
 
-@dataclass(frozen=True)
+@dataclass(frozen=True, repr=False)
 class SCCDFunction(SCCDType):
     param_types: List[SCCDType]
     return_type: Optional[SCCDType] = None
 
     def _str(self):
         if self.param_types:
-            s = "func(" + ",".join(str(p) for p in self.param_types) + ")"
+            s = "func(" + ", ".join(p._str() for p in self.param_types) + ")"
         else:
             s = "func"
         if self.return_type:
-            s += " -> " + str(self.return_type)
+            s += " -> " + self.return_type._str()
         return s
 
-@dataclass(frozen=True)
+@dataclass(frozen=True, repr=False)
 class SCCDArray(SCCDType):
     element_type: SCCDType
 
     def _str(self):
-        return "[" + str(self.element_type) + "]"
+        return "[" + self.element_type._str() + "]"
 
     def is_eq(self, other):
         if isinstance(other, SCCDArray) and self.element_type.is_eq(other.element_type):

+ 73 - 57
src/sccd/statechart/dynamic/round.py

@@ -115,18 +115,65 @@ class CandidateGenerator:
         self.strategy = strategy
         self.cache = strategy.cache_init()
 
-    def generate(self, execution, enabled_events: List[InternalEvent], forbidden_arenas: Bitmap) -> Iterable[Transition]:
-        events_bitmap = bm_from_list(e.id for e in enabled_events)
-        key = self.strategy.key(execution, events_bitmap, forbidden_arenas)
+    def generate(self, execution, enabled_events: List[InternalEvent], forbidden_arenas: Bitmap) -> List[Transition]:
+        with timer.Context("generate candidates"):
+            events_bitmap = bm_from_list(e.id for e in enabled_events)
+            key = self.strategy.key(execution, events_bitmap, forbidden_arenas)
 
-        try:
-            candidates = self.cache[key]
-            ctr.cache_hits += 1
-        except KeyError:
-            candidates = self.cache[key] = self.strategy.generate(execution, events_bitmap, forbidden_arenas)
-            ctr.cache_misses += 1
+            try:
+                candidates = self.cache[key]
+                ctr.cache_hits += 1
+            except KeyError:
+                candidates = self.cache[key] = self.strategy.generate(execution, events_bitmap, forbidden_arenas)
+                ctr.cache_misses += 1
 
-        return filter(self.strategy.filter_f(execution, enabled_events, events_bitmap), candidates)
+            candidates = filter(self.strategy.filter_f(execution, enabled_events, events_bitmap), candidates)
+
+            if DEBUG:
+                candidates = list(candidates) # convert generator to list (gotta do this, otherwise the generator will be all used up by our debug printing
+                if candidates:
+                    print()
+                    if enabled_events:
+                        print("events: " + str(enabled_events))
+                    print("candidates: " + ",  ".join(str(t) for t in candidates))
+                candidates = iter(candidates)
+
+            t = next(candidates, None)
+            if t:
+                return [t]
+            else:
+                return []
+
+
+class ConcurrentCandidateGenerator:
+    __slots__ = ["strategy", "cache", "synchronous"]
+    def __init__(self, strategy, synchronous):
+        self.strategy = strategy
+        self.cache = strategy.cache_init()
+        self.synchronous = synchronous
+
+    def generate(self, execution, enabled_events: List[InternalEvent], forbidden_arenas: Bitmap) -> List[Transition]:
+        with timer.Context("generate candidates"):
+            events_bitmap = bm_from_list(e.id for e in enabled_events)
+            transitions = []
+            while True:
+                key = self.strategy.key(execution, events_bitmap, forbidden_arenas)
+                try:
+                    candidates = self.cache[key]
+                    ctr.cache_hits += 1
+                except KeyError:
+                    candidates = self.cache[key] = self.strategy.generate(execution, events_bitmap, forbidden_arenas)
+                    ctr.cache_misses += 1
+                candidates = filter(self.strategy.filter_f(execution, enabled_events, events_bitmap), candidates)
+                t = next(candidates, None)
+                if t:
+                    transitions.append(t)
+                else:
+                    break
+                if self.synchronous:
+                    events_bitmap |= t.opt.raised_events
+                forbidden_arenas |= t.opt.arena_bitmap
+            return transitions
 
 # 1st bitmap: arenas covered by transitions fired
 # 2nd bitmap: arenas covered by transitions that had a stable target state
@@ -268,60 +315,29 @@ class SuperRoundWithLimit(SuperRound):
 
 
 class SmallStep(Round):
-    __slots__ = ["execution", "generator", "concurrency"]
-    def __init__(self, name, execution, generator: CandidateGenerator, concurrency=False):
+    __slots__ = ["execution", "generator"]
+    def __init__(self, name, execution, generator: CandidateGenerator):
         super().__init__(name)
         self.execution = execution
         self.generator = generator
-        self.concurrency = concurrency
 
     def _run(self, forbidden_arenas: Bitmap) -> RoundResult:
-        enabled_events = None
-        def get_candidates(extra_forbidden):
-            nonlocal enabled_events
-            with timer.Context("get enabled events"):
-                enabled_events = self.enabled_events()
-                # The cost of sorting our enabled events is smaller than the benefit gained by having to loop less often over it in our transition execution code:
-                enabled_events.sort(key=lambda e: e.id)
-
-            candidates = self.generator.generate(self.execution, enabled_events, forbidden_arenas |  extra_forbidden)
-
-            if DEBUG:
-                candidates = list(candidates) # convert generator to list (gotta do this, otherwise the generator will be all used up by our debug printing
-                if candidates:
-                    print()
-                    if enabled_events:
-                        print("events: " + str(enabled_events))
-                    print("candidates: " + ",  ".join(str(t) for t in candidates))
-                candidates = iter(candidates)
+        enabled_events = self.enabled_events()
+        # The cost of sorting our enabled events is smaller than the benefit gained by having to loop less often over it in our transition execution code:
+        enabled_events.sort(key=lambda e: e.id)
 
-            return candidates
+        transitions = self.generator.generate(self.execution, enabled_events, forbidden_arenas)
 
-        arenas = Bitmap()
+        dirty_arenas = Bitmap()
         stable_arenas = Bitmap()
 
-        with timer.Context("candidate generation"):
-            candidates = get_candidates(0)
-            t = next(candidates, None)
-        while t:
+        for t in transitions:
             arena = t.opt.arena_bitmap
-            if not (arenas & arena):
-                self.execution.fire_transition(enabled_events, t)
-                arenas |= arena
-                if t.targets[0].stable:
-                    stable_arenas |= arena
-
-                if not self.concurrency:
-                    # Return after first transition execution
-                    break
-
-                # need to re-generate candidates after firing transition
-                # because possibly the set of current events has changed
-                with timer.Context("candidate generation"):
-                    candidates = get_candidates(extra_forbidden=arenas)
-                    t = next(candidates, None)
-            else:
-                with timer.Context("candidate generation"):
-                    t = next(candidates, None)
-
-        return (arenas, stable_arenas)
+            self.execution.fire_transition(enabled_events, t)
+            dirty_arenas |= arena
+            if t.targets[0].stable:
+                stable_arenas |= arena
+            enabled_events = self.enabled_events()
+            enabled_events.sort(key=lambda e: e.id)
+
+        return (dirty_arenas, stable_arenas)

+ 21 - 24
src/sccd/statechart/dynamic/statechart_execution.py

@@ -24,24 +24,22 @@ class StatechartExecution:
         self.configuration: Bitmap = Bitmap()
 
         # Mapping from history_id to set of states to enter if history is target of transition
-        # By default, if the parent of a history state has never been exited before, the parent's default states should be entered.
-        self.history_values: List[Bitmap] = [h.parent.opt.ts_static for h in statechart.tree.history_states]
+        self.history_values: List[Bitmap] = list(statechart.tree.initial_history_values)
 
         # Scheduled IDs for after triggers
         self.timer_ids = [None] * len(statechart.tree.after_triggers)
 
     # enter default states
     def initialize(self):
-        states = self.statechart.tree.root.opt.ts_static
-        self.configuration = states
+        self.configuration = self.statechart.tree.initial_states
 
         ctx = EvalContext(execution=self, events=[], memory=self.rhs_memory)
         if self.statechart.datamodel is not None:
             self.statechart.datamodel.exec(self.rhs_memory)
 
-        for state in self._ids_to_states(bm_items(states)):
+        for state in self._ids_to_states(bm_items(self.configuration)):
             print_debug(termcolor.colored('  ENTER %s'%state.opt.full_name, 'green'))
-            self._perform_actions(ctx, state.enter)
+            _perform_actions(ctx, state.enter)
             self._start_timers(state.opt.after_triggers)
 
         self.rhs_memory.flush_transition()
@@ -60,10 +58,11 @@ class StatechartExecution:
                     exit_ids = self.configuration & t.opt.arena.opt.descendants
                     exit_set = self._ids_to_states(bm_reverse_items(exit_ids))
 
-
                 with timer.Context("enter set"):
                     # Sequence of enter states is more complex but has for a large part already been computed statically.
-                    enter_ids = t.opt.enter_states_static | reduce(lambda x,y: x|y, (self.history_values[s.history_id] for s in t.opt.enter_states_dynamic), Bitmap())
+                    enter_ids = t.opt.enter_states_static
+                    if t.opt.target_history_id is not None:
+                        enter_ids |= self.history_values[t.opt.target_history_id]
                     enter_set = self._ids_to_states(bm_items(enter_ids))
 
                 ctx = EvalContext(execution=self, events=events, memory=self.rhs_memory)
@@ -71,14 +70,13 @@ class StatechartExecution:
                 print_debug("fire " + str(t))
 
                 with timer.Context("exit states"):
-                    # exit states...
                     for s in exit_set:
                         print_debug(termcolor.colored('  EXIT %s' % s.opt.full_name, 'green'))
                         # remember which state(s) we were in if a history state is present
-                        for h, mask in s.opt.history:
-                            self.history_values[h.history_id] = exit_ids & mask
+                        for history_id, history_mask in s.opt.history:
+                            self.history_values[history_id] = exit_ids & history_mask
                         self._cancel_timers(s.opt.after_triggers)
-                        self._perform_actions(ctx, s.exit)
+                        _perform_actions(ctx, s.exit)
                         self.configuration &= ~s.opt.state_id_bitmap
 
                 # execute transition action(s)
@@ -86,26 +84,25 @@ class StatechartExecution:
                     self.rhs_memory.push_frame(t.scope) # make room for event parameters on stack
                     if t.trigger:
                         t.trigger.copy_params_to_stack(ctx)
-                    self._perform_actions(ctx, t.actions)
+                    _perform_actions(ctx, t.actions)
                     self.rhs_memory.pop_frame()
 
                 with timer.Context("enter states"):
-                    # enter states...
                     for s in enter_set:
                         print_debug(termcolor.colored('  ENTER %s' % s.opt.full_name, 'green'))
                         self.configuration |= s.opt.state_id_bitmap
-                        self._perform_actions(ctx, s.enter)
+                        _perform_actions(ctx, s.enter)
                         self._start_timers(s.opt.after_triggers)
 
                 self.rhs_memory.flush_transition()
 
             # input(">")
 
-        except SCCDRuntimeException as e:
+        except Exception as e:
             e.args = ("During execution of transition %s:\n" % str(t) +str(e),)
             raise
 
-    def check_guard(self, t, events) -> bool:
+    def check_guard(self, t: Transition, events: List[InternalEvent]) -> bool:
         try:
             if t.guard is None:
                 return True
@@ -118,15 +115,10 @@ class StatechartExecution:
                 result = t.guard.eval(self.gc_memory)
                 self.gc_memory.pop_frame()
                 return result
-        except SCCDRuntimeException as e:
+        except Exception as e:
             e.args = ("While checking guard of transition %s:\n" % str(t) +str(e),)
             raise
 
-    @staticmethod
-    def _perform_actions(ctx: EvalContext, actions: List[Action]):
-        for a in actions:
-            a.exec(ctx)
-
     def _start_timers(self, triggers: List[AfterTrigger]):
         for after in triggers:
             delay: Duration = after.delay.eval(
@@ -141,10 +133,15 @@ class StatechartExecution:
 
     # Return whether the current configuration includes ALL the states given.
     def in_state(self, state_strings: List[str]) -> bool:
-        state_ids_bitmap = states_to_bitmap((self.statechart.tree.state_dict[state_string] for state_string in state_strings))
+        state_ids_bitmap = bm_union(self.statechart.tree.state_dict[state_string].opt.state_id_bitmap for state_string in state_strings)
         in_state = bm_has_all(self.configuration, state_ids_bitmap)
         # if in_state:
         #     print_debug("in state"+str(state_strings))
         # else:
         #     print_debug("not in state"+str(state_strings))
         return in_state
+
+
+def _perform_actions(ctx: EvalContext, actions: List[Action]):
+    for a in actions:
+        a.exec(ctx)

+ 8 - 3
src/sccd/statechart/dynamic/statechart_instance.py

@@ -23,6 +23,7 @@ class Instance(ABC):
 # TODO: make this a model parameter
 LIMIT = 100
 
+# An "instance" in the context of the SCCD runtime
 class StatechartInstance(Instance):
     def __init__(self, statechart: Statechart, object_manager, output_callback, schedule_callback, cancel_callback):
         # self.object_manager = object_manager
@@ -48,12 +49,16 @@ class StatechartInstance(Instance):
         strategy = EnabledEventsStrategy(priority_ordered_transitions, statechart)
         # strategy = CurrentConfigAndEnabledEventsStrategy(priority_ordered_transitions, statechart)
 
-        generator = CandidateGenerator(strategy)
+        if semantics.concurrency == Concurrency.SINGLE:
+            generator = CandidateGenerator(strategy)
+        elif semantics.concurrency == Concurrency.MANY:
+            generator = ConcurrentCandidateGenerator(strategy, synchronous=semantics.internal_event_lifeline == InternalEventLifeline.SAME)
+        else:
+            raise Exception("Unsupported option: %s" % semantics.concurrency)
 
         # Big step + combo step maximality semantics
 
-        small_step = SmallStep(termcolor.colored("small", 'blue'), self.execution, generator,
-            concurrency=semantics.concurrency==Concurrency.MANY)
+        small_step = SmallStep(termcolor.colored("small", 'blue'), self.execution, generator)
 
 
         if semantics.big_step_maximality == BigStepMaximality.TAKE_ONE:

+ 3 - 3
src/sccd/statechart/static/action.py

@@ -40,9 +40,6 @@ class RaiseEvent(Action):
     name: str
     params: List[Expression]
 
-    # just a simple string representation for rendering a transition label
-    def render(self) -> str:
-        return '^'+self.name
 
     def _eval_params(self, memory: MemoryInterface) -> List[Any]:
         return [p.eval(memory) for p in self.params]
@@ -51,6 +48,9 @@ class RaiseEvent(Action):
 class RaiseInternalEvent(RaiseEvent):
     event_id: int
 
+    def render(self) -> str:
+        return '^'+self.name
+
     def exec(self, ctx: EvalContext):
         params = self._eval_params(ctx.memory)
         ctx.execution.raise_internal(

+ 135 - 87
src/sccd/statechart/static/tree.py

@@ -31,8 +31,17 @@ class State(Freezable):
         if self.parent is not None:
             self.parent.children.append(self)
 
+
+    # Subset of descendants that are always entered when this state is the target of a transition, minus history states.
+    def _static_target_states(self) -> Bitmap:
+        if self.default_state:
+            return self.opt.state_id_bitmap | self.default_state._static_target_states()
+        else:
+            return self.opt.state_id_bitmap
+
+    # States that are always entered when this state is part of the "enter path", but not the actual target of a transition.
     def _static_additional_target_states(self, exclude: 'State') -> Tuple[Bitmap, List['HistoryState']]:
-        return (self.opt.state_id_bitmap, [])
+        return self.opt.state_id_bitmap
 
     def __repr__(self):
         return "State(\"%s\")" % (self.short_name)
@@ -50,14 +59,17 @@ class HistoryState(State):
     def history_mask(self) -> Bitmap:
         pass
 
-    def _static_additional_target_states(self, exclude: 'State') -> Tuple[Bitmap, List['HistoryState']]:
+    def _static_target_states(self) -> Bitmap:
+        return Bitmap()
+
+    def _static_additional_target_states(self, exclude: 'State') -> Bitmap:
         assert False # history state cannot have children and therefore should never occur in a "enter path"
 
 class ShallowHistoryState(HistoryState):
 
     def history_mask(self) -> Bitmap:
         # Only direct children of parent:
-        return states_to_bitmap(self.parent.children)
+        return bm_union(s.opt.state_id_bitmap for s in self.parent.children)
 
     def __repr__(self):
         return "ShallowHistoryState(\"%s\")" % (self.short_name)
@@ -73,8 +85,11 @@ class DeepHistoryState(HistoryState):
 
 class ParallelState(State):
 
-    def _static_additional_target_states(self, exclude: 'State') -> Tuple[Bitmap, List['HistoryState']]:
-        return (self.opt.ts_static & ~exclude.opt.ts_static, [s for s in self.opt.ts_dynamic if s not in exclude.opt.ts_dynamic])
+    def _static_target_states(self) -> Bitmap:
+        return bm_union(c._static_target_states() for c in self.children if not isinstance(c, HistoryState)) | self.opt.state_id_bitmap
+
+    def _static_additional_target_states(self, exclude: 'State') -> Bitmap:
+        return self._static_target_states() & ~exclude._static_target_states()
 
     def __repr__(self):
         return "ParallelState(\"%s\")" % (self.short_name)
@@ -165,6 +180,8 @@ class AfterTrigger(Trigger):
     def copy_params_to_stack(self, ctx: EvalContext):
         pass
 
+_empty_trigger = Trigger(enabling=[])
+
 class Transition(Freezable):
     __slots__ = ["source", "targets", "scope", "target_string", "guard", "actions", "trigger", "opt"]
 
@@ -179,7 +196,7 @@ class Transition(Freezable):
 
         self.guard: Optional[Expression] = None
         self.actions: List[Action] = []
-        self.trigger: Optional[Trigger] = None
+        self.trigger: Trigger = _empty_trigger
 
         self.opt: Optional['TransitionOptimization'] = None        
                     
@@ -189,7 +206,7 @@ class Transition(Freezable):
 
 # Data that is generated for each state.
 class StateOptimization(Freezable):
-    __slots__ = ["full_name", "depth", "state_id", "state_id_bitmap", "ancestors", "descendants", "history", "ts_static", "ts_dynamic", "after_triggers"]
+    __slots__ = ["full_name", "depth", "state_id", "state_id_bitmap", "ancestors", "descendants", "history", "after_triggers"]
     def __init__(self):
         super().__init__()
 
@@ -202,14 +219,8 @@ class StateOptimization(Freezable):
         self.ancestors: Bitmap = Bitmap()
         self.descendants: Bitmap = Bitmap()
 
-        # Subset of children that are HistoryState.
-        # For each item, the second element of the tuple is the "history mask".
-        self.history: List[Tuple[HistoryState, Bitmap]] = []
-
-        # Subset of descendants that are always entered when this state is the target of a transition
-        self.ts_static: Bitmap = Bitmap() 
-        # Subset of descendants that are history states AND are in the subtree of states automatically entered if this state is the target of a transition.
-        self.ts_dynamic: List[HistoryState] = []
+        # Tuple for each children that is HistoryState: (history-id, history mask)
+        self.history: List[Tuple[int, Bitmap]] = []
 
         # Triggers of outgoing transitions that are AfterTrigger.
         self.after_triggers: List[AfterTrigger] = []
@@ -217,21 +228,23 @@ class StateOptimization(Freezable):
 
 # Data that is generated for each transition.
 class TransitionOptimization(Freezable):
-    __slots__ = ["arena", "arena_bitmap", "enter_states_static", "enter_states_dynamic"]
+    __slots__ = ["arena", "arena_bitmap", "enter_states_static", "target_history_id", "raised_events"]
 
-    def __init__(self, arena: State, arena_bitmap: Bitmap, enter_states_static: Bitmap, enter_states_dynamic: List[HistoryState]):
+    def __init__(self, arena: State, arena_bitmap: Bitmap, enter_states_static: Bitmap, target_history_id: Optional[int], raised_events: Bitmap):
         super().__init__()
         self.arena: State = arena
         self.arena_bitmap: Bitmap = arena_bitmap
-        self.enter_states_static: Bitmap = enter_states_static # The "enter set" can be computed partially statically, and if there are no history states in it, entirely statically
-        self.enter_states_dynamic: List[HistoryState] = enter_states_dynamic # The part of the "enter set" that cannot be computed statically.
+        # The "enter set" can be computed partially statically, or entirely statically if there are no history states in it.
+        self.enter_states_static: Bitmap = enter_states_static
+        self.target_history_id: Optional[int] = target_history_id # History ID if target of transition is a history state, otherwise None.
+        self.raised_events: Bitmap = raised_events # (internal) event IDs raised by this transition
         self.freeze()
 
 
 class StateTree(Freezable):
-    __slots__ = ["root", "transition_list", "state_list", "state_dict", "after_triggers", "stable_bitmap", "history_states"]
+    __slots__ = ["root", "transition_list", "state_list", "state_dict", "after_triggers", "stable_bitmap", "initial_history_values", "initial_states"]
 
-    def __init__(self, root: State, transition_list: List[Transition], state_list: List[State], state_dict: Dict[str, State], after_triggers: List[AfterTrigger], stable_bitmap: Bitmap, history_states: List[HistoryState]):
+    def __init__(self, root: State, transition_list: List[Transition], state_list: List[State], state_dict: Dict[str, State], after_triggers: List[AfterTrigger], stable_bitmap: Bitmap, initial_history_values: List[Bitmap], initial_states: Bitmap):
         super().__init__()
         self.root: State = root
         self.transition_list: List[Transition] = transition_list # depth-first document order
@@ -239,20 +252,14 @@ class StateTree(Freezable):
         self.state_dict: Dict[str, State] = state_dict # mapping from 'full name' to State
         self.after_triggers: List[AfterTrigger] = after_triggers # all after-triggers in the statechart
         self.stable_bitmap: Bitmap = stable_bitmap # set of states that are syntactically marked 'stable'
-        self.history_states: List[HistoryState] = history_states # all the history states in the statechart
+        self.initial_history_values: List[Bitmap] = initial_history_values # targets of each history state before history has been built.
+        self.initial_states: Bitmap = initial_states
         self.freeze()
 
-# Reduce a list of states to a set of states, as a bitmap
-def states_to_bitmap(state_list: List[State]) -> Bitmap:
-    return reduce(lambda x,y: x|y, (s.opt.state_id_bitmap for s in state_list), Bitmap())
-
 def optimize_tree(root: State) -> StateTree:
     with timer.Context("optimize tree"):
 
-        transition_list = []
-        after_triggers = []
-        history_states = []
-        def init_opt():
+        def assign_state_id():
             next_id = 0
             def f(state: State, _=None):
                 state.opt = StateOptimization()
@@ -262,22 +269,8 @@ def optimize_tree(root: State) -> StateTree:
                 state.opt.state_id_bitmap = bit(next_id)
                 next_id += 1
 
-                for t in state.transitions:
-                    transition_list.append(t)
-                    if t.trigger and isinstance(t.trigger, AfterTrigger):
-                        state.opt.after_triggers.append(t.trigger)
-                        after_triggers.append(t.trigger)
-
-                if isinstance(state, HistoryState):
-                    state.history_id = len(history_states)
-                    history_states.append(state)
-
             return f
 
-        def assign_depth(state: State, parent_depth: int = 0):
-            state.opt.depth = parent_depth + 1
-            return parent_depth + 1
-
         def assign_full_name(state: State, parent_full_name: str = ""):
             if state is root:
                 full_name = '/'
@@ -288,6 +281,10 @@ def optimize_tree(root: State) -> StateTree:
             state.opt.full_name = full_name
             return full_name
 
+        def assign_depth(state: State, parent_depth: int = 0):
+            state.opt.depth = parent_depth + 1
+            return parent_depth + 1
+
         state_dict = {}
         state_list = []
         stable_bitmap = Bitmap()
@@ -298,34 +295,34 @@ def optimize_tree(root: State) -> StateTree:
             if state.stable:
                 stable_bitmap |= state.opt.state_id_bitmap
 
-        def set_ancestors(state: State, ancestors=[]):
-            state.opt.ancestors = states_to_bitmap(ancestors)
-            return ancestors + [state]
+        transition_list = []
+        after_triggers = []
+        def visit_transitions(state: State, _=None):
+                for t in state.transitions:
+                    transition_list.append(t)
+                    if t.trigger and isinstance(t.trigger, AfterTrigger):
+                        state.opt.after_triggers.append(t.trigger)
+                        after_triggers.append(t.trigger)
+
+
+        def set_ancestors(state: State, ancestors=Bitmap()):
+            state.opt.ancestors = ancestors
+            return ancestors | state.opt.state_id_bitmap
 
         def set_descendants(state: State, children_descendants):
-            descendants = reduce(lambda x,y: x|y, children_descendants, Bitmap())
+            descendants = bm_union(children_descendants)
             state.opt.descendants = descendants
             return state.opt.state_id_bitmap | descendants
 
-        def set_static_target_states(state: State, _):
-            if isinstance(state, ParallelState):
-                state.opt.ts_static = reduce(lambda x,y: x|y, (s.opt.ts_static for s in state.children), state.opt.state_id_bitmap)
-                state.opt.ts_dynamic = list(itertools.chain.from_iterable(c.opt.ts_dynamic for c in state.children if not isinstance(c, HistoryState)))
-            elif isinstance(state, HistoryState):
-                state.opt.ts_static = Bitmap()
-                state.opt.ts_dynamic = [state]
-            else: # "regular" state:
-                if state.default_state:
-                    state.opt.ts_static = state.opt.state_id_bitmap | state.default_state.opt.ts_static
-                    state.opt.ts_dynamic = state.default_state.opt.ts_dynamic
-                else:
-                    state.opt.ts_static = state.opt.state_id_bitmap
-                    state.opt.ts_dynamic = []
+        # If a history state is entered whose parent has never been exited before, the parent's default states are entered.
+        initial_history_values = []
+        def deal_with_history(state: State, children_history):
+            state.opt.history = [(h.history_id, h.history_mask()) for h in children_history if h is not None]
 
-        def add_history(state: State, _= None):
-            for c in state.children:
-                if isinstance(c, HistoryState):
-                    state.opt.history.append((c, c.history_mask()))
+            if isinstance(state, HistoryState):
+                state.history_id = len(initial_history_values)
+                initial_history_values.append(state.parent._static_target_states())
+                return state
 
         def freeze(state: State, _=None):
             state.freeze()
@@ -333,16 +330,16 @@ def optimize_tree(root: State) -> StateTree:
 
         visit_tree(root, lambda s: s.children,
             before_children=[
-                init_opt(),
+                assign_state_id(),
                 assign_full_name,
                 assign_depth,
                 add_to_list,
+                visit_transitions,
                 set_ancestors,
             ],
             after_children=[
                 set_descendants,
-                add_history,
-                set_static_target_states,
+                deal_with_history,
                 freeze,
             ])
 
@@ -370,31 +367,40 @@ def optimize_tree(root: State) -> StateTree:
             enter_path_iter = bm_items(enter_path)
             state_id = next(enter_path_iter, None)
             enter_states_static = Bitmap()
-            enter_states_dynamic = []
             while state_id is not None:
                 state = state_list[state_id]
                 next_state_id = next(enter_path_iter, None)
                 if next_state_id:
                     # an intermediate state on the path from arena to target
                     next_state = state_list[next_state_id]
-                    static, dynamic = state._static_additional_target_states(next_state)
-                    enter_states_static |= static
-                    enter_states_dynamic += dynamic
+                    enter_states_static |= state._static_additional_target_states(next_state)
                 else:
                     # the actual target of the transition
-                    enter_states_static |= state.opt.ts_static
-                    enter_states_dynamic += state.opt.ts_dynamic
+                    enter_states_static |= state._static_target_states()
                 state_id = next_state_id
 
+            target_history_id = None
+            if isinstance(t.targets[0], HistoryState):
+                target_history_id = t.targets[0].history_id
+
+
+            raised_events = Bitmap()
+            for a in t.actions:
+                if isinstance(a, RaiseInternalEvent):
+                    raised_events |= bit(a.event_id)
+
             t.opt = TransitionOptimization(
                 arena=arena,
                 arena_bitmap=arena.opt.descendants | arena.opt.state_id_bitmap,
                 enter_states_static=enter_states_static,
-                enter_states_dynamic=enter_states_dynamic)
+                target_history_id=target_history_id,
+                raised_events=raised_events)
 
             t.freeze()
 
-        return StateTree(root, transition_list, state_list, state_dict, after_triggers, stable_bitmap, history_states)
+        initial_states = root._static_target_states()
+
+        return StateTree(root, transition_list, state_list, state_dict, after_triggers, stable_bitmap, initial_history_values, initial_states)
 
 
 def priority_source_parent(tree: StateTree) -> List[Transition]:
@@ -404,28 +410,70 @@ def priority_source_parent(tree: StateTree) -> List[Transition]:
 # The following 3 priority implementations all do a stable sort with a partial order-key
 
 def priority_source_child(tree: StateTree) -> List[Transition]:
-    return list(sorted(tree.transition_list, key=lambda t: -t.source.opt.depth))
+    return sorted(tree.transition_list, key=lambda t: -t.source.opt.depth)
 
 def priority_arena_parent(tree: StateTree) -> List[Transition]:
-    return list(sorted(tree.transition_list, key=lambda t: t.opt.arena.opt.depth))
+    return sorted(tree.transition_list, key=lambda t: t.opt.arena.opt.depth)
 
 def priority_arena_child(tree: StateTree) -> List[Transition]:
-    return list(sorted(tree.transition_list, key=lambda t: -t.opt.arena.opt.depth))
+    return sorted(tree.transition_list, key=lambda t: -t.opt.arena.opt.depth)
 
 
 def concurrency_arena_orthogonal(tree: StateTree):
     with timer.Context("concurrency_arena_orthogonal"):
         import collections
-        nonoverlapping = collections.defaultdict(list)
-        for t1,t2 in itertools.combinations(tree.transition_list, r=2):
+        # arena_to_transition = collections.defaultdict(list)
+        # for t in tree.transition_list:
+        #     arena_to_transition[t.opt.arena].append(t)
+
+        # def sets(state: State):
+        #     sets = []
+        #     for t in arena_to_transition[state]:
+        #         sets.append(set((t,)))
+
+        #     for c in state.children:
+
+        #     return sets
+
+
+        # s = sets(tree.root)
+
+        # print(s)
+
+        sets = {}
+        unique_sets = []
+        for t1, t2 in itertools.combinations(tree.transition_list, r=2):
             if not (t1.opt.arena_bitmap & t2.opt.arena_bitmap):
-                nonoverlapping[t1].append(t2)
-                nonoverlapping[t2].append(t1)
+                if t1 in sets:
+                    sets[t1].add(t2)
+                    sets[t2] = sets[t1]
+                elif t2 in sets:
+                    sets[t2].add(t1)
+                    sets[t1] = sets[t2]
+                else:
+                    s = set((t1,t2))
+                    sets[t1] = s
+                    sets[t2] = s
+                    unique_sets.append(s)
+                    print('added', s)
+                    
 
-        for t, ts in nonoverlapping.items():
-            print(str(t), "does not overlap with", ",".join(str(t) for t in ts))
+        print((unique_sets))
 
-        print(len(nonoverlapping), "nonoverlapping pairs of transitions")
+        # concurrent_set = itertools.chain.from_iterable(itertools.combinations(ls,r) for r in range(len(ls)+1))
+        # print(concurrent_set)
+
+        # import collections
+        # nonoverlapping = collections.defaultdict(list)
+        # for t1,t2 in itertools.combinations(tree.transition_list, r=2):
+        #     if not (t1.opt.arena_bitmap & t2.opt.arena_bitmap):
+        #         nonoverlapping[t1].append(t2)
+        #         nonoverlapping[t2].append(t1)
+
+        # for t, ts in nonoverlapping.items():
+        #     print(str(t), "does not overlap with", ",".join(str(t) for t in ts))
+
+        # print(len(nonoverlapping), "nonoverlapping pairs of transitions")
 
 def concurrency_src_dst_orthogonal(tree: StateTree):
     with timer.Context("concurrency_src_dst_orthogonal"):

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

@@ -73,6 +73,7 @@ class Test(unittest.TestCase):
 
       # start the controller
       thread = threading.Thread(target=controller_thread)
+      thread.daemon = True # make controller thread exit when main thread exits
       thread.start()
 
       # check output

+ 4 - 1
src/sccd/util/bitmap.py

@@ -41,9 +41,12 @@ def bit(pos):
 
 # Non-member function variants so they also work on integers:
 def bm_from_list(iterable):
-  v = reduce(lambda x,y: x|1<<y, iterable, 0) # iterable
+  v = reduce(lambda x,y: x|1<<y, iterable, 0)
   return Bitmap(v)
 
+def bm_union(iterable):
+  return reduce(lambda x,y: x|y, iterable, 0)
+
 def bm_has(self, pos):
   return self & 1 << pos
 

+ 11 - 35
src/sccd/util/visit_tree.py

@@ -7,42 +7,18 @@ import itertools
 # 'after_children' is a list of callbacks that will be called with 2 parameters: 2) the current element and 2) An empty list for all the leaf elements or a list with the responses of the children of that element for the callback.
 def visit_tree(node, get_children, before_children=[], after_children=[], parent_values=None):
     # Most parameters unchanged for recursive calls
-    visit = functools.partial(visit_tree, get_children=get_children, before_children=before_children, after_children=after_children)
+    def visit(node, parent_values):
+        if parent_values is None:
+            parent_values = [f(node) for f in before_children]
+        else:
+            parent_values = [f(node, p) for f,p in zip(before_children, parent_values)]
 
-    if parent_values is None:
-        parent_values = [f(node) for f in before_children]
-    else:
-        parent_values = [f(node, p) for f,p in zip(before_children, parent_values)]
+        # child_responses is a list of len(children)-many lists C, where for every child of node, C is the 'to_parent' value returned by every child
+        child_responses = [visit(node=c, parent_values=parent_values) for c in get_children(node)]
 
+        to_parent = [f(node, [cs[i] for cs in child_responses]) for i,f in enumerate(after_children)]
+        # 'to_parent' is the mapping from our after_children-functions to those functions called on the responses we got from our children
 
-    child_responses = (visit(node=c, parent_values=parent_values) for c in get_children(node))
-    # child_responses is a list of len(children)-many lists C, where for every child of node, C is the 'to_parent' value returned by every child
+        return to_parent
 
-    to_parent = [f(node, [cs[i] for cs in child_responses]) for i,f in enumerate(after_children)]
-    # 'to_parent' is the mapping from our after_children-functions to those functions called on the responses we got from our children
-
-    return to_parent
-
-
-# def visit_tree2(node, get_children, before_child=[], after_child=[], parent_values=None):
-#     # Most parameters unchanged for recursive calls
-#     visit = functools.partial(visit_tree2, get_children=get_children, before_child=before_child, after_child=after_child)
-
-#     if parent_values is None:
-#         parent_values = [before_f(node) for before_f in before_child]
-#     else:
-#         parent_values = [before_f(node, p) for before_f,p in zip(before_child, parent_values)]
-
-#     children_responses = [visit(node=c, parent_values=parent_values) for c in get_children(node)]
-#     children_responses2 = [list(itertools.chain.from_iterable(cr[i] for cr in children_responses)) for i,after_f in enumerate(after_child)]
-
-#     to_parent = []
-#     for after_f, res in zip(after_child, children_responses2):
-#         ls = []
-#         after_f(node, res, ls)
-#         to_parent.append(ls)
-
-#     print("visit_tree2, node=",node,"children_responses:",children_responses2)
-#     print("to parent:", to_parent)
-
-#     return to_parent
+    return visit(node, parent_values)

+ 3 - 1
test/test_files/day_atlee/statechart_fig1_redialer.xml

@@ -14,7 +14,7 @@
 
     digit = func(i:int, pos:int) {
       result = i // 10**pos % 10;
-      # log("digit " + int_to_str(pos) + " of " + int_to_str(i) + " is " + int_to_str(result));
+      log("digit " + int_to_str(pos) + " of " + int_to_str(i) + " is " + int_to_str(result));
       return result;
     };
 
@@ -50,6 +50,7 @@
           <!-- t2 -->
           <transition event="dial(d:int), redial" cond="c == 0" target="../DialDigits">
             <code>
+              log("got dial("+int_to_str(d)+")");
               lp = d;
               c = 1;
             </code>
@@ -62,6 +63,7 @@
           <!-- t3 -->
           <transition event="dial(d:int)" cond="c &lt; 10" target=".">
             <code>
+              log("got dial("+int_to_str(d)+")");
               lp = lp * 10 + d;
               c += 1;
             </code>

+ 24 - 0
test/test_files/day_atlee/test_04_counter_many_srcdstortho.xml

@@ -0,0 +1,24 @@
+<?xml version="1.0" ?>
+<test>
+  <statechart src="statechart_fig10_counter.xml">
+    <override_semantics
+      big_step_maximality="take_one"
+      concurrency="many"
+      internal_event_lifeline="same"/>
+  </statechart>
+
+  <input>
+    <!-- setup ... -->
+    <event port="in" name="tk0" time="10 ms"/>
+    <event port="in" name="tk0" time="20 ms"/>
+    <event port="in" name="tk0" time="30 ms"/>
+    <!-- we are now in Bit_12, Bit_22 and Counting -->
+    <event port="in" name="tk0" time="40 ms"/>
+  </input>
+
+  <output>
+    <big_step>
+      <event port="out" name="done"/>
+    </big_step>
+  </output>
+</test>

+ 1 - 1
test/test_files/features/action_lang/test_nested.xml

@@ -9,7 +9,7 @@
         inner = func(j:int) {
           if (i > j) {
             ok = True;
-          };
+          }
         };
         inner(i-1);
       };