scoreboarding 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,51 @@
1
+ """scoreboarding -- pure-Python Thornton Scoreboarding (CDC 6600) simulator.
2
+
3
+ Public API::
4
+
5
+ from scoreboarding import FunctionalUnit, Instruction, run, render_trace, Trace
6
+
7
+ Example::
8
+
9
+ from scoreboarding import FunctionalUnit, Instruction, run, render_trace
10
+
11
+ fus = [
12
+ FunctionalUnit(name="Load1", kind="load", latency=2),
13
+ FunctionalUnit(name="Mult1", kind="mult", latency=10),
14
+ FunctionalUnit(name="Add1", kind="add", latency=2),
15
+ FunctionalUnit(name="Div1", kind="div", latency=40),
16
+ ]
17
+ program = [
18
+ Instruction(op="LD", dest="F6", src1="R2", src2=""),
19
+ Instruction(op="LD", dest="F2", src1="R3", src2=""),
20
+ Instruction(op="MULT", dest="F0", src1="F2", src2="F4"),
21
+ Instruction(op="SUB", dest="F8", src1="F6", src2="F2"),
22
+ Instruction(op="DIV", dest="F10", src1="F0", src2="F6"),
23
+ Instruction(op="ADD", dest="F6", src1="F8", src2="F2"),
24
+ ]
25
+ trace = run(program, functional_units=fus)
26
+ print(render_trace(trace))
27
+ """
28
+
29
+ from scoreboarding.engine import run
30
+ from scoreboarding.model import (
31
+ CycleSnapshot,
32
+ FunctionalUnit,
33
+ FunctionalUnitStatus,
34
+ Instruction,
35
+ InstructionResult,
36
+ InstructionStatus,
37
+ Trace,
38
+ )
39
+ from scoreboarding.render import render_trace
40
+
41
+ __all__ = [
42
+ "CycleSnapshot",
43
+ "FunctionalUnit",
44
+ "FunctionalUnitStatus",
45
+ "Instruction",
46
+ "InstructionResult",
47
+ "InstructionStatus",
48
+ "Trace",
49
+ "render_trace",
50
+ "run",
51
+ ]
scoreboarding/cli.py ADDED
@@ -0,0 +1,189 @@
1
+ """Command-line interface for the scoreboarding simulator.
2
+
3
+ Program text format
4
+ -------------------
5
+ Lines starting with '#' or blank lines are ignored.
6
+
7
+ Functional unit declarations (must come before instructions)::
8
+
9
+ FU <name> <kind> <latency>
10
+
11
+ Example::
12
+
13
+ FU Load1 load 2
14
+ FU Mult1 mult 10
15
+ FU Add1 add 2
16
+ FU Div1 div 40
17
+
18
+ Instruction lines::
19
+
20
+ <op> <dest>, <src1>, <src2>
21
+ <op> <dest>, <src1> # single-source (loads etc.)
22
+
23
+ Example::
24
+
25
+ LD F6, R2
26
+ MULT F0, F2, F4
27
+ ADD F6, F8, F2
28
+
29
+ Usage::
30
+
31
+ scoreboarding program.txt
32
+ scoreboarding --snapshots program.txt
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import argparse
38
+ import sys
39
+
40
+ from scoreboarding.engine import run as engine_run
41
+ from scoreboarding.model import FunctionalUnit, Instruction
42
+ from scoreboarding.render import render_trace
43
+
44
+
45
+ def _parse_program(text: str) -> tuple[list[FunctionalUnit], list[Instruction]]:
46
+ """Parse program text into functional units and instructions.
47
+
48
+ Args:
49
+ text: Full program text with FU declarations followed by instructions.
50
+
51
+ Returns:
52
+ A tuple of (functional_units, instructions).
53
+
54
+ Raises:
55
+ ValueError: If a line cannot be parsed.
56
+ """
57
+ functional_units: list[FunctionalUnit] = []
58
+ instructions: list[Instruction] = []
59
+
60
+ for raw_line in text.splitlines():
61
+ line = raw_line.strip()
62
+ if not line or line.startswith("#"):
63
+ continue
64
+
65
+ if line.upper().startswith("FU "):
66
+ parts = line.split()
67
+ if len(parts) != 4:
68
+ raise ValueError(
69
+ f"FU declaration must be 'FU <name> <kind> <latency>', got: {line!r}"
70
+ )
71
+ try:
72
+ latency = int(parts[3])
73
+ except ValueError:
74
+ raise ValueError(
75
+ f"FU latency must be an integer, got: {parts[3]!r}"
76
+ ) from None
77
+ functional_units.append(
78
+ FunctionalUnit(name=parts[1], kind=parts[2].lower(), latency=latency)
79
+ )
80
+ continue
81
+
82
+ # Instruction line: "OP DEST, SRC1" or "OP DEST, SRC1, SRC2"
83
+ # Split op from the rest.
84
+ space_idx = line.find(" ")
85
+ if space_idx == -1:
86
+ raise ValueError(f"Cannot parse instruction line: {line!r}")
87
+ op = line[:space_idx].strip()
88
+ rest = line[space_idx:].strip()
89
+
90
+ # Split operands by comma.
91
+ operands = [p.strip() for p in rest.split(",")]
92
+ if len(operands) < 2:
93
+ raise ValueError(
94
+ f"Instruction must have dest and at least one source: {line!r}"
95
+ )
96
+ dest = operands[0]
97
+ src1 = operands[1]
98
+ src2 = operands[2] if len(operands) >= 3 else ""
99
+ instructions.append(Instruction(op=op, dest=dest, src1=src1, src2=src2))
100
+
101
+ return functional_units, instructions
102
+
103
+
104
+ def run(argv: list[str]) -> int:
105
+ """Entry point for the CLI, accepting an explicit argv list.
106
+
107
+ Args:
108
+ argv: Command-line arguments (excluding the program name).
109
+
110
+ Returns:
111
+ Exit code (0 = success, non-zero = error).
112
+ """
113
+ parser = argparse.ArgumentParser(
114
+ prog="scoreboarding",
115
+ description=(
116
+ "Cycle-exact Thornton Scoreboarding (CDC 6600) simulator. "
117
+ "Reads a program file and prints the pipeline timing table."
118
+ ),
119
+ )
120
+ parser.add_argument(
121
+ "program",
122
+ metavar="FILE",
123
+ help="Path to program text file (or '-' to read from stdin).",
124
+ )
125
+ parser.add_argument(
126
+ "--snapshots",
127
+ action="store_true",
128
+ default=False,
129
+ help="Print per-cycle state snapshots after the timing table.",
130
+ )
131
+ args = parser.parse_args(argv)
132
+
133
+ try:
134
+ if args.program == "-":
135
+ text = sys.stdin.read()
136
+ else:
137
+ with open(args.program) as fh:
138
+ text = fh.read()
139
+ except OSError as exc:
140
+ print(f"scoreboarding: error reading file: {exc}", file=sys.stderr)
141
+ return 1
142
+
143
+ try:
144
+ functional_units, instructions = _parse_program(text)
145
+ except ValueError as exc:
146
+ print(f"scoreboarding: parse error: {exc}", file=sys.stderr)
147
+ return 1
148
+
149
+ if not functional_units:
150
+ print(
151
+ "scoreboarding: no functional units declared"
152
+ " -- add 'FU <name> <kind> <latency>' lines.",
153
+ file=sys.stderr,
154
+ )
155
+ return 1
156
+
157
+ if not instructions:
158
+ print("scoreboarding: no instructions found.", file=sys.stderr)
159
+ return 1
160
+
161
+ try:
162
+ trace = engine_run(
163
+ instructions,
164
+ functional_units=functional_units,
165
+ capture_snapshots=args.snapshots,
166
+ )
167
+ except (ValueError, RuntimeError) as exc:
168
+ print(f"scoreboarding: simulation error: {exc}", file=sys.stderr)
169
+ return 1
170
+
171
+ print(render_trace(trace))
172
+
173
+ if args.snapshots and trace.snapshots:
174
+ print()
175
+ for snap in trace.snapshots:
176
+ print(f"-- Cycle {snap.cycle} --")
177
+ for i, ist in enumerate(snap.instruction_status):
178
+ issue_s = str(ist.issue) if ist.issue is not None else "-"
179
+ ro_s = str(ist.read_operands) if ist.read_operands is not None else "-"
180
+ ec_s = str(ist.execute_complete) if ist.execute_complete is not None else "-"
181
+ wr_s = str(ist.write_result) if ist.write_result is not None else "-"
182
+ print(f" [{i}] Issue={issue_s} RO={ro_s} EC={ec_s} WR={wr_s}")
183
+
184
+ return 0
185
+
186
+
187
+ def main() -> None:
188
+ """Entry point installed as the 'scoreboarding' console script."""
189
+ sys.exit(run(sys.argv[1:]))
@@ -0,0 +1,347 @@
1
+ """Cycle-exact Scoreboarding simulator engine.
2
+
3
+ Implements Thornton's Scoreboarding algorithm as used in the CDC 6600 (1964),
4
+ with four pipeline stages and three tracking tables.
5
+
6
+ Stage checks (per Thornton):
7
+
8
+ ISSUE
9
+ Preconditions (checked in program order -- no instruction may issue while an
10
+ earlier instruction is stalled):
11
+ 1. No structural hazard: the required functional unit is not busy.
12
+ 2. No WAW hazard: no currently-active instruction will write the same
13
+ destination register.
14
+
15
+ READ OPERANDS
16
+ An instruction that has been issued waits here until BOTH source operands are
17
+ available. An operand is available when no earlier-issued instruction is still
18
+ going to write it (qj/qk = None AND rj/rk = True). This stage resolves RAW.
19
+
20
+ EXECUTE
21
+ The instruction occupies its functional unit for exactly `latency` cycles
22
+ starting the cycle after operands are read.
23
+
24
+ WRITE RESULT
25
+ Precondition -- no WAR hazard: every earlier-issued instruction that reads
26
+ the destination register as a source must have already completed its READ
27
+ OPERANDS stage. Once this holds, the result is written to the register file,
28
+ RegisterResultStatus is cleared, and the functional unit is freed.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import copy
34
+ from dataclasses import dataclass, field
35
+
36
+ from scoreboarding.model import (
37
+ CycleSnapshot,
38
+ FunctionalUnit,
39
+ FunctionalUnitStatus,
40
+ Instruction,
41
+ InstructionResult,
42
+ InstructionStatus,
43
+ Trace,
44
+ )
45
+
46
+
47
+ @dataclass
48
+ class _SimInstruction:
49
+ """Internal per-instruction mutable state."""
50
+
51
+ instruction: Instruction
52
+ program_index: int
53
+ status: InstructionStatus = field(default_factory=InstructionStatus)
54
+ fu_name: str = "" # which FU was assigned at issue
55
+
56
+
57
+ def _op_kind(op: str) -> str:
58
+ """Map an operation name to its functional-unit kind.
59
+
60
+ Recognises load/store, add/sub, mult/div, and falls back to op.lower().
61
+ """
62
+ op_upper = op.upper()
63
+ if op_upper in ("LD", "LW", "LOAD", "ST", "SW", "STORE"):
64
+ return "load"
65
+ if op_upper in ("ADD", "ADDD", "ADDI", "ADDF"):
66
+ return "add"
67
+ if op_upper in ("SUB", "SUBD", "SUBI", "SUBF"):
68
+ return "add" # SUB shares the add/subtract unit by convention
69
+ if op_upper in ("MULT", "MUL", "MULTD", "MULF"):
70
+ return "mult"
71
+ if op_upper in ("DIV", "DIVD", "DIVF"):
72
+ return "div"
73
+ return op.lower()
74
+
75
+
76
+ def run(
77
+ program: list[Instruction],
78
+ *,
79
+ functional_units: list[FunctionalUnit],
80
+ capture_snapshots: bool = False,
81
+ ) -> Trace:
82
+ """Simulate program using Thornton's Scoreboarding algorithm.
83
+
84
+ Args:
85
+ program: Instructions to execute, in program order.
86
+ functional_units: Available functional units.
87
+ capture_snapshots: When True, record a full CycleSnapshot each cycle.
88
+
89
+ Returns:
90
+ A Trace with per-instruction cycle stamps and optional snapshots.
91
+
92
+ Raises:
93
+ ValueError: If an instruction's operation has no matching functional unit.
94
+ """
95
+ if not program:
96
+ return Trace(results=[], snapshots=[], total_cycles=0)
97
+
98
+ # Validate that every instruction has a matching FU kind.
99
+ fu_by_kind: dict[str, list[FunctionalUnit]] = {}
100
+ for fu in functional_units:
101
+ fu_by_kind.setdefault(fu.kind, []).append(fu)
102
+
103
+ for instr in program:
104
+ kind = _op_kind(instr.op)
105
+ if kind not in fu_by_kind:
106
+ raise ValueError(
107
+ f"No functional unit of kind '{kind}' for operation '{instr.op}'"
108
+ )
109
+
110
+ # Build per-FU status table.
111
+ fu_status: dict[str, FunctionalUnitStatus] = {
112
+ fu.name: FunctionalUnitStatus() for fu in functional_units
113
+ }
114
+ fu_latency: dict[str, int] = {fu.name: fu.latency for fu in functional_units}
115
+
116
+ # Register-result-status table: maps register name -> FU name that will write it.
117
+ # None means no pending write (register holds its committed value).
118
+ register_result: dict[str, str | None] = {}
119
+
120
+ # Internal instruction list.
121
+ sim_instrs: list[_SimInstruction] = [
122
+ _SimInstruction(instruction=instr, program_index=i)
123
+ for i, instr in enumerate(program)
124
+ ]
125
+
126
+ # Index of the next instruction to issue (in-order issue pointer).
127
+ next_to_issue: int = 0
128
+ snapshots: list[CycleSnapshot] = []
129
+ cycle = 0
130
+
131
+ # Run until every instruction has a WriteResult.
132
+ while not _all_done(sim_instrs):
133
+ cycle += 1
134
+
135
+ # Determine which instructions attempt write-result, read-operands,
136
+ # and execute this cycle. ISSUE is handled last because it depends on
137
+ # the current-cycle FU state after potential WriteResults free units.
138
+
139
+ # --- WRITE RESULT ---
140
+ # For each issued instruction that has completed execution and not yet
141
+ # written its result, check the WAR condition.
142
+ for si in sim_instrs:
143
+ if not _can_attempt_write(si):
144
+ continue
145
+ dest = si.instruction.dest
146
+ # WAR check: every earlier-issued instruction that lists `dest` as a
147
+ # source (fj or fk) must have already read its operands (rj/rk = False
148
+ # means it already consumed that operand; the flag is True only while
149
+ # waiting to read).
150
+ war_blocked = False
151
+ for other in sim_instrs:
152
+ if other is si:
153
+ continue
154
+ if other.status.issue is None:
155
+ continue
156
+ if other.status.issue >= (si.status.issue or 0):
157
+ # Only earlier-issued instructions can cause WAR.
158
+ continue
159
+ if other.status.read_operands is not None:
160
+ # Already read operands; no WAR from this instruction.
161
+ continue
162
+ fu_other = fu_status.get(other.fu_name)
163
+ if fu_other is None:
164
+ continue
165
+ # Check if the other instruction is waiting to read dest.
166
+ if fu_other.fj == dest and fu_other.rj:
167
+ war_blocked = True
168
+ break
169
+ if fu_other.fk == dest and fu_other.rk:
170
+ war_blocked = True
171
+ break
172
+
173
+ if war_blocked:
174
+ continue
175
+
176
+ # Write result.
177
+ si.status.write_result = cycle
178
+
179
+ # Clear register-result-status if this FU is still the producer.
180
+ if register_result.get(dest) == si.fu_name:
181
+ register_result[dest] = None
182
+
183
+ # Update Qj/Qk of any waiting instruction that depended on this FU.
184
+ for other in sim_instrs:
185
+ if other.status.issue is None or other.status.read_operands is not None:
186
+ continue
187
+ fu_other = fu_status.get(other.fu_name)
188
+ if fu_other is None:
189
+ continue
190
+ if fu_other.qj == si.fu_name:
191
+ fu_other.qj = None
192
+ fu_other.rj = True
193
+ if fu_other.qk == si.fu_name:
194
+ fu_other.qk = None
195
+ fu_other.rk = True
196
+
197
+ # Free the functional unit.
198
+ fu_status[si.fu_name].reset()
199
+
200
+ # --- READ OPERANDS ---
201
+ # An instruction reads operands when both sources are ready.
202
+ for si in sim_instrs:
203
+ if si.status.issue is None or si.status.read_operands is not None:
204
+ continue
205
+ fu_st = fu_status[si.fu_name]
206
+ # Both operands available when qj=None,rj=True AND qk=None,rk=True.
207
+ # For single-source instructions src2="" has rk=True and qk=None by
208
+ # construction.
209
+ if fu_st.qj is None and fu_st.rj and fu_st.qk is None and fu_st.rk:
210
+ si.status.read_operands = cycle
211
+ # Mark operands as consumed (rj=rk=False) so WAR tracking works.
212
+ fu_st.rj = False
213
+ fu_st.rk = False
214
+
215
+ # --- EXECUTE COMPLETE ---
216
+ # Mark execute_complete when the latency has elapsed since execute_start.
217
+ for si in sim_instrs:
218
+ if si.status.read_operands is None or si.status.execute_complete is not None:
219
+ continue
220
+ fu_st = fu_status[si.fu_name]
221
+ # Start executing the cycle after read-operands.
222
+ if fu_st.execute_start is None and si.status.read_operands < cycle:
223
+ fu_st.execute_start = si.status.read_operands + 1
224
+ if fu_st.execute_start is not None:
225
+ elapsed = cycle - fu_st.execute_start + 1
226
+ if elapsed >= fu_latency[si.fu_name]:
227
+ si.status.execute_complete = cycle
228
+
229
+ # --- ISSUE ---
230
+ # In-order: attempt to issue the next unissued instruction.
231
+ if next_to_issue < len(sim_instrs):
232
+ si = sim_instrs[next_to_issue]
233
+ instr = si.instruction
234
+ kind = _op_kind(instr.op)
235
+
236
+ # Find a free FU of the right kind.
237
+ candidate_fu: str | None = None
238
+ for fu in functional_units:
239
+ if fu.kind == kind and not fu_status[fu.name].busy:
240
+ candidate_fu = fu.name
241
+ break
242
+
243
+ if candidate_fu is not None:
244
+ # Check WAW: no active instruction may write the same destination.
245
+ waw_blocked = False
246
+ for other in sim_instrs:
247
+ if other is si:
248
+ continue
249
+ if (
250
+ other.status.issue is not None
251
+ and other.status.write_result is None
252
+ and other.instruction.dest == instr.dest
253
+ ):
254
+ waw_blocked = True
255
+ break
256
+
257
+ if not waw_blocked:
258
+ # Issue.
259
+ si.status.issue = cycle
260
+ si.fu_name = candidate_fu
261
+ next_to_issue += 1
262
+
263
+ fu_st = fu_status[candidate_fu]
264
+ fu_st.busy = True
265
+ fu_st.op = instr.op
266
+ fu_st.fi = instr.dest
267
+
268
+ # Source 1 (fj).
269
+ fu_st.fj = instr.src1
270
+ producing_fj = register_result.get(instr.src1)
271
+ if producing_fj is not None:
272
+ fu_st.qj = producing_fj
273
+ fu_st.rj = False
274
+ else:
275
+ fu_st.qj = None
276
+ fu_st.rj = True
277
+
278
+ # Source 2 (fk).
279
+ if instr.src2:
280
+ fu_st.fk = instr.src2
281
+ producing_fk = register_result.get(instr.src2)
282
+ if producing_fk is not None:
283
+ fu_st.qk = producing_fk
284
+ fu_st.rk = False
285
+ else:
286
+ fu_st.qk = None
287
+ fu_st.rk = True
288
+ else:
289
+ # No second source (e.g. single-operand load).
290
+ fu_st.fk = ""
291
+ fu_st.qk = None
292
+ fu_st.rk = True
293
+
294
+ # Mark this FU as the future producer of dest.
295
+ register_result[instr.dest] = candidate_fu
296
+
297
+ # --- CAPTURE SNAPSHOT ---
298
+ if capture_snapshots:
299
+ snapshots.append(
300
+ CycleSnapshot(
301
+ cycle=cycle,
302
+ instruction_status=[copy.deepcopy(s.status) for s in sim_instrs],
303
+ fu_status=copy.deepcopy(fu_status),
304
+ register_result=dict(register_result),
305
+ )
306
+ )
307
+
308
+ # Safety valve: cap at a generous but finite cycle count.
309
+ max_cycles = 10 + sum(fu_latency[fu.name] for fu in functional_units) * len(
310
+ sim_instrs
311
+ )
312
+ if cycle > max_cycles:
313
+ raise RuntimeError(
314
+ f"Simulation did not terminate within {max_cycles} cycles -- "
315
+ "possible deadlock in the program or functional unit configuration."
316
+ )
317
+
318
+ total = max(
319
+ (s.status.write_result for s in sim_instrs if s.status.write_result is not None),
320
+ default=0,
321
+ )
322
+
323
+ results = [
324
+ InstructionResult(
325
+ instruction=si.instruction,
326
+ issue=si.status.issue or 0,
327
+ read_operands=si.status.read_operands or 0,
328
+ execute_complete=si.status.execute_complete or 0,
329
+ write_result=si.status.write_result or 0,
330
+ )
331
+ for si in sim_instrs
332
+ ]
333
+
334
+ return Trace(results=results, snapshots=snapshots, total_cycles=total)
335
+
336
+
337
+ def _all_done(sim_instrs: list[_SimInstruction]) -> bool:
338
+ """Return True when every instruction has completed WriteResult."""
339
+ return all(si.status.write_result is not None for si in sim_instrs)
340
+
341
+
342
+ def _can_attempt_write(si: _SimInstruction) -> bool:
343
+ """Return True when si is eligible to attempt WriteResult this cycle."""
344
+ return (
345
+ si.status.execute_complete is not None
346
+ and si.status.write_result is None
347
+ )
scoreboarding/model.py ADDED
@@ -0,0 +1,144 @@
1
+ """Data model for the Scoreboarding simulator.
2
+
3
+ Three tables track simulation state:
4
+ - InstructionStatus: per-instruction cycle stamps for each stage.
5
+ - FunctionalUnitStatus: per-FU state (busy, operands, ready flags, etc.).
6
+ - RegisterResultStatus: which FU will produce each register's next value.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass, field
12
+
13
+
14
+ @dataclass
15
+ class FunctionalUnit:
16
+ """Declares one physical functional unit available to the processor.
17
+
18
+ Args:
19
+ name: Unique identifier, e.g. "Add1", "Mult1", "Load1".
20
+ kind: Operation class this unit handles, e.g. "add", "mult", "load", "div".
21
+ latency: Number of cycles the execute stage occupies (>= 1).
22
+ """
23
+
24
+ name: str
25
+ kind: str
26
+ latency: int
27
+
28
+ def __post_init__(self) -> None:
29
+ if self.latency < 1:
30
+ raise ValueError(
31
+ f"FunctionalUnit '{self.name}': latency must be >= 1, got {self.latency}"
32
+ )
33
+
34
+
35
+ @dataclass
36
+ class Instruction:
37
+ """One instruction in the program.
38
+
39
+ For single-source instructions (e.g. loads), pass the base register as
40
+ src1 and an empty string "" as src2.
41
+
42
+ Args:
43
+ op: Operation name, e.g. "LD", "MULT", "ADD", "SUB", "DIV".
44
+ dest: Destination register, e.g. "F6".
45
+ src1: First source register (or base register for loads).
46
+ src2: Second source register; pass "" if not applicable.
47
+ """
48
+
49
+ op: str
50
+ dest: str
51
+ src1: str
52
+ src2: str
53
+
54
+
55
+ @dataclass
56
+ class InstructionStatus:
57
+ """Cycle stamps for one instruction's four pipeline stages.
58
+
59
+ A value of None means the stage has not yet occurred.
60
+ """
61
+
62
+ issue: int | None = None
63
+ read_operands: int | None = None
64
+ execute_complete: int | None = None
65
+ write_result: int | None = None
66
+
67
+
68
+ @dataclass
69
+ class FunctionalUnitStatus:
70
+ """Runtime state of one functional unit (the FU status table).
71
+
72
+ Fields mirror those in Thornton's original scoreboard description:
73
+ - busy: whether the unit is currently occupied.
74
+ - op: operation being performed.
75
+ - fi: destination register name.
76
+ - fj, fk: source register names.
77
+ - qj, qk: names of FUs that will produce fj/fk (None = already available).
78
+ - rj, rk: True when fj/fk are ready to read (operand available and not yet consumed).
79
+ - execute_start: cycle when execution started (None until started).
80
+ """
81
+
82
+ busy: bool = False
83
+ op: str = ""
84
+ fi: str = ""
85
+ fj: str = ""
86
+ fk: str = ""
87
+ qj: str | None = None
88
+ qk: str | None = None
89
+ rj: bool = False
90
+ rk: bool = False
91
+ execute_start: int | None = None
92
+
93
+ def reset(self) -> None:
94
+ """Return this FU to the idle state."""
95
+ self.busy = False
96
+ self.op = ""
97
+ self.fi = ""
98
+ self.fj = ""
99
+ self.fk = ""
100
+ self.qj = None
101
+ self.qk = None
102
+ self.rj = False
103
+ self.rk = False
104
+ self.execute_start = None
105
+
106
+
107
+ @dataclass
108
+ class CycleSnapshot:
109
+ """Full simulator state at the end of one cycle.
110
+
111
+ Useful for step-by-step educational replay.
112
+ """
113
+
114
+ cycle: int
115
+ instruction_status: list[InstructionStatus]
116
+ fu_status: dict[str, FunctionalUnitStatus]
117
+ register_result: dict[str, str | None]
118
+
119
+
120
+ @dataclass
121
+ class InstructionResult:
122
+ """Final cycle stamps for one instruction after simulation completes."""
123
+
124
+ instruction: Instruction
125
+ issue: int
126
+ read_operands: int
127
+ execute_complete: int
128
+ write_result: int
129
+
130
+
131
+ @dataclass
132
+ class Trace:
133
+ """Full simulation output.
134
+
135
+ Attributes:
136
+ results: Per-instruction cycle stamps in program order.
137
+ snapshots: Optional per-cycle state snapshots (populated when
138
+ capture_snapshots=True in run()).
139
+ total_cycles: Cycle number of the final WriteResult.
140
+ """
141
+
142
+ results: list[InstructionResult]
143
+ snapshots: list[CycleSnapshot] = field(default_factory=list)
144
+ total_cycles: int = 0
scoreboarding/py.typed ADDED
File without changes
@@ -0,0 +1,83 @@
1
+ """Render a Trace as a human-readable timing table."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from scoreboarding.model import InstructionResult, Trace
6
+
7
+
8
+ def _result_to_row(r: InstructionResult) -> tuple[str, str, str, str, str]:
9
+ """Convert one InstructionResult to a tuple of string columns."""
10
+ src2_part = f", {r.instruction.src2}" if r.instruction.src2 else ""
11
+ instr_str = f"{r.instruction.op} {r.instruction.dest}, {r.instruction.src1}{src2_part}"
12
+ return (
13
+ instr_str,
14
+ str(r.issue),
15
+ str(r.read_operands),
16
+ str(r.execute_complete),
17
+ str(r.write_result),
18
+ )
19
+
20
+
21
+ def render_trace(trace: Trace) -> str:
22
+ """Return a formatted timing table for the given Trace.
23
+
24
+ Each row shows one instruction and the four pipeline-stage cycle numbers:
25
+ Issue, ReadOperands, ExecuteComplete, WriteResult.
26
+
27
+ Args:
28
+ trace: The simulation output from run().
29
+
30
+ Returns:
31
+ A multi-line string suitable for printing to a terminal.
32
+ """
33
+ if not trace.results:
34
+ return "(empty program)"
35
+
36
+ header_instr = "Instruction"
37
+ header_issue = "Issue"
38
+ header_ro = "ReadOps"
39
+ header_ec = "ExecComp"
40
+ header_wr = "WriteResult"
41
+
42
+ rows = [_result_to_row(r) for r in trace.results]
43
+
44
+ col0_w = max(len(header_instr), *(len(row[0]) for row in rows))
45
+ col1_w = max(len(header_issue), *(len(row[1]) for row in rows))
46
+ col2_w = max(len(header_ro), *(len(row[2]) for row in rows))
47
+ col3_w = max(len(header_ec), *(len(row[3]) for row in rows))
48
+ col4_w = max(len(header_wr), *(len(row[4]) for row in rows))
49
+
50
+ sep = (
51
+ "+"
52
+ + "-" * (col0_w + 2)
53
+ + "+"
54
+ + "-" * (col1_w + 2)
55
+ + "+"
56
+ + "-" * (col2_w + 2)
57
+ + "+"
58
+ + "-" * (col3_w + 2)
59
+ + "+"
60
+ + "-" * (col4_w + 2)
61
+ + "+"
62
+ )
63
+
64
+ def fmt_row(c0: str, c1: str, c2: str, c3: str, c4: str) -> str:
65
+ return (
66
+ f"| {c0:<{col0_w}} "
67
+ f"| {c1:>{col1_w}} "
68
+ f"| {c2:>{col2_w}} "
69
+ f"| {c3:>{col3_w}} "
70
+ f"| {c4:>{col4_w}} |"
71
+ )
72
+
73
+ lines = [
74
+ sep,
75
+ fmt_row(header_instr, header_issue, header_ro, header_ec, header_wr),
76
+ sep,
77
+ ]
78
+ for row in rows:
79
+ lines.append(fmt_row(row[0], row[1], row[2], row[3], row[4]))
80
+ lines.append(sep)
81
+ lines.append(f"Total cycles: {trace.total_cycles}")
82
+
83
+ return "\n".join(lines)
@@ -0,0 +1,211 @@
1
+ Metadata-Version: 2.4
2
+ Name: scoreboarding
3
+ Version: 0.1.0
4
+ Summary: Pure-Python cycle-exact simulator of Thornton's Scoreboarding (CDC 6600) in-order dynamic scheduling, with structural, WAR, and WAW hazard detection and per-instruction traces for computer architecture education.
5
+ Project-URL: Homepage, https://github.com/amaar-mc/scoreboarding
6
+ Project-URL: Repository, https://github.com/amaar-mc/scoreboarding
7
+ Project-URL: Issues, https://github.com/amaar-mc/scoreboarding/issues
8
+ Project-URL: Changelog, https://github.com/amaar-mc/scoreboarding/blob/main/CHANGELOG.md
9
+ Author: Amaar Chughtai
10
+ License: MIT License
11
+
12
+ Copyright (c) 2026 Amaar Chughtai
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Keywords: cdc-6600,computer-architecture,cpu-simulator,education,hazards,in-order,instruction-scheduling,pipeline,scoreboarding,simulation
33
+ Classifier: Development Status :: 3 - Alpha
34
+ Classifier: Intended Audience :: Education
35
+ Classifier: Intended Audience :: Science/Research
36
+ Classifier: License :: OSI Approved :: MIT License
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Programming Language :: Python :: 3.13
42
+ Classifier: Topic :: Education
43
+ Classifier: Topic :: Scientific/Engineering
44
+ Classifier: Typing :: Typed
45
+ Requires-Python: >=3.10
46
+ Provides-Extra: dev
47
+ Requires-Dist: hatchling>=1.25; extra == 'dev'
48
+ Requires-Dist: mypy>=1.11; extra == 'dev'
49
+ Requires-Dist: pytest>=8; extra == 'dev'
50
+ Requires-Dist: ruff>=0.6; extra == 'dev'
51
+ Description-Content-Type: text/markdown
52
+
53
+ # scoreboarding
54
+
55
+ <p align="center">
56
+ <img src="assets/logo.png" alt="scoreboarding logo" width="160">
57
+ </p>
58
+
59
+ Pure-Python cycle-exact simulator of Thornton's **Scoreboarding** algorithm,
60
+ as implemented in the CDC 6600 (1964). Designed for computer architecture
61
+ education: readable source, zero runtime dependencies, and per-instruction
62
+ cycle-number traces.
63
+
64
+ > PyPI publish pending (package-upload quota). `pip install scoreboarding`
65
+ > will work once the first release is live. Install from source in the meantime.
66
+
67
+ ---
68
+
69
+ ## What is Scoreboarding?
70
+
71
+ Scoreboarding is an **in-order issue, out-of-order execution** dynamic
72
+ scheduling technique. The processor issues instructions one at a time
73
+ (program order), but lets them read operands and execute independently once
74
+ their hazards clear. A central scoreboard -- three tables -- tracks every
75
+ in-flight instruction and enforces the classic CDC 6600 hazard rules without
76
+ any register renaming.
77
+
78
+ ### The Four Stages
79
+
80
+ | Stage | What happens | Hazard checked |
81
+ |---|---|---|
82
+ | **Issue** | Assign instruction to a free FU | Structural (FU busy) + WAW (another active insn writes same dest) |
83
+ | **Read Operands** | Read both source registers | RAW (stall until producing FU has written result) |
84
+ | **Execute** | Occupy the FU for its full latency | -- |
85
+ | **Write Result** | Commit result to register file, free FU | WAR (stall until every earlier reader has read its operand) |
86
+
87
+ ### Three Tracking Tables
88
+
89
+ 1. **Instruction Status** -- per-instruction cycle stamps (Issue / ReadOperands / ExecuteComplete / WriteResult).
90
+ 2. **Functional Unit Status** -- per-FU: busy flag, op, destination (Fi), sources (Fj, Fk), producing FUs (Qj, Qk), ready flags (Rj, Rk).
91
+ 3. **Register Result Status** -- which FU will next write each register (None once written).
92
+
93
+ ### How it differs from Tomasulo
94
+
95
+ | | Scoreboarding | Tomasulo |
96
+ |---|---|---|
97
+ | Issue order | In order | In order |
98
+ | Execution order | Out of order | Out of order |
99
+ | WAW handling | Stall Issue | Register renaming (RS tags) |
100
+ | WAR handling | Stall Write Result | Eliminated by renaming |
101
+ | RAW handling | Stall Read Operands | Stall in RS until CDB broadcast |
102
+ | Register renaming | No | Yes (via reservation stations) |
103
+ | Broadcast mechanism | Central scoreboard | Common Data Bus |
104
+
105
+ See the sibling package [tomasulo](https://github.com/amaar-mc/tomasulo) for the
106
+ Tomasulo out-of-order scheduler with register renaming.
107
+
108
+ ---
109
+
110
+ ## Install
111
+
112
+ ```bash
113
+ # From source (until PyPI release):
114
+ git clone https://github.com/amaar-mc/scoreboarding
115
+ cd scoreboarding
116
+ uv pip install -e ".[dev]"
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Usage
122
+
123
+ ### Python API
124
+
125
+ ```python
126
+ from scoreboarding import FunctionalUnit, Instruction, run, render_trace
127
+
128
+ fus = [
129
+ FunctionalUnit(name="Load1", kind="load", latency=2),
130
+ FunctionalUnit(name="Mult1", kind="mult", latency=10),
131
+ FunctionalUnit(name="Add1", kind="add", latency=2),
132
+ FunctionalUnit(name="Div1", kind="div", latency=40),
133
+ ]
134
+
135
+ program = [
136
+ Instruction(op="LD", dest="F6", src1="R2", src2=""),
137
+ Instruction(op="LD", dest="F2", src1="R3", src2=""),
138
+ Instruction(op="MULT", dest="F0", src1="F2", src2="F4"),
139
+ Instruction(op="SUB", dest="F8", src1="F6", src2="F2"),
140
+ Instruction(op="DIV", dest="F10", src1="F0", src2="F6"),
141
+ Instruction(op="ADD", dest="F6", src1="F8", src2="F2"),
142
+ ]
143
+
144
+ trace = run(program, functional_units=fus)
145
+ print(render_trace(trace))
146
+ ```
147
+
148
+ ### Example timing table
149
+
150
+ ```
151
+ +---------------------+-------+---------+----------+-------------+
152
+ | Instruction | Issue | ReadOps | ExecComp | WriteResult |
153
+ +---------------------+-------+---------+----------+-------------+
154
+ | LD F6, R2 | 1 | 1 | 2 | 3 |
155
+ | LD F2, R3 | 3 | 3 | 4 | 5 |
156
+ | MULT F0, F2, F4 | 4 | 5 | 14 | 15 |
157
+ | SUB F8, F6, F2 | 4 | 5 | 6 | 7 |
158
+ | DIV F10, F0, F6 | 5 | 15 | 54 | 55 |
159
+ | ADD F6, F8, F2 | 8 | 8 | 9 | 16 |
160
+ +---------------------+-------+---------+----------+-------------+
161
+ Total cycles: 16
162
+ ```
163
+
164
+ ### CLI
165
+
166
+ ```bash
167
+ # Use the bundled example:
168
+ scoreboarding examples/classic.txt
169
+
170
+ # Enable per-cycle snapshots:
171
+ scoreboarding --snapshots examples/classic.txt
172
+
173
+ # Read from stdin:
174
+ cat examples/classic.txt | scoreboarding -
175
+ ```
176
+
177
+ Program file format:
178
+
179
+ ```
180
+ # Comments start with #
181
+ FU Load1 load 2
182
+ FU Mult1 mult 10
183
+ FU Add1 add 2
184
+ FU Div1 div 40
185
+
186
+ LD F6, R2
187
+ LD F2, R3
188
+ MULT F0, F2, F4
189
+ SUB F8, F6, F2
190
+ DIV F10, F0, F6
191
+ ADD F6, F8, F2
192
+ ```
193
+
194
+ ---
195
+
196
+ ## Development
197
+
198
+ ```bash
199
+ uv run pytest -q
200
+ uv run ruff check .
201
+ uv run mypy src
202
+ uv build
203
+ ```
204
+
205
+ CI runs on Python 3.10, 3.11, 3.12, 3.13 via GitHub Actions.
206
+
207
+ ---
208
+
209
+ ## License
210
+
211
+ MIT -- see [LICENSE](LICENSE).
@@ -0,0 +1,11 @@
1
+ scoreboarding/__init__.py,sha256=NNn0ApX5cldroPqduB-hI0KJ1Q6FdLBBZiRme5-VgLM,1494
2
+ scoreboarding/cli.py,sha256=6PBtYrI3nqM4onLDPjsZT4baiMcv60oFE95aTfQvR_E,5639
3
+ scoreboarding/engine.py,sha256=Nppk6ekyVsipvS-MsAOb-YhZdmBoYqinafvZsMn8tfE,13109
4
+ scoreboarding/model.py,sha256=SONQ41KH-UhxGxQaS7Fl0Tbz1BRqaDBPbESx27EsKg4,3911
5
+ scoreboarding/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ scoreboarding/render.py,sha256=rjmKVIwga2frNdGKQNCfBg7kSaoy6-5BXw3D4HdY4kM,2417
7
+ scoreboarding-0.1.0.dist-info/METADATA,sha256=NczuAVXyJlefKrnuj4qeshswfY0d-f74wu7SGixeKvA,7554
8
+ scoreboarding-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
9
+ scoreboarding-0.1.0.dist-info/entry_points.txt,sha256=3i_fhrWHz9Z5flBtTx3mCHXFdYAByv09KM1jp9vCapg,57
10
+ scoreboarding-0.1.0.dist-info/licenses/LICENSE,sha256=NORTVJb4x206RXQkpZt_vpj8I7aZL6Vn26BvTOq6J40,1071
11
+ scoreboarding-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ scoreboarding = scoreboarding.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Amaar Chughtai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.