syscalls: support generic system calls
[openpower-isa.git] / src / openpower / decoder / test / pysim.py
1 from contextlib import contextmanager
2 import itertools
3 from vcd import VCDWriter
4 from vcd.gtkw import GTKWSave
5
6 from nmigen.hdl import ClockSignal, ResetSignal
7 from nmigen.hdl.ast import SignalDict
8 from nmigen.sim._base import BaseSignalState, BaseSimulation, BaseEngine
9 from openpower.decoder.test._pyrtl import _FragmentCompiler
10 from nmigen.sim._pycoro import PyCoroProcess
11 from nmigen.sim._pyclock import PyClockProcess
12
13 import os
14 import re
15 import sys
16 import shutil
17 import importlib
18
19 from cffi import FFI
20 from os.path import dirname, join
21 from collections import namedtuple
22
23 from openpower.decoder.test.crtl_path import get_crtl_path
24
25 __all__ = ["PySimEngine"]
26
27
28 class _NameExtractor:
29 def __init__(self):
30 self.names = SignalDict()
31
32 def __call__(self, fragment, *, hierarchy=("top",)):
33 def add_signal_name(signal):
34 hierarchical_signal_name = (*hierarchy, signal.name)
35 if signal not in self.names:
36 self.names[signal] = {hierarchical_signal_name}
37 else:
38 self.names[signal].add(hierarchical_signal_name)
39
40 for domain_name, domain_signals in fragment.drivers.items():
41 if domain_name is not None:
42 domain = fragment.domains[domain_name]
43 add_signal_name(domain.clk)
44 if domain.rst is not None:
45 add_signal_name(domain.rst)
46
47 for statement in fragment.statements:
48 for signal in statement._lhs_signals() | statement._rhs_signals():
49 if not isinstance(signal, (ClockSignal, ResetSignal)):
50 add_signal_name(signal)
51
52 for subfragment_index, (subfragment, subfragment_name) in \
53 enumerate(fragment.subfragments):
54 if subfragment_name is None:
55 subfragment_name = "U${}".format(subfragment_index)
56 self(subfragment, hierarchy=(*hierarchy, subfragment_name))
57
58 return self.names
59
60
61 class _VCDWriter:
62 @staticmethod
63 def timestamp_to_vcd(timestamp):
64 return timestamp * (10 ** 10) # 1/(100 ps)
65
66 @staticmethod
67 def decode_to_vcd(signal, value):
68 return signal.decoder(value).expandtabs().replace(" ", "_")
69
70 def __init__(self, fragment, *, vcd_file, gtkw_file=None, traces=()):
71 if isinstance(vcd_file, str):
72 vcd_file = open(vcd_file, "wt")
73 if isinstance(gtkw_file, str):
74 gtkw_file = open(gtkw_file, "wt")
75
76 self.vcd_vars = SignalDict()
77 self.vcd_file = vcd_file
78 self.vcd_writer = vcd_file and VCDWriter(self.vcd_file,
79 timescale="100 ps", comment="Generated by nMigen")
80
81 self.gtkw_names = SignalDict()
82 self.gtkw_file = gtkw_file
83 self.gtkw_save = gtkw_file and GTKWSave(self.gtkw_file)
84
85 self.traces = []
86
87 signal_names = _NameExtractor()(fragment)
88
89 trace_names = SignalDict()
90 for trace in traces:
91 if trace not in signal_names:
92 trace_names[trace] = {("top", trace.name)}
93 self.traces.append(trace)
94
95 if self.vcd_writer is None:
96 return
97
98 for signal, names in (itertools.chain(signal_names.items(),
99 trace_names.items())):
100 if signal.decoder:
101 var_type = "string"
102 var_size = 1
103 var_init = self.decode_to_vcd(signal, signal.reset)
104 else:
105 var_type = "wire"
106 var_size = signal.width
107 var_init = signal.reset
108
109 for (*var_scope, var_name) in names:
110 suffix = None
111 while True:
112 try:
113 if suffix is None:
114 var_name_suffix = var_name
115 else:
116 var_name_suffix = "{}${}".format(var_name, suffix)
117 if signal not in self.vcd_vars:
118 vcd_var = self.vcd_writer.register_var(
119 scope=var_scope, name=var_name_suffix,
120 var_type=var_type, size=var_size, init=var_init)
121 self.vcd_vars[signal] = vcd_var
122 else:
123 self.vcd_writer.register_alias(
124 scope=var_scope, name=var_name_suffix,
125 var=self.vcd_vars[signal])
126 break
127 except KeyError:
128 suffix = (suffix or 0) + 1
129
130 if signal not in self.gtkw_names:
131 self.gtkw_names[signal] = (*var_scope, var_name_suffix)
132
133 def update(self, timestamp, signal, value):
134 vcd_var = self.vcd_vars.get(signal)
135 if vcd_var is None:
136 return
137
138 vcd_timestamp = self.timestamp_to_vcd(timestamp)
139 if signal.decoder:
140 var_value = self.decode_to_vcd(signal, value)
141 else:
142 var_value = value
143 self.vcd_writer.change(vcd_var, vcd_timestamp, var_value)
144
145 def close(self, timestamp):
146 if self.vcd_writer is not None:
147 self.vcd_writer.close(self.timestamp_to_vcd(timestamp))
148
149 if self.gtkw_save is not None:
150 self.gtkw_save.dumpfile(self.vcd_file.name)
151 self.gtkw_save.dumpfile_size(self.vcd_file.tell())
152
153 self.gtkw_save.treeopen("top")
154 for signal in self.traces:
155 if len(signal) > 1 and not signal.decoder:
156 suffix = "[{}:0]".format(len(signal) - 1)
157 else:
158 suffix = ""
159 self.gtkw_save.trace(".".join(self.gtkw_names[signal]) + suffix)
160
161 if self.vcd_file is not None:
162 self.vcd_file.close()
163 if self.gtkw_file is not None:
164 self.gtkw_file.close()
165
166
167 class _Timeline:
168 def __init__(self):
169 self.now = 0.0
170 self.deadlines = dict()
171
172 def reset(self):
173 self.now = 0.0
174 self.deadlines.clear()
175
176 def at(self, run_at, process):
177 assert process not in self.deadlines
178 self.deadlines[process] = run_at
179
180 def delay(self, delay_by, process):
181 if delay_by is None:
182 run_at = self.now
183 else:
184 run_at = self.now + delay_by
185 self.at(run_at, process)
186
187 def advance(self):
188 nearest_processes = set()
189 nearest_deadline = None
190 for process, deadline in self.deadlines.items():
191 if deadline is None:
192 if nearest_deadline is not None:
193 nearest_processes.clear()
194 nearest_processes.add(process)
195 nearest_deadline = self.now
196 break
197 elif nearest_deadline is None or deadline <= nearest_deadline:
198 assert deadline >= self.now
199 if nearest_deadline is not None and deadline < nearest_deadline:
200 nearest_processes.clear()
201 nearest_processes.add(process)
202 nearest_deadline = deadline
203
204 if not nearest_processes:
205 return False
206
207 for process in nearest_processes:
208 process.runnable = True
209 del self.deadlines[process]
210 self.now = nearest_deadline
211
212 return True
213
214
215 class _PySignalState:
216 __slots__ = ("signal", "waiters", "sim_state", "index")
217
218 def __init__(self, signal, index, sim_state):
219 self.signal = signal
220 self.waiters = dict()
221 self.index = index
222 self.sim_state = sim_state # Ugly. need it to have a reference to crtl.
223
224 def set(self, value):
225 self.sim_state.crtl.set(self.index, value)
226
227 def commit(self):
228 if self.sim_state.crtl.capture(self.index) == 0:
229 return False
230
231 # Waiters are not implemented in C yet.
232 awoken_any = False
233 for process, trigger in self.waiters.items():
234 if trigger is None or trigger == self.curr:
235 process.runnable = awoken_any = True
236
237 return awoken_any
238
239 @property
240 def curr(self):
241 return self.sim_state.crtl.get_curr(self.index)
242
243 @property
244 def next(self):
245 return self.sim_state.crtl.get_next(self.index)
246
247
248 class _PySimulation:
249 def __init__(self):
250 self.timeline = _Timeline()
251 self.signals = SignalDict()
252 self.slots = []
253 self.pending = set()
254 self.crtl = None # Initialized later.
255
256 def reset(self):
257 self.timeline.reset()
258 for signal, index in self.signals.items():
259 self.slots[index].curr = self.slots[index].next = signal.reset
260 self.pending.clear()
261
262 def get_signal(self, signal):
263 try:
264 return self.signals[signal]
265 except KeyError:
266 index = len(self.slots)
267 self.slots.append(_PySignalState(signal, index, self))
268 self.signals[signal] = index
269 return index
270
271 def get_signal_macro(self, signal):
272 index = self.get_signal(signal)
273
274 # Replace dots with double underscores, and change everything else that
275 # cannot be put in a C macro identifier to a single underscore.
276 sanitized_name = re.sub(r"\W|^(?=\d)", "_", signal.name)
277
278 return f"{sanitized_name}_{index}"
279
280 def add_trigger(self, process, signal, *, trigger=None):
281 index = self.get_signal(signal)
282 assert (process not in self.slots[index].waiters or
283 self.slots[index].waiters[process] == trigger)
284 self.slots[index].waiters[process] = trigger
285
286 def remove_trigger(self, process, signal):
287 index = self.get_signal(signal)
288 assert process in self.slots[index].waiters
289 del self.slots[index].waiters[process]
290
291 def wait_interval(self, process, interval):
292 self.timeline.delay(interval, process)
293
294 def commit(self, changed=None):
295 converged = True
296
297 for pending_index in range(self.crtl.pending_count):
298 index = self.crtl.pending[pending_index]
299 signal_state = self.slots[index]
300
301 if signal_state.commit():
302 converged = False
303
304 if changed is not None:
305 changed.add(signal_state)
306
307 self.crtl.clear_pending()
308 return converged
309
310
311 class PySimEngine(BaseEngine):
312 _crtl_counter = 0
313
314 def __init__(self, fragment):
315 self._state = _PySimulation()
316 self._timeline = self._state.timeline
317
318 self._fragment = fragment
319
320 # create crtl directory
321 crtl = get_crtl_path()
322
323 # "Processes" are the compiled modules. Each module ends up
324 # with its own run() function
325 self._processes = _FragmentCompiler(self._state)(self._fragment, "TOP")
326
327 # get absolute path of this directory as the base
328 filedir = os.path.dirname(os.path.abspath(__file__))
329
330 # read header file template
331 chdr = os.path.join(filedir, "crtl_template.h")
332 with open(chdr) as cdef_file:
333 template = cdef_file.read()
334
335 # fill in the template
336 cdef = template % (len(self._state.slots), len(self._state.slots))
337
338 # macros to make signals readable
339 cdef += "\n"
340 for signal in self._state.signals:
341 macro = self._state.get_signal_macro(signal)
342 index = self._state.get_signal(signal)
343 cdef += f"#define {macro} {index}\n"
344
345 # run() functions of processes
346 cdef += "\n"
347 for process in self._processes:
348 cdef += f"void run_{process.name}(void);\n"
349
350 # write out the header file
351 with open(os.path.join(crtl, "common.h"), "w") as cdef_file:
352 cdef_file.write(cdef)
353
354 # same with c template: read template first
355 srcf = os.path.join(filedir, "crtl_template.c")
356 with open(srcf) as src_file:
357 template = src_file.read()
358
359 # fill it out
360 src = template % (len(self._state.slots), len(self._state.slots))
361
362 # write it out
363 with open(os.path.join(crtl, "common.c"), "w") as src_file:
364 src_file.write(src)
365
366 # build module named crtlNNN in crtl subdirectory
367 modulename = f"{crtl}.crtl{PySimEngine._crtl_counter}"
368 sources = [os.path.join(crtl, "common.c")]
369 for process in self._processes:
370 sources.append(os.path.join(crtl, f"{process.name}.c"))
371 ffibuilder = FFI()
372 ffibuilder.cdef(cdef)
373 ffibuilder.set_source(modulename, cdef, sources=sources)
374 ffibuilder.compile(verbose=True)
375
376 # append search path of crtl directory before attempting import
377 cwd = os.path.join(os.getcwd())
378 if cwd not in sys.path:
379 sys.path.append(cwd)
380 self._state.crtl = importlib.import_module(modulename).lib
381
382 # Use a counter to generate unique names for modules, because Python
383 # won't reload C extension modules.
384 PySimEngine._crtl_counter += 1
385
386 # for each process (fragment/module) get its run() function
387 for process in self._processes:
388 process.crtl = self._state.crtl
389 process.run = getattr(process.crtl, f"run_{process.name}")
390
391 self._vcd_writers = []
392
393 def add_coroutine_process(self, process, *, default_cmd):
394 self._processes.add(PyCoroProcess(self._state, self._fragment.domains,
395 process,
396 default_cmd=default_cmd))
397
398 def add_clock_process(self, clock, *, phase, period):
399 self._processes.add(PyClockProcess(self._state, clock,
400 phase=phase, period=period))
401
402 def reset(self):
403 self._state.reset()
404 for process in self._processes:
405 process.reset()
406
407 def _step(self):
408 changed = set() if self._vcd_writers else None
409
410 # Performs the two phases of a delta cycle in a loop:
411 converged = False
412 while not converged:
413 # 1. eval: run and suspend every non-waiting process once,
414 # queueing signal changes
415 for process in self._processes:
416 if process.runnable:
417 process.runnable = False
418 process.run()
419
420 # 2. commit: apply every queued signal change,
421 # waking up any waiting processes
422 converged = self._state.commit(changed)
423
424 for vcd_writer in self._vcd_writers:
425 for signal_state in changed:
426 vcd_writer.update(self._timeline.now,
427 signal_state.signal, signal_state.curr)
428
429 def advance(self):
430 self._step()
431 self._timeline.advance()
432 return any(not process.passive for process in self._processes)
433
434 @property
435 def now(self):
436 return self._timeline.now
437
438 @contextmanager
439 def write_vcd(self, *, vcd_file, gtkw_file, traces):
440 vcd_writer = _VCDWriter(self._fragment,
441 vcd_file=vcd_file, gtkw_file=gtkw_file, traces=traces)
442 try:
443 self._vcd_writers.append(vcd_writer)
444 yield
445 finally:
446 vcd_writer.close(self._timeline.now)
447 self._vcd_writers.remove(vcd_writer)