Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
allendowney
GitHub Repository: allendowney/cpython
Path: blob/main/Tools/cases_generator/generate_cases.py
12 views
1
"""Generate the main interpreter switch.
2
3
Reads the instruction definitions from bytecodes.c.
4
Writes the cases to generated_cases.c.h, which is #included in ceval.c.
5
"""
6
7
import argparse
8
import contextlib
9
import dataclasses
10
import os
11
import posixpath
12
import re
13
import sys
14
import typing
15
16
import lexer as lx
17
import parser
18
from parser import StackEffect
19
20
HERE = os.path.dirname(__file__)
21
ROOT = os.path.join(HERE, "../..")
22
THIS = os.path.relpath(__file__, ROOT).replace(os.path.sep, posixpath.sep)
23
24
DEFAULT_INPUT = os.path.relpath(os.path.join(ROOT, "Python/bytecodes.c"))
25
DEFAULT_OUTPUT = os.path.relpath(os.path.join(ROOT, "Python/generated_cases.c.h"))
26
DEFAULT_METADATA_OUTPUT = os.path.relpath(
27
os.path.join(ROOT, "Python/opcode_metadata.h")
28
)
29
DEFAULT_PYMETADATA_OUTPUT = os.path.relpath(
30
os.path.join(ROOT, "Lib/_opcode_metadata.py")
31
)
32
DEFAULT_EXECUTOR_OUTPUT = os.path.relpath(
33
os.path.join(ROOT, "Python/executor_cases.c.h")
34
)
35
BEGIN_MARKER = "// BEGIN BYTECODES //"
36
END_MARKER = "// END BYTECODES //"
37
RE_PREDICTED = (
38
r"^\s*(?:GO_TO_INSTRUCTION\(|DEOPT_IF\(.*?,\s*)(\w+)\);\s*(?://.*)?$"
39
)
40
UNUSED = "unused"
41
BITS_PER_CODE_UNIT = 16
42
43
RESERVED_WORDS = {
44
"co_consts" : "Use FRAME_CO_CONSTS.",
45
"co_names": "Use FRAME_CO_NAMES.",
46
}
47
48
arg_parser = argparse.ArgumentParser(
49
description="Generate the code for the interpreter switch.",
50
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
51
)
52
arg_parser.add_argument(
53
"-o", "--output", type=str, help="Generated code", default=DEFAULT_OUTPUT
54
)
55
arg_parser.add_argument(
56
"-m", "--metadata", type=str, help="Generated C metadata", default=DEFAULT_METADATA_OUTPUT
57
)
58
arg_parser.add_argument(
59
"-p", "--pymetadata", type=str, help="Generated Python metadata", default=DEFAULT_PYMETADATA_OUTPUT
60
)
61
arg_parser.add_argument(
62
"-l", "--emit-line-directives", help="Emit #line directives", action="store_true"
63
)
64
arg_parser.add_argument(
65
"input", nargs=argparse.REMAINDER, help="Instruction definition file(s)"
66
)
67
arg_parser.add_argument(
68
"-e",
69
"--executor-cases",
70
type=str,
71
help="Write executor cases to this file",
72
default=DEFAULT_EXECUTOR_OUTPUT,
73
)
74
75
76
def effect_size(effect: StackEffect) -> tuple[int, str]:
77
"""Return the 'size' impact of a stack effect.
78
79
Returns a tuple (numeric, symbolic) where:
80
81
- numeric is an int giving the statically analyzable size of the effect
82
- symbolic is a string representing a variable effect (e.g. 'oparg*2')
83
84
At most one of these will be non-zero / non-empty.
85
"""
86
if effect.size:
87
assert not effect.cond, "Array effects cannot have a condition"
88
return 0, effect.size
89
elif effect.cond:
90
return 0, f"{maybe_parenthesize(effect.cond)} ? 1 : 0"
91
else:
92
return 1, ""
93
94
95
def maybe_parenthesize(sym: str) -> str:
96
"""Add parentheses around a string if it contains an operator.
97
98
An exception is made for '*' which is common and harmless
99
in the context where the symbolic size is used.
100
"""
101
if re.match(r"^[\s\w*]+$", sym):
102
return sym
103
else:
104
return f"({sym})"
105
106
107
def list_effect_size(effects: list[StackEffect]) -> tuple[int, str]:
108
numeric = 0
109
symbolic: list[str] = []
110
for effect in effects:
111
diff, sym = effect_size(effect)
112
numeric += diff
113
if sym:
114
symbolic.append(maybe_parenthesize(sym))
115
return numeric, " + ".join(symbolic)
116
117
118
def string_effect_size(arg: tuple[int, str]) -> str:
119
numeric, symbolic = arg
120
if numeric and symbolic:
121
return f"{numeric} + {symbolic}"
122
elif symbolic:
123
return symbolic
124
else:
125
return str(numeric)
126
127
128
class Formatter:
129
"""Wraps an output stream with the ability to indent etc."""
130
131
stream: typing.TextIO
132
prefix: str
133
emit_line_directives: bool = False
134
lineno: int # Next line number, 1-based
135
filename: str # Slightly improved stream.filename
136
nominal_lineno: int
137
nominal_filename: str
138
139
def __init__(
140
self, stream: typing.TextIO, indent: int,
141
emit_line_directives: bool = False, comment: str = "//",
142
) -> None:
143
self.stream = stream
144
self.prefix = " " * indent
145
self.emit_line_directives = emit_line_directives
146
self.comment = comment
147
self.lineno = 1
148
filename = os.path.relpath(self.stream.name, ROOT)
149
# Make filename more user-friendly and less platform-specific
150
filename = filename.replace("\\", "/")
151
if filename.startswith("./"):
152
filename = filename[2:]
153
if filename.endswith(".new"):
154
filename = filename[:-4]
155
self.filename = filename
156
self.nominal_lineno = 1
157
self.nominal_filename = filename
158
159
def write_raw(self, s: str) -> None:
160
self.stream.write(s)
161
newlines = s.count("\n")
162
self.lineno += newlines
163
self.nominal_lineno += newlines
164
165
def emit(self, arg: str) -> None:
166
if arg:
167
self.write_raw(f"{self.prefix}{arg}\n")
168
else:
169
self.write_raw("\n")
170
171
def set_lineno(self, lineno: int, filename: str) -> None:
172
if self.emit_line_directives:
173
if lineno != self.nominal_lineno or filename != self.nominal_filename:
174
self.emit(f'#line {lineno} "{filename}"')
175
self.nominal_lineno = lineno
176
self.nominal_filename = filename
177
178
def reset_lineno(self) -> None:
179
if self.lineno != self.nominal_lineno or self.filename != self.nominal_filename:
180
self.set_lineno(self.lineno + 1, self.filename)
181
182
@contextlib.contextmanager
183
def indent(self):
184
self.prefix += " "
185
yield
186
self.prefix = self.prefix[:-4]
187
188
@contextlib.contextmanager
189
def block(self, head: str, tail: str = ""):
190
if head:
191
self.emit(head + " {")
192
else:
193
self.emit("{")
194
with self.indent():
195
yield
196
self.emit("}" + tail)
197
198
def stack_adjust(
199
self,
200
input_effects: list[StackEffect],
201
output_effects: list[StackEffect],
202
):
203
shrink, isym = list_effect_size(input_effects)
204
grow, osym = list_effect_size(output_effects)
205
diff = grow - shrink
206
if isym and isym != osym:
207
self.emit(f"STACK_SHRINK({isym});")
208
if diff < 0:
209
self.emit(f"STACK_SHRINK({-diff});")
210
if diff > 0:
211
self.emit(f"STACK_GROW({diff});")
212
if osym and osym != isym:
213
self.emit(f"STACK_GROW({osym});")
214
215
def declare(self, dst: StackEffect, src: StackEffect | None):
216
if dst.name == UNUSED:
217
return
218
typ = f"{dst.type}" if dst.type else "PyObject *"
219
if src:
220
cast = self.cast(dst, src)
221
init = f" = {cast}{src.name}"
222
elif dst.cond:
223
init = " = NULL"
224
else:
225
init = ""
226
sepa = "" if typ.endswith("*") else " "
227
self.emit(f"{typ}{sepa}{dst.name}{init};")
228
229
def assign(self, dst: StackEffect, src: StackEffect):
230
if src.name == UNUSED:
231
return
232
if src.size:
233
# Don't write sized arrays -- it's up to the user code.
234
return
235
cast = self.cast(dst, src)
236
if re.match(r"^REG\(oparg(\d+)\)$", dst.name):
237
self.emit(f"Py_XSETREF({dst.name}, {cast}{src.name});")
238
else:
239
stmt = f"{dst.name} = {cast}{src.name};"
240
if src.cond:
241
stmt = f"if ({src.cond}) {{ {stmt} }}"
242
self.emit(stmt)
243
244
def cast(self, dst: StackEffect, src: StackEffect) -> str:
245
return f"({dst.type or 'PyObject *'})" if src.type != dst.type else ""
246
247
@dataclasses.dataclass
248
class InstructionFlags:
249
"""Construct and manipulate instruction flags"""
250
251
HAS_ARG_FLAG: bool
252
HAS_CONST_FLAG: bool
253
HAS_NAME_FLAG: bool
254
HAS_JUMP_FLAG: bool
255
256
def __post_init__(self):
257
self.bitmask = {
258
name : (1 << i) for i, name in enumerate(self.names())
259
}
260
261
@staticmethod
262
def fromInstruction(instr: "AnyInstruction"):
263
return InstructionFlags(
264
HAS_ARG_FLAG=variable_used(instr, "oparg"),
265
HAS_CONST_FLAG=variable_used(instr, "FRAME_CO_CONSTS"),
266
HAS_NAME_FLAG=variable_used(instr, "FRAME_CO_NAMES"),
267
HAS_JUMP_FLAG=variable_used(instr, "JUMPBY"),
268
)
269
270
@staticmethod
271
def newEmpty():
272
return InstructionFlags(False, False, False, False)
273
274
def add(self, other: "InstructionFlags") -> None:
275
for name, value in dataclasses.asdict(other).items():
276
if value:
277
setattr(self, name, value)
278
279
def names(self, value=None):
280
if value is None:
281
return dataclasses.asdict(self).keys()
282
return [n for n, v in dataclasses.asdict(self).items() if v == value]
283
284
def bitmap(self) -> int:
285
flags = 0
286
for name in self.names():
287
if getattr(self, name):
288
flags |= self.bitmask[name]
289
return flags
290
291
@classmethod
292
def emit_macros(cls, out: Formatter):
293
flags = cls.newEmpty()
294
for name, value in flags.bitmask.items():
295
out.emit(f"#define {name} ({value})");
296
297
for name, value in flags.bitmask.items():
298
out.emit(
299
f"#define OPCODE_{name[:-len('_FLAG')]}(OP) "
300
f"(_PyOpcode_opcode_metadata[(OP)].flags & ({name}))")
301
302
303
@dataclasses.dataclass
304
class ActiveCacheEffect:
305
"""Wraps a CacheEffect that is actually used, in context."""
306
effect: parser.CacheEffect
307
offset: int
308
309
310
FORBIDDEN_NAMES_IN_UOPS = (
311
"resume_with_error", # Proxy for "goto", which isn't an IDENTIFIER
312
"unbound_local_error",
313
"kwnames",
314
"next_instr",
315
"oparg1", # Proxy for super-instructions like LOAD_FAST_LOAD_FAST
316
"JUMPBY",
317
"DISPATCH",
318
"INSTRUMENTED_JUMP",
319
"throwflag",
320
"exception_unwind",
321
"import_from",
322
"import_name",
323
"_PyObject_CallNoArgs", # Proxy for BEFORE_WITH
324
)
325
326
327
# Interpreter tiers
328
TIER_ONE = 1 # Specializing adaptive interpreter (PEP 659)
329
TIER_TWO = 2 # Experimental tracing interpreter
330
Tiers: typing.TypeAlias = typing.Literal[1, 2]
331
332
333
@dataclasses.dataclass
334
class Instruction:
335
"""An instruction with additional data and code."""
336
337
# Parts of the underlying instruction definition
338
inst: parser.InstDef
339
kind: typing.Literal["inst", "op"]
340
name: str
341
block: parser.Block
342
block_text: list[str] # Block.text, less curlies, less PREDICT() calls
343
block_line: int # First line of block in original code
344
345
# Computed by constructor
346
always_exits: bool
347
cache_offset: int
348
cache_effects: list[parser.CacheEffect]
349
input_effects: list[StackEffect]
350
output_effects: list[StackEffect]
351
unmoved_names: frozenset[str]
352
instr_fmt: str
353
instr_flags: InstructionFlags
354
active_caches: list[ActiveCacheEffect]
355
356
# Set later
357
family: parser.Family | None = None
358
predicted: bool = False
359
360
def __init__(self, inst: parser.InstDef):
361
self.inst = inst
362
self.kind = inst.kind
363
self.name = inst.name
364
self.block = inst.block
365
self.block_text, self.check_eval_breaker, self.block_line = \
366
extract_block_text(self.block)
367
self.always_exits = always_exits(self.block_text)
368
self.cache_effects = [
369
effect for effect in inst.inputs if isinstance(effect, parser.CacheEffect)
370
]
371
self.cache_offset = sum(c.size for c in self.cache_effects)
372
self.input_effects = [
373
effect for effect in inst.inputs if isinstance(effect, StackEffect)
374
]
375
self.output_effects = inst.outputs # For consistency/completeness
376
unmoved_names: set[str] = set()
377
for ieffect, oeffect in zip(self.input_effects, self.output_effects):
378
if ieffect.name == oeffect.name:
379
unmoved_names.add(ieffect.name)
380
else:
381
break
382
self.unmoved_names = frozenset(unmoved_names)
383
384
self.instr_flags = InstructionFlags.fromInstruction(inst)
385
386
self.active_caches = []
387
offset = 0
388
for effect in self.cache_effects:
389
if effect.name != UNUSED:
390
self.active_caches.append(ActiveCacheEffect(effect, offset))
391
offset += effect.size
392
393
if self.instr_flags.HAS_ARG_FLAG:
394
fmt = "IB"
395
else:
396
fmt = "IX"
397
if offset:
398
fmt += "C" + "0"*(offset-1)
399
self.instr_fmt = fmt
400
401
def is_viable_uop(self) -> bool:
402
"""Whether this instruction is viable as a uop."""
403
if self.always_exits:
404
return False
405
if self.instr_flags.HAS_ARG_FLAG:
406
# If the instruction uses oparg, it cannot use any caches
407
if self.active_caches:
408
return False
409
else:
410
# If it doesn't use oparg, it can have one cache entry
411
if len(self.active_caches) > 1:
412
return False
413
for forbidden in FORBIDDEN_NAMES_IN_UOPS:
414
# TODO: Don't check in '#ifdef ENABLE_SPECIALIZATION' regions
415
if variable_used(self.inst, forbidden):
416
return False
417
return True
418
419
def write(self, out: Formatter, tier: Tiers = TIER_ONE) -> None:
420
"""Write one instruction, sans prologue and epilogue."""
421
# Write a static assertion that a family's cache size is correct
422
if family := self.family:
423
if self.name == family.members[0]:
424
if cache_size := family.size:
425
out.emit(
426
f"static_assert({cache_size} == "
427
f'{self.cache_offset}, "incorrect cache size");'
428
)
429
430
# Write input stack effect variable declarations and initializations
431
ieffects = list(reversed(self.input_effects))
432
for i, ieffect in enumerate(ieffects):
433
isize = string_effect_size(
434
list_effect_size([ieff for ieff in ieffects[: i + 1]])
435
)
436
if ieffect.size:
437
src = StackEffect(f"(stack_pointer - {maybe_parenthesize(isize)})", "PyObject **")
438
elif ieffect.cond:
439
src = StackEffect(f"({ieffect.cond}) ? stack_pointer[-{maybe_parenthesize(isize)}] : NULL", "")
440
else:
441
src = StackEffect(f"stack_pointer[-{maybe_parenthesize(isize)}]", "")
442
out.declare(ieffect, src)
443
444
# Write output stack effect variable declarations
445
isize = string_effect_size(list_effect_size(self.input_effects))
446
input_names = {ieffect.name for ieffect in self.input_effects}
447
for i, oeffect in enumerate(self.output_effects):
448
if oeffect.name not in input_names:
449
if oeffect.size:
450
osize = string_effect_size(
451
list_effect_size([oeff for oeff in self.output_effects[:i]])
452
)
453
offset = "stack_pointer"
454
if isize != osize:
455
if isize != "0":
456
offset += f" - ({isize})"
457
if osize != "0":
458
offset += f" + {osize}"
459
src = StackEffect(offset, "PyObject **")
460
out.declare(oeffect, src)
461
else:
462
out.declare(oeffect, None)
463
464
# out.emit(f"next_instr += OPSIZE({self.inst.name}) - 1;")
465
466
self.write_body(out, 0, self.active_caches, tier=tier)
467
468
# Skip the rest if the block always exits
469
if self.always_exits:
470
return
471
472
# Write net stack growth/shrinkage
473
out.stack_adjust(
474
[ieff for ieff in self.input_effects],
475
[oeff for oeff in self.output_effects],
476
)
477
478
# Write output stack effect assignments
479
oeffects = list(reversed(self.output_effects))
480
for i, oeffect in enumerate(oeffects):
481
if oeffect.name in self.unmoved_names:
482
continue
483
osize = string_effect_size(
484
list_effect_size([oeff for oeff in oeffects[: i + 1]])
485
)
486
if oeffect.size:
487
dst = StackEffect(f"stack_pointer - {maybe_parenthesize(osize)}", "PyObject **")
488
else:
489
dst = StackEffect(f"stack_pointer[-{maybe_parenthesize(osize)}]", "")
490
out.assign(dst, oeffect)
491
492
# Write cache effect
493
if tier == TIER_ONE and self.cache_offset:
494
out.emit(f"next_instr += {self.cache_offset};")
495
496
def write_body(
497
self,
498
out: Formatter,
499
dedent: int,
500
active_caches: list[ActiveCacheEffect],
501
tier: Tiers = TIER_ONE,
502
) -> None:
503
"""Write the instruction body."""
504
# Write cache effect variable declarations and initializations
505
for active in active_caches:
506
ceffect = active.effect
507
bits = ceffect.size * BITS_PER_CODE_UNIT
508
if bits == 64:
509
# NOTE: We assume that 64-bit data in the cache
510
# is always an object pointer.
511
# If this becomes false, we need a way to specify
512
# syntactically what type the cache data is.
513
typ = "PyObject *"
514
func = "read_obj"
515
else:
516
typ = f"uint{bits}_t "
517
func = f"read_u{bits}"
518
if tier == TIER_ONE:
519
out.emit(
520
f"{typ}{ceffect.name} = {func}(&next_instr[{active.offset}].cache);"
521
)
522
else:
523
out.emit(f"{typ}{ceffect.name} = ({typ.strip()})operand;")
524
525
# Write the body, substituting a goto for ERROR_IF() and other stuff
526
assert dedent <= 0
527
extra = " " * -dedent
528
names_to_skip = self.unmoved_names | frozenset({UNUSED, "null"})
529
offset = 0
530
context = self.block.context
531
assert context is not None and context.owner is not None
532
filename = context.owner.filename
533
for line in self.block_text:
534
out.set_lineno(self.block_line + offset, filename)
535
offset += 1
536
if m := re.match(r"(\s*)ERROR_IF\((.+), (\w+)\);\s*(?://.*)?$", line):
537
space, cond, label = m.groups()
538
space = extra + space
539
# ERROR_IF() must pop the inputs from the stack.
540
# The code block is responsible for DECREF()ing them.
541
# NOTE: If the label doesn't exist, just add it to ceval.c.
542
543
# Don't pop common input/output effects at the bottom!
544
# These aren't DECREF'ed so they can stay.
545
ieffs = list(self.input_effects)
546
oeffs = list(self.output_effects)
547
while ieffs and oeffs and ieffs[0] == oeffs[0]:
548
ieffs.pop(0)
549
oeffs.pop(0)
550
ninputs, symbolic = list_effect_size(ieffs)
551
if ninputs:
552
label = f"pop_{ninputs}_{label}"
553
if symbolic:
554
out.write_raw(
555
f"{space}if ({cond}) {{ STACK_SHRINK({symbolic}); goto {label}; }}\n"
556
)
557
else:
558
out.write_raw(f"{space}if ({cond}) goto {label};\n")
559
elif m := re.match(r"(\s*)DECREF_INPUTS\(\);\s*(?://.*)?$", line):
560
out.reset_lineno()
561
space = extra + m.group(1)
562
for ieff in self.input_effects:
563
if ieff.name in names_to_skip:
564
continue
565
if ieff.size:
566
out.write_raw(
567
f"{space}for (int _i = {ieff.size}; --_i >= 0;) {{\n"
568
)
569
out.write_raw(f"{space} Py_DECREF({ieff.name}[_i]);\n")
570
out.write_raw(f"{space}}}\n")
571
else:
572
decref = "XDECREF" if ieff.cond else "DECREF"
573
out.write_raw(f"{space}Py_{decref}({ieff.name});\n")
574
else:
575
out.write_raw(extra + line)
576
out.reset_lineno()
577
578
579
InstructionOrCacheEffect = Instruction | parser.CacheEffect
580
StackEffectMapping = list[tuple[StackEffect, StackEffect]]
581
582
583
@dataclasses.dataclass
584
class Component:
585
instr: Instruction
586
input_mapping: StackEffectMapping
587
output_mapping: StackEffectMapping
588
active_caches: list[ActiveCacheEffect]
589
590
def write_body(self, out: Formatter) -> None:
591
with out.block(""):
592
input_names = {ieffect.name for _, ieffect in self.input_mapping}
593
for var, ieffect in self.input_mapping:
594
out.declare(ieffect, var)
595
for _, oeffect in self.output_mapping:
596
if oeffect.name not in input_names:
597
out.declare(oeffect, None)
598
599
self.instr.write_body(out, -4, self.active_caches)
600
601
for var, oeffect in self.output_mapping:
602
out.assign(var, oeffect)
603
604
605
MacroParts = list[Component | parser.CacheEffect]
606
607
608
@dataclasses.dataclass
609
class MacroInstruction:
610
"""A macro instruction."""
611
612
name: str
613
stack: list[StackEffect]
614
initial_sp: int
615
final_sp: int
616
instr_fmt: str
617
instr_flags: InstructionFlags
618
macro: parser.Macro
619
parts: MacroParts
620
cache_offset: int
621
predicted: bool = False
622
623
624
@dataclasses.dataclass
625
class PseudoInstruction:
626
"""A pseudo instruction."""
627
628
name: str
629
targets: list[Instruction]
630
instr_fmt: str
631
instr_flags: InstructionFlags
632
633
634
@dataclasses.dataclass
635
class OverriddenInstructionPlaceHolder:
636
name: str
637
638
639
AnyInstruction = Instruction | MacroInstruction | PseudoInstruction
640
INSTR_FMT_PREFIX = "INSTR_FMT_"
641
642
643
class Analyzer:
644
"""Parse input, analyze it, and write to output."""
645
646
input_filenames: list[str]
647
output_filename: str
648
metadata_filename: str
649
pymetadata_filename: str
650
executor_filename: str
651
errors: int = 0
652
emit_line_directives: bool = False
653
654
def __init__(
655
self,
656
input_filenames: list[str],
657
output_filename: str,
658
metadata_filename: str,
659
pymetadata_filename: str,
660
executor_filename: str,
661
):
662
"""Read the input file."""
663
self.input_filenames = input_filenames
664
self.output_filename = output_filename
665
self.metadata_filename = metadata_filename
666
self.pymetadata_filename = pymetadata_filename
667
self.executor_filename = executor_filename
668
669
def error(self, msg: str, node: parser.Node) -> None:
670
lineno = 0
671
filename = "<unknown file>"
672
if context := node.context:
673
filename = context.owner.filename
674
# Use line number of first non-comment in the node
675
for token in context.owner.tokens[context.begin : context.end]:
676
lineno = token.line
677
if token.kind != "COMMENT":
678
break
679
print(f"{filename}:{lineno}: {msg}", file=sys.stderr)
680
self.errors += 1
681
682
everything: list[
683
parser.InstDef | parser.Macro | parser.Pseudo | OverriddenInstructionPlaceHolder
684
]
685
instrs: dict[str, Instruction] # Includes ops
686
macros: dict[str, parser.Macro]
687
macro_instrs: dict[str, MacroInstruction]
688
families: dict[str, parser.Family]
689
pseudos: dict[str, parser.Pseudo]
690
pseudo_instrs: dict[str, PseudoInstruction]
691
692
def parse(self) -> None:
693
"""Parse the source text.
694
695
We only want the parser to see the stuff between the
696
begin and end markers.
697
"""
698
699
self.everything = []
700
self.instrs = {}
701
self.macros = {}
702
self.families = {}
703
self.pseudos = {}
704
705
instrs_idx: dict[str, int] = dict()
706
707
for filename in self.input_filenames:
708
self.parse_file(filename, instrs_idx)
709
710
files = " + ".join(self.input_filenames)
711
print(
712
f"Read {len(self.instrs)} instructions/ops, "
713
f"{len(self.macros)} macros, {len(self.pseudos)} pseudos, "
714
f"and {len(self.families)} families from {files}",
715
file=sys.stderr,
716
)
717
718
def parse_file(self, filename: str, instrs_idx: dict[str, int]) -> None:
719
with open(filename) as file:
720
src = file.read()
721
722
filename = os.path.relpath(filename, ROOT)
723
# Make filename more user-friendly and less platform-specific
724
filename = filename.replace("\\", "/")
725
if filename.startswith("./"):
726
filename = filename[2:]
727
psr = parser.Parser(src, filename=filename)
728
729
# Skip until begin marker
730
while tkn := psr.next(raw=True):
731
if tkn.text == BEGIN_MARKER:
732
break
733
else:
734
raise psr.make_syntax_error(
735
f"Couldn't find {BEGIN_MARKER!r} in {psr.filename}"
736
)
737
start = psr.getpos()
738
739
# Find end marker, then delete everything after it
740
while tkn := psr.next(raw=True):
741
if tkn.text == END_MARKER:
742
break
743
del psr.tokens[psr.getpos() - 1 :]
744
745
# Parse from start
746
psr.setpos(start)
747
thing: parser.InstDef | parser.Macro | parser.Pseudo | parser.Family | None
748
thing_first_token = psr.peek()
749
while thing := psr.definition():
750
if ws := [w for w in RESERVED_WORDS if variable_used(thing, w)]:
751
self.error(f"'{ws[0]}' is a reserved word. {RESERVED_WORDS[ws[0]]}", thing)
752
753
match thing:
754
case parser.InstDef(name=name):
755
if name in self.instrs:
756
if not thing.override:
757
raise psr.make_syntax_error(
758
f"Duplicate definition of '{name}' @ {thing.context} "
759
f"previous definition @ {self.instrs[name].inst.context}",
760
thing_first_token,
761
)
762
self.everything[instrs_idx[name]] = OverriddenInstructionPlaceHolder(name=name)
763
if name not in self.instrs and thing.override:
764
raise psr.make_syntax_error(
765
f"Definition of '{name}' @ {thing.context} is supposed to be "
766
"an override but no previous definition exists.",
767
thing_first_token,
768
)
769
self.instrs[name] = Instruction(thing)
770
instrs_idx[name] = len(self.everything)
771
self.everything.append(thing)
772
case parser.Macro(name):
773
self.macros[name] = thing
774
self.everything.append(thing)
775
case parser.Family(name):
776
self.families[name] = thing
777
case parser.Pseudo(name):
778
self.pseudos[name] = thing
779
self.everything.append(thing)
780
case _:
781
typing.assert_never(thing)
782
if not psr.eof():
783
raise psr.make_syntax_error(f"Extra stuff at the end of {filename}")
784
785
def analyze(self) -> None:
786
"""Analyze the inputs.
787
788
Raises SystemExit if there is an error.
789
"""
790
self.analyze_macros_and_pseudos()
791
self.find_predictions()
792
self.map_families()
793
self.check_families()
794
795
def find_predictions(self) -> None:
796
"""Find the instructions that need PREDICTED() labels."""
797
for instr in self.instrs.values():
798
targets: set[str] = set()
799
for line in instr.block_text:
800
if m := re.match(RE_PREDICTED, line):
801
targets.add(m.group(1))
802
for target in targets:
803
if target_instr := self.instrs.get(target):
804
target_instr.predicted = True
805
elif target_macro := self.macro_instrs.get(target):
806
target_macro.predicted = True
807
else:
808
self.error(
809
f"Unknown instruction {target!r} predicted in {instr.name!r}",
810
instr.inst, # TODO: Use better location
811
)
812
813
def map_families(self) -> None:
814
"""Link instruction names back to their family, if they have one."""
815
for family in self.families.values():
816
for member in family.members:
817
if member_instr := self.instrs.get(member):
818
if member_instr.family not in (family, None):
819
self.error(
820
f"Instruction {member} is a member of multiple families "
821
f"({member_instr.family.name}, {family.name}).",
822
family,
823
)
824
else:
825
member_instr.family = family
826
elif member_macro := self.macro_instrs.get(member):
827
for part in member_macro.parts:
828
if isinstance(part, Component):
829
if part.instr.family not in (family, None):
830
self.error(
831
f"Component {part.instr.name} of macro {member} "
832
f"is a member of multiple families "
833
f"({part.instr.family.name}, {family.name}).",
834
family,
835
)
836
else:
837
part.instr.family = family
838
else:
839
self.error(
840
f"Unknown instruction {member!r} referenced in family {family.name!r}",
841
family,
842
)
843
844
def check_families(self) -> None:
845
"""Check each family:
846
847
- Must have at least 2 members
848
- All members must be known instructions
849
- All members must have the same cache, input and output effects
850
"""
851
for family in self.families.values():
852
if len(family.members) < 2:
853
self.error(f"Family {family.name!r} has insufficient members", family)
854
members = [
855
member
856
for member in family.members
857
if member in self.instrs or member in self.macro_instrs
858
]
859
if members != family.members:
860
unknown = set(family.members) - set(members)
861
self.error(
862
f"Family {family.name!r} has unknown members: {unknown}", family
863
)
864
if len(members) < 2:
865
continue
866
expected_effects = self.effect_counts(members[0])
867
for member in members[1:]:
868
member_effects = self.effect_counts(member)
869
if member_effects != expected_effects:
870
self.error(
871
f"Family {family.name!r} has inconsistent "
872
f"(cache, input, output) effects:\n"
873
f" {family.members[0]} = {expected_effects}; "
874
f"{member} = {member_effects}",
875
family,
876
)
877
878
def effect_counts(self, name: str) -> tuple[int, int, int]:
879
if instr := self.instrs.get(name):
880
cache = instr.cache_offset
881
input = len(instr.input_effects)
882
output = len(instr.output_effects)
883
elif mac := self.macro_instrs.get(name):
884
cache = mac.cache_offset
885
input, output = 0, 0
886
for part in mac.parts:
887
if isinstance(part, Component):
888
# A component may pop what the previous component pushed,
889
# so we offset the input/output counts by that.
890
delta_i = len(part.instr.input_effects)
891
delta_o = len(part.instr.output_effects)
892
offset = min(delta_i, output)
893
input += delta_i - offset
894
output += delta_o - offset
895
else:
896
assert False, f"Unknown instruction {name!r}"
897
return cache, input, output
898
899
def analyze_macros_and_pseudos(self) -> None:
900
"""Analyze each macro and pseudo instruction."""
901
self.macro_instrs = {}
902
self.pseudo_instrs = {}
903
for name, macro in self.macros.items():
904
self.macro_instrs[name] = self.analyze_macro(macro)
905
for name, pseudo in self.pseudos.items():
906
self.pseudo_instrs[name] = self.analyze_pseudo(pseudo)
907
908
def analyze_macro(self, macro: parser.Macro) -> MacroInstruction:
909
components = self.check_macro_components(macro)
910
stack, initial_sp = self.stack_analysis(components)
911
sp = initial_sp
912
parts: MacroParts = []
913
flags = InstructionFlags.newEmpty()
914
offset = 0
915
for component in components:
916
match component:
917
case parser.CacheEffect() as ceffect:
918
parts.append(ceffect)
919
offset += ceffect.size
920
case Instruction() as instr:
921
part, sp, offset = self.analyze_instruction(instr, stack, sp, offset)
922
parts.append(part)
923
flags.add(instr.instr_flags)
924
case _:
925
typing.assert_never(component)
926
final_sp = sp
927
format = "IB"
928
if offset:
929
format += "C" + "0"*(offset-1)
930
return MacroInstruction(
931
macro.name, stack, initial_sp, final_sp, format, flags, macro, parts, offset
932
)
933
934
def analyze_pseudo(self, pseudo: parser.Pseudo) -> PseudoInstruction:
935
targets = [self.instrs[target] for target in pseudo.targets]
936
assert targets
937
# Make sure the targets have the same fmt
938
fmts = list(set([t.instr_fmt for t in targets]))
939
assert(len(fmts) == 1)
940
assert(len(list(set([t.instr_flags.bitmap() for t in targets]))) == 1)
941
return PseudoInstruction(pseudo.name, targets, fmts[0], targets[0].instr_flags)
942
943
def analyze_instruction(
944
self, instr: Instruction, stack: list[StackEffect], sp: int, offset: int
945
) -> tuple[Component, int, int]:
946
input_mapping: StackEffectMapping = []
947
for ieffect in reversed(instr.input_effects):
948
sp -= 1
949
input_mapping.append((stack[sp], ieffect))
950
output_mapping: StackEffectMapping = []
951
for oeffect in instr.output_effects:
952
output_mapping.append((stack[sp], oeffect))
953
sp += 1
954
active_effects: list[ActiveCacheEffect] = []
955
for ceffect in instr.cache_effects:
956
if ceffect.name != UNUSED:
957
active_effects.append(ActiveCacheEffect(ceffect, offset))
958
offset += ceffect.size
959
return Component(instr, input_mapping, output_mapping, active_effects), sp, offset
960
961
def check_macro_components(
962
self, macro: parser.Macro
963
) -> list[InstructionOrCacheEffect]:
964
components: list[InstructionOrCacheEffect] = []
965
for uop in macro.uops:
966
match uop:
967
case parser.OpName(name):
968
if name not in self.instrs:
969
self.error(f"Unknown instruction {name!r}", macro)
970
components.append(self.instrs[name])
971
case parser.CacheEffect():
972
components.append(uop)
973
case _:
974
typing.assert_never(uop)
975
return components
976
977
def stack_analysis(
978
self, components: typing.Iterable[InstructionOrCacheEffect]
979
) -> tuple[list[StackEffect], int]:
980
"""Analyze a macro.
981
982
Ignore cache effects.
983
984
Return the list of variables (as StackEffects) and the initial stack pointer.
985
"""
986
lowest = current = highest = 0
987
conditions: dict[int, str] = {} # Indexed by 'current'.
988
last_instr: Instruction | None = None
989
for thing in components:
990
if isinstance(thing, Instruction):
991
last_instr = thing
992
for thing in components:
993
match thing:
994
case Instruction() as instr:
995
if any(
996
eff.size for eff in instr.input_effects + instr.output_effects
997
):
998
# TODO: Eventually this will be needed, at least for macros.
999
self.error(
1000
f"Instruction {instr.name!r} has variable-sized stack effect, "
1001
"which are not supported in macro instructions",
1002
instr.inst, # TODO: Pass name+location of macro
1003
)
1004
if any(eff.cond for eff in instr.input_effects):
1005
self.error(
1006
f"Instruction {instr.name!r} has conditional input stack effect, "
1007
"which are not supported in macro instructions",
1008
instr.inst, # TODO: Pass name+location of macro
1009
)
1010
if any(eff.cond for eff in instr.output_effects) and instr is not last_instr:
1011
self.error(
1012
f"Instruction {instr.name!r} has conditional output stack effect, "
1013
"but is not the last instruction in a macro",
1014
instr.inst, # TODO: Pass name+location of macro
1015
)
1016
current -= len(instr.input_effects)
1017
lowest = min(lowest, current)
1018
for eff in instr.output_effects:
1019
if eff.cond:
1020
conditions[current] = eff.cond
1021
current += 1
1022
highest = max(highest, current)
1023
case parser.CacheEffect():
1024
pass
1025
case _:
1026
typing.assert_never(thing)
1027
# At this point, 'current' is the net stack effect,
1028
# and 'lowest' and 'highest' are the extremes.
1029
# Note that 'lowest' may be negative.
1030
stack = [
1031
StackEffect(f"_tmp_{i}", "", conditions.get(highest - i, ""))
1032
for i in reversed(range(1, highest - lowest + 1))
1033
]
1034
return stack, -lowest
1035
1036
def get_stack_effect_info(
1037
self, thing: parser.InstDef | parser.Macro | parser.Pseudo
1038
) -> tuple[AnyInstruction | None, str | None, str | None]:
1039
def effect_str(effects: list[StackEffect]) -> str:
1040
n_effect, sym_effect = list_effect_size(effects)
1041
if sym_effect:
1042
return f"{sym_effect} + {n_effect}" if n_effect else sym_effect
1043
return str(n_effect)
1044
1045
instr: AnyInstruction | None
1046
match thing:
1047
case parser.InstDef():
1048
if thing.kind != "op":
1049
instr = self.instrs[thing.name]
1050
popped = effect_str(instr.input_effects)
1051
pushed = effect_str(instr.output_effects)
1052
else:
1053
instr = None
1054
popped = ""
1055
pushed = ""
1056
case parser.Macro():
1057
instr = self.macro_instrs[thing.name]
1058
parts = [comp for comp in instr.parts if isinstance(comp, Component)]
1059
# Note: stack_analysis() already verifies that macro components
1060
# have no variable-sized stack effects.
1061
low = 0
1062
sp = 0
1063
high = 0
1064
pushed_symbolic: list[str] = []
1065
for comp in parts:
1066
for effect in comp.instr.input_effects:
1067
assert not effect.cond, effect
1068
assert not effect.size, effect
1069
sp -= 1
1070
low = min(low, sp)
1071
for effect in comp.instr.output_effects:
1072
assert not effect.size, effect
1073
if effect.cond:
1074
pushed_symbolic.append(maybe_parenthesize(f"{maybe_parenthesize(effect.cond)} ? 1 : 0"))
1075
sp += 1
1076
high = max(sp, high)
1077
if high != max(0, sp):
1078
# If you get this, intermediate stack growth occurs,
1079
# and stack size calculations may go awry.
1080
# E.g. [push, pop]. The fix would be for stack size
1081
# calculations to use the micro ops.
1082
self.error("Macro has virtual stack growth", thing)
1083
popped = str(-low)
1084
pushed_symbolic.append(str(sp - low - len(pushed_symbolic)))
1085
pushed = " + ".join(pushed_symbolic)
1086
case parser.Pseudo():
1087
instr = self.pseudo_instrs[thing.name]
1088
popped = pushed = None
1089
# Calculate stack effect, and check that it's the the same
1090
# for all targets.
1091
for target in self.pseudos[thing.name].targets:
1092
target_instr = self.instrs.get(target)
1093
# Currently target is always an instr. This could change
1094
# in the future, e.g., if we have a pseudo targetting a
1095
# macro instruction.
1096
assert target_instr
1097
target_popped = effect_str(target_instr.input_effects)
1098
target_pushed = effect_str(target_instr.output_effects)
1099
if popped is None and pushed is None:
1100
popped, pushed = target_popped, target_pushed
1101
else:
1102
assert popped == target_popped
1103
assert pushed == target_pushed
1104
case _:
1105
typing.assert_never(thing)
1106
return instr, popped, pushed
1107
1108
def write_stack_effect_functions(self) -> None:
1109
popped_data: list[tuple[AnyInstruction, str]] = []
1110
pushed_data: list[tuple[AnyInstruction, str]] = []
1111
for thing in self.everything:
1112
if isinstance(thing, OverriddenInstructionPlaceHolder):
1113
continue
1114
instr, popped, pushed = self.get_stack_effect_info(thing)
1115
if instr is not None:
1116
assert popped is not None and pushed is not None
1117
popped_data.append((instr, popped))
1118
pushed_data.append((instr, pushed))
1119
1120
def write_function(
1121
direction: str, data: list[tuple[AnyInstruction, str]]
1122
) -> None:
1123
self.out.emit("")
1124
self.out.emit("#ifndef NEED_OPCODE_METADATA")
1125
self.out.emit(f"extern int _PyOpcode_num_{direction}(int opcode, int oparg, bool jump);")
1126
self.out.emit("#else")
1127
self.out.emit("int")
1128
self.out.emit(f"_PyOpcode_num_{direction}(int opcode, int oparg, bool jump) {{")
1129
self.out.emit(" switch(opcode) {")
1130
for instr, effect in data:
1131
self.out.emit(f" case {instr.name}:")
1132
self.out.emit(f" return {effect};")
1133
self.out.emit(" default:")
1134
self.out.emit(" return -1;")
1135
self.out.emit(" }")
1136
self.out.emit("}")
1137
self.out.emit("#endif")
1138
1139
write_function("popped", popped_data)
1140
write_function("pushed", pushed_data)
1141
self.out.emit("")
1142
1143
def from_source_files(self) -> str:
1144
paths = f"\n{self.out.comment} ".join(
1145
os.path.relpath(filename, ROOT).replace(os.path.sep, posixpath.sep)
1146
for filename in self.input_filenames
1147
)
1148
return f"{self.out.comment} from:\n{self.out.comment} {paths}\n"
1149
1150
def write_provenance_header(self):
1151
self.out.write_raw(f"{self.out.comment} This file is generated by {THIS}\n")
1152
self.out.write_raw(self.from_source_files())
1153
self.out.write_raw(f"{self.out.comment} Do not edit!\n")
1154
1155
def write_metadata(self) -> None:
1156
"""Write instruction metadata to output file."""
1157
1158
# Compute the set of all instruction formats.
1159
all_formats: set[str] = set()
1160
for thing in self.everything:
1161
match thing:
1162
case OverriddenInstructionPlaceHolder():
1163
continue
1164
case parser.InstDef():
1165
format = self.instrs[thing.name].instr_fmt
1166
case parser.Macro():
1167
format = self.macro_instrs[thing.name].instr_fmt
1168
case parser.Pseudo():
1169
format = None
1170
for target in self.pseudos[thing.name].targets:
1171
target_instr = self.instrs.get(target)
1172
assert target_instr
1173
if format is None:
1174
format = target_instr.instr_fmt
1175
else:
1176
assert format == target_instr.instr_fmt
1177
case _:
1178
typing.assert_never(thing)
1179
all_formats.add(format)
1180
# Turn it into a list of enum definitions.
1181
format_enums = [INSTR_FMT_PREFIX + format for format in sorted(all_formats)]
1182
1183
with open(self.metadata_filename, "w") as f:
1184
# Create formatter
1185
self.out = Formatter(f, 0)
1186
1187
self.write_provenance_header()
1188
1189
self.write_pseudo_instrs()
1190
1191
self.out.emit("")
1192
self.write_uop_items(lambda name, counter: f"#define {name} {counter}")
1193
1194
self.write_stack_effect_functions()
1195
1196
# Write type definitions
1197
self.out.emit(f"enum InstructionFormat {{ {', '.join(format_enums)} }};")
1198
1199
InstructionFlags.emit_macros(self.out)
1200
1201
with self.out.block("struct opcode_metadata", ";"):
1202
self.out.emit("bool valid_entry;")
1203
self.out.emit("enum InstructionFormat instr_format;")
1204
self.out.emit("int flags;")
1205
self.out.emit("")
1206
1207
with self.out.block("struct opcode_macro_expansion", ";"):
1208
self.out.emit("int nuops;")
1209
self.out.emit("struct { int16_t uop; int8_t size; int8_t offset; } uops[8];")
1210
self.out.emit("")
1211
1212
self.out.emit("")
1213
self.out.emit("#define OPCODE_METADATA_FMT(OP) "
1214
"(_PyOpcode_opcode_metadata[(OP)].instr_format)")
1215
self.out.emit("#define SAME_OPCODE_METADATA(OP1, OP2) \\")
1216
self.out.emit(" (OPCODE_METADATA_FMT(OP1) == OPCODE_METADATA_FMT(OP2))")
1217
self.out.emit("")
1218
1219
# Write metadata array declaration
1220
self.out.emit("#ifndef NEED_OPCODE_METADATA")
1221
self.out.emit("extern const struct opcode_metadata _PyOpcode_opcode_metadata[512];")
1222
self.out.emit("extern const struct opcode_macro_expansion _PyOpcode_macro_expansion[256];")
1223
self.out.emit("#ifdef Py_DEBUG")
1224
self.out.emit("extern const char * const _PyOpcode_uop_name[512];")
1225
self.out.emit("#endif")
1226
self.out.emit("#else")
1227
1228
self.out.emit("const struct opcode_metadata _PyOpcode_opcode_metadata[512] = {")
1229
1230
# Write metadata for each instruction
1231
for thing in self.everything:
1232
match thing:
1233
case OverriddenInstructionPlaceHolder():
1234
continue
1235
case parser.InstDef():
1236
if thing.kind != "op":
1237
self.write_metadata_for_inst(self.instrs[thing.name])
1238
case parser.Macro():
1239
self.write_metadata_for_macro(self.macro_instrs[thing.name])
1240
case parser.Pseudo():
1241
self.write_metadata_for_pseudo(self.pseudo_instrs[thing.name])
1242
case _:
1243
typing.assert_never(thing)
1244
1245
# Write end of array
1246
self.out.emit("};")
1247
1248
with self.out.block(
1249
"const struct opcode_macro_expansion _PyOpcode_macro_expansion[256] =",
1250
";",
1251
):
1252
# Write macro expansion for each non-pseudo instruction
1253
for thing in self.everything:
1254
match thing:
1255
case OverriddenInstructionPlaceHolder():
1256
pass
1257
case parser.InstDef(name=name):
1258
instr = self.instrs[name]
1259
# Since an 'op' is not a bytecode, it has no expansion; but 'inst' is
1260
if instr.kind == "inst" and instr.is_viable_uop():
1261
# Construct a dummy Component -- input/output mappings are not used
1262
part = Component(instr, [], [], instr.active_caches)
1263
self.write_macro_expansions(instr.name, [part])
1264
case parser.Macro():
1265
mac = self.macro_instrs[thing.name]
1266
self.write_macro_expansions(mac.name, mac.parts)
1267
case parser.Pseudo():
1268
pass
1269
case _:
1270
typing.assert_never(thing)
1271
1272
self.out.emit("#ifdef Py_DEBUG")
1273
with self.out.block("const char * const _PyOpcode_uop_name[512] =", ";"):
1274
self.write_uop_items(lambda name, counter: f"[{counter}] = \"{name}\",")
1275
self.out.emit("#endif")
1276
1277
self.out.emit("#endif")
1278
1279
with open(self.pymetadata_filename, "w") as f:
1280
# Create formatter
1281
self.out = Formatter(f, 0, comment = "#")
1282
1283
self.write_provenance_header()
1284
1285
self.out.emit("")
1286
self.out.emit("_specializations = {")
1287
for name, family in self.families.items():
1288
assert len(family.members) > 1
1289
with self.out.indent():
1290
self.out.emit(f"\"{family.members[0]}\": [")
1291
with self.out.indent():
1292
for m in family.members[1:]:
1293
self.out.emit(f"\"{m}\",")
1294
self.out.emit(f"],")
1295
self.out.emit("}")
1296
1297
# Handle special case
1298
self.out.emit("")
1299
self.out.emit("# An irregular case:")
1300
self.out.emit(
1301
"_specializations[\"BINARY_OP\"].append("
1302
"\"BINARY_OP_INPLACE_ADD_UNICODE\")")
1303
1304
# Make list of specialized instructions
1305
self.out.emit("")
1306
self.out.emit(
1307
"_specialized_instructions = ["
1308
"opcode for family in _specializations.values() for opcode in family"
1309
"]")
1310
1311
def write_pseudo_instrs(self) -> None:
1312
"""Write the IS_PSEUDO_INSTR macro"""
1313
self.out.emit("\n\n#define IS_PSEUDO_INSTR(OP) ( \\")
1314
for op in self.pseudos:
1315
self.out.emit(f" ((OP) == {op}) || \\")
1316
self.out.emit(f" 0)")
1317
1318
def write_uop_items(self, make_text: typing.Callable[[str, int], str]) -> None:
1319
"""Write '#define XXX NNN' for each uop"""
1320
counter = 300 # TODO: Avoid collision with pseudo instructions
1321
def add(name: str) -> None:
1322
nonlocal counter
1323
self.out.emit(make_text(name, counter))
1324
counter += 1
1325
add("EXIT_TRACE")
1326
add("SET_IP")
1327
for instr in self.instrs.values():
1328
if instr.kind == "op" and instr.is_viable_uop():
1329
add(instr.name)
1330
1331
def write_macro_expansions(self, name: str, parts: MacroParts) -> None:
1332
"""Write the macro expansions for a macro-instruction."""
1333
# TODO: Refactor to share code with write_cody(), is_viaible_uop(), etc.
1334
offset = 0 # Cache effect offset
1335
expansions: list[tuple[str, int, int]] = [] # [(name, size, offset), ...]
1336
for part in parts:
1337
if isinstance(part, Component):
1338
# All component instructions must be viable uops
1339
if not part.instr.is_viable_uop():
1340
print(f"NOTE: Part {part.instr.name} of {name} is not a viable uop")
1341
return
1342
if part.instr.instr_flags.HAS_ARG_FLAG or not part.active_caches:
1343
size, offset = 0, 0
1344
else:
1345
# If this assert triggers, is_viable_uops() lied
1346
assert len(part.active_caches) == 1, (name, part.instr.name)
1347
cache = part.active_caches[0]
1348
size, offset = cache.effect.size, cache.offset
1349
expansions.append((part.instr.name, size, offset))
1350
assert len(expansions) > 0, f"Macro {name} has empty expansion?!"
1351
pieces = [f"{{ {name}, {size}, {offset} }}" for name, size, offset in expansions]
1352
self.out.emit(
1353
f"[{name}] = "
1354
f"{{ .nuops = {len(expansions)}, .uops = {{ {', '.join(pieces)} }} }},"
1355
)
1356
1357
def emit_metadata_entry(
1358
self, name: str, fmt: str, flags: InstructionFlags
1359
) -> None:
1360
flag_names = flags.names(value=True)
1361
if not flag_names:
1362
flag_names.append("0")
1363
self.out.emit(
1364
f" [{name}] = {{ true, {INSTR_FMT_PREFIX}{fmt},"
1365
f" {' | '.join(flag_names)} }},"
1366
)
1367
1368
def write_metadata_for_inst(self, instr: Instruction) -> None:
1369
"""Write metadata for a single instruction."""
1370
self.emit_metadata_entry(instr.name, instr.instr_fmt, instr.instr_flags)
1371
1372
def write_metadata_for_macro(self, mac: MacroInstruction) -> None:
1373
"""Write metadata for a macro-instruction."""
1374
self.emit_metadata_entry(mac.name, mac.instr_fmt, mac.instr_flags)
1375
1376
def write_metadata_for_pseudo(self, ps: PseudoInstruction) -> None:
1377
"""Write metadata for a macro-instruction."""
1378
self.emit_metadata_entry(ps.name, ps.instr_fmt, ps.instr_flags)
1379
1380
def write_instructions(self) -> None:
1381
"""Write instructions to output file."""
1382
with open(self.output_filename, "w") as f:
1383
# Create formatter
1384
self.out = Formatter(f, 8, self.emit_line_directives)
1385
1386
self.write_provenance_header()
1387
1388
# Write and count instructions of all kinds
1389
n_instrs = 0
1390
n_macros = 0
1391
n_pseudos = 0
1392
for thing in self.everything:
1393
match thing:
1394
case OverriddenInstructionPlaceHolder():
1395
self.write_overridden_instr_place_holder(thing)
1396
case parser.InstDef():
1397
if thing.kind != "op":
1398
n_instrs += 1
1399
self.write_instr(self.instrs[thing.name])
1400
case parser.Macro():
1401
n_macros += 1
1402
self.write_macro(self.macro_instrs[thing.name])
1403
case parser.Pseudo():
1404
n_pseudos += 1
1405
case _:
1406
typing.assert_never(thing)
1407
1408
print(
1409
f"Wrote {n_instrs} instructions, {n_macros} macros, "
1410
f"and {n_pseudos} pseudos to {self.output_filename}",
1411
file=sys.stderr,
1412
)
1413
1414
def write_executor_instructions(self) -> None:
1415
"""Generate cases for the Tier 2 interpreter."""
1416
with open(self.executor_filename, "w") as f:
1417
self.out = Formatter(f, 8, self.emit_line_directives)
1418
self.write_provenance_header()
1419
for thing in self.everything:
1420
match thing:
1421
case OverriddenInstructionPlaceHolder():
1422
# TODO: Is this helpful?
1423
self.write_overridden_instr_place_holder(thing)
1424
case parser.InstDef():
1425
instr = self.instrs[thing.name]
1426
if instr.is_viable_uop():
1427
self.out.emit("")
1428
with self.out.block(f"case {thing.name}:"):
1429
instr.write(self.out, tier=TIER_TWO)
1430
self.out.emit("break;")
1431
case parser.Macro():
1432
pass
1433
case parser.Pseudo():
1434
pass
1435
case _:
1436
typing.assert_never(thing)
1437
print(
1438
f"Wrote some stuff to {self.executor_filename}",
1439
file=sys.stderr,
1440
)
1441
1442
def write_overridden_instr_place_holder(self,
1443
place_holder: OverriddenInstructionPlaceHolder) -> None:
1444
self.out.emit("")
1445
self.out.emit(
1446
f"{self.out.comment} TARGET({place_holder.name}) overridden by later definition")
1447
1448
def write_instr(self, instr: Instruction) -> None:
1449
name = instr.name
1450
self.out.emit("")
1451
if instr.inst.override:
1452
self.out.emit("{self.out.comment} Override")
1453
with self.out.block(f"TARGET({name})"):
1454
if instr.predicted:
1455
self.out.emit(f"PREDICTED({name});")
1456
instr.write(self.out)
1457
if not instr.always_exits:
1458
if instr.check_eval_breaker:
1459
self.out.emit("CHECK_EVAL_BREAKER();")
1460
self.out.emit(f"DISPATCH();")
1461
1462
def write_macro(self, mac: MacroInstruction) -> None:
1463
"""Write code for a macro instruction."""
1464
last_instr: Instruction | None = None
1465
with self.wrap_macro(mac):
1466
cache_adjust = 0
1467
for part in mac.parts:
1468
match part:
1469
case parser.CacheEffect(size=size):
1470
cache_adjust += size
1471
case Component() as comp:
1472
last_instr = comp.instr
1473
comp.write_body(self.out)
1474
cache_adjust += comp.instr.cache_offset
1475
1476
if cache_adjust:
1477
self.out.emit(f"next_instr += {cache_adjust};")
1478
1479
if (
1480
last_instr
1481
and (family := last_instr.family)
1482
and mac.name == family.members[0]
1483
and (cache_size := family.size)
1484
):
1485
self.out.emit(
1486
f"static_assert({cache_size} == "
1487
f'{cache_adjust}, "incorrect cache size");'
1488
)
1489
1490
@contextlib.contextmanager
1491
def wrap_macro(self, mac: MacroInstruction):
1492
"""Boilerplate for macro instructions."""
1493
# TODO: Somewhere (where?) make it so that if one instruction
1494
# has an output that is input to another, and the variable names
1495
# and types match and don't conflict with other instructions,
1496
# that variable is declared with the right name and type in the
1497
# outer block, rather than trusting the compiler to optimize it.
1498
self.out.emit("")
1499
with self.out.block(f"TARGET({mac.name})"):
1500
if mac.predicted:
1501
self.out.emit(f"PREDICTED({mac.name});")
1502
1503
# The input effects should have no conditionals.
1504
# Only the output effects do (for now).
1505
ieffects = [
1506
StackEffect(eff.name, eff.type) if eff.cond else eff
1507
for eff in mac.stack
1508
]
1509
1510
for i, var in reversed(list(enumerate(ieffects))):
1511
src = None
1512
if i < mac.initial_sp:
1513
src = StackEffect(f"stack_pointer[-{mac.initial_sp - i}]", "")
1514
self.out.declare(var, src)
1515
1516
yield
1517
1518
self.out.stack_adjust(ieffects[:mac.initial_sp], mac.stack[:mac.final_sp])
1519
1520
for i, var in enumerate(reversed(mac.stack[: mac.final_sp]), 1):
1521
dst = StackEffect(f"stack_pointer[-{i}]", "")
1522
self.out.assign(dst, var)
1523
1524
self.out.emit(f"DISPATCH();")
1525
1526
1527
def extract_block_text(block: parser.Block) -> tuple[list[str], bool, int]:
1528
# Get lines of text with proper dedent
1529
blocklines = block.text.splitlines(True)
1530
first_token: lx.Token = block.tokens[0] # IndexError means the context is broken
1531
block_line = first_token.begin[0]
1532
1533
# Remove blank lines from both ends
1534
while blocklines and not blocklines[0].strip():
1535
blocklines.pop(0)
1536
block_line += 1
1537
while blocklines and not blocklines[-1].strip():
1538
blocklines.pop()
1539
1540
# Remove leading and trailing braces
1541
assert blocklines and blocklines[0].strip() == "{"
1542
assert blocklines and blocklines[-1].strip() == "}"
1543
blocklines.pop()
1544
blocklines.pop(0)
1545
block_line += 1
1546
1547
# Remove trailing blank lines
1548
while blocklines and not blocklines[-1].strip():
1549
blocklines.pop()
1550
1551
# Separate CHECK_EVAL_BREAKER() macro from end
1552
check_eval_breaker = \
1553
blocklines != [] and blocklines[-1].strip() == "CHECK_EVAL_BREAKER();"
1554
if check_eval_breaker:
1555
del blocklines[-1]
1556
1557
return blocklines, check_eval_breaker, block_line
1558
1559
1560
def always_exits(lines: list[str]) -> bool:
1561
"""Determine whether a block always ends in a return/goto/etc."""
1562
if not lines:
1563
return False
1564
line = lines[-1].rstrip()
1565
# Indent must match exactly (TODO: Do something better)
1566
if line[:12] != " " * 12:
1567
return False
1568
line = line[12:]
1569
return line.startswith(
1570
(
1571
"goto ",
1572
"return ",
1573
"DISPATCH",
1574
"GO_TO_",
1575
"Py_UNREACHABLE()",
1576
"ERROR_IF(true, ",
1577
)
1578
)
1579
1580
1581
def variable_used(node: parser.Node, name: str) -> bool:
1582
"""Determine whether a variable with a given name is used in a node."""
1583
return any(
1584
token.kind == "IDENTIFIER" and token.text == name for token in node.tokens
1585
)
1586
1587
1588
def main():
1589
"""Parse command line, parse input, analyze, write output."""
1590
args = arg_parser.parse_args() # Prints message and sys.exit(2) on error
1591
if len(args.input) == 0:
1592
args.input.append(DEFAULT_INPUT)
1593
1594
# Raises OSError if input unreadable
1595
a = Analyzer(args.input, args.output, args.metadata, args.pymetadata, args.executor_cases)
1596
1597
if args.emit_line_directives:
1598
a.emit_line_directives = True
1599
a.parse() # Raises SyntaxError on failure
1600
a.analyze() # Prints messages and sets a.errors on failure
1601
if a.errors:
1602
sys.exit(f"Found {a.errors} errors")
1603
a.write_instructions() # Raises OSError if output can't be written
1604
a.write_metadata()
1605
a.write_executor_instructions()
1606
1607
1608
if __name__ == "__main__":
1609
main()
1610
1611