Skip to content

Register allocator correctness verifier#13229

Draft
danocmx wants to merge 112 commits intooracle:masterfrom
danocmx:feat/reg-alloc-verifier
Draft

Register allocator correctness verifier#13229
danocmx wants to merge 112 commits intooracle:masterfrom
danocmx:feat/reg-alloc-verifier

Conversation

@danocmx
Copy link
Copy Markdown

@danocmx danocmx commented Mar 27, 2026

Register allocation verifier, inspired by cranelift's regalloc checker. Works by saving symbols before allocation - variables, constants, registers and stack slots, then matches these to locations inserted by allocator(s) - registers and stack slots. Outputs of instructions then store these symbols to those locations in block's verification state. Spills, register moves and reloads move these around. Before any checking is done, initial allocation state (map of form location → symbol) is calculated for each block based on it's predecessors, when same location with different symbols meet, a conflict is created - can be resolved by writing to the location. Afterwards, checking is done for each block, we verify that symbols before allocation are stored at the locations inserted by allocator(s), taking into account reloads and spills that the allocator created.

Implemented as a phase (RegAllocVerifierPhase) that wraps around both register and stack allocator, collects symbol information before allocation and matches this information to instructions after allocation to create verifier IR that it then uses to update state allocation maps and to verify operands.

When a violation is detected by the verifier, a RAVException is thrown and if dumping is enabled a .rav.txt file is created in graal_dumps directory with the exception details and verifier IR.

Implemented as jdk.graal.compiler.lir.alloc.verifier package, modifies AllocationStage to insert the verification phase when enabled and also changes modifier to public for build method in LocationMarker to access it in the verifier package. No other modification to existing was needed, variable locations stripped by the allocator from label/jump instructions are recovered by finding their first usage in FromUsageGlobalResolver class to not mess with the existing register allocator code.

Some functionality tested in RegAllocVerifierTest, runs valid methods with the verifier, then injects faults and tests if the verifier can detect them. To be expanded.

New compiler flags:

  • EnableRAVerifier - enables the verification for the compilation
  • VerifyStackAllocator - verifies that stack allocator output together with register allocator
  • CollectReferences - uses LocationMarker class to collect GC reference sets before final code analysis stage to verify and invalidate old references

Other functionality

  • Handle rematerialised constants
  • Check kinds between original variables and current locations, as well as the allocation state at said location
  • Check kinds between moved values
  • Make sure callee-saved registers are recovered
  • Check correspondence to operand flags
  • Make sure alive location is not overwritten by the instruction as temp or output
  • Remove old references and check if location marked as reference actually stores one
  • Make sure LIR reference and JavaKind Object match

danocmx added 30 commits March 22, 2026 10:21
@oracle-contributor-agreement
Copy link
Copy Markdown

Thank you for your pull request and welcome to our community! To contribute, please sign the Oracle Contributor Agreement (OCA).
The following contributors of this PR have not signed the OCA:

To sign the OCA, please create an Oracle account and sign the OCA in Oracle's Contributor Agreement Application.

When signing the OCA, please provide your GitHub username. After signing the OCA and getting an OCA approval from Oracle, this PR will be automatically updated.

If you are an Oracle employee, please make sure that you are a member of the main Oracle GitHub organization, and your membership in this organization is public.

@oracle-contributor-agreement oracle-contributor-agreement Bot added the OCA Required At least one contributor does not have an approved Oracle Contributor Agreement. label Mar 27, 2026
@danocmx
Copy link
Copy Markdown
Author

danocmx commented Mar 27, 2026

cc @d-kozak @gergo-

@oracle-contributor-agreement
Copy link
Copy Markdown

Thank you for signing the OCA.

@oracle-contributor-agreement oracle-contributor-agreement Bot added OCA Verified All contributors have signed the Oracle Contributor Agreement. and removed OCA Required At least one contributor does not have an approved Oracle Contributor Agreement. labels Apr 6, 2026
Copy link
Copy Markdown
Member

@gergo- gergo- left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've done a general pass over about half of the code, adding some style and comment remarks. From my point of view, the most important thing for a thorough review would be to have documentation of the "big picture": What is the overall thing we are doing, and which classes cooperate in which way to achieve that goal.

The code seems clean overall, it's clear that you understand what you are doing, now we just need to understand it too :-)

}

public boolean hasConflictedValue(ValueAllocationState valueAllocationState) {
for (var state : this.conflictedStates) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems unfortunate that conflictedStates is a set but you need to do a linear search in it. Maybe instead of a set of states, it should be an RAValue -> ValueAllocationState map?

* @return Newly copied state
*/
@Override
public abstract AllocationState clone();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that you should implement the Cloneable interface if you override Object.clone().

* {@link UnknownAllocationState unknown} - our null state, nothing was stored yet -
* {@link ValueAllocationState value} - symbol that is stored at said location -
* {@link ConflictedAllocationState conflicted} - set of Values that are supposed to be at same
* location
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated formatting destroyed your list, please use proper HTML list syntax in Javadoc comments.

this.putWithoutRegCheck(entry.getKey(), UnknownAllocationState.INSTANCE);
}

var currentValue = this.internalMap.get(entry.getKey());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method would be a bit cleaner if you did this get once and then used the result everywhere, including currentValue == null instead of the containsKey call.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would make sense for this exception class and all of its subclasses to live in a separate subpackage.

protected void checkStateValues(RAVInstruction.Op op) {
if (!op.hasCompleteState()) {
// Some values are null after allocation because of stack slot allocator
// because it is skipped when iteration (StackLockValue).
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does this skipping of StackLockValues happen? Could you check more specifically for this situation?

// TestCase: IntegerDivRemCanonicalizationTest
// instruction r10|QWORD = STACKLEA slot: stack:80|ILLEGAL[*] in B0
// had vstack:0, which is not mentioned in first label or elsewhere
// so symbol vstack:0 won't be found
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So basically you're not checking if the allocation of virtual stack slots to concrete stack slots is consistent in the same way that you check the consistency of the allocation of virtual registers to concrete registers?

// not work, for example, RETURN with rax tends to contain the actual
// generated variable instead of rax symbol, or NEAR_FOREIGN_CALL
// keeps its own registers before and after allocation, but those
// can also contain different variable symbols.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear to me what "RETURN with rax tends to contain the actual generated variable instead of rax symbol" means. I'm probably missing some general context. You should write a package-info.java as an entry point that explains the big picture of how the verifier works and how the main classes work together. See vectorapi/package-info.java as an example.

* In-case comparison of {@link ValueAllocationState} fails, it also might get resolved by this.
* </p>
*/
public interface ConflictResolver {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This interface only seems to have one implementation. Do you expect there to be more, or could the interface be removed?

*/
public class RegAllocVerifierPhase extends RegisterAllocationPhase {
public static class Options {
@Option(help = "Verify that register allocation is indeed, correct", type = OptionType.Debug) public static final OptionKey<Boolean> EnableRAVerifier = new OptionKey<>(true);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please put the @Option annotation on a separate line. The formatter will try to put it on the same line again, we usually wrap the whole block of option declarations in // @formatter:off/// @formatter:on. Alternatively, you can put an empty // comment at the end of the line to prevent merging.

}

public static String getMessage(AllocatableValue orig, AllocatableValue curr, JavaKind kind) {
if (JavaKind.Object.equals(kind)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kind.isObject() would be a bit cleaner.

/**
* Interface for state concrete location is in, stored in {@link AllocationStateMap}.
*/
public abstract class AllocationState implements Cloneable {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this could be a sealed class?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

OCA Verified All contributors have signed the Oracle Contributor Agreement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants