pytecode 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,844 @@
1
+ """Constant-pool builder for JVM class files.
2
+
3
+ Provides ``ConstantPoolBuilder``, a mutable accumulator that manages a JVM
4
+ constant pool (§4.4) with deduplication on insertion, symbol-table-style
5
+ lookups, and deterministic ordering.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import copy
11
+ from typing import TYPE_CHECKING
12
+
13
+ __all__ = ["ConstantPoolBuilder"]
14
+
15
+ from .constant_pool import (
16
+ ClassInfo,
17
+ ConstantPoolInfo,
18
+ DoubleInfo,
19
+ DynamicInfo,
20
+ FieldrefInfo,
21
+ FloatInfo,
22
+ IntegerInfo,
23
+ InterfaceMethodrefInfo,
24
+ InvokeDynamicInfo,
25
+ LongInfo,
26
+ MethodHandleInfo,
27
+ MethodrefInfo,
28
+ MethodTypeInfo,
29
+ ModuleInfo,
30
+ NameAndTypeInfo,
31
+ PackageInfo,
32
+ StringInfo,
33
+ Utf8Info,
34
+ )
35
+ from .modified_utf8 import decode_modified_utf8, encode_modified_utf8
36
+
37
+ if TYPE_CHECKING:
38
+ pass
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # JVM constant-pool tag constants (§4.4)
42
+ # ---------------------------------------------------------------------------
43
+
44
+ _TAG_UTF8: int = 1
45
+ _TAG_INTEGER: int = 3
46
+ _TAG_FLOAT: int = 4
47
+ _TAG_LONG: int = 5
48
+ _TAG_DOUBLE: int = 6
49
+ _TAG_CLASS: int = 7
50
+ _TAG_STRING: int = 8
51
+ _TAG_FIELDREF: int = 9
52
+ _TAG_METHODREF: int = 10
53
+ _TAG_INTERFACE_METHODREF: int = 11
54
+ _TAG_NAME_AND_TYPE: int = 12
55
+ _TAG_METHOD_HANDLE: int = 15
56
+ _TAG_METHOD_TYPE: int = 16
57
+ _TAG_DYNAMIC: int = 17
58
+ _TAG_INVOKE_DYNAMIC: int = 18
59
+ _TAG_MODULE: int = 19
60
+ _TAG_PACKAGE: int = 20
61
+
62
+ # The JVM spec allows constant_pool_count up to 65535 (u2). Valid entry indexes
63
+ # are 1 … constant_pool_count-1. Long/Double entries occupy two slots, so the
64
+ # second slot index must also fit within that range.
65
+ _CP_MAX_SINGLE_INDEX: int = 65534 # max index for a single-slot entry
66
+ _CP_MAX_DOUBLE_INDEX: int = 65533 # max index for a double-slot entry (needs +1 gap)
67
+ _UTF8_MAX_BYTES: int = 65535
68
+
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # Internal helpers
72
+ # ---------------------------------------------------------------------------
73
+
74
+ # Deduplication key: a tuple whose elements identify the *content* of an entry,
75
+ # ignoring its index and file offset. Utf8 entries include a bytes element;
76
+ # all other entries contain only int elements.
77
+ type _CPKey = tuple[int | bytes, ...]
78
+
79
+
80
+ def _entry_key(entry: ConstantPoolInfo) -> _CPKey:
81
+ """Return the deduplication key for *entry* based solely on its content."""
82
+ if isinstance(entry, Utf8Info):
83
+ return (_TAG_UTF8, entry.str_bytes)
84
+ if isinstance(entry, IntegerInfo):
85
+ return (_TAG_INTEGER, entry.value_bytes)
86
+ if isinstance(entry, FloatInfo):
87
+ return (_TAG_FLOAT, entry.value_bytes)
88
+ if isinstance(entry, LongInfo):
89
+ return (_TAG_LONG, entry.high_bytes, entry.low_bytes)
90
+ if isinstance(entry, DoubleInfo):
91
+ return (_TAG_DOUBLE, entry.high_bytes, entry.low_bytes)
92
+ if isinstance(entry, ClassInfo):
93
+ return (_TAG_CLASS, entry.name_index)
94
+ if isinstance(entry, StringInfo):
95
+ return (_TAG_STRING, entry.string_index)
96
+ if isinstance(entry, FieldrefInfo):
97
+ return (_TAG_FIELDREF, entry.class_index, entry.name_and_type_index)
98
+ if isinstance(entry, MethodrefInfo):
99
+ return (_TAG_METHODREF, entry.class_index, entry.name_and_type_index)
100
+ if isinstance(entry, InterfaceMethodrefInfo):
101
+ return (_TAG_INTERFACE_METHODREF, entry.class_index, entry.name_and_type_index)
102
+ if isinstance(entry, NameAndTypeInfo):
103
+ return (_TAG_NAME_AND_TYPE, entry.name_index, entry.descriptor_index)
104
+ if isinstance(entry, MethodHandleInfo):
105
+ return (_TAG_METHOD_HANDLE, entry.reference_kind, entry.reference_index)
106
+ if isinstance(entry, MethodTypeInfo):
107
+ return (_TAG_METHOD_TYPE, entry.descriptor_index)
108
+ if isinstance(entry, DynamicInfo):
109
+ return (_TAG_DYNAMIC, entry.bootstrap_method_attr_index, entry.name_and_type_index)
110
+ if isinstance(entry, InvokeDynamicInfo):
111
+ return (_TAG_INVOKE_DYNAMIC, entry.bootstrap_method_attr_index, entry.name_and_type_index)
112
+ if isinstance(entry, ModuleInfo):
113
+ return (_TAG_MODULE, entry.name_index)
114
+ if isinstance(entry, PackageInfo):
115
+ return (_TAG_PACKAGE, entry.name_index)
116
+ raise ValueError(f"Unknown constant pool entry type: {type(entry).__name__}")
117
+
118
+
119
+ def _is_double_slot(entry: ConstantPoolInfo) -> bool:
120
+ """Return *True* if *entry* is a Long or Double (occupies two CP slots)."""
121
+ return isinstance(entry, (LongInfo, DoubleInfo))
122
+
123
+
124
+ def _copy_entry(entry: ConstantPoolInfo | None) -> ConstantPoolInfo | None:
125
+ return copy.copy(entry) if entry is not None else None
126
+
127
+
128
+ def _require_pool_entry(
129
+ pool: list[ConstantPoolInfo | None],
130
+ index: int,
131
+ *,
132
+ context: str,
133
+ ) -> ConstantPoolInfo:
134
+ if index <= 0 or index >= len(pool):
135
+ raise ValueError(f"{context} index {index} out of range [1, {len(pool) - 1}]")
136
+
137
+ entry = pool[index]
138
+ if entry is None:
139
+ raise ValueError(f"{context} index {index} points to an empty constant-pool slot")
140
+
141
+ return entry
142
+
143
+
144
+ def _validate_utf8_entry(entry: Utf8Info) -> None:
145
+ if entry.length != len(entry.str_bytes):
146
+ raise ValueError(f"Utf8Info length {entry.length} does not match payload size {len(entry.str_bytes)}")
147
+ if entry.length > _UTF8_MAX_BYTES:
148
+ raise ValueError(f"Utf8Info payload exceeds JVM u2 length limit of {_UTF8_MAX_BYTES} bytes")
149
+ try:
150
+ decode_modified_utf8(entry.str_bytes)
151
+ except UnicodeDecodeError as exc:
152
+ raise ValueError(f"Utf8Info contains invalid modified UTF-8: {exc.reason}") from exc
153
+
154
+
155
+ def _method_handle_member_name(
156
+ pool: list[ConstantPoolInfo | None],
157
+ reference_entry: MethodrefInfo | InterfaceMethodrefInfo,
158
+ ) -> str:
159
+ nat_entry = _require_pool_entry(
160
+ pool,
161
+ reference_entry.name_and_type_index,
162
+ context="MethodHandle name_and_type",
163
+ )
164
+ if not isinstance(nat_entry, NameAndTypeInfo):
165
+ raise ValueError(
166
+ "MethodHandle reference target must point to a Methodref/InterfaceMethodref "
167
+ "whose name_and_type_index resolves to CONSTANT_NameAndType"
168
+ )
169
+
170
+ name_entry = _require_pool_entry(pool, nat_entry.name_index, context="MethodHandle member name")
171
+ if not isinstance(name_entry, Utf8Info):
172
+ raise ValueError("MethodHandle member name must resolve to CONSTANT_Utf8")
173
+
174
+ try:
175
+ return decode_modified_utf8(name_entry.str_bytes)
176
+ except UnicodeDecodeError as exc:
177
+ raise ValueError(f"MethodHandle member name is not valid modified UTF-8: {exc.reason}") from exc
178
+
179
+
180
+ def _validate_method_handle(
181
+ pool: list[ConstantPoolInfo | None],
182
+ reference_kind: int,
183
+ reference_index: int,
184
+ ) -> None:
185
+ if not 1 <= reference_kind <= 9:
186
+ raise ValueError(f"reference_kind must be in range [1, 9], got {reference_kind}")
187
+
188
+ target = _require_pool_entry(pool, reference_index, context="MethodHandle reference")
189
+
190
+ if reference_kind in (1, 2, 3, 4):
191
+ if not isinstance(target, FieldrefInfo):
192
+ raise ValueError(f"reference_kind {reference_kind} requires CONSTANT_Fieldref, got {type(target).__name__}")
193
+ return
194
+
195
+ if reference_kind in (5, 8):
196
+ if not isinstance(target, MethodrefInfo):
197
+ raise ValueError(
198
+ f"reference_kind {reference_kind} requires CONSTANT_Methodref, got {type(target).__name__}"
199
+ )
200
+ elif reference_kind in (6, 7):
201
+ if not isinstance(target, (MethodrefInfo, InterfaceMethodrefInfo)):
202
+ raise ValueError(
203
+ "reference_kind "
204
+ f"{reference_kind} requires CONSTANT_Methodref or CONSTANT_InterfaceMethodref, "
205
+ f"got {type(target).__name__}"
206
+ )
207
+ else:
208
+ if not isinstance(target, InterfaceMethodrefInfo):
209
+ raise ValueError(
210
+ f"reference_kind {reference_kind} requires CONSTANT_InterfaceMethodref, got {type(target).__name__}"
211
+ )
212
+
213
+ member_name = _method_handle_member_name(pool, target)
214
+ if reference_kind == 8:
215
+ if member_name != "<init>":
216
+ raise ValueError("reference_kind 8 (REF_newInvokeSpecial) must target a <init> method")
217
+ return
218
+
219
+ if member_name in {"<init>", "<clinit>"}:
220
+ raise ValueError(f"reference_kind {reference_kind} cannot target special method {member_name!r}")
221
+
222
+
223
+ def _validate_import_pool(pool: list[ConstantPoolInfo | None]) -> None:
224
+ if not pool:
225
+ raise ValueError("constant pool must include index 0")
226
+ if pool[0] is not None:
227
+ raise ValueError("constant pool index 0 must be None")
228
+
229
+ index = 1
230
+ while index < len(pool):
231
+ entry = pool[index]
232
+ if entry is None:
233
+ raise ValueError(f"constant pool index {index} is None but not reserved as a Long/Double gap")
234
+ if entry.index != index:
235
+ raise ValueError(f"constant pool entry at position {index} reports mismatched index {entry.index}")
236
+
237
+ if isinstance(entry, Utf8Info):
238
+ _validate_utf8_entry(entry)
239
+ elif isinstance(entry, MethodHandleInfo):
240
+ _validate_method_handle(pool, entry.reference_kind, entry.reference_index)
241
+
242
+ if _is_double_slot(entry):
243
+ gap_index = index + 1
244
+ if gap_index >= len(pool) or pool[gap_index] is not None:
245
+ raise ValueError(f"double-slot entry at index {index} must be followed by a None gap slot")
246
+ index += 2
247
+ continue
248
+
249
+ index += 1
250
+
251
+
252
+ # ---------------------------------------------------------------------------
253
+ # ConstantPoolBuilder
254
+ # ---------------------------------------------------------------------------
255
+
256
+
257
+ class ConstantPoolBuilder:
258
+ """Mutable accumulator for building a JVM constant pool with deduplication.
259
+
260
+ Entries are assigned indexes in insertion order starting at 1 (index 0 is
261
+ always ``None`` per the JVM spec §4.1). Inserting an already-present entry
262
+ returns the existing index without growing the pool.
263
+
264
+ High-level convenience methods (e.g. ``add_class``) automatically create
265
+ any prerequisite entries (e.g. the ``CONSTANT_Utf8`` name string) and
266
+ deduplicate the entire chain.
267
+
268
+ Use ``from_pool`` to seed from a parsed ``ClassFile.constant_pool`` list;
269
+ new entries appended afterwards will not disturb existing indexes.
270
+
271
+ Use ``build`` to export the pool as a ``list[ConstantPoolInfo | None]``
272
+ compatible with ``ClassFile.constant_pool``.
273
+ """
274
+
275
+ def __init__(self) -> None:
276
+ """Initialize an empty constant pool with only the index-0 placeholder."""
277
+ # Index 0 is always None per the JVM spec.
278
+ self._pool: list[ConstantPoolInfo | None] = [None]
279
+ self._next_index: int = 1
280
+ # Content-keyed dedup map: _entry_key(entry) → CP index.
281
+ self._key_to_index: dict[_CPKey, int] = {}
282
+ # Fast reverse lookup for Utf8 entries: str_bytes → CP index.
283
+ self._utf8_to_index: dict[bytes, int] = {}
284
+
285
+ # ------------------------------------------------------------------
286
+ # Construction
287
+ # ------------------------------------------------------------------
288
+
289
+ @classmethod
290
+ def from_pool(cls, pool: list[ConstantPoolInfo | None]) -> ConstantPoolBuilder:
291
+ """Seed a new builder from an existing parsed constant pool.
292
+
293
+ The original indexes are preserved so that all existing CP references
294
+ in the class file remain valid. New entries appended after import
295
+ receive fresh indexes starting immediately after the last imported
296
+ entry.
297
+
298
+ Args:
299
+ pool: Constant pool list where index 0 is ``None`` and each
300
+ subsequent element is a ``ConstantPoolInfo`` or ``None`` for
301
+ double-slot gaps.
302
+
303
+ Returns:
304
+ A new builder pre-populated with copies of the imported entries.
305
+
306
+ Raises:
307
+ ValueError: If the pool structure is invalid (e.g. missing
308
+ index-0 placeholder, mismatched entry indexes, or malformed
309
+ entries).
310
+ """
311
+ builder = cls()
312
+ _validate_import_pool(pool)
313
+ # Shallow-copy each entry. All ConstantPoolInfo fields are int or
314
+ # bytes (both immutable), so a shallow copy is sufficient to ensure
315
+ # the builder owns independent objects.
316
+ builder._pool = [_copy_entry(entry) for entry in pool]
317
+ builder._next_index = len(pool)
318
+ # Rebuild dedup maps from the copied entries.
319
+ for entry in builder._pool:
320
+ if entry is None:
321
+ continue
322
+ key = _entry_key(entry)
323
+ builder._key_to_index.setdefault(key, entry.index)
324
+ if isinstance(entry, Utf8Info):
325
+ builder._utf8_to_index.setdefault(entry.str_bytes, entry.index)
326
+ return builder
327
+
328
+ # ------------------------------------------------------------------
329
+ # Internal allocation
330
+ # ------------------------------------------------------------------
331
+
332
+ def _allocate(self, entry: ConstantPoolInfo) -> int:
333
+ """Assign *entry* the next available CP index, or return the existing index.
334
+
335
+ The entry's ``index`` field is updated to the allocated index and
336
+ ``offset`` is set to ``0`` (no source-file position for builder entries).
337
+ Double-slot entries (Long, Double) automatically consume two slots.
338
+ """
339
+ key = _entry_key(entry)
340
+ if key in self._key_to_index:
341
+ return self._key_to_index[key]
342
+
343
+ double = _is_double_slot(entry)
344
+ limit = _CP_MAX_DOUBLE_INDEX if double else _CP_MAX_SINGLE_INDEX
345
+ if self._next_index > limit:
346
+ raise ValueError(
347
+ f"Constant pool overflow: cannot add {'double-slot ' if double else ''}"
348
+ f"entry at index {self._next_index} (maximum is {limit})"
349
+ )
350
+
351
+ index = self._next_index
352
+ entry.index = index
353
+ entry.offset = 0
354
+ self._pool.append(entry)
355
+ self._key_to_index[key] = index
356
+
357
+ if isinstance(entry, Utf8Info):
358
+ self._utf8_to_index[entry.str_bytes] = index
359
+
360
+ if double:
361
+ self._pool.append(None) # Long/Double second slot is always None
362
+ self._next_index += 2
363
+ else:
364
+ self._next_index += 1
365
+
366
+ return index
367
+
368
+ def _validate_entry(self, entry: ConstantPoolInfo) -> None:
369
+ if isinstance(entry, Utf8Info):
370
+ _validate_utf8_entry(entry)
371
+ elif isinstance(entry, MethodHandleInfo):
372
+ _validate_method_handle(self._pool, entry.reference_kind, entry.reference_index)
373
+
374
+ # ------------------------------------------------------------------
375
+ # Low-level entry insertion
376
+ # ------------------------------------------------------------------
377
+
378
+ def add_entry(self, entry: ConstantPoolInfo) -> int:
379
+ """Insert an arbitrary constant pool entry with deduplication.
380
+
381
+ The caller's object is never mutated; an internal copy is stored.
382
+
383
+ Args:
384
+ entry: Any ``ConstantPoolInfo`` subclass instance to add.
385
+
386
+ Returns:
387
+ The CP index of the entry — existing if a duplicate was found,
388
+ otherwise newly allocated.
389
+
390
+ Raises:
391
+ ValueError: If the entry fails validation (e.g. invalid modified
392
+ UTF-8 or illegal ``MethodHandle`` reference kind).
393
+ """
394
+ entry_copy = copy.copy(entry)
395
+ self._validate_entry(entry_copy)
396
+ return self._allocate(entry_copy)
397
+
398
+ # ------------------------------------------------------------------
399
+ # Utf8 and primitive entries
400
+ # ------------------------------------------------------------------
401
+
402
+ def add_utf8(self, value: str) -> int:
403
+ """Add a ``CONSTANT_Utf8`` entry for a Python string (§4.4.7).
404
+
405
+ The string is encoded to JVM modified UTF-8.
406
+
407
+ Args:
408
+ value: The Python string to store.
409
+
410
+ Returns:
411
+ The CP index of the (possibly pre-existing) entry.
412
+
413
+ Raises:
414
+ ValueError: If the encoded form exceeds the 65 535-byte JVM limit.
415
+ """
416
+ encoded = encode_modified_utf8(value)
417
+ if len(encoded) > _UTF8_MAX_BYTES:
418
+ raise ValueError(f"Modified UTF-8 payload exceeds JVM u2 length limit of {_UTF8_MAX_BYTES} bytes")
419
+
420
+ # Fast path: Utf8 lookup bypasses the general key dict.
421
+ existing = self._utf8_to_index.get(encoded)
422
+ if existing is not None:
423
+ return existing
424
+ entry = Utf8Info(index=0, offset=0, tag=_TAG_UTF8, length=len(encoded), str_bytes=encoded)
425
+ return self._allocate(entry)
426
+
427
+ def add_integer(self, value: int) -> int:
428
+ """Add a ``CONSTANT_Integer`` entry (§4.4.4).
429
+
430
+ Args:
431
+ value: Raw 4-byte integer value stored as the ``bytes`` field.
432
+
433
+ Returns:
434
+ The CP index of the (possibly pre-existing) entry.
435
+ """
436
+ entry = IntegerInfo(index=0, offset=0, tag=_TAG_INTEGER, value_bytes=value)
437
+ return self._allocate(entry)
438
+
439
+ def add_float(self, raw_bits: int) -> int:
440
+ """Add a ``CONSTANT_Float`` entry (§4.4.4).
441
+
442
+ Args:
443
+ raw_bits: IEEE 754 single-precision bit pattern.
444
+
445
+ Returns:
446
+ The CP index of the (possibly pre-existing) entry.
447
+ """
448
+ entry = FloatInfo(index=0, offset=0, tag=_TAG_FLOAT, value_bytes=raw_bits)
449
+ return self._allocate(entry)
450
+
451
+ def add_long(self, high: int, low: int) -> int:
452
+ """Add a ``CONSTANT_Long`` entry (§4.4.5, double-slot).
453
+
454
+ Args:
455
+ high: Upper 32 bits of the long value.
456
+ low: Lower 32 bits of the long value.
457
+
458
+ Returns:
459
+ The CP index of the (possibly pre-existing) entry.
460
+ """
461
+ entry = LongInfo(index=0, offset=0, tag=_TAG_LONG, high_bytes=high, low_bytes=low)
462
+ return self._allocate(entry)
463
+
464
+ def add_double(self, high: int, low: int) -> int:
465
+ """Add a ``CONSTANT_Double`` entry (§4.4.5, double-slot).
466
+
467
+ Args:
468
+ high: Upper 32 bits of the double value.
469
+ low: Lower 32 bits of the double value.
470
+
471
+ Returns:
472
+ The CP index of the (possibly pre-existing) entry.
473
+ """
474
+ entry = DoubleInfo(index=0, offset=0, tag=_TAG_DOUBLE, high_bytes=high, low_bytes=low)
475
+ return self._allocate(entry)
476
+
477
+ # ------------------------------------------------------------------
478
+ # Compound entries (auto-create prerequisites)
479
+ # ------------------------------------------------------------------
480
+
481
+ def add_class(self, name: str) -> int:
482
+ """Add a ``CONSTANT_Class`` entry (§4.4.1).
483
+
484
+ The required ``CONSTANT_Utf8`` name entry is created automatically.
485
+
486
+ Args:
487
+ name: Class or interface name in JVM internal form
488
+ (e.g. ``java/lang/Object``).
489
+
490
+ Returns:
491
+ The CP index of the (possibly pre-existing) entry.
492
+ """
493
+ name_index = self.add_utf8(name)
494
+ entry = ClassInfo(index=0, offset=0, tag=_TAG_CLASS, name_index=name_index)
495
+ return self._allocate(entry)
496
+
497
+ def add_string(self, value: str) -> int:
498
+ """Add a ``CONSTANT_String`` entry (§4.4.3).
499
+
500
+ The required ``CONSTANT_Utf8`` entry is created automatically.
501
+
502
+ Args:
503
+ value: The string literal value.
504
+
505
+ Returns:
506
+ The CP index of the (possibly pre-existing) entry.
507
+ """
508
+ string_index = self.add_utf8(value)
509
+ entry = StringInfo(index=0, offset=0, tag=_TAG_STRING, string_index=string_index)
510
+ return self._allocate(entry)
511
+
512
+ def add_name_and_type(self, name: str, descriptor: str) -> int:
513
+ """Add a ``CONSTANT_NameAndType`` entry (§4.4.6).
514
+
515
+ Both ``CONSTANT_Utf8`` entries are created automatically.
516
+
517
+ Args:
518
+ name: Unqualified field or method name.
519
+ descriptor: Field or method descriptor string.
520
+
521
+ Returns:
522
+ The CP index of the (possibly pre-existing) entry.
523
+ """
524
+ name_index = self.add_utf8(name)
525
+ descriptor_index = self.add_utf8(descriptor)
526
+ entry = NameAndTypeInfo(
527
+ index=0,
528
+ offset=0,
529
+ tag=_TAG_NAME_AND_TYPE,
530
+ name_index=name_index,
531
+ descriptor_index=descriptor_index,
532
+ )
533
+ return self._allocate(entry)
534
+
535
+ def add_fieldref(self, class_name: str, field_name: str, descriptor: str) -> int:
536
+ """Add a ``CONSTANT_Fieldref`` entry (§4.4.2).
537
+
538
+ Prerequisite ``CONSTANT_Class`` and ``CONSTANT_NameAndType`` entries
539
+ (and their ``CONSTANT_Utf8`` dependencies) are created automatically.
540
+
541
+ Args:
542
+ class_name: Owning class in JVM internal form.
543
+ field_name: Unqualified field name.
544
+ descriptor: Field descriptor string.
545
+
546
+ Returns:
547
+ The CP index of the (possibly pre-existing) entry.
548
+ """
549
+ class_index = self.add_class(class_name)
550
+ nat_index = self.add_name_and_type(field_name, descriptor)
551
+ entry = FieldrefInfo(
552
+ index=0,
553
+ offset=0,
554
+ tag=_TAG_FIELDREF,
555
+ class_index=class_index,
556
+ name_and_type_index=nat_index,
557
+ )
558
+ return self._allocate(entry)
559
+
560
+ def add_methodref(self, class_name: str, method_name: str, descriptor: str) -> int:
561
+ """Add a ``CONSTANT_Methodref`` entry (§4.4.2).
562
+
563
+ Prerequisite entries are created automatically.
564
+
565
+ Args:
566
+ class_name: Owning class in JVM internal form.
567
+ method_name: Unqualified method name.
568
+ descriptor: Method descriptor string.
569
+
570
+ Returns:
571
+ The CP index of the (possibly pre-existing) entry.
572
+ """
573
+ class_index = self.add_class(class_name)
574
+ nat_index = self.add_name_and_type(method_name, descriptor)
575
+ entry = MethodrefInfo(
576
+ index=0,
577
+ offset=0,
578
+ tag=_TAG_METHODREF,
579
+ class_index=class_index,
580
+ name_and_type_index=nat_index,
581
+ )
582
+ return self._allocate(entry)
583
+
584
+ def add_interface_methodref(self, class_name: str, method_name: str, descriptor: str) -> int:
585
+ """Add a ``CONSTANT_InterfaceMethodref`` entry (§4.4.2).
586
+
587
+ Prerequisite entries are created automatically.
588
+
589
+ Args:
590
+ class_name: Owning interface in JVM internal form.
591
+ method_name: Unqualified method name.
592
+ descriptor: Method descriptor string.
593
+
594
+ Returns:
595
+ The CP index of the (possibly pre-existing) entry.
596
+ """
597
+ class_index = self.add_class(class_name)
598
+ nat_index = self.add_name_and_type(method_name, descriptor)
599
+ entry = InterfaceMethodrefInfo(
600
+ index=0,
601
+ offset=0,
602
+ tag=_TAG_INTERFACE_METHODREF,
603
+ class_index=class_index,
604
+ name_and_type_index=nat_index,
605
+ )
606
+ return self._allocate(entry)
607
+
608
+ # ------------------------------------------------------------------
609
+ # Remaining entry types
610
+ # ------------------------------------------------------------------
611
+
612
+ def add_method_handle(self, reference_kind: int, reference_index: int) -> int:
613
+ """Add a ``CONSTANT_MethodHandle`` entry (§4.4.8).
614
+
615
+ Args:
616
+ reference_kind: JVM reference-kind value (1–9), denoting the
617
+ bytecode behavior of the handle.
618
+ reference_index: CP index of the target ``Fieldref``,
619
+ ``Methodref``, or ``InterfaceMethodref`` entry.
620
+
621
+ Returns:
622
+ The CP index of the (possibly pre-existing) entry.
623
+
624
+ Raises:
625
+ ValueError: If *reference_kind* is out of range or the target
626
+ entry type is incompatible with the specified kind.
627
+ """
628
+ _validate_method_handle(self._pool, reference_kind, reference_index)
629
+ entry = MethodHandleInfo(
630
+ index=0,
631
+ offset=0,
632
+ tag=_TAG_METHOD_HANDLE,
633
+ reference_kind=reference_kind,
634
+ reference_index=reference_index,
635
+ )
636
+ return self._allocate(entry)
637
+
638
+ def add_method_type(self, descriptor: str) -> int:
639
+ """Add a ``CONSTANT_MethodType`` entry (§4.4.9).
640
+
641
+ The required ``CONSTANT_Utf8`` descriptor entry is created
642
+ automatically.
643
+
644
+ Args:
645
+ descriptor: Method descriptor string.
646
+
647
+ Returns:
648
+ The CP index of the (possibly pre-existing) entry.
649
+ """
650
+ descriptor_index = self.add_utf8(descriptor)
651
+ entry = MethodTypeInfo(index=0, offset=0, tag=_TAG_METHOD_TYPE, descriptor_index=descriptor_index)
652
+ return self._allocate(entry)
653
+
654
+ def add_dynamic(self, bootstrap_method_attr_index: int, name: str, descriptor: str) -> int:
655
+ """Add a ``CONSTANT_Dynamic`` entry (§4.4.10).
656
+
657
+ The ``CONSTANT_NameAndType`` entry and its ``CONSTANT_Utf8``
658
+ dependencies are created automatically.
659
+
660
+ Args:
661
+ bootstrap_method_attr_index: Index into the
662
+ ``BootstrapMethods`` attribute table.
663
+ name: Unqualified name of the dynamically-computed constant.
664
+ descriptor: Field descriptor for the constant's type.
665
+
666
+ Returns:
667
+ The CP index of the (possibly pre-existing) entry.
668
+ """
669
+ nat_index = self.add_name_and_type(name, descriptor)
670
+ entry = DynamicInfo(
671
+ index=0,
672
+ offset=0,
673
+ tag=_TAG_DYNAMIC,
674
+ bootstrap_method_attr_index=bootstrap_method_attr_index,
675
+ name_and_type_index=nat_index,
676
+ )
677
+ return self._allocate(entry)
678
+
679
+ def add_invoke_dynamic(self, bootstrap_method_attr_index: int, name: str, descriptor: str) -> int:
680
+ """Add a ``CONSTANT_InvokeDynamic`` entry (§4.4.10).
681
+
682
+ The ``CONSTANT_NameAndType`` entry and its ``CONSTANT_Utf8``
683
+ dependencies are created automatically.
684
+
685
+ Args:
686
+ bootstrap_method_attr_index: Index into the
687
+ ``BootstrapMethods`` attribute table.
688
+ name: Unqualified method name for the call site.
689
+ descriptor: Method descriptor for the call site.
690
+
691
+ Returns:
692
+ The CP index of the (possibly pre-existing) entry.
693
+ """
694
+ nat_index = self.add_name_and_type(name, descriptor)
695
+ entry = InvokeDynamicInfo(
696
+ index=0,
697
+ offset=0,
698
+ tag=_TAG_INVOKE_DYNAMIC,
699
+ bootstrap_method_attr_index=bootstrap_method_attr_index,
700
+ name_and_type_index=nat_index,
701
+ )
702
+ return self._allocate(entry)
703
+
704
+ def add_module(self, name: str) -> int:
705
+ """Add a ``CONSTANT_Module`` entry (§4.4.11).
706
+
707
+ The required ``CONSTANT_Utf8`` name entry is created automatically.
708
+
709
+ Args:
710
+ name: Module name.
711
+
712
+ Returns:
713
+ The CP index of the (possibly pre-existing) entry.
714
+ """
715
+ name_index = self.add_utf8(name)
716
+ entry = ModuleInfo(index=0, offset=0, tag=_TAG_MODULE, name_index=name_index)
717
+ return self._allocate(entry)
718
+
719
+ def add_package(self, name: str) -> int:
720
+ """Add a ``CONSTANT_Package`` entry (§4.4.12).
721
+
722
+ The required ``CONSTANT_Utf8`` name entry is created automatically.
723
+
724
+ Args:
725
+ name: Package name in JVM internal form (e.g. ``java/lang``).
726
+
727
+ Returns:
728
+ The CP index of the (possibly pre-existing) entry.
729
+ """
730
+ name_index = self.add_utf8(name)
731
+ entry = PackageInfo(index=0, offset=0, tag=_TAG_PACKAGE, name_index=name_index)
732
+ return self._allocate(entry)
733
+
734
+ # ------------------------------------------------------------------
735
+ # Lookups
736
+ # ------------------------------------------------------------------
737
+
738
+ def get(self, index: int) -> ConstantPoolInfo | None:
739
+ """Return the entry at a given CP index.
740
+
741
+ Returns a defensive copy; ``None`` is returned for index 0 and
742
+ double-slot gap positions.
743
+
744
+ Args:
745
+ index: Constant pool index to retrieve.
746
+
747
+ Returns:
748
+ A copy of the entry, or ``None`` for placeholder/gap slots.
749
+
750
+ Raises:
751
+ IndexError: If *index* is out of the pool's range.
752
+ """
753
+ if index < 0 or index >= len(self._pool):
754
+ raise IndexError(f"CP index {index} out of range [0, {len(self._pool) - 1}]")
755
+ return _copy_entry(self._pool[index])
756
+
757
+ def find_utf8(self, value: str) -> int | None:
758
+ """Look up the CP index of a ``CONSTANT_Utf8`` entry by string value.
759
+
760
+ Args:
761
+ value: The Python string to search for.
762
+
763
+ Returns:
764
+ The CP index if found, otherwise ``None``.
765
+ """
766
+ return self._utf8_to_index.get(encode_modified_utf8(value))
767
+
768
+ def find_class(self, name: str) -> int | None:
769
+ """Look up the CP index of a ``CONSTANT_Class`` entry by class name.
770
+
771
+ Args:
772
+ name: Class name in JVM internal form (e.g. ``java/lang/Object``).
773
+
774
+ Returns:
775
+ The CP index if found, otherwise ``None``.
776
+ """
777
+ utf8_idx = self.find_utf8(name)
778
+ if utf8_idx is None:
779
+ return None
780
+ key: _CPKey = (_TAG_CLASS, utf8_idx)
781
+ return self._key_to_index.get(key)
782
+
783
+ def find_name_and_type(self, name: str, descriptor: str) -> int | None:
784
+ """Look up the CP index of a ``CONSTANT_NameAndType`` entry.
785
+
786
+ Args:
787
+ name: Unqualified field or method name.
788
+ descriptor: Field or method descriptor string.
789
+
790
+ Returns:
791
+ The CP index if found, otherwise ``None``.
792
+ """
793
+ name_idx = self.find_utf8(name)
794
+ if name_idx is None:
795
+ return None
796
+ desc_idx = self.find_utf8(descriptor)
797
+ if desc_idx is None:
798
+ return None
799
+ key: _CPKey = (_TAG_NAME_AND_TYPE, name_idx, desc_idx)
800
+ return self._key_to_index.get(key)
801
+
802
+ def resolve_utf8(self, index: int) -> str:
803
+ """Decode the ``CONSTANT_Utf8`` entry at a CP index to a Python string.
804
+
805
+ Args:
806
+ index: Constant pool index of the ``CONSTANT_Utf8`` entry.
807
+
808
+ Returns:
809
+ The decoded Python string.
810
+
811
+ Raises:
812
+ ValueError: If the entry at *index* is not a ``CONSTANT_Utf8``.
813
+ """
814
+ entry = self.get(index)
815
+ if not isinstance(entry, Utf8Info):
816
+ raise ValueError(f"CP index {index} is not a CONSTANT_Utf8 entry: {type(entry).__name__}")
817
+ return decode_modified_utf8(entry.str_bytes)
818
+
819
+ # ------------------------------------------------------------------
820
+ # Output
821
+ # ------------------------------------------------------------------
822
+
823
+ def build(self) -> list[ConstantPoolInfo | None]:
824
+ """Export the constant pool as a spec-format list.
825
+
826
+ The returned list has the same structure as
827
+ ``ClassFile.constant_pool``: index 0 is ``None``, Long/Double
828
+ second slots are ``None``, and all other positions hold a copy of a
829
+ ``ConstantPoolInfo`` instance. Use ``count`` as the
830
+ ``constant_pool_count`` field when serializing.
831
+
832
+ Returns:
833
+ A new list of entry copies suitable for class file emission.
834
+ """
835
+ return [_copy_entry(entry) for entry in self._pool]
836
+
837
+ @property
838
+ def count(self) -> int:
839
+ """The ``constant_pool_count`` value (§4.1) for class file serialization."""
840
+ return self._next_index
841
+
842
+ def __len__(self) -> int:
843
+ """Return the number of logical entries, excluding placeholder and gap slots."""
844
+ return sum(1 for e in self._pool if e is not None)