Skip to content

Conversation

fabianschuiki
Copy link
Contributor

Add the Contract API which allows users to express the pre and post-conditions of a piece of hardware. These can be used during formal verification to prove that the hardware upholds the conditions laid out in the contract. Additionally, contracts may be used to implement a divide-and-conquer strategy to simplify larger proofs since they act like a cutpoint and an assume-guarantee substitution.

Contributor Checklist

  • Did you add Scaladoc to every public function/method?
  • Did you add at least one test demonstrating the PR?
  • Did you delete any extraneous printlns/debugging code?
  • Did you specify the type of improvement?
  • Did you add appropriate documentation in docs/src?
  • Did you request a desired merge strategy?
  • Did you add text to be included in the Release Notes for this change?

Type of Improvement

  • Feature (or new API)

Desired Merge Strategy

  • Squash: The PR will be squashed and merged (choose this if you have no preference).

Release Notes

Reviewer Checklist (only modified by reviewer)

  • Did you add the appropriate labels? (Select the most appropriate one based on the "Type of Improvement")
  • Did you mark the proper milestone (Bug fix: 3.6.x, 5.x, or 6.x depending on impact, API modification or big change: 7.0)?
  • Did you review?
  • Did you check whether all relevant Contributor checkboxes have been checked?
  • Did you do one of the following when ready to merge:
    • Squash: You/ the contributor Enable auto-merge (squash) and clean up the commit message.
    • Merge: Ensure that contributor has cleaned up their commit history, then merge with Create a merge commit.

@fabianschuiki fabianschuiki added the Feature New feature, will be included in release notes label Feb 12, 2025
it should "support contracts with a single operand" in {
class Foo extends RawModule {
val a = IO(Input(Bool()))
val b = Contract(a) { x =>
Copy link
Contributor

Choose a reason for hiding this comment

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

i am missing the basic user understanding of when something should go into the argument list of the Contract or just be captured in the closure of the body.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Your actual hardware would go into the argument list. And the body then describes what your hardware does:

val z_impl = Instance(new MyFancyMultiplier(a, b))
val z = FormalContract(z_impl) { case z =>
  EnsureProperty(z === a * b)
}

This will turn into a AssertProperty(z_impl === a * b) to check that the multiplier works, and a separate val z = a * b during formal verification.

Comment on lines 49 to 53
* // Contract with single argument: "(a<<3)+a is equivalent to a*9".
* // During formal verification, this is interpreted as:
* // - "assert((a<<3)+a === a*9)" as a proof that the contract holds
* // - "assume(b === a*9)" as a simplification of other proofs
* val b = Contract((a<<3)+a) { b =>
Copy link
Contributor

Choose a reason for hiding this comment

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

what is the user intuition for why you put the (a << 3) + a term into the argument list for the Contract but the a*9 term is only used in the body of the contract?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The example is a bit simplistic, but you would pass the actual implementation of your circuit, for example a radix-4 Booth multiplier, to the contract as an argument. The rationale is that you want the contract to act as a cutpoint, detaching the implementation and replacing it with a symbolic value during formal verification. You'd then use a simpler expression inside the contract to characterize your circuit, for example a * b for your multiplier.

Comment on lines 49 to 53
* // Contract with single argument: "(a<<3)+a is equivalent to a*9".
* // During formal verification, this is interpreted as:
* // - "assert((a<<3)+a === a*9)" as a proof that the contract holds
* // - "assume(b === a*9)" as a simplification of other proofs
* val b = Contract((a<<3)+a) { b =>
Copy link
Contributor

Choose a reason for hiding this comment

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

kind of a nit but using the same variable b both inside and as the return value of the contract is a little confusing. What is the point of the return value b, how does one use that returned value?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The body of the contract wants to talk about the result of the contract, which is either what you pass into the argument list (during synthesis and when checking that your hardware upholds the contract), or it's a symbolic value (during formal verification when you assume that the contract holds). There isn't really a good way to do this in Scala... so the contract passes the value it produces as a result to the body as well.

* // as a simplification of other proofs
* val csa_s = p^q^r
* val csa_c = (p&q | (p^q)&r) << 1
* val (u,v) = Contract(csa_c, csa_s) { case (u,v) =>
Copy link
Contributor

Choose a reason for hiding this comment

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

again, what is the point of val (u, v) that is returned here? what are they and what do you do with them?

/** Create a `contract` block with one or more arguments and results.
*
* Outside of verification uses, the arguments passed to the contract are
* simply returned as contract results. During formal verification, the
Copy link
Contributor

@mwachs5 mwachs5 Feb 18, 2025

Choose a reason for hiding this comment

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

what are "the contract results"? what does that mean? If I am passing the arguments anyway, why do i need the contract to return them again, couldn't i just use that myself like:

val a = ...
Contract(a) { b => 
  assume (b == ...)
}
AssertProperty(a)

i guess if you didn't to have to give a named variable to a before you pass it to the contract, this can help

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 is due to the cutpoint behaviour of a contract during formal verification. When doing formal verification, the contract doesn't just forward its argument list to its results, but instead creates symbolic values as its results that are then constrained by the contract body. (Which is why the body takes arguments as well: it needs to constrain either the hardware, or the symbolic value, depending on which part you're verifying.)

Builder.forcedUserModule.withRegion(contract.region)(layer.elideBlocks(body(mapped)))
mapped
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

What about Scala 3? The code that can be shared should be in src/main/scala. The macro is cool and I do think it's better, but if you just did the annoying thing and write the method for 1 to N arguments (e.g. DataProduct support for Tuples), then it would work for both Scala 2 and 3.

Copy link
Contributor Author

@fabianschuiki fabianschuiki Feb 27, 2025

Choose a reason for hiding this comment

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

Good call. I've moved the common code into scala/chisel3 and added an explicit scala-3/chisel3 with the Scala 3 specific stuff.

Copy link
Member

@seldridge seldridge left a comment

Choose a reason for hiding this comment

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

Superficial nits. Land whenever you're ready.

Comment on lines 54 to 55
*/
private[chisel3] trait ObjectFormalContractImpl {
Copy link
Member

Choose a reason for hiding this comment

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

This is fantastic Scaladoc. It's on a private trait, though. Where is it expected to show up? Is there some other place it should go?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My hope was that this would show up on Scala 2 and Scala 3 object FormalContract which extends ObjectFormalContractImpl. Let me check how doc inheritance works for private base classes/traits -- otherwise I can always factor this into a public dummy base as we do for some of the source location macro stuff.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just realized that this should actually be public. The apply and mapped functions are supposed to be available to users of the API. That should also fix the doc issue.

Copy link
Contributor

Choose a reason for hiding this comment

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

I just realized that this should actually be public

This should not be public--we don't want the Scala 2 / Scala 3 split visible in the public API.

The apply and mapped functions are supposed to be available to users of the API.

Even wit the trait package private, apply and mapped are public on any classes/objects that mix the trait in so no problem there.

That should also fix the doc issue.

That won't really fix the doc issue because users would still have to click through to the trait to see the doc which isn't what we want, we want it to show up on the object FormalContract.

I think you need to put /** @inheritdoc */ on each object FormalContract to make inheritance work. Now if that can't work from a package private trait, then you could move the Scala doc to a public trait that is nothing but the doc, but I think the implementations of mapped and apply should stick around on a private trait.


All of this being said

I'd like to invert the public/private relationship such that the public classes and objects lives in src/main/scala and only package private traits that provide the public methods using macros live in src/main/scala-2 and src/main/scala-3. So here's what I think you should really do.

Create a new package private trait (probably called FormalContract$VirtualMethods1) in this file (core/src/main/scala/chisel3/FormalContractImpl.scala) that just defines the virtual method mapped. Change the code in src/main/scala-2 and src/main/scala-3 to be package private traits (probably called FormalContract$Intf) that extend FormalContract$VirtualMethods and self-typed on FormalContract.type2 but still provide the public methods that use macros. Then back in this file, make object FormalContract extends FormalContract$Intf and you'll just need the ScalaDoc there. It's more clear than the old pattern IMO, I need to roll out this new approach to the rest of the code base.

Footnotes

  1. The $ is because in Scala, an object's underlying type is named with a trailing $ in Java bytecode. This is mostly hidden in the Scala API but leaks at times in errors. @seldridge and I discussed that the pattern ObjectModuleImpl is confusing that it's referring to object module and that Module$Impl would be more clear so that's the new style I'm proposing.

  2. FormalContract.type is technically the type for object FormalContract, but as stated above, it desugars/lowers to FormalContract$ in Java bytecode.

Copy link
Contributor

Choose a reason for hiding this comment

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

A lot of this FormalContractIntf vs. FormalContract$Intf may feel silly since there's no class FormalContract, but elsewhere in the code base we have classes and companion objects so I want to define a single style that works across the board.

Copy link
Contributor

Choose a reason for hiding this comment

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

I've laid this out more explicitly and with an example in #4768

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Really nice. I've moved the contracts to this new pattern.

@sequencer
Copy link
Member

It would be super cool to add some mdoc-based documentation for the contract + hierarchical formal example!

Add the `Contract` API which allows users to express the pre and
post-conditions of a piece of hardware. These can be used during formal
verification to prove that the hardware upholds the conditions laid out
in the contract. Additionally, contracts may be used to implement a
divide-and-conquer strategy to simplify larger proofs since they act
like a cutpoint and an assume-guarantee substitution.
@fabianschuiki
Copy link
Contributor Author

Thanks for the reviews and great suggestions. Going to land this now.

@fabianschuiki fabianschuiki merged commit b06b7ff into chipsalliance:main Mar 4, 2025
15 checks passed
@fabianschuiki fabianschuiki deleted the add-contracts branch March 4, 2025 20:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature New feature, will be included in release notes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants