-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsimorisc
More file actions
executable file
·5996 lines (5453 loc) · 260 KB
/
Copy pathsimorisc
File metadata and controls
executable file
·5996 lines (5453 loc) · 260 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
"""simorisc — Object RISC instruction-set simulator.
Loads an .orx binary per CONTRACT.md Section 1, sets up the initial task
state per Section 2, and executes Object RISC instructions with the
architectural semantics of Volume II. Implements the firmware primitives
TaskExit (0x001), ObjAlloc (0x100), ObjFree (0x101), ObjDerive (0x103),
InstallHandler (0x200), and ConsoleWrite (0x320). Every other CALL
primitive returns R2 = ENOSYS.
Multi-CPU mode (`--processors N`) instantiates N CPUs sharing a crossbar.
The same .orx is loaded onto each CPU; they branch on R7 (PROCID).
Each CPU also receives a "service" object: O4 = my own (full caps),
O5..O5+N-2 = the other CPUs' service objects (R+S only). SEND through a
remote object reference is queued to the home CPU's inbox; that CPU
dispatches the registered handler when its main task is idle (one task
per CPU at a time).
The simulator models architectural ISA semantics, not microarchitecture:
no pipeline, no caches, no real TLB. Architectural delay slots ARE
honoured (branch decision committed before the delay-slot instruction
executes). Cross-CPU OL/OS access is dispatched synchronously through
the home CPU's local descriptor table.
"""
import argparse
import colorsys
import re
import select
import struct
import sys
import time
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
MAGIC = b"ORISC\x00\x00\x00"
# Capability bits (Volume III): R W X S V M C reserved (LSB to MSB).
CAP_R = 1 << 0
CAP_W = 1 << 1
CAP_X = 1 << 2
CAP_S = 1 << 3
CAP_V = 1 << 4
CAP_M = 1 << 5
CAP_C = 1 << 6
# Trap cause codes (Volume II Section 13).
CAUSE_EXTERNAL_INTERRUPT= 0x01
CAUSE_BUS_ERROR_I = 0x02
CAUSE_BUS_ERROR_D = 0x03
CAUSE_ADDR_MISALIGN_I = 0x04
CAUSE_ADDR_MISALIGN_D = 0x05
CAUSE_TLB_MISS_I = 0x06
CAUSE_TLB_MISS_D = 0x07
CAUSE_PAGE_PERMISSION = 0x08
CAUSE_ARITH_OVERFLOW = 0x09
CAUSE_RESERVED_INSTR = 0x0A
CAUSE_PRIV_INSTR = 0x0B
CAUSE_BREAKPOINT = 0x0C
CAUSE_NULL_DEREF = 0x10
CAUSE_STALE_REF = 0x11
CAUSE_BOUNDS = 0x12
CAUSE_CAP_VIOLATION = 0x13
CAUSE_SEND_OVERFLOW = 0x14
CAUSE_FIRMWARE_CALL = 0x20
CAUSE_RESERVED_CALL = 0x21
CAUSE_NAMES = {
CAUSE_EXTERNAL_INTERRUPT: "external-interrupt",
CAUSE_BUS_ERROR_I: "bus-error-i",
CAUSE_BUS_ERROR_D: "bus-error-d",
CAUSE_ADDR_MISALIGN_I:"address-misaligned-i",
CAUSE_ADDR_MISALIGN_D:"address-misaligned-d",
CAUSE_TLB_MISS_I: "tlb-miss-i",
CAUSE_TLB_MISS_D: "tlb-miss-d",
CAUSE_PAGE_PERMISSION:"page-permission",
CAUSE_ARITH_OVERFLOW: "arithmetic-overflow",
CAUSE_RESERVED_INSTR: "reserved-instruction",
CAUSE_PRIV_INSTR: "privileged-instruction",
CAUSE_BREAKPOINT: "breakpoint",
CAUSE_NULL_DEREF: "null-dereference",
CAUSE_STALE_REF: "stale-reference",
CAUSE_BOUNDS: "bounds-violation",
CAUSE_CAP_VIOLATION: "capability-violation",
CAUSE_SEND_OVERFLOW: "send-buffer-overflow",
CAUSE_FIRMWARE_CALL: "firmware-call",
CAUSE_RESERVED_CALL: "reserved-call",
}
# Standard firmware error codes (Volume VI Section 2.4).
ERR_OK = 0
ERR_EINVAL = 1
ERR_ENOMEM = 2
ERR_EPERM = 3
ERR_ENOSYS = 4
ERR_EBUSY = 5
ERR_ENOENT = 6
ERR_ETIMEOUT = 7
ERR_EFAULT = 8
ERR_EAGAIN = 9
ERR_ESTALE = 10
ERR_EREMOTE = 11
# Type tags reserved by the contract loader.
TAG_CODE = 0x4100
TAG_STACK = 0x4101
TAG_DATA = 0x4102
TAG_SERVICE = 0x4103
TAG_TASK = 0x4104 # task object (Volume VI Section 4)
TAG_INPUT_SINK = 0x4105 # Phase 60 step 3 — host-input-event sink
TAG_FRAMEBUFFER = 0x4106 # Phase 60 — host-display-backed pixel object
# (was 0x4104, which collided with TAG_TASK;
# the free paths branch on `== TAG_TASK`, so a
# framebuffer was misclassified as a task)
# Task lifecycle states (Volume VI Section 4.2). Internal numbering;
# returned by TaskQuery in the low 8 bits of the packed state word.
TASK_STATE_NEW = 0
TASK_STATE_RUNNABLE = 1
TASK_STATE_RUNNING = 2
TASK_STATE_SUSPENDED = 3
TASK_STATE_BLOCKED = 4 # parked on IPC; not yet used
TASK_STATE_EXITED = 5
# Service object layout: 64 bytes, present on every CPU in multi-CPU mode.
SERVICE_OBJ_SIZE = 64
# Privilege modes (Volume II Section 13). Numerically ordered: a check
# `cpu.mode >= MODE_SUPERVISOR` admits both supervisor and firmware.
MODE_USER = 0
MODE_SUPERVISOR = 1
MODE_FIRMWARE = 2
MODE_NAMES = {0: "user", 1: "supervisor", 2: "firmware"}
# Control register numbers (Volume V Section 2.10). Registers 0–7 are
# accessible from supervisor mode; 8 and above are firmware-only. Only a
# subset is materialised in this simulator — see CPU._ctrl_read/_write.
CTRL_STATUS = 0
CTRL_CAUSE = 1
CTRL_EPC = 2
CTRL_BADVADDR = 3
CTRL_CONTEXT = 4
CTRL_COUNT = 5
CTRL_COMPARE = 6
CTRL_PROCID = 7
CTRL_VECBASE = 8
CTRL_SV_MAX = 7 # any ctrl > this requires firmware mode
# Trap-vector offsets per Volume II Appendix B. The vector base lives in
# the VECBASE control register; on a trap delivery the CPU jumps to
# `VECBASE + CAUSE_VECTOR_OFFSET[cause]`. Each vector is 64 bytes
# (16 instructions) — enough for a short trampoline that branches out to
# a longer handler. Causes not listed here can't be delivered to a
# vector and fall back to the fatal-trap path (current behaviour).
CAUSE_VECTOR_OFFSET = {
0x00: 0x000, # reset (synthetic; never raised at runtime)
0x01: 0x040, # external-interrupt
0x02: 0x080, # bus-error-i
0x03: 0x0C0, # bus-error-d
0x04: 0x100, # address-misaligned-i
0x05: 0x140, # address-misaligned-d
0x06: 0x180, # tlb-miss-i
0x07: 0x1C0, # tlb-miss-d
0x08: 0x200, # page-permission
0x09: 0x240, # arithmetic-overflow
0x0A: 0x280, # reserved-instruction
0x0B: 0x2C0, # privileged-instruction
0x0C: 0x300, # breakpoint
0x10: 0x400, # null-dereference
0x11: 0x440, # stale-reference
0x12: 0x480, # bounds-violation
0x13: 0x4C0, # capability-violation
0x14: 0x500, # send-buffer-overflow
0x20: 0x800, # firmware-call (CALL)
0x21: 0x840, # reserved-call
}
# ---------------------------------------------------------------------------
# Object descriptor and reference layout
# ---------------------------------------------------------------------------
@dataclass
class Descriptor:
"""An object descriptor in a CPU's local object table.
We don't model the 32-byte on-the-wire layout from Volume III Section
3.2 here; we keep the architecturally observable state in Python form
and rely on the generation-counter semantics to make accesses
cache-coherent (Volume III Section 4)."""
storage: bytearray
length: int
generation: int
type_tag: int
max_caps: int
flags: int = 0
live: bool = True
send_handler_ref: int = 0
send_handler_off: int = 0
send_handler_va: int = 0 # populated by InstallHandler from object's mapping
objstore: bool = False # if True, storage is OR-typed (OREFLD/OREFST only)
# Receive-queue mode: when set by ReceiveQueueAttach, incoming SENDs are
# appended here instead of dispatching a handler task. queue_max_depth
# bounds the queue length; arrivals beyond it are dropped.
queue: Optional[List] = None
queue_max_depth: int = 0
# Phase 60 — host-display backing. Set on TAG_FRAMEBUFFER objects
# by primitive_ObjAllocFramebuffer. fb_dirty is toggled by
# ObjStoreBytes; the --display worker repaints when it sees the
# flag set, then clears it. fb_width/fb_height let the worker
# build a Tk PhotoImage of the right shape.
fb_width: int = 0
fb_height: int = 0
fb_dirty: bool = False
# Phase 60 step 3 — host-input-event sink. Set on TAG_INPUT_SINK
# objects allocated via primitive_ObjAllocInputSink.
# input_kind: 0 = keyboard, 1 = pointer.
input_kind: int = -1
def make_ref(generation: int, home: int, index: int, caps: int) -> int:
"""Pack a 64-bit object reference per Volume III Section 2.1.
63 48 47 40 39 16 15 8 7 0
generation home pr local idx caps rsv
"""
assert 0 <= generation < (1 << 16)
assert 0 <= home < (1 << 8)
assert 0 <= index < (1 << 24)
assert 0 <= caps < (1 << 8)
return ((generation & 0xFFFF) << 48) | \
((home & 0xFF) << 40) | \
((index & 0xFFFFFF) << 16) | \
((caps & 0xFF) << 8)
def ref_generation(ref: int) -> int: return (ref >> 48) & 0xFFFF
def ref_home(ref: int) -> int: return (ref >> 40) & 0xFF
def ref_index(ref: int) -> int: return (ref >> 16) & 0xFFFFFF
def ref_caps(ref: int) -> int: return (ref >> 8) & 0xFF
# ---------------------------------------------------------------------------
# Tasks (Volume VI Section 4)
# ---------------------------------------------------------------------------
@dataclass
class Task:
"""A swappable execution context. The CPU's per-task fields
(general/object registers, PC, mode, the supervisor-visible control
registers, and the address-space mappings) are mirrored here so the
scheduler can save out the running task and load another in.
Per-CPU state (descriptor table, inbox, request/response queues,
cycle counter, VECBASE) lives on the CPU, not the task. VECBASE
is firmware-installed once per CPU and shared by every task."""
descriptor_idx: int # index in cpu.descriptors for the task ref
code_ref: int = 0 # original code object (kept for restart/debug)
stack_ref: int = 0 # original stack object
state: int = TASK_STATE_NEW
exit_code: Optional[int] = None
# Saved register file. CPU's gpr/opr/etc. become these on context-in.
gpr: List[int] = field(default_factory=lambda: [0]*32)
opr: List[int] = field(default_factory=lambda: [0]*16)
hi: int = 0
lo: int = 0
pc: int = 0
next_pc: int = 0
# Privilege state — current mode and whatever ERET / trap delivery
# has stashed so far. Each task's traps are private to it.
mode: int = MODE_SUPERVISOR
saved_mode: int = MODE_SUPERVISOR
saved_pc: int = 0
cause_reg: int = 0
badvaddr_reg: int = 0
# Address space. Each task carries its own virtual-address layout;
# the CPU's `mappings` list IS the running task's layout.
mappings: List[Tuple[int,int,int,int,int]] = field(default_factory=list)
# Tasks parked in TaskWait on this one. When this task transitions to
# EXITED, every entry's R3 is filled with the exit code, R2 with OK,
# and the waiter is moved BLOCKED → RUNNABLE. The list is then
# cleared (waiters can't double-collect).
waiters: List['Task'] = field(default_factory=list)
# Phase 45d: remote callers waiting on this task via TASK_WAIT_REQ.
# Each entry is (waiter_pid, trans_id); on EXITED we send a
# TASK_WAIT_RESP back to each. Cleared after the responses go out.
remote_waiters: List[Tuple[int, int]] = field(default_factory=list)
# IPC blocked_on saved with this task. When the current task hits
# a blocking primitive (ReceiveQueuePoll on an empty queue, remote
# OL/OS waiting on a response) and there's another runnable task
# we'd like to schedule, save the BlockedSignal here so the
# scheduler can switch in the runnable task and process this
# task's unblock condition asynchronously. Phase 36.
blocked_on: Optional['BlockedSignal'] = None
# ---------------------------------------------------------------------------
# Trap exception
# ---------------------------------------------------------------------------
class Trap(Exception):
def __init__(self, cause: int, faulting_pc: int, detail: str = "",
bad_vaddr: int = 0):
self.cause = cause
self.faulting_pc = faulting_pc
self.bad_vaddr = bad_vaddr & 0xFFFFFFFF
self.detail = detail
super().__init__(f"trap {CAUSE_NAMES.get(cause, hex(cause))} @ "
f"0x{faulting_pc:08x}: {detail}")
class TaskExitSignal(Exception):
"""Raised by primitive_TaskExit; caught by the System scheduler."""
def __init__(self, code: int):
self.code = code
class BlockedSignal(Exception):
"""Base class for "hold the CPU at this instruction" signals. Two
subclasses cover the cases we currently need; both are caught at the
instruction-dispatch site (CALL or OL/OS) and observed by the
scheduler to suspend and later resume the CPU."""
pass
class BlockedOnQueue(BlockedSignal):
"""Raised by ReceiveQueuePoll when its target queue is empty and the
timeout is non-zero. The scheduler holds the CPU at the CALL until
the queue receives a message (or timeout expires); on resume it
populates registers per Volume VI Section 6 and advances PC."""
def __init__(self, descriptor_idx: int, timeout: int):
self.descriptor_idx = descriptor_idx
# timeout: -1 = infinite; otherwise number of scheduler ticks remaining
self.timeout = timeout
class BlockedOnQueueSet(BlockedSignal):
"""Raised by WaitAnyQueue (#0x206) when EVERY queue in the listed set
is empty. The scheduler holds the CPU at the CALL until ANY listed
queue receives a message, then returns ERR_OK and advances PC — a
pure readiness wait, no message delivery (mirror of BlockedOnQueue
but for a set, and it never pops). `deadline` (absolute wall-clock
time.time() seconds, or None) optionally bounds the wait: once
time.time() >= deadline with no queue ready, _try_unblock_queue_set
resumes the CALL with ERR_ETIMEOUT instead of ERR_OK. The bound is
WALL-CLOCK, not scheduler ticks (unlike BlockedOnQueue), so it fires
correctly even while the WM is parked and other tasks run, and a
requested microsecond delay maps straight to real time.
`descriptor_indices` is the set of local descriptor indices resolved
(and validated) at primitive-entry time. The WM owns every queue in
the set (home == self) and is single-threaded while blocked, so the
set can't be mutated under us; the wake/unblock checks still re-probe
each descriptor defensively before reading its queue."""
def __init__(self, descriptor_indices, deadline=None):
self.descriptor_indices = descriptor_indices
self.deadline = deadline
class BlockedOnResponse(BlockedSignal):
"""Raised by remote OL/OS instructions after queueing an OBJ_READ_REQ
or OBJ_WRITE_REQ on the home CPU. The scheduler holds the CPU at the
OL/OS instruction until a matching response arrives; on resume it
either writes the loaded value into the destination register
(`OBJ_READ_RESP` with flags=OK), no-ops (`OBJ_WRITE_RESP` with
flags=OK), or raises a Trap (any non-OK flags)."""
def __init__(self, trans_id: int, *, target_reg: Optional[int] = None,
width: int = 0, signed: bool = False, is_write: bool = False):
self.trans_id = trans_id
self.target_reg = target_reg
self.width = width
self.signed = signed
self.is_write = is_write
class BlockedOnExitWait(BlockedSignal):
"""Set by primitive_TaskExit when the exiting task was the only
runnable task but other tasks remain BLOCKED (e.g., a shell
parked on RecvQueuePoll waiting for a key). The CPU stays alive
so _process_requests can answer in-flight OBJ_READ_REQs from
the just-finished task's data segment, and so _wake_blocked_tasks
can promote a blocked task once its condition fires; the
blocked-but-runnable branch in run() then context-switches into
it. There's no "unblock" condition tied to this signal — it's a
sentinel, not a wait — so _try_unblock returns False and the
blocked-but-runnable branch is the only path that consumes it."""
pass
class BlockedOnTaskWait(BlockedSignal):
"""Phase 45d. Raised by primitive_TaskWait when the target ref's
home is a different CPU. The caller queues a TASK_WAIT_REQ on the
home and parks here until the matching TASK_WAIT_RESP arrives;
on resume R2 receives the status word, R3 the exit code (when
status == OK), and PC advances past the CALL."""
def __init__(self, trans_id: int):
self.trans_id = trans_id
class BlockedOnTaskQuery(BlockedSignal):
"""Phase 45d. Raised by primitive_TaskQuery for remote refs. On
response R2 = status, R3 = packed state word; PC advances."""
def __init__(self, trans_id: int):
self.trans_id = trans_id
class BlockedOnTaskKill(BlockedSignal):
"""Phase 45d. Raised by primitive_TaskKill for remote refs. On
response R2 = status; no other GPRs touched; PC advances."""
def __init__(self, trans_id: int):
self.trans_id = trans_id
class BlockedOnObjFetch(BlockedSignal):
"""Phase 45e. Raised by primitive_ObjFetchBytes when the source ref
is remote. The CALL holds PC until the matching OBJ_READ_RESP
arrives; on resume the response data is copied into the local
destination descriptor at (dst_idx, dst_off..dst_off+byte_count),
R2 = ERR_OK or remote-fault-derived ERR_*, R3 = bytes copied,
PC advances. On a remote-side fault flag we never end up writing
the destination — caller sees R3 = 0."""
def __init__(self, trans_id: int, dst_idx: int, dst_off: int,
byte_count: int):
self.trans_id = trans_id
self.dst_idx = dst_idx
self.dst_off = dst_off
self.byte_count = byte_count
class BlockedOnObjStore(BlockedSignal):
"""Phase 59 (Object RISC Vol VI #0x109). Raised by
primitive_ObjStoreBytes when the destination ref is remote.
The CALL holds PC until the matching OBJ_WRITE_RESP arrives;
on resume R2 = ERR_OK / ERR_*, R3 = byte_count on OK or 0 on
fault, PC advances. Mirror of BlockedOnObjFetch for the write
direction — used by the WM's glyph renderer to push bitmap-font
rows to a remote framebuffer in a single wire RTT."""
def __init__(self, trans_id: int, byte_count: int):
self.trans_id = trans_id
self.byte_count = byte_count
# ---------------------------------------------------------------------------
# Wire-level packet format (Volume IV §3, §4)
# ---------------------------------------------------------------------------
#
# Every packet is:
# header (8 bytes) | payload (length × 4 bytes) | checksum (4 bytes)
#
# The header layout (Volume IV §3.1) is:
# src_pid (8) | dst_pid (8) | type (8) | flags (8) | trans_id (16) | length (16)
# where length is the number of payload words (not bytes), big-endian throughout.
# The trailing checksum is the bitwise XOR of every header and payload word.
#
# Message types are taken from Volume IV §4. Phase 1 implements only
# SEND_DELIVER (0x20) on the wire; Phase 2 will add OBJ_READ_REQ/RESP (0x10/11)
# and OBJ_WRITE_REQ/RESP (0x12/13) so that remote OL/OS likewise flow as
# real packets.
# Packet type codes.
PKT_OBJ_READ_REQ = 0x10
PKT_OBJ_READ_RESP = 0x11
PKT_OBJ_WRITE_REQ = 0x12
PKT_OBJ_WRITE_RESP = 0x13
PKT_SEND_DELIVER = 0x20
PKT_SEND_ACK = 0x21
PKT_DESC_REQ = 0x30
PKT_DESC_RESP = 0x31
PKT_DESC_FORWARD = 0x32
PKT_DESC_INVALIDATE = 0x33
# Phase 45d — remote task primitives. The home CPU is the source of truth
# for a task's state; remote callers exchange these request/response
# packets to invoke TaskWait/Query/Kill across CPU boundaries. Status
# travels in the response payload (rather than the flags byte) because
# the architectural Task API uses ERR_* codes that don't all map to the
# 6-bit RESP_* fault codes used by OL/OS responses.
PKT_TASK_WAIT_REQ = 0x40
PKT_TASK_WAIT_RESP = 0x41
PKT_TASK_QUERY_REQ = 0x42
PKT_TASK_QUERY_RESP = 0x43
PKT_TASK_KILL_REQ = 0x44
PKT_TASK_KILL_RESP = 0x45
# Phase 60 follow-up — headless host-input injection. A test harness
# (fake_terminal) sends this to a WM's CPU to deliver a keyboard/pointer
# event into that CPU's local TAG_INPUT_SINK, exactly as the Tk display
# worker does for live input. payload = [kind, w0, w1, w2, w3] where
# kind 0=keyboard ([code, mods, 0, 0]), 1=pointer ([evt, xy, btn, state]).
PKT_HOST_INPUT = 0x50
PKT_LINK_FLOW_CREDIT= 0xF0
PKT_LINK_ERROR = 0xF1
PKT_LINK_HEARTBEAT = 0xFE
PKT_LINK_RESET = 0xFF
def pack_packet(src_pid: int, dst_pid: int, msg_type: int, flags: int,
trans_id: int, payload_words: List[int]) -> bytes:
"""Build a wire-format packet per Volume IV §3.1 + §3.2."""
length_words = len(payload_words)
header_bytes = struct.pack(">BBBBHH",
src_pid & 0xFF, dst_pid & 0xFF,
msg_type & 0xFF, flags & 0xFF,
trans_id & 0xFFFF, length_words & 0xFFFF)
payload_bytes = b''.join(struct.pack(">I", w & 0xFFFFFFFF)
for w in payload_words)
# Checksum = XOR of header and payload words (header is two 32-bit words).
h0, h1 = struct.unpack(">II", header_bytes)
chk = h0 ^ h1
for w in payload_words:
chk ^= w & 0xFFFFFFFF
return header_bytes + payload_bytes + struct.pack(">I", chk & 0xFFFFFFFF)
def unpack_packet(data: bytes) -> Dict:
"""Parse a wire-format packet and validate its size/checksum.
Returns dict with src_pid, dst_pid, type, flags, trans_id, payload (list of words).
Raises ValueError on malformed input."""
if len(data) < 12:
raise ValueError(f"packet too short: {len(data)} bytes")
src, dst, mtype, flags, trans_id, length_words = \
struct.unpack(">BBBBHH", data[:8])
expected_total = 8 + length_words * 4 + 4
if len(data) != expected_total:
raise ValueError(
f"packet length mismatch: header says {length_words} payload "
f"words, total bytes {len(data)} != expected {expected_total}")
payload_bytes = data[8:8 + length_words * 4]
payload_words = list(struct.unpack(f">{length_words}I", payload_bytes)
if length_words else [])
h0, h1 = struct.unpack(">II", data[:8])
chk = h0 ^ h1
for w in payload_words:
chk ^= w
expected_chk = struct.unpack(">I", data[-4:])[0]
if (chk & 0xFFFFFFFF) != expected_chk:
raise ValueError(
f"checksum mismatch: computed {chk:#010x}, "
f"packet says {expected_chk:#010x}")
return {
"src_pid": src, "dst_pid": dst, "type": mtype, "flags": flags,
"trans_id": trans_id, "payload": payload_words,
}
def build_send_deliver(src_pid: int, dst_pid: int, trans_id: int,
recipient_ref: int, int_payload: List[int],
or_payload: List[int]) -> bytes:
"""Construct a SEND_DELIVER packet (Volume IV §4.2) from an in-memory
PendingSend's contents. Object references travel low-half-first
(Volume IV §4.1)."""
payload: List[int] = []
payload.append(recipient_ref & 0xFFFFFFFF)
payload.append((recipient_ref >> 32) & 0xFFFFFFFF)
for w in int_payload[:4]:
payload.append(w & 0xFFFFFFFF)
for ref in or_payload[:4]:
payload.append(ref & 0xFFFFFFFF)
payload.append((ref >> 32) & 0xFFFFFFFF)
return pack_packet(src_pid, dst_pid, PKT_SEND_DELIVER, 0, trans_id, payload)
def parse_send_deliver(packet: Dict) -> Tuple[int, List[int], List[int]]:
"""Inverse of build_send_deliver. Returns (recipient_ref, int_payload, or_payload)."""
if packet["type"] != PKT_SEND_DELIVER:
raise ValueError(f"expected SEND_DELIVER ({PKT_SEND_DELIVER:#x}), "
f"got {packet['type']:#x}")
p = packet["payload"]
if len(p) != 14:
raise ValueError(f"SEND_DELIVER payload must be 14 words; got {len(p)}")
recipient_ref = p[0] | (p[1] << 32)
int_payload = list(p[2:6])
or_payload = []
for i in range(4):
ref = p[6 + 2*i] | (p[7 + 2*i] << 32)
or_payload.append(ref)
return recipient_ref, int_payload, or_payload
# OBJ_READ_REQ / OBJ_READ_RESP / OBJ_WRITE_REQ / OBJ_WRITE_RESP
# (Volume IV §4.1). The home processor performs the validity, bounds,
# and capability checks itself; on failure the response carries a fault
# code in the low six bits of the `flags` byte instead of data.
# Response fault codes (Vol IV §4.1, low 6 bits of the response flags byte).
RESP_OK = 0x00
RESP_STALE = 0x01
RESP_BOUNDS = 0x02
RESP_CAP = 0x03
RESP_BUS_ERROR = 0x04
RESP_FORWARDED = 0x3F
def build_obj_read_req(src_pid: int, dst_pid: int, trans_id: int,
ref: int, offset: int, width: int) -> bytes:
payload = [
ref & 0xFFFFFFFF,
(ref >> 32) & 0xFFFFFFFF,
offset & 0xFFFFFFFF,
width & 0xFFFFFFFF,
]
return pack_packet(src_pid, dst_pid, PKT_OBJ_READ_REQ, 0, trans_id, payload)
def build_obj_read_resp(src_pid: int, dst_pid: int, trans_id: int,
flags: int, data: bytes) -> bytes:
"""Pad data to a 4-byte word boundary; pack words as the payload."""
if flags == RESP_OK:
pad = (-len(data)) % 4
padded = data + bytes(pad)
words = list(struct.unpack(f">{len(padded)//4}I", padded)) if padded else []
else:
words = []
return pack_packet(src_pid, dst_pid, PKT_OBJ_READ_RESP, flags, trans_id, words)
def build_obj_write_req(src_pid: int, dst_pid: int, trans_id: int,
ref: int, offset: int, width: int, data: bytes) -> bytes:
pad = (-len(data)) % 4
padded = data + bytes(pad)
data_words = list(struct.unpack(f">{len(padded)//4}I", padded)) if padded else []
payload = [
ref & 0xFFFFFFFF,
(ref >> 32) & 0xFFFFFFFF,
offset & 0xFFFFFFFF,
width & 0xFFFFFFFF,
] + data_words
return pack_packet(src_pid, dst_pid, PKT_OBJ_WRITE_REQ, 0, trans_id, payload)
def build_obj_write_resp(src_pid: int, dst_pid: int, trans_id: int,
flags: int) -> bytes:
return pack_packet(src_pid, dst_pid, PKT_OBJ_WRITE_RESP, flags, trans_id, [])
# Phase 45d — remote Task primitive packets. Each REQ carries the target
# task ref (2 words, low-half first per Vol IV §4.1); the corresponding
# RESP carries an ERR_* status word plus any primitive-specific result
# words. flags is unused in these packet types.
def build_task_wait_req(src_pid: int, dst_pid: int, trans_id: int,
ref: int) -> bytes:
payload = [ref & 0xFFFFFFFF, (ref >> 32) & 0xFFFFFFFF]
return pack_packet(src_pid, dst_pid, PKT_TASK_WAIT_REQ, 0, trans_id, payload)
def build_task_wait_resp(src_pid: int, dst_pid: int, trans_id: int,
status: int, exit_code: int) -> bytes:
"""status = ERR_*; exit_code is meaningful only when status == ERR_OK."""
payload = [status & 0xFFFFFFFF, exit_code & 0xFFFFFFFF]
return pack_packet(src_pid, dst_pid, PKT_TASK_WAIT_RESP, 0, trans_id, payload)
def build_task_query_req(src_pid: int, dst_pid: int, trans_id: int,
ref: int) -> bytes:
payload = [ref & 0xFFFFFFFF, (ref >> 32) & 0xFFFFFFFF]
return pack_packet(src_pid, dst_pid, PKT_TASK_QUERY_REQ, 0, trans_id, payload)
def build_task_query_resp(src_pid: int, dst_pid: int, trans_id: int,
status: int, packed: int) -> bytes:
"""status = ERR_*; packed is the architectural Vol VI #0x008 packed
state word (state | proc<<8 | exit_code<<16) — meaningful only when
status == ERR_OK."""
payload = [status & 0xFFFFFFFF, packed & 0xFFFFFFFF]
return pack_packet(src_pid, dst_pid, PKT_TASK_QUERY_RESP, 0, trans_id, payload)
def build_task_kill_req(src_pid: int, dst_pid: int, trans_id: int,
ref: int, exit_code: int) -> bytes:
payload = [ref & 0xFFFFFFFF, (ref >> 32) & 0xFFFFFFFF,
exit_code & 0xFFFFFFFF]
return pack_packet(src_pid, dst_pid, PKT_TASK_KILL_REQ, 0, trans_id, payload)
def build_task_kill_resp(src_pid: int, dst_pid: int, trans_id: int,
status: int) -> bytes:
payload = [status & 0xFFFFFFFF]
return pack_packet(src_pid, dst_pid, PKT_TASK_KILL_RESP, 0, trans_id, payload)
# Map response-flag fault codes to architectural trap causes for the
# issuer to raise upon receiving a non-OK response.
RESP_FLAG_TO_CAUSE = {
RESP_STALE: CAUSE_STALE_REF,
RESP_BOUNDS: CAUSE_BOUNDS,
RESP_CAP: CAUSE_CAP_VIOLATION,
RESP_BUS_ERROR: CAUSE_BUS_ERROR_D,
}
# ---------------------------------------------------------------------------
# Pending SEND message (host-side parsed form of a SEND_DELIVER packet)
# ---------------------------------------------------------------------------
@dataclass
class PendingSend:
recipient_ref: int # full 64-bit ref of the target object
int_payload: List[int] # 4 ints (R4..R7 of issuer at SEND time)
or_payload: List[int] # 4 OR refs (O1..O4 of issuer at SEND time)
src_pid: int # which CPU issued this SEND
trans_id: int = 0 # per-source transaction id
def to_wire(self, dst_pid: int) -> bytes:
return build_send_deliver(self.src_pid, dst_pid, self.trans_id,
self.recipient_ref, self.int_payload,
self.or_payload)
@classmethod
def from_wire(cls, data: bytes) -> 'PendingSend':
pkt = unpack_packet(data)
recipient_ref, int_payload, or_payload = parse_send_deliver(pkt)
return cls(recipient_ref=recipient_ref,
int_payload=int_payload,
or_payload=or_payload,
src_pid=pkt["src_pid"],
trans_id=pkt["trans_id"])
# ---------------------------------------------------------------------------
# Crossbar interface and in-process implementation
# ---------------------------------------------------------------------------
#
# Everything that crosses between CPUs (and, in a future revision, between
# CPUs and devices) goes through a Crossbar. The Crossbar owns nothing
# semantic about the protocol — it inspects each packet's `dst_pid` field
# and hands the packet off to that destination's Port. A Port is anything
# that can receive packets: today, only CPUs implement Port; tomorrow,
# device processes will too.
#
# The InProcessCrossbar implementation routes by direct method dispatch
# inside one Python process. A SocketCrossbar (next phase) will route
# bytes over a UNIX domain socket to peer CPU processes.
class Port:
"""A participant on the crossbar. Any object that can receive packet
bytes addressed to a particular pid is a Port."""
def receive_packet(self, packet: bytes) -> None:
raise NotImplementedError
class Crossbar:
"""Interface for the crossbar interconnect.
`send_packet(packet)` routes one wire-format packet to the Port
registered for its `dst_pid` field. Implementations may be
in-process, over a socket, or over a real fabric — the rest of the
simulator only sees this method.
`poll_incoming()` is called once per scheduler tick; it lets
transport-bearing implementations (e.g., SocketCrossbar) drain any
bytes from their wire and deliver complete packets into the local
Port. The default in-process implementation does nothing."""
def send_packet(self, packet: bytes) -> None:
raise NotImplementedError
def attach_port(self, pid: int, port: Port) -> None:
raise NotImplementedError
def poll_incoming(self) -> bool:
return False
def close(self) -> None:
pass
class SocketCrossbar(Crossbar):
"""Crossbar implementation that talks to an external `oriscbar`
process over a UNIX domain socket. Wire framing and handshake are
documented in oriscbar's module docstring. The simulator runs in
single-CPU mode in this configuration; the external crossbar
routes between many such simorisc instances and any device
processes."""
HELLO_MAGIC = 0xC0FFEEAA
HELLO_OK = 0x00000000
def __init__(self, sock_path: str, my_pid: int,
trace_enabled: bool = False):
import socket
self.sock_path = sock_path
self.my_pid = my_pid
self.trace_enabled = trace_enabled
self.local_port: Optional[Port] = None
self.recv_buf = bytearray()
self.connected = True
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
# Brief retry loop in case the launcher is still bringing
# oriscbar up.
deadline = time.time() + 5.0
while True:
try:
self.sock.connect(sock_path)
break
except (FileNotFoundError, ConnectionRefusedError) as e:
if time.time() >= deadline:
raise SystemExit(
f"simorisc: could not connect to crossbar at "
f"{sock_path}: {e}")
time.sleep(0.05)
# Handshake.
self.sock.sendall(struct.pack(">II", self.HELLO_MAGIC, my_pid))
reply = self._recv_exact(8)
magic, status = struct.unpack(">II", reply)
if magic != self.HELLO_MAGIC or status != self.HELLO_OK:
raise SystemExit(
f"simorisc: crossbar handshake failed "
f"(magic={magic:#x}, status={status:#x})")
self.sock.setblocking(False)
def _recv_exact(self, n: int) -> bytes:
out = bytearray()
while len(out) < n:
chunk = self.sock.recv(n - len(out))
if not chunk:
raise SystemExit("simorisc: crossbar closed during handshake")
out.extend(chunk)
return bytes(out)
def attach_port(self, pid: int, port: Port) -> None:
if pid != self.my_pid:
raise ValueError(f"SocketCrossbar can only host pid={self.my_pid}, "
f"not pid={pid}")
self.local_port = port
def send_packet(self, packet: bytes) -> None:
try:
self.sock.sendall(struct.pack(">I", len(packet)) + packet)
except OSError as e:
print(f"simorisc: socket send failed: {e}", file=sys.stderr)
return
if self.trace_enabled:
self._trace_outgoing(packet)
def poll_incoming(self) -> bool:
worked = False
while True:
try:
chunk = self.sock.recv(65536)
except (BlockingIOError, InterruptedError):
break
except OSError:
self.connected = False
break
if not chunk:
self.connected = False
break
self.recv_buf.extend(chunk)
worked = True
while len(self.recv_buf) >= 4:
(length,) = struct.unpack(">I", bytes(self.recv_buf[:4]))
if len(self.recv_buf) < 4 + length:
break
packet = bytes(self.recv_buf[4:4 + length])
del self.recv_buf[:4 + length]
if self.trace_enabled:
self._trace_incoming(packet)
if self.local_port is not None:
self.local_port.receive_packet(packet)
worked = True
return worked
def _trace_outgoing(self, packet: bytes) -> None:
try:
p = unpack_packet(packet)
print(f" ; CROSSBAR(socket) tx -> CPU{p['dst_pid']} "
f"type=0x{p['type']:02x} bytes={packet.hex()}", file=sys.stderr)
except ValueError:
print(f" ; CROSSBAR(socket) tx malformed bytes={packet.hex()}",
file=sys.stderr)
def _trace_incoming(self, packet: bytes) -> None:
try:
p = unpack_packet(packet)
print(f" ; CROSSBAR(socket) rx <- CPU{p['src_pid']} "
f"type=0x{p['type']:02x} bytes={packet.hex()}", file=sys.stderr)
except ValueError:
print(f" ; CROSSBAR(socket) rx malformed bytes={packet.hex()}",
file=sys.stderr)
def close(self) -> None:
try:
self.sock.close()
except OSError:
pass
class InProcessCrossbar(Crossbar):
"""Default implementation: an in-memory routing table. Each call to
`send_packet` immediately delivers to the destination Port's
`receive_packet`. No latency model, no flow control, no real link
arbitration — just direct dispatch sufficient for ISA-level
simulation."""
def __init__(self, trace_enabled: bool = False):
self.ports: Dict[int, Port] = {}
self.trace_enabled = trace_enabled
def attach_port(self, pid: int, port: Port) -> None:
self.ports[pid] = port
def send_packet(self, packet: bytes) -> None:
try:
pkt = unpack_packet(packet)
except ValueError as e:
if self.trace_enabled:
print(f" ; CROSSBAR drop (malformed): {e}", file=sys.stderr)
return
dst = pkt["dst_pid"]
if dst not in self.ports:
if self.trace_enabled:
print(f" ; CROSSBAR drop: dst CPU{dst} not connected",
file=sys.stderr)
return
if self.trace_enabled:
self._trace(packet, pkt)
self.ports[dst].receive_packet(packet)
def _trace(self, packet: bytes, pkt: Dict) -> None:
type_name = {
PKT_OBJ_READ_REQ: "OBJ_READ_REQ",
PKT_OBJ_READ_RESP: "OBJ_READ_RESP",
PKT_OBJ_WRITE_REQ: "OBJ_WRITE_REQ",
PKT_OBJ_WRITE_RESP: "OBJ_WRITE_RESP",
PKT_SEND_DELIVER: "SEND_DELIVER",
PKT_TASK_WAIT_REQ: "TASK_WAIT_REQ",
PKT_TASK_WAIT_RESP: "TASK_WAIT_RESP",
PKT_TASK_QUERY_REQ: "TASK_QUERY_REQ",
PKT_TASK_QUERY_RESP: "TASK_QUERY_RESP",
PKT_TASK_KILL_REQ: "TASK_KILL_REQ",
PKT_TASK_KILL_RESP: "TASK_KILL_RESP",
PKT_HOST_INPUT: "HOST_INPUT",
}.get(pkt["type"], f"type=0x{pkt['type']:02x}")
print(f" ; CROSSBAR CPU{pkt['src_pid']} -> CPU{pkt['dst_pid']} "
f"{type_name} trans={pkt['trans_id']} len={len(pkt['payload'])}w "
f"bytes={packet.hex()}", file=sys.stderr)
# ---------------------------------------------------------------------------
# CPU state (per-processor)
# ---------------------------------------------------------------------------
class CPU(Port):
def __init__(self, pid: int = 0, system: Optional['System'] = None):
self.pid = pid
self.system = system
self.gpr = [0] * 32 # R0..R31, R0 hardwired zero
self.opr = [0] * 16 # O0..O15, O0 hardwired null
self.hi = 0
self.lo = 0
self.pc = 0
self.next_pc = 0
self.cycles = 0
# Object table — local to this processor; remote references are
# routed through System.lookup_remote_descriptor().
self.descriptors: List[Optional[Descriptor]] = [None] # idx 0 reserved
self.next_obj_idx = 1
# Reusable freed-slot list (FIFO); generation already incremented.
self.free_indices: List[int] = []
# Page-table substitute: VA range -> (descriptor index, base offset, prot).
self.mappings: List[Tuple[int,int,int,int,int]] = []
# Multi-CPU bookkeeping.
self.active = False # True when a task is running
self.exit_code: Optional[int] = None # set by TaskExit
# inbox holds raw SEND_DELIVER packet bytes per Volume IV. The
# crossbar deposits packets here; dispatch_handler pops bytes,
# parses to a PendingSend, and runs the registered handler.
# Per-object queues installed by ReceiveQueueAttach hold packet
# bytes too (in Descriptor.queue).
self.inbox: List[bytes] = []
# Wire-level request/response queues for OBJ_READ/WRITE traffic.
# `requests` holds OBJ_READ_REQ / OBJ_WRITE_REQ packets that this
# CPU is the home of; the scheduler drains them autonomously each
# tick (the CPU's "memory controller"). `responses` holds the
# corresponding *_RESP packets addressed to this CPU as the
# original issuer; a blocked CPU watches these for matching
# trans_ids.
self.requests: List[bytes] = []
self.responses: List[bytes] = []
self.boot_state: Optional[Dict] = None # snapshot used to relaunch handler tasks
self.blocked_on: Optional[BlockedSignal] = None # poll/response wait state
self.next_trans_id = 0 # per-CPU SEND/OL/OS transaction id counter
# Set to True by primitives that have already moved PC into the
# right place (InstallProgram, TaskYield, TaskExit-with-successor)
# so the CALL dispatch site won't bump it past the redirect.
self._call_redirected_pc = False
# Preemption plumbing (Phase 35). `in_trap_handler` is True
# between deliver_trap and the next ERET; while it's set,
# TaskYield-from-the-handler doesn't context-switch
# immediately (which would lose the trap's saved state).