frames,jar,passes: verifier-driven hardening from real jars

Proven by paranoid runs over lorep (17 classes, fully green),
LoVisual (4081 classes) and oc2r (1278 classes) with per-class
java -Xverify:all, each fix first reproduced as a red test:

- merge: Top absorbs instead of Top-wins (javac declares Top for
  slots unwritten on some inflow; precise types reject those edges).
  Regression: loop_head_keeps_top_for_loop_local.
- emit: collapse [Long|Double, Top] pairs on operand stacks
  (javac writes one entry; the pair is rejected). Regression:
  wide_stack_join_emits_single_entries.
- xfer ArrayLoad: push the wide pair, fixing lcmp underflow.
  Regression: array_load_wide_pushes_pair.
- xfer <init>: convert only the receiver's own allocation site
  instead of every Uninitialized (nested construction clobbered
  the outer object). Regression: nested_construction_keeps_outer.
- xfer ANewArray: array operands gain one dimension ([B] -> [[B)
  instead of an illegal L-wrap ([L[B;). Covered by the same test.
- jar load: duplicate class names (multi-release module-info
  twins) keep the first occurrence instead of crashing save.
  Test: duplicate_class_names_keep_first.
- strings/constants: skip methods past MAX_METHOD_INSNS (8000)
  so giants cannot overflow the 64KB writer ceiling. Tests:
  oversized_methods_are_skipped in both passes.
- merge errors now name both types being merged.

Also: em-dash removed repo-wide (724 spots, rule added to
todo.md), allow-lints only in tests (scratch example deleted),
code comments migrated to English in every file this change
touches.
This commit is contained in:
Loki 2026-10-09 13:47:27 +02:00
parent 2172206e83
commit 139aea9a8f
Signed by: boba
GPG key ID: 253067914055423B
96 changed files with 1053 additions and 831 deletions

View file

@ -1,7 +1,7 @@
//! Emitting compact frames at the points where the verifier expects them.
//!
//! A frame's kind is chosen by comparing with the previous frame
//! (`Same`/`Append`/`Chop`/`SameLocals`, otherwise `Full`) — like `javac`.
//! (`Same`/`Append`/`Chop`/`SameLocals`, otherwise `Full`) - like `javac`.
//! Trailing `Top`s are trimmed both when comparing and when writing: an empty
//! slot is not a type.
@ -12,7 +12,7 @@ use super::xfer::State;
use super::{StackMapFrame, VerificationType};
use crate::error::ClassfileError;
/// Frames at every point in `targets` (sorted — deltas in order).
/// Frames at every point in `targets` (sorted - deltas in order).
///
/// Deltas are in instruction numbers (like the backend after reading); the writer
/// recomputes them into bytes itself.
@ -50,7 +50,7 @@ fn select(
previous: &[VerificationType],
stack: &[VerificationType],
) -> StackMapFrame {
// Comparison is at variable granularity (`long` — one entry, like `javac`);
// Comparison is at variable granularity (`long` - one entry, like `javac`);
// otherwise halves would produce `Append{[Top, Long]}`.
let current = vars(locals);
let base = vars(previous);
@ -98,7 +98,7 @@ fn select(
}
}
/// Stack entries → values: `[Double, Top]` — one entry (like `javac`).
/// Stack entries → values: `[Double, Top]` - one entry (like `javac`).
///
/// Unlike [`vars`], no trailing-`Top` trimming: every stack entry is live by
/// construction, and dropping one would corrupt the depth.
@ -122,7 +122,7 @@ fn stack_vars(stack: &[VerificationType]) -> Vec<VerificationType> {
out
}
/// Slots → variables: `[Long, Top]` — one entry; trailing `Top`s — gone.
/// Slots → variables: `[Long, Top]` - one entry; trailing `Top`s - gone.
///
/// Single `Top`s inside (holes) are kept: in `Full` they are a legal type, and we
/// don't allow them in `Append` (see `select`).

View file

@ -31,7 +31,7 @@ pub(crate) fn split_array(descriptor: &str) -> Option<(usize, &str)> {
Some((dims, descriptor[dims..].trim_end_matches(';')))
}
/// Slots for a single field descriptor (`long`/`double` — pair with `Top`).
/// Slots for a single field descriptor (`long`/`double` - pair with `Top`).
pub(crate) fn field_slots(descriptor: &str) -> Result<Vec<VerificationType>, DescError> {
let mut chars = descriptor.chars();
match chars.next() {
@ -57,10 +57,10 @@ pub(crate) fn field_slots(descriptor: &str) -> Result<Vec<VerificationType>, Des
}
}
/// Parameter and return slots (`None` — `void`).
/// Parameter and return slots (`None` - `void`).
type MethodSlots = (Vec<Vec<VerificationType>>, Option<Vec<VerificationType>>);
/// Method descriptor: parameter slots and return slots (`None` — `void`).
/// Method descriptor: parameter slots and return slots (`None` - `void`).
pub(crate) fn method_slots(descriptor: &str) -> Result<MethodSlots, DescError> {
let (params, ret) = descriptor
.split_once('(')

View file

@ -1,12 +1,12 @@
//! Type merging (LUB) and descriptor parsing.
//!
//! The lattice: `Top` is "empty" (the neighbour wins), `Null` is absorbed into a
//! reference, equal types map to themselves, references take the least upper
//! bound over the hierarchy. Incompatible initialized types (`int` vs a reference
//! after `astore` in a handler) yield `Top`, like `javac`'s truncated frames: a
//! `Top` in a frame accepts any actual value, so this is always sound. The
//! exception is uninitialized types: merging them means a broken construction
//! flow — here an honest error, not a silent `Top`.
//! The lattice: `Top` absorbs (a slot unwritten on any inflow stays `Top`,
//! like `javac` - a precise type would reject the inflow that never wrote
//! it). `Null` is absorbed into a reference, equal types map to themselves,
//! references take the least upper bound over the hierarchy. Incompatible
//! initialized types (`int` vs a reference) yield `Top`. The exception is
//! uninitialized types: merging them means a broken construction flow -
//! here an honest error, not a silent `Top`.
pub(super) mod descs;
@ -19,7 +19,7 @@ use descs::write_local;
/// The class hierarchy for LUB: the superclass chain is enough.
pub trait Hierarchy {
/// The direct superclass (`None` — `java/lang/Object` or unknown).
/// The direct superclass (`None` - `java/lang/Object` or unknown).
fn superclass(&self, class: &str) -> Option<String>;
}
@ -38,7 +38,7 @@ impl Merger<'_> {
}
}
/// Merge an incoming state; `true` — the slot changed.
/// Merge an incoming state; `true` - the slot changed.
pub fn merge_into(
&self,
slot: &mut Option<State>,
@ -75,11 +75,11 @@ impl Merger<'_> {
VerificationType::Object(self.class.clone())
};
slot = write_local(&mut locals, slot, vec![this])
.map_err(|_| self.invalid("параметры не влезают в locals"))?;
.map_err(|_| self.invalid("parameters do not fit into locals"))?;
}
for param in params {
slot = write_local(&mut locals, slot, param)
.map_err(|_| self.invalid("параметры не влезают в locals"))?;
.map_err(|_| self.invalid("parameters do not fit into locals"))?;
}
Ok(State {
locals,
@ -89,10 +89,10 @@ impl Merger<'_> {
fn lub_states(&self, first: &State, second: &State) -> Result<State, ClassfileError> {
if first.locals.len() != second.locals.len() {
return Err(self.invalid("разная длина locals при слиянии"));
return Err(self.invalid("locals length differs across the merge"));
}
if first.stack.len() != second.stack.len() {
return Err(self.invalid("разная глубина стека при слиянии"));
return Err(self.invalid("stack depth differs across the merge"));
}
Ok(State {
locals: first
@ -120,7 +120,10 @@ impl Merger<'_> {
return Ok(first.clone());
}
match (first, second) {
(Top, other) | (other, Top) => Ok(other.clone()),
// A slot unwritten on any inflow stays `Top` (like `javac`): the
// frame keeps accepting every inflow, while a precise type would
// reject the one that never wrote the slot.
(Top, _) | (_, Top) => Ok(Top),
(Null, other @ Object(_)) | (other @ Object(_), Null) => Ok(other.clone()),
(Null, Null) => Ok(Null),
(Integer, Integer) | (Float, Float) | (Long, Long) | (Double, Double) => {
@ -128,10 +131,12 @@ impl Merger<'_> {
}
(Object(a), Object(b)) => Ok(Object(self.common_superclass(a, b))),
// Equal ones are handled above; here uninitialized are always
// different — that's a broken construction flow.
// different - that's a broken construction flow.
(VerificationType::UninitializedThis | VerificationType::Uninitialized(_), _)
| (_, VerificationType::UninitializedThis | VerificationType::Uninitialized(_)) => {
Err(self.invalid("слияние неинициализированных типов"))
Err(self.invalid(&format!(
"merging uninitialized types ({first:?} with {second:?})"
)))
}
// Incompatible initialized (e.g. `int` from the `try` body and
// `Throwable` from a handler in one slot): `Top`, like `javac`.
@ -139,7 +144,7 @@ impl Merger<'_> {
}
}
/// Least common ancestor: climb the chains, fallback — `Object`.
/// Least common ancestor: climb the chains, fallback - `Object`.
fn common_superclass(&self, first: &str, second: &str) -> String {
if first == second {
return first.to_string();

View file

@ -2,11 +2,11 @@
//!
//! Frames are the most expensive part of any Rust obfuscator: the JVM verifier
//! (class version ≥ 50) demands an exact `StackMapTable`, or the class won't load.
//! Implementation options and their costs — `docs/stack.md`.
//! Implementation options and their costs - `docs/stack.md`.
//!
//! Method: CFG over labels → worklist with types (`xfer`) → LUB merge (`merge`)
//! → emit compact frames (`emit`). Byte offsets come from the same encoding the
//! writer uses — otherwise frames would drift apart from the bytes.
//! writer uses - otherwise frames would drift apart from the bytes.
mod emit;
mod flow;
@ -113,7 +113,7 @@ pub fn compute_frames(
emit::emit_frames(&states, &targets, &initial.locals, &merger)
}
/// The byte offset of each instruction — using the same encoding as the writer.
/// The byte offset of each instruction - using the same encoding as the writer.
fn layout(
bare: &[&Insn],
labels: &BTreeMap<Label, u32>,
@ -188,7 +188,7 @@ fn run_worklist(
Ok(())
}
/// Merge a state into an instruction; `true` — changed, keep walking.
/// Merge a state into an instruction; `true` - changed, keep walking.
fn flow_into(
merger: &Merger<'_>,
states: &mut [Option<State>],
@ -220,7 +220,7 @@ fn flow_into(
}
/// CFG edges without exceptions: fallthrough + branch targets.
/// The exception type at a handler's entry (`catch-all` — any throwable).
/// The exception type at a handler's entry (`catch-all` - any throwable).
fn catch_type(catch: &Option<String>) -> VerificationType {
match catch {
Some(name) => VerificationType::Object(name.clone()),

View file

@ -0,0 +1,191 @@
//! Join-shape tests: loop heads, wide array loads, nested construction.
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::super::{StackMapFrame, VerificationType, compute_frames};
use super::{NoHierarchy, code_method, load, load_ref, store, store_ref};
use crate::insn::{BranchCond, Cond, Const, Insn, Prim};
/// `anewarray [B` (a partially bound multi-dim array) yields its descriptor
/// verbatim: wrapping it again (`[L[B;`) is not a class and HotSpot rejects
/// the pool entry.
#[test]
fn anewarray_of_array_keeps_descriptor() {
let method = code_method(
"make",
"()Ljava/lang/Object;",
2,
1,
vec![
Insn::Mark(0),
Insn::Const(Const::Int(3)),
Insn::ANewArray("[B".to_string()),
store_ref(0),
load_ref(0),
Insn::Branch(BranchCond::Null { is_null: false }, 1),
Insn::AconstNull,
store_ref(0),
Insn::Goto(2),
Insn::Mark(1),
Insn::Mark(2),
load_ref(0),
Insn::AReturn,
],
vec![],
);
let frames = compute_frames("demo/Make", &method, &NoHierarchy).expect("frames");
assert_eq!(
frames,
vec![StackMapFrame::Append {
offset_delta: 8,
locals: vec![VerificationType::Object("[[B".to_string())],
},]
);
}
/// A loop head keeps `Top` for a slot written only inside the loop.
///
/// Inflows: entry (never wrote it) and back edge (wrote `Integer`). Like
/// `javac`, the join declares `Top`: a precise type would reject the inflow
/// that never wrote the slot (`Type top is not assignable`).
#[test]
fn loop_head_keeps_top_for_loop_local() {
let method = code_method(
"sum",
"()I",
2,
3,
vec![
Insn::Mark(0),
Insn::Const(Const::Int(0)),
store(0),
Insn::Const(Const::Int(0)),
store(1),
Insn::Mark(1),
load(1),
Insn::Const(Const::Int(10)),
Insn::Branch(BranchCond::Int(Cond::Ge), 2),
load(1),
store(2),
load(2),
store(0),
Insn::Iinc { index: 1, delta: 1 },
Insn::Goto(1),
Insn::Mark(2),
load(0),
Insn::Return(Some(Prim::Int)),
],
vec![],
);
let frames = compute_frames("demo/Sum", &method, &NoHierarchy).expect("frames");
assert_eq!(
frames,
vec![
StackMapFrame::Append {
offset_delta: 4,
locals: vec![VerificationType::Integer, VerificationType::Integer],
},
StackMapFrame::Same { offset_delta: 8 },
]
);
}
/// A nested construction (`new Outer(new Inner())`) initializes only the
/// inner object: the outer `Uninitialized` survives with its own offset, so
/// a later join still sees it instead of a clobbered `Object(Inner)`.
#[test]
fn nested_construction_keeps_outer_uninitialized() {
let method = code_method(
"make",
"()I",
4,
1,
vec![
Insn::Mark(0),
Insn::New("demo/Outer".to_string()),
Insn::Dup,
Insn::New("demo/Inner".to_string()),
Insn::Dup,
Insn::Invoke {
kind: crate::insn::InvokeKind::Special,
owner: "demo/Inner".to_string(),
name: "<init>".to_string(),
descriptor: "()V".to_string(),
itf: false,
},
Insn::Invoke {
kind: crate::insn::InvokeKind::Special,
owner: "demo/Outer".to_string(),
name: "<init>".to_string(),
descriptor: "(Ldemo/Inner;)V".to_string(),
itf: false,
},
store_ref(0),
load_ref(0),
Insn::Branch(BranchCond::Null { is_null: false }, 1),
Insn::Const(Const::Int(0)),
Insn::Goto(2),
Insn::Mark(1),
Insn::Const(Const::Int(1)),
Insn::Mark(2),
Insn::Return(Some(Prim::Int)),
],
vec![],
);
let frames = compute_frames("demo/Make", &method, &NoHierarchy).expect("frames");
assert_eq!(
frames,
vec![
StackMapFrame::Append {
offset_delta: 11,
locals: vec![VerificationType::Object("demo/Outer".to_string())],
},
StackMapFrame::SameLocals {
offset_delta: 0,
local: VerificationType::Integer,
},
]
);
}
/// `laload` pushes a pair (`long` + `Top`): a following `lcmp` needs four
/// entries and must not underflow.
#[test]
fn array_load_wide_pushes_pair() {
let method = code_method(
"get",
"([JI)J",
4,
2,
vec![
Insn::Mark(0),
load_ref(0),
load(1),
Insn::ArrayLoad(crate::insn::ArrayKind::Long),
Insn::Const(Const::Long(0)),
Insn::Cmp {
prim: Prim::Long,
nan_great: false,
},
Insn::Branch(BranchCond::Zero(Cond::Ne), 1),
Insn::Const(Const::Long(1)),
Insn::Goto(2),
Insn::Mark(1),
Insn::Const(Const::Long(0)),
Insn::Mark(2),
Insn::Return(Some(Prim::Long)),
],
vec![],
);
let frames = compute_frames("demo/Wide", &method, &NoHierarchy).expect("frames");
assert_eq!(
frames,
vec![
StackMapFrame::Same { offset_delta: 8 },
StackMapFrame::SameLocals {
offset_delta: 0,
local: VerificationType::Long,
},
]
);
}

View file

@ -1,4 +1,4 @@
//! Unit-тесты `compute_frames` на синтезированном IR.
//! Unit tests for `compute_frames` on synthesized IR.
#![allow(clippy::expect_used, clippy::unwrap_used)]
@ -7,8 +7,8 @@ use crate::Hierarchy;
use crate::insn::{BranchCond, Cond, Const, Insn, Prim, SlotKind};
use crate::model::{Code, ExceptionHandler, Method};
/// Пустая иерархия: всё неизвестное — наследники `Object` (для LUB хватает).
struct NoHierarchy;
/// Empty hierarchy: everything unknown descends from `Object` (enough for LUB).
pub(super) struct NoHierarchy;
impl Hierarchy for NoHierarchy {
fn superclass(&self, _class: &str) -> Option<String> {
@ -16,7 +16,7 @@ impl Hierarchy for NoHierarchy {
}
}
fn code_method(
pub(super) fn code_method(
name: &str,
descriptor: &str,
max_stack: u16,
@ -40,7 +40,7 @@ fn code_method(
}
}
/// Ветвь → два кадра (`Same`, `SameLocals`).
/// A branch emits two frames (`Same`, `SameLocals`).
#[test]
fn branch_emits_same_frames() {
let mut method = code_method(
@ -76,7 +76,11 @@ fn branch_emits_same_frames() {
);
}
/// `try/catch`: handler получает `Full`, продолжение — `Append`.
/// `try/catch`: the handler gets `Full`, the continuation - `Same`.
///
/// Slot 1 is written only inside the handler, so the join keeps `Top` there
/// (like `javac`): a precise type would reject the inflow that never wrote
/// it (`Type top is not assignable`).
#[test]
fn handler_emits_full_frame() {
let method = code_method(
@ -115,15 +119,12 @@ fn handler_emits_full_frame() {
locals: vec![VerificationType::Integer],
stack: vec![VerificationType::Object("java/lang/Exception".to_string())],
},
StackMapFrame::Append {
offset_delta: 2,
locals: vec![VerificationType::Object("java/lang/Exception".to_string())],
},
StackMapFrame::Same { offset_delta: 2 },
]
);
}
/// Два `new` на разных ветвях нельзя слить — честная ошибка, не `Top`.
/// Two `new` on different branches cannot merge - an honest error, not `Top`.
#[test]
fn uninitialized_merge_is_rejected() {
let method = code_method(
@ -150,28 +151,37 @@ fn uninitialized_merge_is_rejected() {
],
vec![],
);
let err = compute_frames("demo/Pick", &method, &NoHierarchy).expect_err("слияние");
let err = compute_frames("demo/Pick", &method, &NoHierarchy).expect_err("merge");
assert!(
err.to_string().contains("неинициализированных"),
"не тот текст: {err}"
err.to_string().contains("uninitialized"),
"wrong message: {err}"
);
}
fn load(slot: u16) -> Insn {
mod joins;
pub(super) fn load(slot: u16) -> Insn {
Insn::Load {
slot,
kind: SlotKind::Int,
}
}
fn store(slot: u16) -> Insn {
pub(super) fn load_ref(slot: u16) -> Insn {
Insn::Load {
slot,
kind: SlotKind::Ref,
}
}
pub(super) fn store(slot: u16) -> Insn {
Insn::Store {
slot,
kind: SlotKind::Int,
}
}
fn store_ref(slot: u16) -> Insn {
pub(super) fn store_ref(slot: u16) -> Insn {
Insn::Store {
slot,
kind: SlotKind::Ref,

View file

@ -1,4 +1,4 @@
//! Вызовы и константы: дескрипторы, `<init>`, слоты значений.
//! Calls and constants: descriptors, `<init>`, value slots.
use super::super::VerificationType;
use super::super::merge::descs::{DescError, field_slots, method_slots};
@ -34,18 +34,12 @@ pub(super) fn invoke(
descriptor: &str,
) -> Result<(), ClassfileError> {
let (params, ret) = method_slots(descriptor).map_err(|e| context(class, method, e))?;
super::stack::pop_slots(
state,
count_slots(&params),
class,
method,
"аргументы вызова",
)?;
super::stack::pop_slots(state, count_slots(&params), class, method, "call arguments")?;
let mut receiver = None;
if kind != InvokeKind::Static {
receiver = super::stack::pop_slots(state, 1, class, method, "objectref")?.pop();
}
// `<init>` инициализирует получателя во ВСЕХ его копиях.
// `<init>` initializes the receiver in ALL its copies.
if name == "<init>" {
match receiver {
Some(VerificationType::UninitializedThis) => {
@ -55,9 +49,11 @@ pub(super) fn invoke(
&initialized(class),
);
}
Some(VerificationType::Uninitialized(_)) => {
// Only the receiver's own allocation site: an enclosing `new`
// whose `<init>` has not run yet must stay uninitialized.
Some(VerificationType::Uninitialized(offset)) => {
let fresh = VerificationType::Object(owner.to_string());
replace_uninit(state, &fresh);
replace(state, &VerificationType::Uninitialized(offset), &fresh);
}
_ => {}
}
@ -68,12 +64,12 @@ pub(super) fn invoke(
Ok(())
}
/// Число слотов всех параметров.
/// Slot count across all parameters.
pub(super) fn count_slots(params: &[Vec<VerificationType>]) -> usize {
params.iter().map(Vec::len).sum()
}
/// Заменить все вхождения типа новым (инициализация через `<init>`).
/// Replace every occurrence of a type with a new one (init via `<init>`).
fn replace(state: &mut State, from: &VerificationType, to: &VerificationType) {
for slot in state.locals.iter_mut().chain(state.stack.iter_mut()) {
if slot == from {
@ -82,15 +78,6 @@ fn replace(state: &mut State, from: &VerificationType, to: &VerificationType) {
}
}
/// Любой `Uninitialized` (кроме `this`) → тип владельца (созданный `new`).
fn replace_uninit(state: &mut State, to: &VerificationType) {
for slot in state.locals.iter_mut().chain(state.stack.iter_mut()) {
if matches!(slot, VerificationType::Uninitialized(_)) {
*slot = to.clone();
}
}
}
fn initialized(class: &str) -> VerificationType {
VerificationType::Object(class.to_string())
}
@ -115,7 +102,7 @@ pub(super) fn prim_push(prim: Prim) -> Vec<VerificationType> {
}
}
/// `(слоты аргумента, примитив результата)` для конверсии.
/// `(argument slots, result primitive)` for a conversion.
pub(super) fn convert_shapes(op: Convert) -> (usize, Prim) {
use Convert::{D2F, D2I, D2L, F2D, F2I, F2L, I2B, I2C, I2D, I2F, I2L, I2S, L2D, L2F, L2I};
use Prim::{Double, Float, Int, Long};

View file

@ -1,8 +1,8 @@
//! Состояние типов и transfer-функции инструкций.
//! Type states and per-instruction transfer functions.
//!
//! Стек растёт вправо (`push` — в конец). `long`/`double` занимают два слота:
//! значение + `Top`. Проверяется только ФОРМА (глубина, категории), не
//! соответствие типов — неверные типы честно упадут на верификаторе JVM.
//! The stack grows right (`push` appends). `long`/`double` take two slots:
//! value + `Top`. Only the SHAPE is checked (depth, categories), not
//! type agreement - wrong types honestly fail on the JVM verifier.
pub(super) mod calls;
pub(super) mod stack;
@ -15,14 +15,14 @@ use crate::insn::{Arith, ArrayKind, BranchCond, Insn, SlotKind};
use calls::{const_slots, context, convert_shapes, count_slots, invoke, prim_push, prim_slots};
use stack::{dup, dup2, local_get, pop_slots, push, store_local, wide_push};
/// Типы locals + operand stack в одной точке программы.
/// Local types plus the operand stack at one program point.
#[derive(Debug, Clone, Default, PartialEq)]
pub(super) struct State {
pub locals: Vec<VerificationType>,
pub stack: Vec<VerificationType>,
}
/// Применить инструкцию к состоянию (на месте).
/// Apply one instruction to the state (in place).
pub(super) fn transfer(
class: &str,
method: &str,
@ -31,7 +31,11 @@ pub(super) fn transfer(
state: &mut State,
) -> Result<(), ClassfileError> {
match insn {
Insn::Mark(label) => Err(invalid(class, method, &format!("маркер {label} не снят"))),
Insn::Mark(label) => Err(invalid(
class,
method,
&format!("marker {label} was not stripped"),
)),
Insn::Nop => Ok(()),
Insn::AconstNull => push(state, vec![VerificationType::Null]),
Insn::Const(value) => {
@ -51,29 +55,29 @@ pub(super) fn transfer(
Insn::Iinc { index, .. } => {
let current = local_get(state, *index, class, method)?;
if current != VerificationType::Integer {
return Err(invalid(class, method, "iinc не по int"));
return Err(invalid(class, method, "iinc over a non-int"));
}
Ok(())
}
Insn::ArrayLoad(kind) => {
pop_slots(state, 1, class, method, "индекс массива")?;
let array = pop_slots(state, 1, class, method, "массив")?.remove(0);
push(state, vec![array_element(&array, *kind)])
pop_slots(state, 1, class, method, "array index")?;
let array = pop_slots(state, 1, class, method, "array")?.remove(0);
push(state, wide_push(array_element(&array, *kind)))
}
Insn::ArrayStore(kind) => {
let count = match kind {
ArrayKind::Long | ArrayKind::Double => 2,
_ => 1,
};
pop_slots(state, count, class, method, "значение")?;
pop_slots(state, 1, class, method, "индекс массива")?;
pop_slots(state, 1, class, method, "массив")?;
pop_slots(state, count, class, method, "value")?;
pop_slots(state, 1, class, method, "array index")?;
pop_slots(state, 1, class, method, "array")?;
Ok(())
}
Insn::Pop => {
let top = pop_slots(state, 1, class, method, "pop")?.remove(0);
if top == VerificationType::Top {
return Err(invalid(class, method, "pop половины long/double"));
return Err(invalid(class, method, "pop of a long/double half"));
}
Ok(())
}
@ -90,7 +94,7 @@ pub(super) fn transfer(
Insn::Swap => {
let mut taken = pop_slots(state, 2, class, method, "swap")?;
if taken.contains(&VerificationType::Top) {
return Err(invalid(class, method, "swap с половиной"));
return Err(invalid(class, method, "swap with a half"));
}
taken.reverse();
push(state, taken)
@ -100,7 +104,7 @@ pub(super) fn transfer(
if *op == Arith::Neg {
pop_slots(state, slots as usize, class, method, "neg")?;
} else if matches!(op, Arith::Shl | Arith::Shr | Arith::Ushr) {
// Сдвиг берёт (значение, int), а не два значения.
// A shift takes (value, int), not two values.
pop_slots(state, slots as usize + 1, class, method, "shift")?;
} else {
pop_slots(state, 2 * slots as usize, class, method, "arith")?;
@ -128,7 +132,7 @@ pub(super) fn transfer(
Insn::Jsr(_) | Insn::Ret(_) => Err(invalid(
class,
method,
"подпрограммы jsr/ret не поддерживаются",
"jsr/ret subroutines are not supported",
)),
Insn::TableSwitch { .. } | Insn::LookupSwitch { .. } => {
pop_slots(state, 1, class, method, "switch")?;
@ -192,7 +196,7 @@ pub(super) fn transfer(
ArrayKind::Int => "[I",
ArrayKind::Long => "[J",
ArrayKind::Reference => {
return Err(invalid(class, method, "newarray ссылочного типа"));
return Err(invalid(class, method, "newarray of a reference type"));
}
};
push(
@ -202,7 +206,16 @@ pub(super) fn transfer(
}
Insn::ANewArray(name) => {
pop_slots(state, 1, class, method, "anewarray")?;
push(state, vec![VerificationType::Object(format!("[L{name};"))])
// `anewarray` adds one dimension: a plain class becomes
// `[L...;`, an already-array operand (`anewarray [B` for a
// partially bound multi-dim) gains one more `[`. Wrapping an
// array operand in `L...;` (`[L[B;`) is not a class.
let descriptor = if name.starts_with('[') {
format!("[{name}")
} else {
format!("[L{name};")
};
push(state, vec![VerificationType::Object(descriptor)])
}
Insn::MultiANewArray { descriptor, dims } => {
pop_slots(state, *dims as usize, class, method, "multianewarray")?;

View file

@ -5,7 +5,7 @@ use super::State;
use crate::error::ClassfileError;
use crate::insn::SlotKind;
/// Запись в local с учётом ширины (`long`/`double` — парой с `Top`).
/// Запись в local с учётом ширины (`long`/`double` - парой с `Top`).
pub(super) fn store_local(
state: &mut State,
slot: u16,
@ -38,7 +38,7 @@ pub(super) fn push(state: &mut State, slots: Vec<VerificationType>) -> Result<()
Ok(())
}
/// Снять `n` слотов (снизу вверх); нехватка — честная ошибка.
/// Снять `n` слотов (снизу вверх); нехватка - честная ошибка.
pub(super) fn pop_slots(
state: &mut State,
n: usize,
@ -84,7 +84,7 @@ pub(super) fn local_set(
Ok(())
}
/// Значение local'а целиком: `long`/`double` — парой с `Top`.
/// Значение local'а целиком: `long`/`double` - парой с `Top`.
pub(super) fn wide_push(value: VerificationType) -> Vec<VerificationType> {
match value {
VerificationType::Long | VerificationType::Double => vec![value, VerificationType::Top],
@ -112,7 +112,7 @@ pub(super) fn dup(
push(state, out)
}
/// `dup2*`: форма выбирается по вершине (`Top` — вторая половина `long`).
/// `dup2*`: форма выбирается по вершине (`Top` - вторая половина `long`).
pub(super) fn dup2(
state: &mut State,
class: &str,

View file

@ -3,7 +3,7 @@
//! `BootstrapMethods`, `EnclosingMethod`, `MethodParameters` and `ConstantValue`
//! are modeled explicitly (they hold pool references that survive rebuilding);
//! everything else that is structured but unneeded by us goes to `Raw` as-is.
//! `Code` and `StackMapTable` are not handled here — the calling method code
//! `Code` and `StackMapTable` are not handled here - the calling method code
//! parses them (see `super::class`).
use ristretto_classfile::ConstantPool;
@ -131,7 +131,7 @@ fn inner(
})
}
/// The enclosing class's method (`0` — class only, no method).
/// The enclosing class's method (`0` - class only, no method).
fn enclosing_method(
pool: &ConstantPool<'_>,
index: u16,

View file

@ -11,7 +11,7 @@ use super::super::pool::class_name;
use crate::error::ClassfileError;
use crate::format::{StackMapFrame, VerificationType};
/// Backend frames → ours (index deltas — as-is).
/// Backend frames → ours (index deltas - as-is).
pub(super) fn to_frames(
frames: &[StackFrame],
pool: &ConstantPool<'_>,
@ -100,7 +100,7 @@ fn vtype(value: &RtType, pool: &ConstantPool<'_>) -> Result<VerificationType, Cl
})
}
/// The catch type (`0` — catch-all for `finally`/`any`).
/// The catch type (`0` - catch-all for `finally`/`any`).
pub(super) fn catch_type(
pool: &ConstantPool<'_>,
index: u16,

View file

@ -1,6 +1,6 @@
//! Method-body labels: where they must stand and how they resolve.
//!
//! The backend numbers instructions in order — labels are placed directly on the
//! The backend numbers instructions in order - labels are placed directly on the
//! numbers, without byte offsets.
use std::collections::{BTreeMap, BTreeSet};
@ -13,7 +13,7 @@ use crate::insn::Label;
/// Instruction numbers that must carry a label.
///
/// `extra` — debug-table positions (already as numbers): pass insertions must
/// `extra` - debug-table positions (already as numbers): pass insertions must
/// not leave them without a label.
pub(super) fn label_indexes(
code: &[Instruction],

View file

@ -1,6 +1,6 @@
//! Backend `Code` → our IR method body: labels, exceptions, frames.
//!
//! The backend already turned byte offsets into instruction indices — the
//! The backend already turned byte offsets into instruction indices - the
//! converters work directly with them: labels are placed on branch targets,
//! try-block boundaries and debug-table positions; frames are carried over as-is
//! (index deltas). `LineNumberTable`/`LocalVariableTable` positions are labels

View file

@ -7,7 +7,7 @@ use super::super::pool::const_from;
use crate::error::ClassfileError;
use crate::insn::{Arith, ArrayKind, Const, Convert, Insn, Prim, SlotKind};
/// `Some` — the instruction is covered; `None` — hand it to the next converters.
/// `Some` - the instruction is covered; `None` - hand it to the next converters.
pub(super) fn convert_basic(
instruction: &Instruction,
pool: &ConstantPool<'_>,

View file

@ -6,7 +6,7 @@ use super::Resolver;
use crate::error::ClassfileError;
use crate::insn::{BranchCond, Cond, Insn, Label, Prim};
/// `Some` — the instruction is covered; `None` — hand it to the next converters.
/// `Some` - the instruction is covered; `None` - hand it to the next converters.
pub(super) fn convert_flow(
instruction: &Instruction,
index: u16,
@ -52,7 +52,7 @@ fn branch(cond: BranchCond, target: u16, resolve: &Resolver<'_>) -> Result<Insn,
Ok(Insn::Branch(cond, resolve(target)?))
}
/// A `switch` target — an instruction number: base plus the backend's relative delta.
/// A `switch` target - an instruction number: base plus the backend's relative delta.
fn abs(index: u16, delta: i32, resolve: &Resolver<'_>) -> Result<Label, ClassfileError> {
let absolute = i32::from(index)
.checked_add(delta)

View file

@ -2,7 +2,7 @@
//!
//! Branch offsets come from the backend (absolute for `if*`/`goto`, relative to
//! the instruction start for `switch`), so conversion takes a resolver from an
//! offset to a [`Label`](crate::insn::Label) — the calling code assigns labels.
//! offset to a [`Label`](crate::insn::Label) - the calling code assigns labels.
mod basic;
mod flow;
@ -17,7 +17,7 @@ use crate::insn::{Insn, Label};
/// Resolves an absolute bytecode offset into a label.
pub(super) type Resolver<'a> = dyn Fn(u16) -> Result<Label, ClassfileError> + 'a;
/// Convert one instruction. `index` — its number in the method body (`switch` needs it).
/// Convert one instruction. `index` - its number in the method body (`switch` needs it).
pub(super) fn convert(
instruction: &Instruction,
index: u16,

View file

@ -7,7 +7,7 @@ use super::super::pool::{class_name, dynamic_parts, expect_kind, is_interface, r
use crate::error::ClassfileError;
use crate::insn::{ArrayKind, Insn, InvokeKind};
/// `Some` — the instruction is covered; `None` — hand it to the next converters.
/// `Some` - the instruction is covered; `None` - hand it to the next converters.
pub(super) fn convert_ref(
instruction: &Instruction,
pool: &ConstantPool<'_>,

View file

@ -1,6 +1,6 @@
//! Our IR attributes → backend attributes.
//!
//! Attribute names (`SourceFile`, …) are interned into the pool right here — the
//! Attribute names (`SourceFile`, …) are interned into the pool right here - the
//! backend stores only `name_index`, and the string is recovered on read.
use ristretto_classfile::attributes::{

View file

@ -2,7 +2,7 @@
//!
//! Our IR stores `offset_delta` in instruction numbers; the backend recomputes
//! them into bytes on `to_bytes`. The compact form (`Same`/`SameLocals`) is valid
//! only when the **byte** delta is ≤ 63 — otherwise the extended form is needed.
//! only when the **byte** delta is ≤ 63 - otherwise the extended form is needed.
//! We choose by the byte delta: bytes are computed from the current method's layout.
use ristretto_classfile::attributes::{StackFrame as RtFrame, VerificationType as RtType};
@ -14,7 +14,7 @@ use crate::format::{StackMapFrame, VerificationType};
/// The byte offset of an instruction's start (for computing frame deltas).
type ByteOf<'a> = &'a (dyn Fn(u32) -> Result<u16, ClassfileError> + 'a);
/// IR frames → backend frames (index deltas — as-is).
/// IR frames → backend frames (index deltas - as-is).
pub(super) fn convert_frames(
frames: &[StackMapFrame],
pool: &mut Pool,

View file

@ -1,7 +1,7 @@
//! Our [`Insn`](crate::insn::Insn) → the backend's `Instruction`.
//!
//! The reverse side of `super::super::parse::insns`: labels resolve into
//! absolute instruction numbers (`switch` — into deltas from itself), constants
//! absolute instruction numbers (`switch` - into deltas from itself), constants
//! and references go to the pool via [`Pool`](super::super::pool::Pool).
use indexmap::IndexMap;
@ -13,9 +13,9 @@ use super::{branch, delta, fit_index, ret_op};
use crate::error::ClassfileError;
use crate::insn::{Insn, Label, SlotKind};
/// One IR instruction → a backend instruction (`None` — a label marker).
/// One IR instruction → a backend instruction (`None` - a label marker).
///
/// `index` — the instruction's number in the method (needed for `switch` deltas).
/// `index` - the instruction's number in the method (needed for `switch` deltas).
pub(crate) fn convert(
insn: &Insn,
index: u32,

View file

@ -132,7 +132,7 @@ pub(crate) fn measure(insns: &[RtInsn]) -> Result<BTreeMap<u32, u16>, ClassfileE
Ok(map)
}
/// Branch targets don't affect size — substitute the current position for sizing.
/// Branch targets don't affect size - substitute the current position for sizing.
fn smash_targets(insn: &mut RtInsn, position: u16) {
match insn {
RtInsn::Ifeq(target)

View file

@ -229,7 +229,7 @@ fn arg_count(descriptor: &str) -> Result<u8, ClassfileError> {
.map_err(|_| ClassfileError::Write(format!("слишком много аргументов в {descriptor}")))
}
/// Skip characters up to the separator (inclusive); `false` — the string ended.
/// Skip characters up to the separator (inclusive); `false` - the string ended.
fn skip_to(chars: &mut std::str::Chars<'_>, end: char) -> bool {
for char in chars {
if char == end {

View file

@ -1,7 +1,7 @@
//! Helpers building `InnerClasses`/`BootstrapMethods`/`MethodParameters`.
//!
//! Moved out of `attrs` so it stays within the 250-line limit; the logic is
//! unchanged — only the location. Each builds a backend entry from an IR
//! unchanged - only the location. Each builds a backend entry from an IR
//! element, interning names/descriptors and resolving `CONSTANT_Class` indices
//! through the pool.

View file

@ -2,10 +2,10 @@
//!
//! The pool is built from scratch by a deterministic walk (classes, fields,
//! methods, attributes, code), so identical IR yields identical bytes. `Raw`
//! attribute indices are not carried over — see `super::attrs`.
//! attribute indices are not carried over - see `super::attrs`.
//!
//! The backend does not deduplicate: `add_*` always pushes a new entry. We cache
//! semantic keys ourselves — otherwise `LocalVariableTable` and
//! semantic keys ourselves - otherwise `LocalVariableTable` and
//! `LocalVariableTypeTable` would get different `name_index` values for the same
//! name, and HotSpot would reject the class (`LVTT entry ... does not match any
//! LVT entry`). The walk order is deterministic, so the cache only changes pool

View file

@ -2,7 +2,7 @@
//!
//! This is an `impl Pool` from [`super`], split out so the parent file stays
//! within the structure limit. Each builder is memoized by a semantic key
//! (owner/name/descriptor/…) — an identical reference always gets the same index,
//! (owner/name/descriptor/…) - an identical reference always gets the same index,
//! and the pool doesn't bloat with duplicates (bloat would otherwise widen `ldc`
//! into `ldc_w` and change code length on relocation).

View file

@ -88,7 +88,7 @@ pub enum InvokeKind {
Interface,
}
/// Type conversion (`i2l`, `f2b`, ... — JVMS §6.5.i2<b|c|s|l|f|d>).
/// Type conversion (`i2l`, `f2b`, ... - JVMS §6.5.i2<b|c|s|l|f|d>).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Convert {
I2L,

View file

@ -1,7 +1,7 @@
//! IR instructions: no offsets, branches address a [`Label`].
//!
//! This is fundamental: any pass that changes instruction order/length need not
//! recompute jump offsets — the writer does that once at the end of the pipeline.
//! recompute jump offsets - the writer does that once at the end of the pipeline.
mod kinds;
@ -27,7 +27,7 @@ pub enum Insn {
slot: u16,
kind: SlotKind,
},
/// `iinc` — increase a local variable by a constant.
/// `iinc` - increase a local variable by a constant.
Iinc {
index: u16,
delta: i32,
@ -45,7 +45,7 @@ pub enum Insn {
Dup2X1,
Dup2X2,
Swap,
/// `iadd`, `lmul`, `lneg`, ... — type + operation.
/// `iadd`, `lmul`, `lneg`, ... - type + operation.
Binary {
prim: Prim,
op: Arith,
@ -59,9 +59,9 @@ pub enum Insn {
Convert(Convert),
Branch(BranchCond, Label),
Goto(Label),
/// `jsr` — subroutine call (only version ≤ 50 classes).
/// `jsr` - subroutine call (only version ≤ 50 classes).
Jsr(Label),
/// `ret` — return from a subroutine (`jsr`/`ret` come in pairs).
/// `ret` - return from a subroutine (`jsr`/`ret` come in pairs).
Ret(u16),
TableSwitch {
default: Label,
@ -73,9 +73,9 @@ pub enum Insn {
default: Label,
pairs: Vec<(i32, Label)>,
},
/// `return`/`ireturn`/...; `None` — void return.
/// `return`/`ireturn`/...; `None` - void return.
Return(Option<Prim>),
/// `areturn` — return a reference ([`Prim`] has no reference type).
/// `areturn` - return a reference ([`Prim`] has no reference type).
AReturn,
GetStatic {
owner: String,
@ -112,7 +112,7 @@ pub enum Insn {
New(String),
NewArray(ArrayKind),
ANewArray(String),
/// `multianewarray` — array of any dimension; `dims` — how many dimensions from the stack.
/// `multianewarray` - array of any dimension; `dims` - how many dimensions from the stack.
MultiANewArray {
descriptor: String,
dims: u8,

View file

@ -27,3 +27,11 @@ pub use model::{
LocalVariable, LocalVariableType, Method, MethodParameter, Program, RecordComponent, Resource,
Version,
};
/// Safety budget: a method body that would grow past this many instructions
/// is left alone by the expanding passes (`strings`, `constants`).
///
/// The writer caps a method body at 64KB; at a pessimistic 8 bytes per
/// instruction this count can never overflow it, while real methods
/// (hundreds of instructions) never come close.
pub const MAX_METHOD_INSNS: usize = 8_000;

View file

@ -17,7 +17,7 @@ pub struct ClassBuilder {
impl ClassBuilder {
/// A new `public` class with the given superclass (`None` = `java/lang/Object`,
/// except for `java/lang/Object` itself — it has no superclass).
/// except for `java/lang/Object` itself - it has no superclass).
pub fn new(this_class: impl Into<String>, super_class: Option<&str>) -> Self {
let this_class = this_class.into();
let super_class = match super_class {

View file

@ -1,4 +1,4 @@
//! The "program" structure — the unit of work for the pipeline.
//! The "program" structure - the unit of work for the pipeline.
pub mod builder;
pub mod version;
@ -8,7 +8,7 @@ pub use version::Version;
use crate::format::StackMapFrame;
use crate::insn::{Const, Insn, Label};
/// Unknown/specific attributes are stored as bytes — lossless round-trip.
/// Unknown/specific attributes are stored as bytes - lossless round-trip.
#[derive(Debug, Clone, PartialEq)]
pub enum Attribute {
SourceFile(String),
@ -32,7 +32,7 @@ pub enum Attribute {
class: String,
method: Option<(String, String)>,
},
/// `MethodParameters`: formal parameter names (`None` — unnamed).
/// `MethodParameters`: formal parameter names (`None` - unnamed).
MethodParameters(Vec<MethodParameter>),
/// `Record`: the record class's components.
Record(Vec<RecordComponent>),
@ -47,7 +47,7 @@ pub enum Attribute {
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LineNumber {
/// Start label (the writer computes bytes — survives pass insertions).
/// Start label (the writer computes bytes - survives pass insertions).
pub start: Label,
pub line: u16,
}
@ -159,7 +159,7 @@ impl Code {
#[derive(Debug, Clone, PartialEq)]
pub struct ExceptionHandler {
/// `None` — catch-all (`finally`).
/// `None` - catch-all (`finally`).
pub start: Label,
pub end: Label,
pub handler: Label,

View file

@ -3,7 +3,7 @@
//! Frames are dropped and recomputed. Judges: determinism + IR round-trip +
//! real `java -Xverify:all` for classes without external dependencies.
//! (The backend's `verify_class` is unfit for acceptance: it expects frames
//! in bytes while `from_bytes` hands out indices — see `todo.md`.)
//! in bytes while `from_bytes` hands out indices - see `todo.md`.)
#![allow(clippy::expect_used, clippy::unwrap_used)]
@ -28,7 +28,7 @@ impl Hierarchy for MapHierarchy {
}
}
/// JDK-only classes — `java` checks them fully.
/// JDK-only classes - `java` checks them fully.
const JDK_ONLY: [&str; 2] = [
"dev/loki/lorep/util/TimeUtil",
"dev/loki/lorep/util/PaginationUtil",
@ -95,7 +95,7 @@ fn fixtures_recompute_and_verify() {
/// A join with a `double` below on the stack must emit it as ONE entry.
///
/// Regression: the emitter used to write the slot pair (`[Double, Top]`),
/// like `javac` does for locals — but on the operand stack `javac` writes a
/// like `javac` does for locals - but on the operand stack `javac` writes a
/// bare `[Double]`, and HotSpot rejects the pair with
/// `Inconsistent stackmap frames`.
#[test]

View file

@ -4,7 +4,7 @@
//! сопоставляет запись `LocalVariableTypeTable` с `LocalVariableTable` по
//! сырому `name_index`. Дублирующий `Utf8` для одного имени даёт разные индексы
//! и рушит класс: `ClassFormatError: LVTT entry ... does not match any LVT
//! entry`. Писатель обязан интернировать строки — поэтому в записанном пуле
//! entry`. Писатель обязан интернировать строки - поэтому в записанном пуле
//! каждый `CONSTANT_Utf8` уникален.
#![allow(clippy::expect_used, clippy::unwrap_used)]

View file

@ -2,7 +2,7 @@
//!
//! Пул при записи строится заново (порядок другой), поэтому сравнивается IR,
//! а не байты. Записанные байты обязаны проходить структурный `verify`
//! backend'а — это ловит битые ссылки пула и кривые атрибуты.
//! backend'а - это ловит битые ссылки пула и кривые атрибуты.
#![allow(clippy::expect_used, clippy::unwrap_used)]

View file

@ -1,8 +1,8 @@
//! Spike, часть 2: доступна ли **генерация** кадров `StackMapTable`.
//!
//! Ключевой вопрос `docs/stack.md` §3: считает ли `ristretto_classfile` кадры
//! сам и отдаёт ли их наружу. Если да — наша работа сводится к записи; если
//! нет — кадры придётся считать (или вендорить их `verifiers::bytecode`).
//! сам и отдаёт ли их наружу. Если да - наша работа сводится к записи; если
//! нет - кадры придётся считать (или вендорить их `verifiers::bytecode`).
#![allow(clippy::expect_used, clippy::unwrap_used)]
@ -82,7 +82,7 @@ fn inference_config() -> VerifierConfig {
VerifierConfig::default().with_fallback_strategy(FallbackStrategy::AlwaysInference)
}
/// Без `StackMapTable` inference обязан пройти сам — значит, движок кадров у
/// Без `StackMapTable` inference обязан пройти сам - значит, движок кадров у
/// крейта есть, просто его результат не отдаётся наружу.
#[test]
fn inference_path_verifies_without_stack_map_table() {
@ -94,7 +94,7 @@ fn inference_path_verifies_without_stack_map_table() {
assert_eq!(results[0].path_used, VerificationPath::Inference);
}
/// Строгий режим без кадров обязан упасть — иначе требование `StackMapTable`
/// Строгий режим без кадров обязан упасть - иначе требование `StackMapTable`
/// для классов ≥ 50 не проверяется вовсе.
#[test]
fn strict_mode_rejects_missing_stack_map_table() {
@ -102,7 +102,7 @@ fn strict_mode_rejects_missing_stack_map_table() {
let config = VerifierConfig::default().with_fallback_strategy(FallbackStrategy::Strict);
let outcome = verify_class(&class_file, &Permissive, &config);
// Строгий режим падает либо `Err`, либо `success == false` — оба варианта
// Строгий режим падает либо `Err`, либо `success == false` - оба варианта
// приемлемы, важен сам отказ.
let rejected = match &outcome {
Err(_) => true,
@ -113,13 +113,13 @@ fn strict_mode_rejects_missing_stack_map_table() {
/// Негативный результат spike'а: кадр не на том сдвиге не отдаёт диагностику.
///
/// Ждали `Ok(vec![result])` с заполненным `diagnostic.expected` — тогда
/// Ждали `Ok(vec![result])` с заполненным `diagnostic.expected` - тогда
/// генерация кадров шла бы итеративным путём через публичный API. Фактически
/// `verify_class` в строгом режиме возвращает `Err(VerifyError)`, и ожидаемый
/// кадр до нас не доходит: из крейта достаётся только место ошибки
/// («merge point at offset N»), но не её содержимое.
///
/// Вывод для `docs/stack.md` §3: проверять кадры крейт умеет, отдавать — нет.
/// Вывод для `docs/stack.md` §3: проверять кадры крейт умеет, отдавать - нет.
#[test]
fn diagnostic_exposes_expected_frame_on_failure() {
let mut class_file = branch_class();
@ -127,7 +127,7 @@ fn diagnostic_exposes_expected_frame_on_failure() {
.constant_pool
.add_utf8("StackMapTable")
.expect("utf8");
// Кадр на сдвиге 0, а настоящий якорь — на сдвиге 3.
// Кадр на сдвиге 0, а настоящий якорь - на сдвиге 3.
class_file.attributes.push(Attribute::StackMapTable {
name_index,
frames: vec![StackFrame::SameFrame { frame_type: 0 }],

View file

@ -1,6 +1,6 @@
//! Spike: пригоден ли `ristretto_classfile` как backend для чтения/записи.
//!
//! Критерии — `docs/stack.md` §2. Тест существует, чтобы ответы на вопросы
//! Критерии - `docs/stack.md` §2. Тест существует, чтобы ответы на вопросы
//! spike'а не устаревали: если крейт сломает хотя бы один пункт, сборка упадёт,
//! и решение о backend'е придётся пересмотреть.
//!
@ -59,7 +59,7 @@ fn round_trip_preserves_identity() {
assert_eq!(restored.constant_pool.len(), source.constant_pool.len());
}
/// Кадры `StackMapTable` конструируются и сериализуются публичным API —
/// Кадры `StackMapTable` конструируются и сериализуются публичным API -
/// значит, мы можем **записывать** кадры, а не только читать.
#[test]
fn stack_map_frames_are_constructible() {