QuizGenerator 0.4.2__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.
- QuizGenerator/README.md +5 -0
- QuizGenerator/__init__.py +27 -0
- QuizGenerator/__main__.py +7 -0
- QuizGenerator/canvas/__init__.py +13 -0
- QuizGenerator/canvas/canvas_interface.py +627 -0
- QuizGenerator/canvas/classes.py +235 -0
- QuizGenerator/constants.py +149 -0
- QuizGenerator/contentast.py +1955 -0
- QuizGenerator/generate.py +253 -0
- QuizGenerator/logging.yaml +55 -0
- QuizGenerator/misc.py +579 -0
- QuizGenerator/mixins.py +548 -0
- QuizGenerator/performance.py +202 -0
- QuizGenerator/premade_questions/__init__.py +0 -0
- QuizGenerator/premade_questions/basic.py +103 -0
- QuizGenerator/premade_questions/cst334/__init__.py +1 -0
- QuizGenerator/premade_questions/cst334/languages.py +391 -0
- QuizGenerator/premade_questions/cst334/math_questions.py +297 -0
- QuizGenerator/premade_questions/cst334/memory_questions.py +1400 -0
- QuizGenerator/premade_questions/cst334/ostep13_vsfs.py +572 -0
- QuizGenerator/premade_questions/cst334/persistence_questions.py +451 -0
- QuizGenerator/premade_questions/cst334/process.py +648 -0
- QuizGenerator/premade_questions/cst463/__init__.py +0 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/__init__.py +3 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +369 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +305 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +650 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +73 -0
- QuizGenerator/premade_questions/cst463/math_and_data/__init__.py +2 -0
- QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +631 -0
- QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +534 -0
- QuizGenerator/premade_questions/cst463/models/__init__.py +0 -0
- QuizGenerator/premade_questions/cst463/models/attention.py +192 -0
- QuizGenerator/premade_questions/cst463/models/cnns.py +186 -0
- QuizGenerator/premade_questions/cst463/models/matrices.py +24 -0
- QuizGenerator/premade_questions/cst463/models/rnns.py +202 -0
- QuizGenerator/premade_questions/cst463/models/text.py +203 -0
- QuizGenerator/premade_questions/cst463/models/weight_counting.py +227 -0
- QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py +6 -0
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +1314 -0
- QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py +6 -0
- QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +936 -0
- QuizGenerator/qrcode_generator.py +293 -0
- QuizGenerator/question.py +715 -0
- QuizGenerator/quiz.py +467 -0
- QuizGenerator/regenerate.py +472 -0
- QuizGenerator/typst_utils.py +113 -0
- quizgenerator-0.4.2.dist-info/METADATA +265 -0
- quizgenerator-0.4.2.dist-info/RECORD +52 -0
- quizgenerator-0.4.2.dist-info/WHEEL +4 -0
- quizgenerator-0.4.2.dist-info/entry_points.txt +3 -0
- quizgenerator-0.4.2.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,1400 @@
|
|
|
1
|
+
#!env python
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import abc
|
|
5
|
+
import collections
|
|
6
|
+
import copy
|
|
7
|
+
import enum
|
|
8
|
+
import logging
|
|
9
|
+
import math
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
|
|
12
|
+
from QuizGenerator.contentast import ContentAST
|
|
13
|
+
from QuizGenerator.question import Question, Answer, QuestionRegistry, RegenerableChoiceMixin
|
|
14
|
+
from QuizGenerator.mixins import TableQuestionMixin, BodyTemplatesMixin
|
|
15
|
+
|
|
16
|
+
log = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MemoryQuestion(Question, abc.ABC):
|
|
20
|
+
def __init__(self, *args, **kwargs):
|
|
21
|
+
kwargs["topic"] = kwargs.get("topic", Question.Topic.MEMORY)
|
|
22
|
+
super().__init__(*args, **kwargs)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@QuestionRegistry.register("VirtualAddressParts")
|
|
26
|
+
class VirtualAddressParts(MemoryQuestion, TableQuestionMixin):
|
|
27
|
+
MAX_BITS = 64
|
|
28
|
+
|
|
29
|
+
class Target(enum.Enum):
|
|
30
|
+
VA_BITS = "# VA Bits"
|
|
31
|
+
VPN_BITS = "# VPN Bits"
|
|
32
|
+
OFFSET_BITS = "# Offset Bits"
|
|
33
|
+
|
|
34
|
+
def refresh(self, rng_seed=None, *args, **kwargs):
|
|
35
|
+
super().refresh(rng_seed=rng_seed, *args, **kwargs)
|
|
36
|
+
|
|
37
|
+
# Generate baselines, if not given
|
|
38
|
+
self.num_bits_va = kwargs.get("num_bits_va", self.rng.randint(2, self.MAX_BITS))
|
|
39
|
+
self.num_bits_offset = self.rng.randint(1, self.num_bits_va - 1)
|
|
40
|
+
self.num_bits_vpn = self.num_bits_va - self.num_bits_offset
|
|
41
|
+
|
|
42
|
+
self.possible_answers = {
|
|
43
|
+
self.Target.VA_BITS: Answer.integer("answer__num_bits_va", self.num_bits_va),
|
|
44
|
+
self.Target.OFFSET_BITS: Answer.integer("answer__num_bits_offset", self.num_bits_offset),
|
|
45
|
+
self.Target.VPN_BITS: Answer.integer("answer__num_bits_vpn", self.num_bits_vpn)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Select what kind of question we are going to be
|
|
49
|
+
self.blank_kind = self.rng.choice(list(self.Target))
|
|
50
|
+
|
|
51
|
+
self.answers['answer'] = self.possible_answers[self.blank_kind]
|
|
52
|
+
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
def get_body(self, **kwargs) -> ContentAST.Section:
|
|
56
|
+
# Create table data with one blank cell
|
|
57
|
+
table_data = [{}]
|
|
58
|
+
for target in list(self.Target):
|
|
59
|
+
if target == self.blank_kind:
|
|
60
|
+
# This cell should be an answer blank
|
|
61
|
+
table_data[0][target.value] = ContentAST.Answer(self.possible_answers[target], " bits")
|
|
62
|
+
else:
|
|
63
|
+
# This cell shows the value
|
|
64
|
+
table_data[0][target.value] = f"{self.possible_answers[target].display} bits"
|
|
65
|
+
|
|
66
|
+
table = self.create_fill_in_table(
|
|
67
|
+
headers=[t.value for t in list(self.Target)],
|
|
68
|
+
template_rows=table_data
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
body = ContentAST.Section()
|
|
72
|
+
body.add_element(
|
|
73
|
+
ContentAST.Paragraph(
|
|
74
|
+
[
|
|
75
|
+
"Given the information in the below table, please complete the table as appropriate."
|
|
76
|
+
]
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
body.add_element(table)
|
|
80
|
+
return body
|
|
81
|
+
|
|
82
|
+
def get_explanation(self, **kwargs) -> ContentAST.Section:
|
|
83
|
+
explanation = ContentAST.Section()
|
|
84
|
+
|
|
85
|
+
explanation.add_element(
|
|
86
|
+
ContentAST.Paragraph(
|
|
87
|
+
[
|
|
88
|
+
"Remember, when we are calculating the size of virtual address spaces, "
|
|
89
|
+
"the number of bits in the overall address space is equal to the number of bits in the VPN "
|
|
90
|
+
"plus the number of bits for the offset.",
|
|
91
|
+
"We don't waste any bits!"
|
|
92
|
+
]
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
explanation.add_element(
|
|
97
|
+
ContentAST.Paragraph(
|
|
98
|
+
[
|
|
99
|
+
ContentAST.Text(f"{self.num_bits_va}", emphasis=(self.blank_kind == self.Target.VA_BITS)),
|
|
100
|
+
ContentAST.Text(" = "),
|
|
101
|
+
ContentAST.Text(f"{self.num_bits_vpn}", emphasis=(self.blank_kind == self.Target.VPN_BITS)),
|
|
102
|
+
ContentAST.Text(" + "),
|
|
103
|
+
ContentAST.Text(f"{self.num_bits_offset}", emphasis=(self.blank_kind == self.Target.OFFSET_BITS))
|
|
104
|
+
]
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return explanation
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@QuestionRegistry.register()
|
|
112
|
+
class CachingQuestion(MemoryQuestion, RegenerableChoiceMixin, TableQuestionMixin, BodyTemplatesMixin):
|
|
113
|
+
class Kind(enum.Enum):
|
|
114
|
+
FIFO = enum.auto()
|
|
115
|
+
LRU = enum.auto()
|
|
116
|
+
BELADY = enum.auto()
|
|
117
|
+
|
|
118
|
+
def __str__(self):
|
|
119
|
+
return self.name
|
|
120
|
+
|
|
121
|
+
class Cache:
|
|
122
|
+
def __init__(self, kind: CachingQuestion.Kind, cache_size: int, all_requests: List[int] = None):
|
|
123
|
+
self.kind = kind
|
|
124
|
+
self.cache_size = cache_size
|
|
125
|
+
self.all_requests = all_requests
|
|
126
|
+
|
|
127
|
+
self.cache_state = []
|
|
128
|
+
self.last_used = collections.defaultdict(lambda: -math.inf)
|
|
129
|
+
self.frequency = collections.defaultdict(lambda: 0)
|
|
130
|
+
|
|
131
|
+
def query_cache(self, request, request_number):
|
|
132
|
+
was_hit = request in self.cache_state
|
|
133
|
+
|
|
134
|
+
evicted = None
|
|
135
|
+
if was_hit:
|
|
136
|
+
# hit!
|
|
137
|
+
pass
|
|
138
|
+
else:
|
|
139
|
+
# miss!
|
|
140
|
+
if len(self.cache_state) == self.cache_size:
|
|
141
|
+
# Then we are full and need to evict
|
|
142
|
+
evicted = self.cache_state[0]
|
|
143
|
+
self.cache_state = self.cache_state[1:]
|
|
144
|
+
|
|
145
|
+
# Add to cache
|
|
146
|
+
self.cache_state.append(request)
|
|
147
|
+
|
|
148
|
+
# update state variable
|
|
149
|
+
self.last_used[request] = request_number
|
|
150
|
+
self.frequency[request] += 1
|
|
151
|
+
|
|
152
|
+
# update cache state
|
|
153
|
+
if self.kind == CachingQuestion.Kind.FIFO:
|
|
154
|
+
pass
|
|
155
|
+
elif self.kind == CachingQuestion.Kind.LRU:
|
|
156
|
+
self.cache_state = sorted(
|
|
157
|
+
self.cache_state,
|
|
158
|
+
key=(lambda e: self.last_used[e]),
|
|
159
|
+
reverse=False
|
|
160
|
+
)
|
|
161
|
+
# elif self.kind == CachingQuestion.Kind.LFU:
|
|
162
|
+
# self.cache_state = sorted(
|
|
163
|
+
# self.cache_state,
|
|
164
|
+
# key=(lambda e: (self.frequency[e], e)),
|
|
165
|
+
# reverse=False
|
|
166
|
+
# )
|
|
167
|
+
elif self.kind == CachingQuestion.Kind.BELADY:
|
|
168
|
+
upcoming_requests = self.all_requests[request_number + 1:]
|
|
169
|
+
self.cache_state = sorted(
|
|
170
|
+
self.cache_state,
|
|
171
|
+
# key=(lambda e: (upcoming_requests.index(e), e) if e in upcoming_requests else (-math.inf, e)),
|
|
172
|
+
key=(lambda e: (upcoming_requests.index(e), -e) if e in upcoming_requests else (math.inf, -e)),
|
|
173
|
+
reverse=True
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return (was_hit, evicted, self.cache_state)
|
|
177
|
+
|
|
178
|
+
def __init__(self, *args, **kwargs):
|
|
179
|
+
# Store parameters in kwargs for config_params BEFORE calling super().__init__()
|
|
180
|
+
kwargs['num_elements'] = kwargs.get("num_elements", 5)
|
|
181
|
+
kwargs['cache_size'] = kwargs.get("cache_size", 3)
|
|
182
|
+
kwargs['num_requests'] = kwargs.get("num_requests", 10)
|
|
183
|
+
|
|
184
|
+
# Register the regenerable choice using the mixin
|
|
185
|
+
policy_str = (kwargs.get("policy") or kwargs.get("algo"))
|
|
186
|
+
self.register_choice('policy', self.Kind, policy_str, kwargs)
|
|
187
|
+
|
|
188
|
+
super().__init__(*args, **kwargs)
|
|
189
|
+
|
|
190
|
+
self.num_elements = self.config_params.get("num_elements", 5)
|
|
191
|
+
self.cache_size = self.config_params.get("cache_size", 3)
|
|
192
|
+
self.num_requests = self.config_params.get("num_requests", 10)
|
|
193
|
+
|
|
194
|
+
self.hit_rate = 0. # placeholder
|
|
195
|
+
|
|
196
|
+
def refresh(self, previous: Optional[CachingQuestion] = None, *args, hard_refresh: bool = False, **kwargs):
|
|
197
|
+
# Call parent refresh which seeds RNG and calls is_interesting()
|
|
198
|
+
# Note: We ignore the parent's return value since we need to generate the workload first
|
|
199
|
+
super().refresh(*args, **kwargs)
|
|
200
|
+
|
|
201
|
+
# Use the mixin to get the cache policy (randomly selected or fixed)
|
|
202
|
+
self.cache_policy = self.get_choice('policy', self.Kind)
|
|
203
|
+
|
|
204
|
+
self.requests = (
|
|
205
|
+
list(range(self.cache_size)) # Prime the cache with the capacity misses
|
|
206
|
+
+ self.rng.choices(
|
|
207
|
+
population=list(range(self.cache_size - 1)), k=1
|
|
208
|
+
) # Add in one request to an earlier that will differentiate clearly between FIFO and LRU
|
|
209
|
+
+ self.rng.choices(
|
|
210
|
+
population=list(range(self.cache_size, self.num_elements)), k=1
|
|
211
|
+
) ## Add in the rest of the requests
|
|
212
|
+
+ self.rng.choices(population=list(range(self.num_elements)), k=(self.num_requests - 2))
|
|
213
|
+
## Add in the rest of the requests
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
self.cache = CachingQuestion.Cache(self.cache_policy, self.cache_size, self.requests)
|
|
217
|
+
|
|
218
|
+
self.request_results = {}
|
|
219
|
+
number_of_hits = 0
|
|
220
|
+
for (request_number, request) in enumerate(self.requests):
|
|
221
|
+
was_hit, evicted, cache_state = self.cache.query_cache(request, request_number)
|
|
222
|
+
if was_hit:
|
|
223
|
+
number_of_hits += 1
|
|
224
|
+
self.request_results[request_number] = {
|
|
225
|
+
"request": (f"[answer__request]", request),
|
|
226
|
+
"hit": (f"[answer__hit-{request_number}]", ('hit' if was_hit else 'miss')),
|
|
227
|
+
"evicted": (f"[answer__evicted-{request_number}]", ('-' if evicted is None else f"{evicted}")),
|
|
228
|
+
"cache_state": (f"[answer__cache_state-{request_number}]", ','.join(map(str, cache_state)))
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
self.answers.update(
|
|
232
|
+
{
|
|
233
|
+
f"answer__hit-{request_number}": Answer.string(
|
|
234
|
+
f"answer__hit-{request_number}", ('hit' if was_hit else 'miss')
|
|
235
|
+
),
|
|
236
|
+
f"answer__evicted-{request_number}": Answer.string(
|
|
237
|
+
f"answer__evicted-{request_number}", ('-' if evicted is None else f"{evicted}")
|
|
238
|
+
),
|
|
239
|
+
f"answer__cache_state-{request_number}": Answer.list_value(
|
|
240
|
+
f"answer__cache_state-{request_number}", copy.copy(cache_state)
|
|
241
|
+
),
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
self.hit_rate = 100 * number_of_hits / (self.num_requests)
|
|
246
|
+
self.answers.update(
|
|
247
|
+
{
|
|
248
|
+
"answer__hit_rate": Answer.auto_float("answer__hit_rate", self.hit_rate)
|
|
249
|
+
}
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Return whether this workload is interesting
|
|
253
|
+
return self.is_interesting()
|
|
254
|
+
|
|
255
|
+
def get_body(self, **kwargs) -> ContentAST.Section:
|
|
256
|
+
# Create table data for cache simulation
|
|
257
|
+
table_rows = []
|
|
258
|
+
for request_number in sorted(self.request_results.keys()):
|
|
259
|
+
table_rows.append(
|
|
260
|
+
{
|
|
261
|
+
"Page Requested": f"{self.requests[request_number]}",
|
|
262
|
+
"Hit/Miss": f"answer__hit-{request_number}", # Answer key
|
|
263
|
+
"Evicted": f"answer__evicted-{request_number}", # Answer key
|
|
264
|
+
"Cache State": f"answer__cache_state-{request_number}" # Answer key
|
|
265
|
+
}
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Create table using mixin - automatically handles answer conversion
|
|
269
|
+
cache_table = self.create_answer_table(
|
|
270
|
+
headers=["Page Requested", "Hit/Miss", "Evicted", "Cache State"],
|
|
271
|
+
data_rows=table_rows,
|
|
272
|
+
answer_columns=["Hit/Miss", "Evicted", "Cache State"]
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# Create hit rate answer block
|
|
276
|
+
hit_rate_block = ContentAST.AnswerBlock(
|
|
277
|
+
ContentAST.Answer(
|
|
278
|
+
answer=self.answers["answer__hit_rate"],
|
|
279
|
+
label=f"Hit rate, excluding capacity misses. If appropriate, round to {Answer.DEFAULT_ROUNDING_DIGITS} decimal digits.",
|
|
280
|
+
unit="%"
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# Use mixin to create complete body
|
|
285
|
+
intro_text = (
|
|
286
|
+
f"Assume we are using a <b>{self.cache_policy}</b> caching policy and a cache size of <b>{self.cache_size}</b>. "
|
|
287
|
+
"Given the below series of requests please fill in the table. "
|
|
288
|
+
"For the hit/miss column, please write either \"hit\" or \"miss\". "
|
|
289
|
+
"For the eviction column, please write either the number of the evicted page or simply a dash (e.g. \"-\")."
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
instructions = ContentAST.OnlyHtml([
|
|
293
|
+
"For the cache state, please enter the cache contents in the order suggested in class, "
|
|
294
|
+
"which means separated by commas with spaces (e.g. \"1, 2, 3\") "
|
|
295
|
+
"and with the left-most being the next to be evicted. "
|
|
296
|
+
"In the case where there is a tie, order by increasing number."
|
|
297
|
+
])
|
|
298
|
+
|
|
299
|
+
body = self.create_fill_in_table_body(intro_text, instructions, cache_table)
|
|
300
|
+
body.add_element(hit_rate_block)
|
|
301
|
+
return body
|
|
302
|
+
|
|
303
|
+
def get_explanation(self, **kwargs) -> ContentAST.Section:
|
|
304
|
+
explanation = ContentAST.Section()
|
|
305
|
+
|
|
306
|
+
explanation.add_element(ContentAST.Paragraph(["The full caching table can be seen below."]))
|
|
307
|
+
|
|
308
|
+
explanation.add_element(
|
|
309
|
+
ContentAST.Table(
|
|
310
|
+
headers=["Page", "Hit/Miss", "Evicted", "Cache State"],
|
|
311
|
+
data=[
|
|
312
|
+
[
|
|
313
|
+
self.request_results[request]["request"][1],
|
|
314
|
+
self.request_results[request]["hit"][1],
|
|
315
|
+
f'{self.request_results[request]["evicted"][1]}',
|
|
316
|
+
f'{self.request_results[request]["cache_state"][1]}',
|
|
317
|
+
]
|
|
318
|
+
for (request_number, request) in enumerate(sorted(self.request_results.keys()))
|
|
319
|
+
]
|
|
320
|
+
)
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
explanation.add_element(
|
|
324
|
+
ContentAST.Paragraph(
|
|
325
|
+
[
|
|
326
|
+
"To calculate the hit rate we calculate the percentage of requests "
|
|
327
|
+
"that were cache hits out of the total number of requests. "
|
|
328
|
+
f"In this case we are counting only all but {self.cache_size} requests, "
|
|
329
|
+
f"since we are excluding capacity misses."
|
|
330
|
+
]
|
|
331
|
+
)
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
return explanation
|
|
335
|
+
|
|
336
|
+
def is_interesting(self) -> bool:
|
|
337
|
+
# todo: interesting is more likely based on whether I can differentiate between it and another algo,
|
|
338
|
+
# so maybe rerun with a different approach but same requests?
|
|
339
|
+
return (self.hit_rate / 100.0) < 0.7
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class MemoryAccessQuestion(MemoryQuestion, abc.ABC):
|
|
343
|
+
PROBABILITY_OF_VALID = .875
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@QuestionRegistry.register()
|
|
347
|
+
class BaseAndBounds(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
|
|
348
|
+
MAX_BITS = 32
|
|
349
|
+
MIN_BOUNDS_BIT = 5
|
|
350
|
+
MAX_BOUNDS_BITS = 16
|
|
351
|
+
|
|
352
|
+
def refresh(self, rng_seed=None, *args, **kwargs):
|
|
353
|
+
super().refresh(rng_seed=rng_seed, *args, **kwargs)
|
|
354
|
+
|
|
355
|
+
max_bound_bits = kwargs.get("max_bound_bits")
|
|
356
|
+
|
|
357
|
+
bounds_bits = self.rng.randint(
|
|
358
|
+
self.MIN_BOUNDS_BIT,
|
|
359
|
+
self.MAX_BOUNDS_BITS
|
|
360
|
+
)
|
|
361
|
+
base_bits = self.MAX_BITS - bounds_bits
|
|
362
|
+
|
|
363
|
+
self.bounds = int(math.pow(2, bounds_bits))
|
|
364
|
+
self.base = self.rng.randint(1, int(math.pow(2, base_bits))) * self.bounds
|
|
365
|
+
self.virtual_address = self.rng.randint(1, int(self.bounds / self.PROBABILITY_OF_VALID))
|
|
366
|
+
|
|
367
|
+
if self.virtual_address < self.bounds:
|
|
368
|
+
self.answers["answer"] = Answer.binary_hex(
|
|
369
|
+
"answer__physical_address",
|
|
370
|
+
self.base + self.virtual_address,
|
|
371
|
+
length=math.ceil(math.log2(self.base + self.virtual_address))
|
|
372
|
+
)
|
|
373
|
+
else:
|
|
374
|
+
self.answers["answer"] = Answer.string("answer__physical_address", "INVALID")
|
|
375
|
+
|
|
376
|
+
def get_body(self) -> ContentAST.Section:
|
|
377
|
+
# Use mixin to create parameter table with answer
|
|
378
|
+
parameter_info = {
|
|
379
|
+
"Base": f"0x{self.base:X}",
|
|
380
|
+
"Bounds": f"0x{self.bounds:X}",
|
|
381
|
+
"Virtual Address": f"0x{self.virtual_address:X}"
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
table = self.create_parameter_answer_table(
|
|
385
|
+
parameter_info=parameter_info,
|
|
386
|
+
answer_label="Physical Address",
|
|
387
|
+
answer_key="answer",
|
|
388
|
+
transpose=True
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
return self.create_parameter_calculation_body(
|
|
392
|
+
intro_text=(
|
|
393
|
+
"Given the information in the below table, "
|
|
394
|
+
"please calcuate the physical address associated with the given virtual address. "
|
|
395
|
+
"If the virtual address is invalid please simply write ***INVALID***."
|
|
396
|
+
),
|
|
397
|
+
parameter_table=table
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
def get_explanation(self) -> ContentAST.Section:
|
|
401
|
+
explanation = ContentAST.Section()
|
|
402
|
+
|
|
403
|
+
explanation.add_element(
|
|
404
|
+
ContentAST.Paragraph(
|
|
405
|
+
[
|
|
406
|
+
"There's two steps to figuring out base and bounds.",
|
|
407
|
+
"1. Are we within the bounds?\n",
|
|
408
|
+
"2. If so, add to our base.\n",
|
|
409
|
+
"",
|
|
410
|
+
]
|
|
411
|
+
)
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
explanation.add_element(
|
|
415
|
+
ContentAST.Paragraph(
|
|
416
|
+
[
|
|
417
|
+
f"Step 1: 0x{self.virtual_address:X} < 0x{self.bounds:X} "
|
|
418
|
+
f"--> {'***VALID***' if (self.virtual_address < self.bounds) else 'INVALID'}"
|
|
419
|
+
]
|
|
420
|
+
)
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
if self.virtual_address < self.bounds:
|
|
424
|
+
explanation.add_element(
|
|
425
|
+
ContentAST.Paragraph(
|
|
426
|
+
[
|
|
427
|
+
f"Step 2: Since the previous check passed, we calculate "
|
|
428
|
+
f"0x{self.base:X} + 0x{self.virtual_address:X} "
|
|
429
|
+
f"= ***0x{self.base + self.virtual_address:X}***.",
|
|
430
|
+
"If it had been invalid we would have simply written INVALID"
|
|
431
|
+
]
|
|
432
|
+
)
|
|
433
|
+
)
|
|
434
|
+
else:
|
|
435
|
+
explanation.add_element(
|
|
436
|
+
ContentAST.Paragraph(
|
|
437
|
+
[
|
|
438
|
+
f"Step 2: Since the previous check failed, we simply write ***INVALID***.",
|
|
439
|
+
"***If*** it had been valid, we would have calculated "
|
|
440
|
+
f"0x{self.base:X} + 0x{self.virtual_address:X} "
|
|
441
|
+
f"= 0x{self.base + self.virtual_address:X}.",
|
|
442
|
+
]
|
|
443
|
+
)
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
return explanation
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
@QuestionRegistry.register()
|
|
450
|
+
class Segmentation(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
|
|
451
|
+
MAX_BITS = 20
|
|
452
|
+
MIN_VIRTUAL_BITS = 5
|
|
453
|
+
MAX_VIRTUAL_BITS = 10
|
|
454
|
+
|
|
455
|
+
def __within_bounds(self, segment, offset, bounds):
|
|
456
|
+
if segment == "unallocated":
|
|
457
|
+
return False
|
|
458
|
+
elif bounds < offset:
|
|
459
|
+
return False
|
|
460
|
+
else:
|
|
461
|
+
return True
|
|
462
|
+
|
|
463
|
+
def refresh(self, *args, **kwargs):
|
|
464
|
+
super().refresh(*args, **kwargs)
|
|
465
|
+
|
|
466
|
+
# Pick how big each of our address spaces will be
|
|
467
|
+
self.virtual_bits = self.rng.randint(self.MIN_VIRTUAL_BITS, self.MAX_VIRTUAL_BITS)
|
|
468
|
+
self.physical_bits = self.rng.randint(self.virtual_bits + 1, self.MAX_BITS)
|
|
469
|
+
|
|
470
|
+
# Start with blank base and bounds
|
|
471
|
+
self.base = {
|
|
472
|
+
"code": 0,
|
|
473
|
+
"heap": 0,
|
|
474
|
+
"stack": 0,
|
|
475
|
+
}
|
|
476
|
+
self.bounds = {
|
|
477
|
+
"code": 0,
|
|
478
|
+
"heap": 0,
|
|
479
|
+
"stack": 0,
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
min_bounds = 4
|
|
483
|
+
max_bounds = int(2 ** (self.virtual_bits - 2))
|
|
484
|
+
|
|
485
|
+
def segment_collision(base, bounds):
|
|
486
|
+
# lol, I think this is probably silly, but should work
|
|
487
|
+
return 0 != len(
|
|
488
|
+
set.intersection(
|
|
489
|
+
*[
|
|
490
|
+
set(range(base[segment], base[segment] + bounds[segment] + 1))
|
|
491
|
+
for segment in base.keys()
|
|
492
|
+
]
|
|
493
|
+
)
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
self.base["unallocated"] = 0
|
|
497
|
+
self.bounds["unallocated"] = 0
|
|
498
|
+
|
|
499
|
+
# Make random placements and check to make sure they are not overlapping
|
|
500
|
+
while (segment_collision(self.base, self.bounds)):
|
|
501
|
+
for segment in self.base.keys():
|
|
502
|
+
self.bounds[segment] = self.rng.randint(min_bounds, max_bounds - 1)
|
|
503
|
+
self.base[segment] = self.rng.randint(0, (2 ** self.physical_bits - self.bounds[segment]))
|
|
504
|
+
|
|
505
|
+
# Pick a random segment for us to use
|
|
506
|
+
self.segment = self.rng.choice(list(self.base.keys()))
|
|
507
|
+
self.segment_bits = {
|
|
508
|
+
"code": 0,
|
|
509
|
+
"heap": 1,
|
|
510
|
+
"unallocated": 2,
|
|
511
|
+
"stack": 3
|
|
512
|
+
}[self.segment]
|
|
513
|
+
|
|
514
|
+
# Try to pick a random address within that range
|
|
515
|
+
try:
|
|
516
|
+
self.offset = self.rng.randint(
|
|
517
|
+
1,
|
|
518
|
+
min(
|
|
519
|
+
[
|
|
520
|
+
max_bounds - 1,
|
|
521
|
+
int(self.bounds[self.segment] / self.PROBABILITY_OF_VALID)
|
|
522
|
+
]
|
|
523
|
+
)
|
|
524
|
+
)
|
|
525
|
+
except KeyError:
|
|
526
|
+
# If we are in an unallocated section, we'll get a key error (I think)
|
|
527
|
+
self.offset = self.rng.randint(0, max_bounds - 1)
|
|
528
|
+
|
|
529
|
+
# Calculate a virtual address based on the segment and the offset
|
|
530
|
+
self.virtual_address = (
|
|
531
|
+
(self.segment_bits << (self.virtual_bits - 2))
|
|
532
|
+
+ self.offset
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
# Calculate physical address based on offset
|
|
536
|
+
self.physical_address = self.base[self.segment] + self.offset
|
|
537
|
+
|
|
538
|
+
# Set answers based on whether it's in bounds or not
|
|
539
|
+
if self.__within_bounds(self.segment, self.offset, self.bounds[self.segment]):
|
|
540
|
+
self.answers["answer__physical_address"] = Answer.binary_hex(
|
|
541
|
+
"answer__physical_address",
|
|
542
|
+
self.physical_address,
|
|
543
|
+
length=self.physical_bits
|
|
544
|
+
)
|
|
545
|
+
else:
|
|
546
|
+
self.answers["answer__physical_address"] = Answer.string(
|
|
547
|
+
"answer__physical_address",
|
|
548
|
+
"INVALID"
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
self.answers["answer__segment"] = Answer.string("answer__segment", self.segment)
|
|
552
|
+
|
|
553
|
+
def get_body(self) -> ContentAST.Section:
|
|
554
|
+
body = ContentAST.Section()
|
|
555
|
+
|
|
556
|
+
body.add_element(
|
|
557
|
+
ContentAST.Paragraph(
|
|
558
|
+
[
|
|
559
|
+
f"Given a virtual address space of {self.virtual_bits}bits, "
|
|
560
|
+
f"and a physical address space of {self.physical_bits}bits, "
|
|
561
|
+
"what is the physical address associated with the virtual address "
|
|
562
|
+
f"0b{self.virtual_address:0{self.virtual_bits}b}?",
|
|
563
|
+
"If it is invalid simply type INVALID.",
|
|
564
|
+
"Note: assume that the stack grows in the same way as the code and the heap."
|
|
565
|
+
]
|
|
566
|
+
)
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
# Create segment table using mixin
|
|
570
|
+
segment_rows = [
|
|
571
|
+
{"": "code", "base": f"0b{self.base['code']:0{self.physical_bits}b}", "bounds": f"0b{self.bounds['code']:0b}"},
|
|
572
|
+
{"": "heap", "base": f"0b{self.base['heap']:0{self.physical_bits}b}", "bounds": f"0b{self.bounds['heap']:0b}"},
|
|
573
|
+
{"": "stack", "base": f"0b{self.base['stack']:0{self.physical_bits}b}", "bounds": f"0b{self.bounds['stack']:0b}"}
|
|
574
|
+
]
|
|
575
|
+
|
|
576
|
+
segment_table = self.create_answer_table(
|
|
577
|
+
headers=["", "base", "bounds"],
|
|
578
|
+
data_rows=segment_rows,
|
|
579
|
+
answer_columns=[] # No answer columns in this table
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
body.add_element(segment_table)
|
|
583
|
+
|
|
584
|
+
body.add_element(
|
|
585
|
+
ContentAST.AnswerBlock(
|
|
586
|
+
[
|
|
587
|
+
ContentAST.Answer(
|
|
588
|
+
self.answers["answer__segment"],
|
|
589
|
+
label="Segment name"
|
|
590
|
+
),
|
|
591
|
+
ContentAST.Answer(
|
|
592
|
+
self.answers["answer__physical_address"],
|
|
593
|
+
label="Physical Address"
|
|
594
|
+
)
|
|
595
|
+
]
|
|
596
|
+
)
|
|
597
|
+
)
|
|
598
|
+
return body
|
|
599
|
+
|
|
600
|
+
def get_explanation(self) -> ContentAST.Section:
|
|
601
|
+
explanation = ContentAST.Section()
|
|
602
|
+
|
|
603
|
+
explanation.add_element(
|
|
604
|
+
ContentAST.Paragraph(
|
|
605
|
+
[
|
|
606
|
+
"The core idea to keep in mind with segmentation is that you should always check ",
|
|
607
|
+
"the first two bits of the virtual address to see what segment it is in and then go from there."
|
|
608
|
+
"Keep in mind, "
|
|
609
|
+
"we also may need to include padding if our virtual address has a number of leading zeros left off!"
|
|
610
|
+
]
|
|
611
|
+
)
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
explanation.add_element(
|
|
615
|
+
ContentAST.Paragraph(
|
|
616
|
+
[
|
|
617
|
+
f"In this problem our virtual address, "
|
|
618
|
+
f"converted to binary and including padding, is 0b{self.virtual_address:0{self.virtual_bits}b}.",
|
|
619
|
+
f"From this we know that our segment bits are 0b{self.segment_bits:02b}, "
|
|
620
|
+
f"meaning that we are in the ***{self.segment}*** segment.",
|
|
621
|
+
""
|
|
622
|
+
]
|
|
623
|
+
)
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
if self.segment == "unallocated":
|
|
627
|
+
explanation.add_element(
|
|
628
|
+
ContentAST.Paragraph(
|
|
629
|
+
[
|
|
630
|
+
"Since this is the unallocated segment there are no possible valid translations, so we enter ***INVALID***."
|
|
631
|
+
]
|
|
632
|
+
)
|
|
633
|
+
)
|
|
634
|
+
else:
|
|
635
|
+
explanation.add_element(
|
|
636
|
+
ContentAST.Paragraph(
|
|
637
|
+
[
|
|
638
|
+
f"Since we are in the {self.segment} segment, "
|
|
639
|
+
f"we see from our table that our bounds are {self.bounds[self.segment]}. "
|
|
640
|
+
f"Remember that our check for our {self.segment} segment is: ",
|
|
641
|
+
f"`if (offset >= bounds({self.segment})) : INVALID`",
|
|
642
|
+
"which becomes"
|
|
643
|
+
f"`if ({self.offset:0b} > {self.bounds[self.segment]:0b}) : INVALID`"
|
|
644
|
+
]
|
|
645
|
+
)
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
if not self.__within_bounds(self.segment, self.offset, self.bounds[self.segment]):
|
|
649
|
+
# then we are outside of bounds
|
|
650
|
+
explanation.add_element(
|
|
651
|
+
ContentAST.Paragraph(
|
|
652
|
+
[
|
|
653
|
+
"We can therefore see that we are outside of bounds so we should put ***INVALID***.",
|
|
654
|
+
"If we <i>were</i> requesting a valid memory location we could use the below steps to do so."
|
|
655
|
+
"<hr>"
|
|
656
|
+
]
|
|
657
|
+
)
|
|
658
|
+
)
|
|
659
|
+
else:
|
|
660
|
+
explanation.add_element(
|
|
661
|
+
ContentAST.Paragraph(
|
|
662
|
+
[
|
|
663
|
+
"We are therefore in bounds so we can calculate our physical address, as we do below."
|
|
664
|
+
]
|
|
665
|
+
)
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
explanation.add_element(
|
|
669
|
+
ContentAST.Paragraph(
|
|
670
|
+
[
|
|
671
|
+
"To find the physical address we use the formula:",
|
|
672
|
+
"<code>physical_address = base(segment) + offset</code>",
|
|
673
|
+
"which becomes",
|
|
674
|
+
f"<code>physical_address = {self.base[self.segment]:0b} + {self.offset:0b}</code>.",
|
|
675
|
+
""
|
|
676
|
+
]
|
|
677
|
+
)
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
explanation.add_element(
|
|
681
|
+
ContentAST.Paragraph(
|
|
682
|
+
[
|
|
683
|
+
"Lining this up for ease we can do this calculation as:"
|
|
684
|
+
]
|
|
685
|
+
)
|
|
686
|
+
)
|
|
687
|
+
explanation.add_element(
|
|
688
|
+
ContentAST.Code(
|
|
689
|
+
f" 0b{self.base[self.segment]:0{self.physical_bits}b}\n"
|
|
690
|
+
f"<u>+ 0b{self.offset:0{self.physical_bits}b}</u>\n"
|
|
691
|
+
f" 0b{self.physical_address:0{self.physical_bits}b}\n"
|
|
692
|
+
)
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
return explanation
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
@QuestionRegistry.register()
|
|
699
|
+
class Paging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
|
|
700
|
+
MIN_OFFSET_BITS = 3
|
|
701
|
+
MIN_VPN_BITS = 3
|
|
702
|
+
MIN_PFN_BITS = 3
|
|
703
|
+
|
|
704
|
+
MAX_OFFSET_BITS = 8
|
|
705
|
+
MAX_VPN_BITS = 8
|
|
706
|
+
MAX_PFN_BITS = 16
|
|
707
|
+
|
|
708
|
+
def refresh(self, rng_seed=None, *args, **kwargs):
|
|
709
|
+
super().refresh(rng_seed=rng_seed, *args, **kwargs)
|
|
710
|
+
|
|
711
|
+
self.num_bits_offset = self.rng.randint(self.MIN_OFFSET_BITS, self.MAX_OFFSET_BITS)
|
|
712
|
+
self.num_bits_vpn = self.rng.randint(self.MIN_VPN_BITS, self.MAX_VPN_BITS)
|
|
713
|
+
self.num_bits_pfn = self.rng.randint(max([self.MIN_PFN_BITS, self.num_bits_vpn]), self.MAX_PFN_BITS)
|
|
714
|
+
|
|
715
|
+
self.virtual_address = self.rng.randint(1, 2 ** (self.num_bits_vpn + self.num_bits_offset))
|
|
716
|
+
|
|
717
|
+
# Calculate these two
|
|
718
|
+
self.offset = self.virtual_address % (2 ** (self.num_bits_offset))
|
|
719
|
+
self.vpn = self.virtual_address // (2 ** (self.num_bits_offset))
|
|
720
|
+
|
|
721
|
+
# Generate this randomly
|
|
722
|
+
self.pfn = self.rng.randint(0, 2 ** (self.num_bits_pfn))
|
|
723
|
+
|
|
724
|
+
# Calculate this
|
|
725
|
+
self.physical_address = self.pfn * (2 ** self.num_bits_offset) + self.offset
|
|
726
|
+
|
|
727
|
+
if self.rng.choices([True, False], weights=[(self.PROBABILITY_OF_VALID), (1 - self.PROBABILITY_OF_VALID)], k=1)[0]:
|
|
728
|
+
self.is_valid = True
|
|
729
|
+
# Set our actual entry to be in the table and valid
|
|
730
|
+
self.pte = self.pfn + (2 ** (self.num_bits_pfn))
|
|
731
|
+
# self.physical_address_var = VariableHex("Physical Address", self.physical_address, num_bits=(self.num_pfn_bits+self.num_offset_bits), default_presentation=VariableHex.PRESENTATION.BINARY)
|
|
732
|
+
# self.pfn_var = VariableHex("PFN", self.pfn, num_bits=self.num_pfn_bits, default_presentation=VariableHex.PRESENTATION.BINARY)
|
|
733
|
+
else:
|
|
734
|
+
self.is_valid = False
|
|
735
|
+
# Leave it as invalid
|
|
736
|
+
self.pte = self.pfn
|
|
737
|
+
# self.physical_address_var = Variable("Physical Address", "INVALID")
|
|
738
|
+
# self.pfn_var = Variable("PFN", "INVALID")
|
|
739
|
+
|
|
740
|
+
# self.pte_var = VariableHex("PTE", self.pte, num_bits=(self.num_pfn_bits+1), default_presentation=VariableHex.PRESENTATION.BINARY)
|
|
741
|
+
|
|
742
|
+
# Generate page table (moved from get_body to ensure deterministic generation)
|
|
743
|
+
table_size = self.rng.randint(5, 8)
|
|
744
|
+
|
|
745
|
+
lowest_possible_bottom = max([0, self.vpn - table_size])
|
|
746
|
+
highest_possible_bottom = min([2 ** self.num_bits_vpn - table_size, self.vpn])
|
|
747
|
+
|
|
748
|
+
table_bottom = self.rng.randint(lowest_possible_bottom, highest_possible_bottom)
|
|
749
|
+
table_top = table_bottom + table_size
|
|
750
|
+
|
|
751
|
+
self.page_table = {}
|
|
752
|
+
self.page_table[self.vpn] = self.pte
|
|
753
|
+
|
|
754
|
+
# Fill in the rest of the table
|
|
755
|
+
for vpn in range(table_bottom, table_top):
|
|
756
|
+
if vpn == self.vpn: continue
|
|
757
|
+
pte = self.page_table[self.vpn]
|
|
758
|
+
while pte in self.page_table.values():
|
|
759
|
+
pte = self.rng.randint(0, 2 ** self.num_bits_pfn - 1)
|
|
760
|
+
if self.rng.choices([True, False], weights=[(1 - self.PROBABILITY_OF_VALID), self.PROBABILITY_OF_VALID], k=1)[0]:
|
|
761
|
+
# Randomly set it to be valid
|
|
762
|
+
pte += (2 ** (self.num_bits_pfn))
|
|
763
|
+
# Once we have a unique random entry, put it into the Page Table
|
|
764
|
+
self.page_table[vpn] = pte
|
|
765
|
+
|
|
766
|
+
self.answers.update(
|
|
767
|
+
{
|
|
768
|
+
"answer__vpn": Answer.binary_hex("answer__vpn", self.vpn, length=self.num_bits_vpn),
|
|
769
|
+
"answer__offset": Answer.binary_hex("answer__offset", self.offset, length=self.num_bits_offset),
|
|
770
|
+
"answer__pte": Answer.binary_hex("answer__pte", self.pte, length=(self.num_bits_pfn + 1)),
|
|
771
|
+
}
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
if self.is_valid:
|
|
775
|
+
self.answers.update(
|
|
776
|
+
{
|
|
777
|
+
"answer__is_valid": Answer.string("answer__is_valid", "VALID"),
|
|
778
|
+
"answer__pfn": Answer.binary_hex("answer__pfn", self.pfn, length=self.num_bits_pfn),
|
|
779
|
+
"answer__physical_address": Answer.binary_hex(
|
|
780
|
+
"answer__physical_address", self.physical_address, length=(self.num_bits_pfn + self.num_bits_offset)
|
|
781
|
+
),
|
|
782
|
+
}
|
|
783
|
+
)
|
|
784
|
+
else:
|
|
785
|
+
self.answers.update(
|
|
786
|
+
{
|
|
787
|
+
"answer__is_valid": Answer.string("answer__is_valid", "INVALID"),
|
|
788
|
+
"answer__pfn": Answer.string("answer__pfn", "INVALID"),
|
|
789
|
+
"answer__physical_address": Answer.string("answer__physical_address", "INVALID"),
|
|
790
|
+
}
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
def get_body(self, *args, **kwargs) -> ContentAST.Section:
|
|
794
|
+
body = ContentAST.Section()
|
|
795
|
+
|
|
796
|
+
body.add_element(
|
|
797
|
+
ContentAST.Paragraph(
|
|
798
|
+
[
|
|
799
|
+
"Given the below information please calculate the equivalent physical address of the given virtual address, filling out all steps along the way.",
|
|
800
|
+
"Remember, we typically have the MSB representing valid or invalid."
|
|
801
|
+
]
|
|
802
|
+
)
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
# Create parameter info table using mixin
|
|
806
|
+
parameter_info = {
|
|
807
|
+
"Virtual Address": f"0b{self.virtual_address:0{self.num_bits_vpn + self.num_bits_offset}b}",
|
|
808
|
+
"# VPN bits": f"{self.num_bits_vpn}",
|
|
809
|
+
"# PFN bits": f"{self.num_bits_pfn}"
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
body.add_element(self.create_info_table(parameter_info))
|
|
813
|
+
|
|
814
|
+
# Use the page table generated in refresh() for deterministic output
|
|
815
|
+
# Add in ellipses before and after page table entries, if appropriate
|
|
816
|
+
value_matrix = []
|
|
817
|
+
|
|
818
|
+
if min(self.page_table.keys()) != 0:
|
|
819
|
+
value_matrix.append(["...", "..."])
|
|
820
|
+
|
|
821
|
+
value_matrix.extend(
|
|
822
|
+
[
|
|
823
|
+
[f"0b{vpn:0{self.num_bits_vpn}b}", f"0b{pte:0{(self.num_bits_pfn + 1)}b}"]
|
|
824
|
+
for vpn, pte in sorted(self.page_table.items())
|
|
825
|
+
]
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
if (max(self.page_table.keys()) + 1) != 2 ** self.num_bits_vpn:
|
|
829
|
+
value_matrix.append(["...", "..."])
|
|
830
|
+
|
|
831
|
+
body.add_element(
|
|
832
|
+
ContentAST.Table(
|
|
833
|
+
headers=["VPN", "PTE"],
|
|
834
|
+
data=value_matrix
|
|
835
|
+
)
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
body.add_element(
|
|
839
|
+
ContentAST.AnswerBlock(
|
|
840
|
+
[
|
|
841
|
+
|
|
842
|
+
ContentAST.Answer(self.answers["answer__vpn"], label="VPN"),
|
|
843
|
+
ContentAST.Answer(self.answers["answer__offset"], label="Offset"),
|
|
844
|
+
ContentAST.Answer(self.answers["answer__pte"], label="PTE"),
|
|
845
|
+
ContentAST.Answer(self.answers["answer__is_valid"], label="VALID or INVALID?"),
|
|
846
|
+
ContentAST.Answer(self.answers["answer__pfn"], label="PFN"),
|
|
847
|
+
ContentAST.Answer(self.answers["answer__physical_address"], label="Physical Address"),
|
|
848
|
+
]
|
|
849
|
+
)
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
return body
|
|
853
|
+
|
|
854
|
+
def get_explanation(self, *args, **kwargs) -> ContentAST.Section:
|
|
855
|
+
explanation = ContentAST.Section()
|
|
856
|
+
|
|
857
|
+
explanation.add_element(
|
|
858
|
+
ContentAST.Paragraph(
|
|
859
|
+
[
|
|
860
|
+
"The core idea of Paging is we want to break the virtual address into the VPN and the offset. "
|
|
861
|
+
"From here, we get the Page Table Entry corresponding to the VPN, and check the validity of the entry. "
|
|
862
|
+
"If it is valid, we clear the metadata and attach the PFN to the offset and have our physical address.",
|
|
863
|
+
]
|
|
864
|
+
)
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
explanation.add_element(
|
|
868
|
+
ContentAST.Paragraph(
|
|
869
|
+
[
|
|
870
|
+
"Don't forget to pad with the appropriate number of 0s (the appropriate number is the number of bits)!",
|
|
871
|
+
]
|
|
872
|
+
)
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
explanation.add_element(
|
|
876
|
+
ContentAST.Paragraph(
|
|
877
|
+
[
|
|
878
|
+
f"Virtual Address = VPN | offset",
|
|
879
|
+
f"<tt>0b{self.virtual_address:0{self.num_bits_vpn + self.num_bits_offset}b}</tt> "
|
|
880
|
+
f"= <tt>0b{self.vpn:0{self.num_bits_vpn}b}</tt> | <tt>0b{self.offset:0{self.num_bits_offset}b}</tt>",
|
|
881
|
+
]
|
|
882
|
+
)
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
explanation.add_element(
|
|
886
|
+
ContentAST.Paragraph(
|
|
887
|
+
[
|
|
888
|
+
"We next use our VPN to index into our page table and find the corresponding entry."
|
|
889
|
+
f"Our Page Table Entry is ",
|
|
890
|
+
f"<tt>0b{self.pte:0{(self.num_bits_pfn + 1)}b}</tt>"
|
|
891
|
+
f"which we found by looking for our VPN in the page table.",
|
|
892
|
+
]
|
|
893
|
+
)
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
if self.is_valid:
|
|
897
|
+
explanation.add_element(
|
|
898
|
+
ContentAST.Paragraph(
|
|
899
|
+
[
|
|
900
|
+
f"In our PTE we see that the first bit is <b>{self.pte // (2 ** self.num_bits_pfn)}</b> meaning that the translation is <b>VALID</b>"
|
|
901
|
+
]
|
|
902
|
+
)
|
|
903
|
+
)
|
|
904
|
+
else:
|
|
905
|
+
explanation.add_element(
|
|
906
|
+
ContentAST.Paragraph(
|
|
907
|
+
[
|
|
908
|
+
f"In our PTE we see that the first bit is <b>{self.pte // (2 ** self.num_bits_pfn)}</b> meaning that the translation is <b>INVALID</b>.",
|
|
909
|
+
"Therefore, we just write \"INVALID\" as our answer.",
|
|
910
|
+
"If it were valid we would complete the below steps.",
|
|
911
|
+
"<hr>"
|
|
912
|
+
]
|
|
913
|
+
)
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
explanation.add_element(
|
|
917
|
+
ContentAST.Paragraph(
|
|
918
|
+
[
|
|
919
|
+
"Next, we convert our PTE to our PFN by removing our metadata. "
|
|
920
|
+
"In this case we're just removing the leading bit. We can do this by applying a binary mask.",
|
|
921
|
+
f"PFN = PTE & mask",
|
|
922
|
+
f"which is,"
|
|
923
|
+
]
|
|
924
|
+
)
|
|
925
|
+
)
|
|
926
|
+
explanation.add_element(
|
|
927
|
+
ContentAST.Equation(
|
|
928
|
+
f"\\texttt{{{self.pfn:0{self.num_bits_pfn}b}}} "
|
|
929
|
+
f"= \\texttt{{0b{self.pte:0{self.num_bits_pfn + 1}b}}} "
|
|
930
|
+
f"\\& \\texttt{{0b{(2 ** self.num_bits_pfn) - 1:0{self.num_bits_pfn + 1}b}}}"
|
|
931
|
+
)
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
explanation.add_elements(
|
|
935
|
+
[
|
|
936
|
+
ContentAST.Paragraph(
|
|
937
|
+
[
|
|
938
|
+
"We then add combine our PFN and offset, "
|
|
939
|
+
"Physical Address = PFN | offset",
|
|
940
|
+
]
|
|
941
|
+
),
|
|
942
|
+
ContentAST.Equation(
|
|
943
|
+
fr"{r'\mathbf{' if self.is_valid else ''}\mathtt{{0b{self.physical_address:0{self.num_bits_pfn + self.num_bits_offset}b}}}{r'}' if self.is_valid else ''} = \mathtt{{0b{self.pfn:0{self.num_bits_pfn}b}}} \mid \mathtt{{0b{self.offset:0{self.num_bits_offset}b}}}"
|
|
944
|
+
)
|
|
945
|
+
]
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
explanation.add_elements(
|
|
949
|
+
[
|
|
950
|
+
ContentAST.Paragraph(["Note: Strictly speaking, this calculation is:", ]),
|
|
951
|
+
ContentAST.Equation(
|
|
952
|
+
fr"{r'\mathbf{' if self.is_valid else ''}\mathtt{{0b{self.physical_address:0{self.num_bits_pfn + self.num_bits_offset}b}}}{r'}' if self.is_valid else ''} = \mathtt{{0b{self.pfn:0{self.num_bits_pfn}b}{0:0{self.num_bits_offset}}}} + \mathtt{{0b{self.offset:0{self.num_bits_offset}b}}}"
|
|
953
|
+
),
|
|
954
|
+
ContentAST.Paragraph(["But that's a lot of extra 0s, so I'm splitting them up for succinctness"])
|
|
955
|
+
]
|
|
956
|
+
)
|
|
957
|
+
|
|
958
|
+
return explanation
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
@QuestionRegistry.register()
|
|
962
|
+
class HierarchicalPaging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
|
|
963
|
+
MIN_OFFSET_BITS = 3
|
|
964
|
+
MIN_PDI_BITS = 2
|
|
965
|
+
MIN_PTI_BITS = 2
|
|
966
|
+
MIN_PFN_BITS = 4
|
|
967
|
+
|
|
968
|
+
MAX_OFFSET_BITS = 5
|
|
969
|
+
MAX_PDI_BITS = 3
|
|
970
|
+
MAX_PTI_BITS = 3
|
|
971
|
+
MAX_PFN_BITS = 6
|
|
972
|
+
|
|
973
|
+
def refresh(self, rng_seed=None, *args, **kwargs):
|
|
974
|
+
super().refresh(rng_seed=rng_seed, *args, **kwargs)
|
|
975
|
+
|
|
976
|
+
# Set up bit counts
|
|
977
|
+
self.num_bits_offset = self.rng.randint(self.MIN_OFFSET_BITS, self.MAX_OFFSET_BITS)
|
|
978
|
+
self.num_bits_pdi = self.rng.randint(self.MIN_PDI_BITS, self.MAX_PDI_BITS)
|
|
979
|
+
self.num_bits_pti = self.rng.randint(self.MIN_PTI_BITS, self.MAX_PTI_BITS)
|
|
980
|
+
self.num_bits_pfn = self.rng.randint(self.MIN_PFN_BITS, self.MAX_PFN_BITS)
|
|
981
|
+
|
|
982
|
+
# Total VPN bits = PDI + PTI
|
|
983
|
+
self.num_bits_vpn = self.num_bits_pdi + self.num_bits_pti
|
|
984
|
+
|
|
985
|
+
# Generate a random virtual address
|
|
986
|
+
self.virtual_address = self.rng.randint(1, 2 ** (self.num_bits_vpn + self.num_bits_offset))
|
|
987
|
+
|
|
988
|
+
# Extract components from virtual address
|
|
989
|
+
self.offset = self.virtual_address % (2 ** self.num_bits_offset)
|
|
990
|
+
vpn = self.virtual_address // (2 ** self.num_bits_offset)
|
|
991
|
+
|
|
992
|
+
self.pti = vpn % (2 ** self.num_bits_pti)
|
|
993
|
+
self.pdi = vpn // (2 ** self.num_bits_pti)
|
|
994
|
+
|
|
995
|
+
# Generate PFN randomly
|
|
996
|
+
self.pfn = self.rng.randint(0, 2 ** self.num_bits_pfn - 1)
|
|
997
|
+
|
|
998
|
+
# Calculate physical address
|
|
999
|
+
self.physical_address = self.pfn * (2 ** self.num_bits_offset) + self.offset
|
|
1000
|
+
|
|
1001
|
+
# Determine validity at both levels
|
|
1002
|
+
# PD entry can be valid or invalid
|
|
1003
|
+
self.pd_valid = self.rng.choices([True, False], weights=[self.PROBABILITY_OF_VALID, 1 - self.PROBABILITY_OF_VALID], k=1)[0]
|
|
1004
|
+
|
|
1005
|
+
# PT entry only matters if PD is valid
|
|
1006
|
+
if self.pd_valid:
|
|
1007
|
+
self.pt_valid = self.rng.choices([True, False], weights=[self.PROBABILITY_OF_VALID, 1 - self.PROBABILITY_OF_VALID], k=1)[0]
|
|
1008
|
+
else:
|
|
1009
|
+
self.pt_valid = False # Doesn't matter, won't be checked
|
|
1010
|
+
|
|
1011
|
+
# Generate a page table number (PTBR - Page Table Base Register value in the PD entry)
|
|
1012
|
+
# This represents which page table to use
|
|
1013
|
+
self.page_table_number = self.rng.randint(0, 2 ** self.num_bits_pfn - 1)
|
|
1014
|
+
|
|
1015
|
+
# Create PD entry: valid bit + page table number
|
|
1016
|
+
if self.pd_valid:
|
|
1017
|
+
self.pd_entry = (2 ** self.num_bits_pfn) + self.page_table_number
|
|
1018
|
+
else:
|
|
1019
|
+
self.pd_entry = self.page_table_number # Invalid, no valid bit set
|
|
1020
|
+
|
|
1021
|
+
# Create PT entry: valid bit + PFN
|
|
1022
|
+
if self.pt_valid:
|
|
1023
|
+
self.pte = (2 ** self.num_bits_pfn) + self.pfn
|
|
1024
|
+
else:
|
|
1025
|
+
self.pte = self.pfn # Invalid, no valid bit set
|
|
1026
|
+
|
|
1027
|
+
# Overall validity requires both levels to be valid
|
|
1028
|
+
self.is_valid = self.pd_valid and self.pt_valid
|
|
1029
|
+
|
|
1030
|
+
# Build page directory - show 3-4 entries
|
|
1031
|
+
pd_size = self.rng.randint(3, 4)
|
|
1032
|
+
lowest_pd_bottom = max([0, self.pdi - pd_size])
|
|
1033
|
+
highest_pd_bottom = min([2 ** self.num_bits_pdi - pd_size, self.pdi])
|
|
1034
|
+
pd_bottom = self.rng.randint(lowest_pd_bottom, highest_pd_bottom)
|
|
1035
|
+
pd_top = pd_bottom + pd_size
|
|
1036
|
+
|
|
1037
|
+
self.page_directory = {}
|
|
1038
|
+
self.page_directory[self.pdi] = self.pd_entry
|
|
1039
|
+
|
|
1040
|
+
# Fill in other PD entries
|
|
1041
|
+
for pdi in range(pd_bottom, pd_top):
|
|
1042
|
+
if pdi == self.pdi:
|
|
1043
|
+
continue
|
|
1044
|
+
# Generate random PD entry
|
|
1045
|
+
pt_num = self.rng.randint(0, 2 ** self.num_bits_pfn - 1)
|
|
1046
|
+
while pt_num == self.page_table_number: # Make sure it's different
|
|
1047
|
+
pt_num = self.rng.randint(0, 2 ** self.num_bits_pfn - 1)
|
|
1048
|
+
|
|
1049
|
+
# Randomly valid or invalid
|
|
1050
|
+
if self.rng.choices([True, False], weights=[self.PROBABILITY_OF_VALID, 1 - self.PROBABILITY_OF_VALID], k=1)[0]:
|
|
1051
|
+
pd_val = (2 ** self.num_bits_pfn) + pt_num
|
|
1052
|
+
else:
|
|
1053
|
+
pd_val = pt_num
|
|
1054
|
+
|
|
1055
|
+
self.page_directory[pdi] = pd_val
|
|
1056
|
+
|
|
1057
|
+
# Build 2-3 page tables to show
|
|
1058
|
+
# Always include the one we need, plus 1-2 others
|
|
1059
|
+
num_page_tables_to_show = self.rng.randint(2, 3)
|
|
1060
|
+
|
|
1061
|
+
# Get unique page table numbers from the PD entries (extract PT numbers from valid entries)
|
|
1062
|
+
shown_pt_numbers = set()
|
|
1063
|
+
for pdi, pd_val in self.page_directory.items():
|
|
1064
|
+
pt_num = pd_val % (2 ** self.num_bits_pfn) # Extract PT number (remove valid bit)
|
|
1065
|
+
shown_pt_numbers.add(pt_num)
|
|
1066
|
+
|
|
1067
|
+
# Ensure our required page table is included
|
|
1068
|
+
shown_pt_numbers.add(self.page_table_number)
|
|
1069
|
+
|
|
1070
|
+
# Limit to requested number, but ALWAYS keep the required page table
|
|
1071
|
+
shown_pt_numbers_list = list(shown_pt_numbers)
|
|
1072
|
+
if self.page_table_number in shown_pt_numbers_list:
|
|
1073
|
+
# Remove it temporarily so we can add it back first
|
|
1074
|
+
shown_pt_numbers_list.remove(self.page_table_number)
|
|
1075
|
+
# Start with required page table, then add others up to the limit
|
|
1076
|
+
shown_pt_numbers = [self.page_table_number] + shown_pt_numbers_list[:num_page_tables_to_show - 1]
|
|
1077
|
+
|
|
1078
|
+
# Build each page table
|
|
1079
|
+
self.page_tables = {} # Dict mapping PT number -> dict of PTI -> PTE
|
|
1080
|
+
|
|
1081
|
+
# Use consistent size for all page tables for cleaner presentation
|
|
1082
|
+
pt_size = self.rng.randint(2, 4)
|
|
1083
|
+
|
|
1084
|
+
# Determine the PTI range that all tables will use (based on target PTI)
|
|
1085
|
+
# This ensures all tables show the same PTI values for consistency
|
|
1086
|
+
lowest_pt_bottom = max([0, self.pti - pt_size + 1])
|
|
1087
|
+
highest_pt_bottom = min([2 ** self.num_bits_pti - pt_size, self.pti])
|
|
1088
|
+
pt_bottom = self.rng.randint(lowest_pt_bottom, highest_pt_bottom)
|
|
1089
|
+
pt_top = pt_bottom + pt_size
|
|
1090
|
+
|
|
1091
|
+
# Generate all page tables using the SAME PTI range
|
|
1092
|
+
for pt_num in shown_pt_numbers:
|
|
1093
|
+
self.page_tables[pt_num] = {}
|
|
1094
|
+
|
|
1095
|
+
for pti in range(pt_bottom, pt_top):
|
|
1096
|
+
if pt_num == self.page_table_number and pti == self.pti:
|
|
1097
|
+
# Use the actual answer for the target page table entry
|
|
1098
|
+
self.page_tables[pt_num][pti] = self.pte
|
|
1099
|
+
else:
|
|
1100
|
+
# Generate random PTE for all other entries
|
|
1101
|
+
pfn = self.rng.randint(0, 2 ** self.num_bits_pfn - 1)
|
|
1102
|
+
if self.rng.choices([True, False], weights=[self.PROBABILITY_OF_VALID, 1 - self.PROBABILITY_OF_VALID], k=1)[0]:
|
|
1103
|
+
pte_val = (2 ** self.num_bits_pfn) + pfn
|
|
1104
|
+
else:
|
|
1105
|
+
pte_val = pfn
|
|
1106
|
+
|
|
1107
|
+
self.page_tables[pt_num][pti] = pte_val
|
|
1108
|
+
|
|
1109
|
+
# Set up answers
|
|
1110
|
+
self.answers.update({
|
|
1111
|
+
"answer__pdi": Answer.binary_hex("answer__pdi", self.pdi, length=self.num_bits_pdi),
|
|
1112
|
+
"answer__pti": Answer.binary_hex("answer__pti", self.pti, length=self.num_bits_pti),
|
|
1113
|
+
"answer__offset": Answer.binary_hex("answer__offset", self.offset, length=self.num_bits_offset),
|
|
1114
|
+
"answer__pd_entry": Answer.binary_hex("answer__pd_entry", self.pd_entry, length=(self.num_bits_pfn + 1)),
|
|
1115
|
+
"answer__pt_number": Answer.binary_hex("answer__pt_number", self.page_table_number, length=self.num_bits_pfn) if self.pd_valid else Answer.string("answer__pt_number", "INVALID"),
|
|
1116
|
+
})
|
|
1117
|
+
|
|
1118
|
+
# PTE answer: if PD is valid, accept the actual PTE value from the table
|
|
1119
|
+
# (regardless of whether that PTE is valid or invalid)
|
|
1120
|
+
if self.pd_valid:
|
|
1121
|
+
self.answers.update({
|
|
1122
|
+
"answer__pte": Answer.binary_hex("answer__pte", self.pte, length=(self.num_bits_pfn + 1)),
|
|
1123
|
+
})
|
|
1124
|
+
else:
|
|
1125
|
+
# If PD is invalid, student can't look up the page table
|
|
1126
|
+
# Accept both "INVALID" (for consistency) and "N/A" (for accuracy)
|
|
1127
|
+
self.answers.update({
|
|
1128
|
+
"answer__pte": Answer.string("answer__pte", ["INVALID", "N/A"]),
|
|
1129
|
+
})
|
|
1130
|
+
|
|
1131
|
+
# Validity, PFN, and Physical Address depend on BOTH levels being valid
|
|
1132
|
+
if self.pd_valid and self.pt_valid:
|
|
1133
|
+
self.answers.update({
|
|
1134
|
+
"answer__is_valid": Answer.string("answer__is_valid", "VALID"),
|
|
1135
|
+
"answer__pfn": Answer.binary_hex("answer__pfn", self.pfn, length=self.num_bits_pfn),
|
|
1136
|
+
"answer__physical_address": Answer.binary_hex(
|
|
1137
|
+
"answer__physical_address", self.physical_address, length=(self.num_bits_pfn + self.num_bits_offset)
|
|
1138
|
+
),
|
|
1139
|
+
})
|
|
1140
|
+
else:
|
|
1141
|
+
self.answers.update({
|
|
1142
|
+
"answer__is_valid": Answer.string("answer__is_valid", "INVALID"),
|
|
1143
|
+
"answer__pfn": Answer.string("answer__pfn", "INVALID"),
|
|
1144
|
+
"answer__physical_address": Answer.string("answer__physical_address", "INVALID"),
|
|
1145
|
+
})
|
|
1146
|
+
|
|
1147
|
+
def get_body(self, *args, **kwargs) -> ContentAST.Section:
|
|
1148
|
+
body = ContentAST.Section()
|
|
1149
|
+
|
|
1150
|
+
body.add_element(
|
|
1151
|
+
ContentAST.Paragraph([
|
|
1152
|
+
"Given the below information please calculate the equivalent physical address of the given virtual address, filling out all steps along the way.",
|
|
1153
|
+
"This problem uses <b>two-level (hierarchical) paging</b>.",
|
|
1154
|
+
"Remember, we typically have the MSB representing valid or invalid."
|
|
1155
|
+
])
|
|
1156
|
+
)
|
|
1157
|
+
|
|
1158
|
+
# Create parameter info table using mixin (same format as Paging question)
|
|
1159
|
+
parameter_info = {
|
|
1160
|
+
"Virtual Address": f"0b{self.virtual_address:0{self.num_bits_vpn + self.num_bits_offset}b}",
|
|
1161
|
+
"# PDI bits": f"{self.num_bits_pdi}",
|
|
1162
|
+
"# PTI bits": f"{self.num_bits_pti}",
|
|
1163
|
+
"# Offset bits": f"{self.num_bits_offset}",
|
|
1164
|
+
"# PFN bits": f"{self.num_bits_pfn}"
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
body.add_element(self.create_info_table(parameter_info))
|
|
1168
|
+
|
|
1169
|
+
# Page Directory table
|
|
1170
|
+
pd_matrix = []
|
|
1171
|
+
if min(self.page_directory.keys()) != 0:
|
|
1172
|
+
pd_matrix.append(["...", "..."])
|
|
1173
|
+
|
|
1174
|
+
pd_matrix.extend([
|
|
1175
|
+
[f"0b{pdi:0{self.num_bits_pdi}b}", f"0b{pd_val:0{self.num_bits_pfn + 1}b}"]
|
|
1176
|
+
for pdi, pd_val in sorted(self.page_directory.items())
|
|
1177
|
+
])
|
|
1178
|
+
|
|
1179
|
+
if (max(self.page_directory.keys()) + 1) != 2 ** self.num_bits_pdi:
|
|
1180
|
+
pd_matrix.append(["...", "..."])
|
|
1181
|
+
|
|
1182
|
+
# Use a simple text paragraph - the bold will come from markdown conversion
|
|
1183
|
+
body.add_element(
|
|
1184
|
+
ContentAST.Paragraph([
|
|
1185
|
+
"**Page Directory:**"
|
|
1186
|
+
])
|
|
1187
|
+
)
|
|
1188
|
+
body.add_element(
|
|
1189
|
+
ContentAST.Table(
|
|
1190
|
+
headers=["PDI", "PD Entry"],
|
|
1191
|
+
data=pd_matrix
|
|
1192
|
+
)
|
|
1193
|
+
)
|
|
1194
|
+
|
|
1195
|
+
# Page Tables - use TableGroup for side-by-side display
|
|
1196
|
+
table_group = ContentAST.TableGroup()
|
|
1197
|
+
|
|
1198
|
+
for pt_num in sorted(self.page_tables.keys()):
|
|
1199
|
+
pt_matrix = []
|
|
1200
|
+
pt_entries = self.page_tables[pt_num]
|
|
1201
|
+
|
|
1202
|
+
min_pti = min(pt_entries.keys())
|
|
1203
|
+
max_pti = max(pt_entries.keys())
|
|
1204
|
+
max_possible_pti = 2 ** self.num_bits_pti - 1
|
|
1205
|
+
|
|
1206
|
+
# Smart leading ellipsis: only if there are 2+ hidden entries before
|
|
1207
|
+
# (if only 1 hidden, we should just show it)
|
|
1208
|
+
if min_pti > 1:
|
|
1209
|
+
pt_matrix.append(["...", "..."])
|
|
1210
|
+
elif min_pti == 1:
|
|
1211
|
+
# Show the 0th entry instead of "..."
|
|
1212
|
+
pfn = self.rng.randint(0, 2 ** self.num_bits_pfn - 1)
|
|
1213
|
+
if self.rng.choices([True, False], weights=[self.PROBABILITY_OF_VALID, 1 - self.PROBABILITY_OF_VALID], k=1)[0]:
|
|
1214
|
+
pte_val = (2 ** self.num_bits_pfn) + pfn
|
|
1215
|
+
else:
|
|
1216
|
+
pte_val = pfn
|
|
1217
|
+
pt_matrix.append([f"0b{0:0{self.num_bits_pti}b}", f"0b{pte_val:0{self.num_bits_pfn + 1}b}"])
|
|
1218
|
+
|
|
1219
|
+
# Add actual entries
|
|
1220
|
+
pt_matrix.extend([
|
|
1221
|
+
[f"0b{pti:0{self.num_bits_pti}b}", f"0b{pte:0{self.num_bits_pfn + 1}b}"]
|
|
1222
|
+
for pti, pte in sorted(pt_entries.items())
|
|
1223
|
+
])
|
|
1224
|
+
|
|
1225
|
+
# Smart trailing ellipsis: only if there are 2+ hidden entries after
|
|
1226
|
+
hidden_after = max_possible_pti - max_pti
|
|
1227
|
+
if hidden_after > 1:
|
|
1228
|
+
pt_matrix.append(["...", "..."])
|
|
1229
|
+
elif hidden_after == 1:
|
|
1230
|
+
# Show the last entry instead of "..."
|
|
1231
|
+
pfn = self.rng.randint(0, 2 ** self.num_bits_pfn - 1)
|
|
1232
|
+
if self.rng.choices([True, False], weights=[self.PROBABILITY_OF_VALID, 1 - self.PROBABILITY_OF_VALID], k=1)[0]:
|
|
1233
|
+
pte_val = (2 ** self.num_bits_pfn) + pfn
|
|
1234
|
+
else:
|
|
1235
|
+
pte_val = pfn
|
|
1236
|
+
pt_matrix.append([f"0b{max_possible_pti:0{self.num_bits_pti}b}", f"0b{pte_val:0{self.num_bits_pfn + 1}b}"])
|
|
1237
|
+
|
|
1238
|
+
table_group.add_table(
|
|
1239
|
+
label=f"PTC 0b{pt_num:0{self.num_bits_pfn}b}:",
|
|
1240
|
+
table=ContentAST.Table(headers=["PTI", "PTE"], data=pt_matrix)
|
|
1241
|
+
)
|
|
1242
|
+
|
|
1243
|
+
body.add_element(table_group)
|
|
1244
|
+
|
|
1245
|
+
# Answer block
|
|
1246
|
+
body.add_element(
|
|
1247
|
+
ContentAST.AnswerBlock([
|
|
1248
|
+
ContentAST.Answer(self.answers["answer__pdi"], label="PDI (Page Directory Index)"),
|
|
1249
|
+
ContentAST.Answer(self.answers["answer__pti"], label="PTI (Page Table Index)"),
|
|
1250
|
+
ContentAST.Answer(self.answers["answer__offset"], label="Offset"),
|
|
1251
|
+
ContentAST.Answer(self.answers["answer__pd_entry"], label="PD Entry (from Page Directory)"),
|
|
1252
|
+
ContentAST.Answer(self.answers["answer__pt_number"], label="Page Table Number"),
|
|
1253
|
+
ContentAST.Answer(self.answers["answer__pte"], label="PTE (from Page Table)"),
|
|
1254
|
+
ContentAST.Answer(self.answers["answer__is_valid"], label="VALID or INVALID?"),
|
|
1255
|
+
ContentAST.Answer(self.answers["answer__pfn"], label="PFN"),
|
|
1256
|
+
ContentAST.Answer(self.answers["answer__physical_address"], label="Physical Address"),
|
|
1257
|
+
])
|
|
1258
|
+
)
|
|
1259
|
+
|
|
1260
|
+
return body
|
|
1261
|
+
|
|
1262
|
+
def get_explanation(self, *args, **kwargs) -> ContentAST.Section:
|
|
1263
|
+
explanation = ContentAST.Section()
|
|
1264
|
+
|
|
1265
|
+
explanation.add_element(
|
|
1266
|
+
ContentAST.Paragraph([
|
|
1267
|
+
"Two-level paging requires two lookups: first in the Page Directory, then in a Page Table.",
|
|
1268
|
+
"The virtual address is split into three parts: PDI | PTI | Offset."
|
|
1269
|
+
])
|
|
1270
|
+
)
|
|
1271
|
+
|
|
1272
|
+
explanation.add_element(
|
|
1273
|
+
ContentAST.Paragraph([
|
|
1274
|
+
"Don't forget to pad with the appropriate number of 0s!"
|
|
1275
|
+
])
|
|
1276
|
+
)
|
|
1277
|
+
|
|
1278
|
+
# Step 1: Extract PDI, PTI, Offset
|
|
1279
|
+
explanation.add_element(
|
|
1280
|
+
ContentAST.Paragraph([
|
|
1281
|
+
f"<b>Step 1: Extract components from Virtual Address</b>",
|
|
1282
|
+
f"Virtual Address = PDI | PTI | Offset",
|
|
1283
|
+
f"<tt>0b{self.virtual_address:0{self.num_bits_vpn + self.num_bits_offset}b}</tt> = "
|
|
1284
|
+
f"<tt>0b{self.pdi:0{self.num_bits_pdi}b}</tt> | "
|
|
1285
|
+
f"<tt>0b{self.pti:0{self.num_bits_pti}b}</tt> | "
|
|
1286
|
+
f"<tt>0b{self.offset:0{self.num_bits_offset}b}</tt>"
|
|
1287
|
+
])
|
|
1288
|
+
)
|
|
1289
|
+
|
|
1290
|
+
# Step 2: Look up PD Entry
|
|
1291
|
+
explanation.add_element(
|
|
1292
|
+
ContentAST.Paragraph([
|
|
1293
|
+
f"<b>Step 2: Look up Page Directory Entry</b>",
|
|
1294
|
+
f"Using PDI = <tt>0b{self.pdi:0{self.num_bits_pdi}b}</tt>, we find PD Entry = <tt>0b{self.pd_entry:0{self.num_bits_pfn + 1}b}</tt>"
|
|
1295
|
+
])
|
|
1296
|
+
)
|
|
1297
|
+
|
|
1298
|
+
# Step 3: Check PD validity
|
|
1299
|
+
pd_valid_bit = self.pd_entry // (2 ** self.num_bits_pfn)
|
|
1300
|
+
explanation.add_element(
|
|
1301
|
+
ContentAST.Paragraph([
|
|
1302
|
+
f"<b>Step 3: Check Page Directory Entry validity</b>",
|
|
1303
|
+
f"The MSB (valid bit) is <b>{pd_valid_bit}</b>, so this PD Entry is <b>{'VALID' if self.pd_valid else 'INVALID'}</b>."
|
|
1304
|
+
])
|
|
1305
|
+
)
|
|
1306
|
+
|
|
1307
|
+
if not self.pd_valid:
|
|
1308
|
+
explanation.add_element(
|
|
1309
|
+
ContentAST.Paragraph([
|
|
1310
|
+
"Since the Page Directory Entry is invalid, the translation fails here.",
|
|
1311
|
+
"We write <b>INVALID</b> for all remaining fields.",
|
|
1312
|
+
"If it were valid, we would continue with the steps below.",
|
|
1313
|
+
"<hr>"
|
|
1314
|
+
])
|
|
1315
|
+
)
|
|
1316
|
+
|
|
1317
|
+
# Step 4: Extract PT number (if PD valid)
|
|
1318
|
+
explanation.add_element(
|
|
1319
|
+
ContentAST.Paragraph([
|
|
1320
|
+
f"<b>Step 4: Extract Page Table Number</b>",
|
|
1321
|
+
"We remove the valid bit from the PD Entry to get the Page Table Number:"
|
|
1322
|
+
])
|
|
1323
|
+
)
|
|
1324
|
+
|
|
1325
|
+
explanation.add_element(
|
|
1326
|
+
ContentAST.Equation(
|
|
1327
|
+
f"\\texttt{{{self.page_table_number:0{self.num_bits_pfn}b}}} = "
|
|
1328
|
+
f"\\texttt{{0b{self.pd_entry:0{self.num_bits_pfn + 1}b}}} \\& "
|
|
1329
|
+
f"\\texttt{{0b{(2 ** self.num_bits_pfn) - 1:0{self.num_bits_pfn + 1}b}}}"
|
|
1330
|
+
)
|
|
1331
|
+
)
|
|
1332
|
+
|
|
1333
|
+
if self.pd_valid:
|
|
1334
|
+
explanation.add_element(
|
|
1335
|
+
ContentAST.Paragraph([
|
|
1336
|
+
f"This tells us to use <b>Page Table #{self.page_table_number}</b>."
|
|
1337
|
+
])
|
|
1338
|
+
)
|
|
1339
|
+
|
|
1340
|
+
# Step 5: Look up PTE
|
|
1341
|
+
explanation.add_element(
|
|
1342
|
+
ContentAST.Paragraph([
|
|
1343
|
+
f"<b>Step 5: Look up Page Table Entry</b>",
|
|
1344
|
+
f"Using PTI = <tt>0b{self.pti:0{self.num_bits_pti}b}</tt> in Page Table #{self.page_table_number}, "
|
|
1345
|
+
f"we find PTE = <tt>0b{self.pte:0{self.num_bits_pfn + 1}b}</tt>"
|
|
1346
|
+
])
|
|
1347
|
+
)
|
|
1348
|
+
|
|
1349
|
+
# Step 6: Check PT validity
|
|
1350
|
+
pt_valid_bit = self.pte // (2 ** self.num_bits_pfn)
|
|
1351
|
+
explanation.add_element(
|
|
1352
|
+
ContentAST.Paragraph([
|
|
1353
|
+
f"<b>Step 6: Check Page Table Entry validity</b>",
|
|
1354
|
+
f"The MSB (valid bit) is <b>{pt_valid_bit}</b>, so this PTE is <b>{'VALID' if self.pt_valid else 'INVALID'}</b>."
|
|
1355
|
+
])
|
|
1356
|
+
)
|
|
1357
|
+
|
|
1358
|
+
if not self.pt_valid:
|
|
1359
|
+
explanation.add_element(
|
|
1360
|
+
ContentAST.Paragraph([
|
|
1361
|
+
"Since the Page Table Entry is invalid, the translation fails.",
|
|
1362
|
+
"We write <b>INVALID</b> for PFN and Physical Address.",
|
|
1363
|
+
"If it were valid, we would continue with the steps below.",
|
|
1364
|
+
"<hr>"
|
|
1365
|
+
])
|
|
1366
|
+
)
|
|
1367
|
+
|
|
1368
|
+
# Step 7: Extract PFN
|
|
1369
|
+
explanation.add_element(
|
|
1370
|
+
ContentAST.Paragraph([
|
|
1371
|
+
f"<b>Step 7: Extract PFN</b>",
|
|
1372
|
+
"We remove the valid bit from the PTE to get the PFN:"
|
|
1373
|
+
])
|
|
1374
|
+
)
|
|
1375
|
+
|
|
1376
|
+
explanation.add_element(
|
|
1377
|
+
ContentAST.Equation(
|
|
1378
|
+
f"\\texttt{{{self.pfn:0{self.num_bits_pfn}b}}} = "
|
|
1379
|
+
f"\\texttt{{0b{self.pte:0{self.num_bits_pfn + 1}b}}} \\& "
|
|
1380
|
+
f"\\texttt{{0b{(2 ** self.num_bits_pfn) - 1:0{self.num_bits_pfn + 1}b}}}"
|
|
1381
|
+
)
|
|
1382
|
+
)
|
|
1383
|
+
|
|
1384
|
+
# Step 8: Construct physical address
|
|
1385
|
+
explanation.add_element(
|
|
1386
|
+
ContentAST.Paragraph([
|
|
1387
|
+
f"<b>Step 8: Construct Physical Address</b>",
|
|
1388
|
+
"Physical Address = PFN | Offset"
|
|
1389
|
+
])
|
|
1390
|
+
)
|
|
1391
|
+
|
|
1392
|
+
explanation.add_element(
|
|
1393
|
+
ContentAST.Equation(
|
|
1394
|
+
fr"{r'\mathbf{' if self.is_valid else ''}\mathtt{{0b{self.physical_address:0{self.num_bits_pfn + self.num_bits_offset}b}}}{r'}' if self.is_valid else ''} = "
|
|
1395
|
+
f"\\mathtt{{0b{self.pfn:0{self.num_bits_pfn}b}}} \\mid "
|
|
1396
|
+
f"\\mathtt{{0b{self.offset:0{self.num_bits_offset}b}}}"
|
|
1397
|
+
)
|
|
1398
|
+
)
|
|
1399
|
+
|
|
1400
|
+
return explanation
|