Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement single return invariant (diamond shaped procedures) #178

Closed
wants to merge 5 commits into from

Conversation

ailrst
Copy link
Contributor

@ailrst ailrst commented Feb 16, 2024

Adds return statement and ensures a procedure always has exactly one return statement (which may not always be reachable).

To simplify this we also make the entryBlock and returnBlock of a procedure always defined. The returnBlock must contain a return, and the entryBlock is assumed to have no blocks that jump to it.

  • If a procedure is constructed with no blocks then it consists of an empty entryBlock that jumps directly to the returnBlock.
  • Adds internalBlocks containing the mutable block list
  • Now Procedure.blocks returns entryBlock ++ internalBlocks ++ returnBlock.
  • Removes all the cruft maintaining the entry and return blocks and creating them from the added blocks in case they are removed: they are now not allowed to be removed.
  • Adds Procedure.hasImplementation method to define whether the procedure is a stub.
  • the addBlock and removeBlock methods on procedure ensure that the only return statement in the procedure is the one in the return block by:
    1. When a block is added its return call is replaced with a GoToReturn(returnBlock) (where gotoReturn is a subclass of GoTo that only allows one target that cannot be changed).
    2. When a block is removed its return call is replaced with a Return again.

make the presence of begin and end block invariant in procedure
@ailrst ailrst marked this pull request as ready for review February 28, 2024 07:16
Copy link
Contributor

Choose a reason for hiding this comment

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

You've duplicated this since it was moved

// TODO: this method doesn't require aftercall blocks only have 1 incoming jump
def isAfterCall : Boolean = p.incomingJumps.nonEmpty && p.incomingJumps.forall(_.isAfterCall)

def begin: CFGPosition = p
def end: CFGPosition = p.jump

extension (p: Command)
def isReturn: Boolean = p.parent.parent.returnBlock.jump eq p
Copy link
Contributor

Choose a reason for hiding this comment

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

shouldn't this just be replaced entirely since we can match on Return now

Comment on lines 97 to 98
case (false , false, true) => "Test passed"
case (_, _, false) => "Prover error: unknown result: " + boogieResult
case (false, false, true) => "Test passed"
case (_, _, false) => "Prover error: unknown result"
Copy link
Contributor

Choose a reason for hiding this comment

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

Why change this, this was useful

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was probably unintentionally removed in a merge

@l-kent
Copy link
Contributor

l-kent commented Mar 1, 2024

What's the point of mandating empty entry blocks? Doesn't this just make Procedure completely redundant as a node in the CFG? It doesn't really make sense to have both?

@ailrst
Copy link
Contributor Author

ailrst commented Mar 1, 2024

Rather than requiring it has no statements I think its more important to require it has no incoming (intraprocedural) jumps and only one outgoing jump.

  1. The entry block cannot be added/removed/replaced like the rest of the blocks which makes the everything simpler because it can't be undefined, but means you can't just visit it and replace it, or add blocks before it, so its simpler and more consistent to have the entry and exit be "outside the implementation" and somewhat immutable. E.g. now setting the 'entry block' is linking the control flow rather than setting a field in procedure, which is consistent with any other control flow modification. The main thing I was thinking of was if the entry block was the head of the loop you might have to adapt the loop reducing algorithm or special case the entry block.
  2. Yes it makes the procedure cfg node somewhat redundant from the perspective of analyses, I think its useful to have the entry/exit symmetry at the block level, as you've said ideally the iterator works at the block level not over the 3 levels of the heirarchy. I'm hesitant to remove the procedure node from the cfg though because it might make the code more convoluted but I haven't really looked at it.

@l-kent
Copy link
Contributor

l-kent commented Mar 1, 2024

Looking at removing Procedure from the CFG sounds like a good idea to me

# Conflicts:
#	src/main/scala/analysis/SSAForm.scala
#	src/main/scala/ir/Program.scala
#	src/main/scala/ir/Statement.scala
@l-kent l-kent changed the base branch from staging to main March 11, 2024 23:30
Comment on lines 90 to 94
val procs = program.procs
assert(liveVarAnalysisResults(procs("main")) == Map(R30 -> TwoElementTop))
assert(liveVarAnalysisResults(procs("callee1")) == Map(R0 -> TwoElementTop, R30 -> TwoElementTop))
assert(liveVarAnalysisResults(procs("callee2")) == Map(R1 -> TwoElementTop, R30 -> TwoElementTop))
// assert(liveVarAnalysisResults(procs("main")) == Map(R30 -> TwoElementTop))
assert(liveVarAnalysisResults(procs("callee1")) == Map(R0 -> TwoElementTop))
assert(liveVarAnalysisResults(procs("callee2")) == Map(R1 -> TwoElementTop))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want to remove R30 from these? It makes sense to still consider it 'live' since it is called in a Return?

@l-kent
Copy link
Contributor

l-kent commented Mar 11, 2024

We probably need to figure out more broadly how this should relate to non-returning procedures, and how they should be handled in the analyses (which generally do not account for their existence).

@ailrst ailrst changed the title implement single return invariant implement single return invariant (diamond shaped procedures) Jun 4, 2024
@ailrst
Copy link
Contributor Author

ailrst commented Jun 4, 2024

We probably need to figure out more broadly how this should relate to non-returning procedures, and how they should be handled in the analyses (which generally do not account for their existence).

I think to address this we might add a "Halt" statement, and just have an analysis determine for each procedure if all paths reach a halt statement or non-returning call. I don't think it necessarily interacts with this PR, because we don't want a single halt per-procedure in the same way we do returns for backwards analysis.

Otherwise, I think this PR is overcomplicated. I like having a fixed entry and return block, but the automatic rewriting of Returns into jumps to the exit block is unneccessary. It would make more sense to just have an analysis find all the returns and rewrite the procedure into a diamond shape, and have an invariant checker make sure this stays the case through subsequent transforms.

@l-kent
Copy link
Contributor

l-kent commented Jun 4, 2024

Is the idea that the 'halt' statement would follow a non-returning call? This could just be an 'assume false' then, but it could be made its own special type for ease of matching.

I agree that automatically rewriting returns like this is more complicated than necessary, and what you now suggest sounds like a better approach.

@l-kent
Copy link
Contributor

l-kent commented Jun 7, 2024

I think that creating a new Return class is a good idea though. It's worth making a simpler PR that just does that part of it.

This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants