gini-toolkit 6.0.1.dev0__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.
Files changed (278) hide show
  1. gini/__init__.py +12 -0
  2. gini/__main__.py +107 -0
  3. gini/_version.py +24 -0
  4. gini/agent/__init__.py +17 -0
  5. gini/agent/agent_gamemaster.py +140 -0
  6. gini/agent/api.py +291 -0
  7. gini/agent/ask.py +123 -0
  8. gini/agent/authoring.py +72 -0
  9. gini/agent/blackboard.py +114 -0
  10. gini/agent/contracts.py +142 -0
  11. gini/agent/domains.py +91 -0
  12. gini/agent/embed.py +123 -0
  13. gini/agent/gamemaster.py +256 -0
  14. gini/agent/kb.py +148 -0
  15. gini/agent/lesson_resolver.py +261 -0
  16. gini/agent/llm/__init__.py +5 -0
  17. gini/agent/llm/backend.py +43 -0
  18. gini/agent/llm/fake.py +25 -0
  19. gini/agent/llm/ollama.py +206 -0
  20. gini/agent/loop.py +258 -0
  21. gini/agent/mcp_server.py +86 -0
  22. gini/agent/meaning.py +225 -0
  23. gini/agent/mission.py +210 -0
  24. gini/agent/mission_controller.py +208 -0
  25. gini/agent/narration.py +116 -0
  26. gini/agent/notifier.py +86 -0
  27. gini/agent/personas.py +79 -0
  28. gini/agent/reasoning.py +172 -0
  29. gini/agent/recall.py +248 -0
  30. gini/agent/session.py +79 -0
  31. gini/agent/teaching_center.py +482 -0
  32. gini/agent/tools/__init__.py +3 -0
  33. gini/agent/tools/registry.py +193 -0
  34. gini/agent/twin/__init__.py +28 -0
  35. gini/agent/twin/authoring.py +71 -0
  36. gini/agent/twin/contracts.py +54 -0
  37. gini/agent/twin/dialectic.py +189 -0
  38. gini/agent/twin/harness.py +93 -0
  39. gini/agent/twin/justify.py +156 -0
  40. gini/agent/twin/learner.py +64 -0
  41. gini/agent/twin/mission.py +60 -0
  42. gini/agent/twin/os_coach.py +79 -0
  43. gini/agent/twin/salience.py +30 -0
  44. gini/agent/understand.py +250 -0
  45. gini/agent/verifiers.py +106 -0
  46. gini/agent/wizard.py +178 -0
  47. gini/agent/xv6_pack.py +74 -0
  48. gini/app/__init__.py +3 -0
  49. gini/app/context.py +368 -0
  50. gini/app/paths.py +121 -0
  51. gini/data/README.md +21 -0
  52. gini/domain/__init__.py +9 -0
  53. gini/domain/assembly.py +209 -0
  54. gini/domain/authoring.py +353 -0
  55. gini/domain/blueprints.py +5 -0
  56. gini/domain/capabilities.py +177 -0
  57. gini/domain/catalog.py +85 -0
  58. gini/domain/certify.py +201 -0
  59. gini/domain/compose.py +413 -0
  60. gini/domain/composition.py +88 -0
  61. gini/domain/concepts.py +383 -0
  62. gini/domain/connection_rules.py +269 -0
  63. gini/domain/constraints.py +153 -0
  64. gini/domain/content.py +59 -0
  65. gini/domain/cpu_journey.py +89 -0
  66. gini/domain/devices.py +747 -0
  67. gini/domain/diagnose.py +201 -0
  68. gini/domain/element_guide.py +327 -0
  69. gini/domain/explain.py +90 -0
  70. gini/domain/fingerprint.py +201 -0
  71. gini/domain/firewall.py +34 -0
  72. gini/domain/flowlog.py +61 -0
  73. gini/domain/flowtable.py +179 -0
  74. gini/domain/fragment_yaml.py +230 -0
  75. gini/domain/fragments.py +169 -0
  76. gini/domain/games/__init__.py +2 -0
  77. gini/domain/games/paging_games.py +119 -0
  78. gini/domain/games/policy_game.py +86 -0
  79. gini/domain/games/process_game.py +48 -0
  80. gini/domain/games/thrash_game.py +75 -0
  81. gini/domain/games/translate_game.py +60 -0
  82. gini/domain/games/trap_game.py +86 -0
  83. gini/domain/grader.py +155 -0
  84. gini/domain/grouping.py +67 -0
  85. gini/domain/legality.py +103 -0
  86. gini/domain/lesson.py +241 -0
  87. gini/domain/lexicon.py +150 -0
  88. gini/domain/machine_state.py +410 -0
  89. gini/domain/missions/networking/basic-lan.yaml +32 -0
  90. gini/domain/missions/networking/cache-in-front.yaml +23 -0
  91. gini/domain/missions/networking/decouple-with-queue.yaml +31 -0
  92. gini/domain/missions/networking/drive-load.yaml +20 -0
  93. gini/domain/missions/networking/fix-the-address.yaml +75 -0
  94. gini/domain/missions/networking/fix-the-lan.yaml +43 -0
  95. gini/domain/missions/networking/inspect-flows.yaml +16 -0
  96. gini/domain/missions/networking/k8s-autoscale.yaml +27 -0
  97. gini/domain/missions/networking/least-privilege.yaml +21 -0
  98. gini/domain/missions/networking/load-balanced-web.yaml +29 -0
  99. gini/domain/missions/networking/observe-it.yaml +24 -0
  100. gini/domain/missions/networking/put-in-vpc.yaml +30 -0
  101. gini/domain/missions/networking/reachability-boundary.yaml +56 -0
  102. gini/domain/missions/networking/sdn-reactive.yaml +35 -0
  103. gini/domain/missions/networking/send-request.yaml +19 -0
  104. gini/domain/missions/networking/serverless-api.yaml +25 -0
  105. gini/domain/missions/networking/service-chain.yaml +33 -0
  106. gini/domain/missions/os/lottery-fix.yaml +19 -0
  107. gini/domain/missions/os/priority-fix.yaml +24 -0
  108. gini/domain/missions.py +111 -0
  109. gini/domain/modulechain.py +36 -0
  110. gini/domain/objectives.py +488 -0
  111. gini/domain/os_zoo.py +79 -0
  112. gini/domain/paging_sim.py +141 -0
  113. gini/domain/pricing.py +199 -0
  114. gini/domain/probes.py +226 -0
  115. gini/domain/profile.py +142 -0
  116. gini/domain/recipes.py +738 -0
  117. gini/domain/riders.py +309 -0
  118. gini/domain/router_modules.py +224 -0
  119. gini/domain/routetable.py +67 -0
  120. gini/domain/scoring.py +76 -0
  121. gini/domain/staging.py +122 -0
  122. gini/domain/syscall_builder.py +144 -0
  123. gini/domain/topic_cloud.py +62 -0
  124. gini/domain/topology.py +213 -0
  125. gini/domain/vocabulary.py +51 -0
  126. gini/domain/xv6.py +808 -0
  127. gini/domain/xv6_fs.py +250 -0
  128. gini/domain/xv6_runner.py +113 -0
  129. gini/domain/xv6_vm.py +385 -0
  130. gini/gloader.py +17 -0
  131. gini/runtime/__init__.py +18 -0
  132. gini/runtime/cloudfabric_agent.py +370 -0
  133. gini/runtime/console.py +68 -0
  134. gini/runtime/control.py +70 -0
  135. gini/runtime/frame.py +138 -0
  136. gini/runtime/gbridge.py +638 -0
  137. gini/runtime/grouter.py +223 -0
  138. gini/runtime/hostsim.py +90 -0
  139. gini/runtime/shuttle.py +348 -0
  140. gini/runtime/switch.py +109 -0
  141. gini/runtime/transport.py +77 -0
  142. gini/runtime/xv6_bridge.py +312 -0
  143. gini/server/__init__.py +22 -0
  144. gini/server/__main__.py +74 -0
  145. gini/server/app.py +140 -0
  146. gini/server/auth.py +82 -0
  147. gini/server/policy.py +57 -0
  148. gini/server/session.py +23 -0
  149. gini/services/__init__.py +15 -0
  150. gini/services/boardflash.py +248 -0
  151. gini/services/boardsetup.py +374 -0
  152. gini/services/cloud_catalog.py +143 -0
  153. gini/services/compiler.py +1858 -0
  154. gini/services/discovery.py +324 -0
  155. gini/services/gloader.py +183 -0
  156. gini/services/orchestrator.py +1460 -0
  157. gini/services/persistence.py +28 -0
  158. gini/services/probe_runner.py +149 -0
  159. gini/services/project.py +217 -0
  160. gini/services/remote.py +93 -0
  161. gini/services/rider_runner.py +96 -0
  162. gini/services/rider_session.py +171 -0
  163. gini/services/shadow_store.py +52 -0
  164. gini/services/terminal.py +45 -0
  165. gini/setup/__init__.py +17 -0
  166. gini/setup/cli.py +109 -0
  167. gini/setup/images.py +33 -0
  168. gini/setup/marker.py +43 -0
  169. gini/setup/runtime.py +69 -0
  170. gini/ui/__init__.py +3 -0
  171. gini/ui/assets/app_icon.icns +0 -0
  172. gini/ui/assets/app_icon.ico +0 -0
  173. gini/ui/assets/app_icon.png +0 -0
  174. gini/ui/assets/app_icon_1024.png +0 -0
  175. gini/ui/assets/cue/_w.txt +1 -0
  176. gini/ui/assets/cue/ai.png +0 -0
  177. gini/ui/assets/cue/canvas.png +0 -0
  178. gini/ui/assets/cue/cloud.png +0 -0
  179. gini/ui/assets/cue/cost.png +0 -0
  180. gini/ui/assets/cue/dark/ai.png +0 -0
  181. gini/ui/assets/cue/dark/canvas.png +0 -0
  182. gini/ui/assets/cue/dark/cloud.png +0 -0
  183. gini/ui/assets/cue/dark/cost.png +0 -0
  184. gini/ui/assets/cue/dark/metrics.png +0 -0
  185. gini/ui/assets/cue/dark/router.png +0 -0
  186. gini/ui/assets/cue/dark/run.png +0 -0
  187. gini/ui/assets/cue/dark/serverless.png +0 -0
  188. gini/ui/assets/cue/dark/settings.png +0 -0
  189. gini/ui/assets/cue/dark/welcome.png +0 -0
  190. gini/ui/assets/cue/dark/wizard.png +0 -0
  191. gini/ui/assets/cue/ginibrand/ai.png +0 -0
  192. gini/ui/assets/cue/ginibrand/canvas.png +0 -0
  193. gini/ui/assets/cue/ginibrand/cloud.png +0 -0
  194. gini/ui/assets/cue/ginibrand/cost.png +0 -0
  195. gini/ui/assets/cue/ginibrand/metrics.png +0 -0
  196. gini/ui/assets/cue/ginibrand/router.png +0 -0
  197. gini/ui/assets/cue/ginibrand/run.png +0 -0
  198. gini/ui/assets/cue/ginibrand/serverless.png +0 -0
  199. gini/ui/assets/cue/ginibrand/settings.png +0 -0
  200. gini/ui/assets/cue/ginibrand/welcome.png +0 -0
  201. gini/ui/assets/cue/ginibrand/wizard.png +0 -0
  202. gini/ui/assets/cue/highcontrast/ai.png +0 -0
  203. gini/ui/assets/cue/highcontrast/canvas.png +0 -0
  204. gini/ui/assets/cue/highcontrast/cloud.png +0 -0
  205. gini/ui/assets/cue/highcontrast/cost.png +0 -0
  206. gini/ui/assets/cue/highcontrast/metrics.png +0 -0
  207. gini/ui/assets/cue/highcontrast/router.png +0 -0
  208. gini/ui/assets/cue/highcontrast/run.png +0 -0
  209. gini/ui/assets/cue/highcontrast/serverless.png +0 -0
  210. gini/ui/assets/cue/highcontrast/settings.png +0 -0
  211. gini/ui/assets/cue/highcontrast/welcome.png +0 -0
  212. gini/ui/assets/cue/highcontrast/wizard.png +0 -0
  213. gini/ui/assets/cue/light/ai.png +0 -0
  214. gini/ui/assets/cue/light/canvas.png +0 -0
  215. gini/ui/assets/cue/light/cloud.png +0 -0
  216. gini/ui/assets/cue/light/cost.png +0 -0
  217. gini/ui/assets/cue/light/metrics.png +0 -0
  218. gini/ui/assets/cue/light/router.png +0 -0
  219. gini/ui/assets/cue/light/run.png +0 -0
  220. gini/ui/assets/cue/light/serverless.png +0 -0
  221. gini/ui/assets/cue/light/settings.png +0 -0
  222. gini/ui/assets/cue/light/welcome.png +0 -0
  223. gini/ui/assets/cue/light/wizard.png +0 -0
  224. gini/ui/assets/cue/metrics.png +0 -0
  225. gini/ui/assets/cue/router.png +0 -0
  226. gini/ui/assets/cue/run.png +0 -0
  227. gini/ui/assets/cue/serverless.png +0 -0
  228. gini/ui/assets/cue/settings.png +0 -0
  229. gini/ui/assets/cue/welcome.png +0 -0
  230. gini/ui/assets/cue/wizard.png +0 -0
  231. gini/ui/assistant.py +2111 -0
  232. gini/ui/author_dialog.py +184 -0
  233. gini/ui/board_dialog.py +247 -0
  234. gini/ui/branding.py +21 -0
  235. gini/ui/canvas.py +2007 -0
  236. gini/ui/chat_panel.py +7 -0
  237. gini/ui/cpu_journey.py +212 -0
  238. gini/ui/cpu_lab.py +306 -0
  239. gini/ui/cue_cards.py +214 -0
  240. gini/ui/dashboard.py +222 -0
  241. gini/ui/diagnose_game.py +336 -0
  242. gini/ui/fingerprint_lab.py +219 -0
  243. gini/ui/flash_dialog.py +244 -0
  244. gini/ui/flow_layout.py +63 -0
  245. gini/ui/fragment_manager.py +1415 -0
  246. gini/ui/game_catalog.py +184 -0
  247. gini/ui/game_renderers.py +340 -0
  248. gini/ui/games_lab.py +90 -0
  249. gini/ui/inspector.py +1055 -0
  250. gini/ui/live_metrics.py +130 -0
  251. gini/ui/machine_lab.py +1412 -0
  252. gini/ui/main_window.py +3153 -0
  253. gini/ui/memory_lab.py +371 -0
  254. gini/ui/mission_panel.py +302 -0
  255. gini/ui/mode_indicator.py +227 -0
  256. gini/ui/palette.py +112 -0
  257. gini/ui/peripherals.py +218 -0
  258. gini/ui/process_tree.py +130 -0
  259. gini/ui/reset_dialog.py +179 -0
  260. gini/ui/router_lab.py +776 -0
  261. gini/ui/run_button.py +183 -0
  262. gini/ui/settings_dialog.py +234 -0
  263. gini/ui/signin_dialog.py +111 -0
  264. gini/ui/storage_lab.py +219 -0
  265. gini/ui/syscall_builder.py +235 -0
  266. gini/ui/syscall_lab.py +152 -0
  267. gini/ui/theme/__init__.py +5 -0
  268. gini/ui/theme/icons.py +145 -0
  269. gini/ui/theme/manager.py +291 -0
  270. gini/ui/theme/tokens.py +194 -0
  271. gini/ui/trap_lab.py +270 -0
  272. gini/ui/worker_host.py +102 -0
  273. gini/ui/zoo_lab.py +112 -0
  274. gini_toolkit-6.0.1.dev0.dist-info/METADATA +77 -0
  275. gini_toolkit-6.0.1.dev0.dist-info/RECORD +278 -0
  276. gini_toolkit-6.0.1.dev0.dist-info/WHEEL +5 -0
  277. gini_toolkit-6.0.1.dev0.dist-info/entry_points.txt +3 -0
  278. gini_toolkit-6.0.1.dev0.dist-info/top_level.txt +1 -0
gini/domain/xv6_vm.py ADDED
@@ -0,0 +1,385 @@
1
+ """xv6 virtual-memory state model — the Memory face's read side.
2
+
3
+ xv6 on RISC-V uses Sv39 three-level page tables: `satp` points at the root, and a 39-bit virtual
4
+ address is split into three 9-bit indices (levels 2/1/0) plus a 12-bit offset. A leaf PTE carries
5
+ a physical page number and permission flags V/R/W/X/U. This module models the flattened set of
6
+ leaf mappings, the process address-space regions (text/data/heap/guard/stack/trapframe/trampoline),
7
+ the physical page allocator, and page faults — with a parser for xv6's own `vmprint()` output, so
8
+ the VA reconstruction, the flag decoding, and the DemoVm feed are all unit-tested without QEMU.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from dataclasses import dataclass, field
14
+
15
+ PGSIZE = 4096
16
+ MAXVA = 1 << 38
17
+ TRAMPOLINE = MAXVA - PGSIZE # 0x3FFFFFF000
18
+ TRAPFRAME = TRAMPOLINE - PGSIZE # 0x3FFFFFE000
19
+
20
+ # PTE flag bits (RISC-V)
21
+ _FLAGS = (("V", 1), ("R", 2), ("W", 4), ("X", 8), ("U", 16), ("G", 32), ("A", 64), ("D", 128))
22
+
23
+
24
+ def perms(pte: int) -> str:
25
+ """The R/W/X/U permission string of a PTE value (e.g. 'rwx-' , 'r-x u')."""
26
+ out = ""
27
+ for ch, bit in (("r", 2), ("w", 4), ("x", 8), ("u", 16)):
28
+ out += ch if (pte & bit) else "-"
29
+ return out
30
+
31
+
32
+ def rsw_bits(flags: int) -> int:
33
+ """The two reserved-for-software bits (8,9) of a PTE. xv6's COW lab parks its COW flag here,
34
+ so a non-zero RSW on a user page is our (design-agnostic) 'this page is marked by the student'
35
+ signal — we badge it without needing to know which bit they chose."""
36
+ return (flags >> 8) & 0x3
37
+
38
+
39
+ @dataclass
40
+ class Pte:
41
+ va: int
42
+ pa: int
43
+ perms: str = "----"
44
+ valid: bool = True
45
+ flags: int = 0 # raw low-10 PTE bits (V R W X U G A D + RSW8/9); 0 if unknown
46
+
47
+ @property
48
+ def writable(self) -> bool:
49
+ return "w" in self.perms
50
+
51
+ @property
52
+ def user(self) -> bool:
53
+ return "u" in self.perms
54
+
55
+ @property
56
+ def rsw(self) -> int:
57
+ return rsw_bits(self.flags)
58
+
59
+ @property
60
+ def cow(self) -> bool:
61
+ """A COW candidate: a user page that is present, NOT writable, and carries an RSW mark —
62
+ the exact shape xv6's COW fork leaves behind (shared, write-protected, COW-tagged)."""
63
+ return self.user and not self.writable and self.rsw != 0
64
+
65
+
66
+ @dataclass
67
+ class Region:
68
+ name: str
69
+ start: int
70
+ end: int
71
+ perms: str = ""
72
+
73
+ @property
74
+ def pages(self) -> int:
75
+ return max((self.end - self.start) // PGSIZE, 1)
76
+
77
+
78
+ @dataclass
79
+ class PhysMem:
80
+ total_pages: int = 0
81
+ free_pages: int = 0
82
+
83
+ @property
84
+ def used_pages(self) -> int:
85
+ return max(self.total_pages - self.free_pages, 0)
86
+
87
+ @property
88
+ def used_frac(self) -> float:
89
+ return (self.used_pages / self.total_pages) if self.total_pages else 0.0
90
+
91
+
92
+ @dataclass
93
+ class Fault:
94
+ va: int
95
+ cause: str
96
+ pid: int | None = None
97
+
98
+
99
+ # scause codes for the three page-fault kinds (RISC-V privileged spec)
100
+ SCAUSE_NAMES = {12: "instruction page fault", 13: "load page fault", 15: "store page fault"}
101
+
102
+
103
+ @dataclass
104
+ class PageFault:
105
+ """One captured page fault from the kernel's live fault ring (gini_faultdump)."""
106
+ pid: int
107
+ scause: int
108
+ va: int
109
+ epc: int
110
+ kind: str = "" # filled by classify_faults: lazy / cow-write / stack / illegal
111
+
112
+ @property
113
+ def cause(self) -> str:
114
+ return SCAUSE_NAMES.get(self.scause, f"scause {self.scause}")
115
+
116
+
117
+ @dataclass
118
+ class ProcVm:
119
+ """One process's address space from the all-procs dump (gini_vmdump_all)."""
120
+ pid: int
121
+ name: str = ""
122
+ sz: int = 0 # p->sz — the *virtual* size (top of the heap/brk)
123
+ leaves: list = field(default_factory=list) # [Pte]
124
+
125
+ @property
126
+ def resident_pages(self) -> int:
127
+ """User pages actually backed by physical frames (excludes kernel trampoline/trapframe)."""
128
+ return sum(1 for p in self.leaves if p.user)
129
+
130
+ @property
131
+ def virtual_pages(self) -> int:
132
+ return max((self.sz + PGSIZE - 1) // PGSIZE, 0)
133
+
134
+
135
+ @dataclass
136
+ class VmSnapshot:
137
+ satp: int = 0
138
+ leaves: list = field(default_factory=list) # [Pte]
139
+ regions: list = field(default_factory=list) # [Region]
140
+ phys: PhysMem = field(default_factory=PhysMem)
141
+ faults: list = field(default_factory=list) # [Fault]
142
+ # Provenance (see FsSnapshot): "real" from the kernel vs "demo" stand-in; ok=False means a
143
+ # real read yielded nothing (show an error, don't fake). Page-table leaves + faults are
144
+ # dumped for real; the region map + physical allocator bar are not yet (so not in `have`).
145
+ source: str = "real"
146
+ ok: bool = True
147
+ have: tuple = ("pagetable", "faults")
148
+
149
+
150
+ # -- parser: xv6 vmprint() -------------------------------------------------- #
151
+ _ROOT = re.compile(r"page table\s+(0x[0-9a-fA-F]+)")
152
+ _LINE = re.compile(r"^((?:\.\.\s*)+)(\d+):\s*pte\s+(0x[0-9a-fA-F]+)\s+pa\s+(0x[0-9a-fA-F]+)")
153
+
154
+
155
+ def parse_vmprint(text: str) -> VmSnapshot:
156
+ """Parse xv6's `vmprint()` tree into leaf mappings. Level depth = number of `..` groups
157
+ (1,2,3 for Sv39 levels 2,1,0); a leaf is depth 3, and its VA is rebuilt from the index path."""
158
+ satp = 0
159
+ path: dict[int, int] = {}
160
+ leaves: list[Pte] = []
161
+ for line in (text or "").splitlines():
162
+ s = line.strip()
163
+ mr = _ROOT.search(s)
164
+ if mr and not leaves and satp == 0:
165
+ satp = int(mr.group(1), 16)
166
+ continue
167
+ m = _LINE.match(s)
168
+ if not m:
169
+ continue
170
+ depth = m.group(1).count("..")
171
+ idx = int(m.group(2))
172
+ pte = int(m.group(3), 16)
173
+ pa = int(m.group(4), 16)
174
+ path[depth] = idx
175
+ if depth == 3: # leaf (level 0)
176
+ va = (path.get(1, 0) << 30) | (path.get(2, 0) << 21) | (idx << 12)
177
+ leaves.append(Pte(va=va, pa=pa, perms=perms(pte), valid=bool(pte & 1)))
178
+ return VmSnapshot(satp=satp, leaves=leaves)
179
+
180
+
181
+ # -- parser: live fault ring (gini_faultdump) ------------------------------- #
182
+ # `FLT <pid> <scause> <va_hex> <epc_hex>` — one per captured page fault, oldest first.
183
+ _FLT = re.compile(r"^FLT\s+(-?\d+)\s+(\d+)\s+(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)")
184
+
185
+
186
+ def parse_faults(text: str) -> list:
187
+ out: list = []
188
+ for line in (text or "").splitlines():
189
+ m = _FLT.match(line.strip())
190
+ if m:
191
+ out.append(PageFault(pid=int(m.group(1)), scause=int(m.group(2)),
192
+ va=int(m.group(3), 16), epc=int(m.group(4), 16)))
193
+ return out
194
+
195
+
196
+ # -- parser: all-procs page tables (gini_vmdump_all) ------------------------ #
197
+ # `VP <pid> <name> <sz_hex>` header, then `VL <pid> <va_hex> <pa_hex> <flags_int>` per leaf.
198
+ _VP = re.compile(r"^VP\s+(\d+)\s+(\S+)\s+(0x[0-9a-fA-F]+)")
199
+ _VL = re.compile(r"^VL\s+(\d+)\s+(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+(\d+)")
200
+
201
+
202
+ def parse_vmall(text: str) -> dict:
203
+ """Parse every user process's leaf mappings into {pid: ProcVm}. Leaves carry the raw PTE
204
+ flag bits, so permissions AND the student's RSW/COW mark survive to the UI."""
205
+ procs: dict = {}
206
+ for line in (text or "").splitlines():
207
+ s = line.strip()
208
+ mp = _VP.match(s)
209
+ if mp:
210
+ pid = int(mp.group(1))
211
+ procs[pid] = ProcVm(pid=pid, name=mp.group(2), sz=int(mp.group(3), 16))
212
+ continue
213
+ ml = _VL.match(s)
214
+ if ml:
215
+ pid = int(ml.group(1))
216
+ flags = int(ml.group(4))
217
+ pte = Pte(va=int(ml.group(2), 16), pa=int(ml.group(3), 16),
218
+ perms=perms(flags), valid=bool(flags & 1), flags=flags)
219
+ procs.setdefault(pid, ProcVm(pid=pid)).leaves.append(pte)
220
+ return procs
221
+
222
+
223
+ def shared_frames(vmall: dict) -> dict:
224
+ """Physical pages mapped by MORE THAN ONE process — i.e. sharing, derived directly from the
225
+ page tables (no kernel refcount needed). Restricted to USER pages so the trampoline/trapframe
226
+ kernel mappings (shared by everyone by construction) don't drown out the COW signal.
227
+
228
+ Returns {pa: sorted [pids]} for every physically-shared user frame — this is what makes
229
+ 'parent and child share pages until one writes' visible: watch a pa's pid-list shrink from
230
+ [parent, child] to [parent] on the first write."""
231
+ owners: dict = {}
232
+ for pid, pv in vmall.items():
233
+ for pte in pv.leaves:
234
+ if pte.user and pte.valid:
235
+ owners.setdefault(pte.pa, set()).add(pid)
236
+ return {pa: sorted(pids) for pa, pids in owners.items() if len(pids) > 1}
237
+
238
+
239
+ def classify_faults(faults: list, vmall: dict | None = None,
240
+ regions: list | None = None) -> list:
241
+ """Best-effort label for each fault, using whatever address-space picture we have at read
242
+ time. Heuristic (the kernel stays dumb, the classification is improvable pure Python):
243
+
244
+ • store to a present, non-writable user page -> cow-write (the write that triggers a copy)
245
+ • fault on an unmapped VA below p->sz -> lazy-alloc (demand paging the heap)
246
+ • fault on an unmapped VA inside a valid region -> lazy-alloc / stack-growth
247
+ • otherwise -> illegal (out of any region -> segfault)
248
+ """
249
+ for f in faults:
250
+ pv = (vmall or {}).get(f.pid)
251
+ page_va = f.va & ~(PGSIZE - 1)
252
+ leaf = next((p for p in pv.leaves if p.va == page_va), None) if pv else None
253
+ if leaf is not None and not leaf.writable and f.scause == 15:
254
+ f.kind = "cow-write"
255
+ elif leaf is None and pv is not None and page_va < pv.sz:
256
+ f.kind = "lazy-alloc"
257
+ elif leaf is None and regions and region_for(f.va, regions) not in ("?",):
258
+ f.kind = "stack-growth" if region_for(f.va, regions) == "stack" else "lazy-alloc"
259
+ elif leaf is None:
260
+ f.kind = "illegal"
261
+ else:
262
+ f.kind = f.cause
263
+ return faults
264
+
265
+
266
+ def memory_summary(vm: VmSnapshot) -> str:
267
+ """Compact, LLM-facing summary of the virtual-memory state (for the Ask GINI card, L2)."""
268
+ if vm is None:
269
+ return ""
270
+ lines = [f"memory (virtual, Sv39): satp={hex(vm.satp)} · {len(vm.leaves)} leaf mappings"]
271
+ if vm.regions:
272
+ lines.append(" regions (low→high VA): "
273
+ + ", ".join(f"{r.name}[{r.perms}]" for r in vm.regions))
274
+ ph = vm.phys
275
+ if ph and ph.total_pages:
276
+ lines.append(f" physical pages: {ph.used_pages} used / {ph.free_pages} free of "
277
+ f"{ph.total_pages} ({ph.used_frac * 100:.0f}% used)")
278
+ if vm.faults:
279
+ lines.append(" recent page faults: "
280
+ + "; ".join(f"{hex(f.va)} {f.cause}" for f in vm.faults[-3:]))
281
+ return "\n".join(lines)
282
+
283
+
284
+ def region_for(va: int, regions) -> str:
285
+ for r in regions:
286
+ if r.start <= va <= r.end: # end is the region's last byte (inclusive)
287
+ return r.name
288
+ if va == TRAMPOLINE:
289
+ return "trampoline"
290
+ if va == TRAPFRAME:
291
+ return "trapframe"
292
+ return "?"
293
+
294
+
295
+ # -- offline demo ----------------------------------------------------------- #
296
+ class DemoVm:
297
+ """A deterministic small user address space + simulated growth/faults, so the Memory face is
298
+ explorable and testable without QEMU. Faithful-shaped, not a real MMU."""
299
+
300
+ def __init__(self) -> None:
301
+ self.satp = 0x8000000000087f6e
302
+ self._sp = 0x4000 # top of a one-page user stack region
303
+ self.phys = PhysMem(total_pages=32768, free_pages=32510)
304
+ self._fault_log: list = [] # simulated demand-fault log (NOT the faults() ring)
305
+ self._leaves = [
306
+ Pte(0x0000, 0x87f6b000, "r-x-"), # text
307
+ Pte(0x1000, 0x87f6a000, "rw--"), # data
308
+ Pte(0x2000, 0x87f69000, "rw--"), # bss/heap
309
+ Pte(0x3000, 0x87f68000, "----"), # guard page (no access)
310
+ Pte(0x4000, 0x87f67000, "rw-u"), # user stack
311
+ Pte(TRAPFRAME, 0x87f66000, "rw--"), # trapframe
312
+ Pte(TRAMPOLINE, 0x87f6f000, "r-x-"), # trampoline (shared)
313
+ ]
314
+
315
+ def _regions(self):
316
+ return [
317
+ Region("text", 0x0000, 0x0FFF, "r-x-"),
318
+ Region("data", 0x1000, 0x1FFF, "rw--"),
319
+ Region("heap", 0x2000, 0x2FFF, "rw--"),
320
+ Region("guard", 0x3000, 0x3FFF, "----"),
321
+ Region("stack", 0x4000, self._sp - 1, "rw-u") if self._sp > 0x4000
322
+ else Region("stack", 0x4000, 0x4FFF, "rw-u"),
323
+ Region("trapframe", TRAPFRAME, TRAPFRAME, "rw--"),
324
+ Region("trampoline", TRAMPOLINE, TRAMPOLINE, "r-x-"),
325
+ ]
326
+
327
+ def snapshot(self) -> VmSnapshot:
328
+ return VmSnapshot(satp=self.satp, leaves=list(self._leaves), regions=self._regions(),
329
+ phys=self.phys, faults=list(self._fault_log), source="demo",
330
+ have=("pagetable", "regions", "phys", "faults"))
331
+
332
+ def simulate_fault(self) -> VmSnapshot:
333
+ """Grow the stack by one page on a fault (lazy/demand allocation): record the fault,
334
+ allocate a physical page, and add the new leaf mapping."""
335
+ new_va = 0x4000 + len(self._fault_log) * PGSIZE + PGSIZE
336
+ self._fault_log.append(Fault(new_va, "store page fault (stack growth)", pid=3))
337
+ if self.phys.free_pages > 0:
338
+ self.phys.free_pages -= 1
339
+ self._sp = new_va + PGSIZE
340
+ self._leaves.insert(5, Pte(new_va, 0x87f60000 - len(self._fault_log) * PGSIZE, "rw-u"))
341
+ return self.snapshot()
342
+
343
+ # -- COW / all-procs demo (offline) ------------------------------------- #
344
+ _COW = 1 | 2 | 16 | 0x100 # V R U + RSW8 : present, read-only, user, COW-tagged
345
+ _RW = 1 | 2 | 4 | 16 # V R W U : present, writable, user (private after copy)
346
+ _RX = 1 | 2 | 8 | 16 # V R X U : text
347
+
348
+ def __post_cow(self):
349
+ if not hasattr(self, "_cow_written"):
350
+ self._cow_written = False
351
+
352
+ def all_procs(self) -> dict:
353
+ """A parent+child pair right after fork(): they SHARE the data/stack physical pages,
354
+ write-protected and COW-tagged, until `simulate_cow_write()` copies the child's data page.
355
+ This is the observable heart of the COW assignment."""
356
+ self.__post_cow()
357
+ text = Pte(0x0000, 0x87001000, perms(self._RX), True, self._RX)
358
+ # shared, COW-marked pages (same PA in both procs)
359
+ data_shared = Pte(0x1000, 0x87002000, perms(self._COW), True, self._COW)
360
+ stack_shared = Pte(0x3000, 0x87003000, perms(self._COW), True, self._COW)
361
+ parent = ProcVm(3, "forktest", 0x6000, [text, data_shared, stack_shared,
362
+ Pte(TRAPFRAME, 0x87f10000, perms(self._RW & ~16), True, self._RW & ~16)])
363
+ child_data = (Pte(0x1000, 0x87005000, perms(self._RW), True, self._RW) # copied -> private
364
+ if self._cow_written else data_shared)
365
+ child = ProcVm(4, "forktest", 0x6000, [text, child_data, stack_shared,
366
+ Pte(TRAPFRAME, 0x87f11000, perms(self._RW & ~16), True, self._RW & ~16)])
367
+ return {3: parent, 4: child}
368
+
369
+ def simulate_cow_write(self) -> dict:
370
+ """The child writes its shared data page -> the store page-fault copies it: a fresh
371
+ physical frame, now writable and no longer COW-tagged. The shared PA's owner list shrinks
372
+ from [parent, child] to [parent]."""
373
+ self.__post_cow()
374
+ self._cow_written = True
375
+ if self.phys.free_pages > 0:
376
+ self.phys.free_pages -= 1
377
+ return self.all_procs()
378
+
379
+ def faults(self) -> list:
380
+ """A representative fault stream: a COW write, a lazy heap fault, and an illegal access."""
381
+ return [
382
+ PageFault(4, 15, 0x1000, 0x0000000000001abc), # store to the shared data page -> cow
383
+ PageFault(3, 15, 0x5000, 0x0000000000001de0), # unmapped, below sz -> lazy-alloc
384
+ PageFault(3, 13, 0x40000000, 0x0000000000002100), # way out of range -> illegal
385
+ ]
gini/gloader.py ADDED
@@ -0,0 +1,17 @@
1
+ """gLoader -- the GINI topology loader (top-level entry point).
2
+
3
+ The implementation lives in :mod:`gini.services.gloader`; this module exposes it as a
4
+ headline GINI component (alongside gBuilder and gRouter) and provides the CLI::
5
+
6
+ python -m gini.gloader topology.gini # compile the spec + launch on Docker
7
+ python -m gini.gloader topology.gini --sim # run the in-process simulator
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from .services.gloader import GLoader, main
12
+
13
+ __all__ = ["GLoader", "main"]
14
+
15
+
16
+ if __name__ == "__main__":
17
+ raise SystemExit(main())
@@ -0,0 +1,18 @@
1
+ """GINI portable runtime — user-space data plane (Ethernet-in-UDP).
2
+
3
+ Productized from the proven R0 spike: machines (TAP+shuttle) and a fabric of
4
+ user-space switches and gRouters, wired by UDP. The compiler lowers a Topology
5
+ onto these; the orchestrator launches them (in-process for tests, Docker for real).
6
+ """
7
+ from .frame import (
8
+ BROADCAST, ETH_ARP, ETH_IP, PROTO_ICMP, build_eth, parse_eth,
9
+ )
10
+ from .grouter import Router
11
+ from .hostsim import HostSim
12
+ from .switch import Hub, LearningSwitch, make_switch
13
+ from .transport import Port, run_loop
14
+
15
+ __all__ = [
16
+ "Router", "HostSim", "LearningSwitch", "Hub", "make_switch", "Port", "run_loop",
17
+ "BROADCAST", "ETH_ARP", "ETH_IP", "PROTO_ICMP", "build_eth", "parse_eth",
18
+ ]