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

[Compiler] Support compilation for before statements #3763

Open
wants to merge 4 commits into
base: feature/compiler
Choose a base branch
from

Conversation

SupunS
Copy link
Member

@SupunS SupunS commented Feb 7, 2025

Work towards #3742

Description

Before functions/statements are treated similar to pre/post conditions.

For concrete type functions

Before-statements are inlined and added to the beginning of the function.

struct Test {
    var i: Int

    init() {
        self.i = 2
    }

    fun test() {
        post {
            print(before(self.i).toString())
        }
        self.i = 8
    }
}

becomes:

struct Test {
    var i: Int
    
    init() {
        self.i = 2
    }
    
    fun test() {
        var �exp�0 = self.i    // Inlined before-statement
        self.i = 8
        if !print(�exp�0.toString()) {    // Post condition refers to the variable declared above
            panic("pre/post condition failed")
        }
        return
    }
}

For interface methods

  • Before-statements are extracted out as a separate function, and are invoked explicitly by the implementation.
  • Post condition accept the before-statement results are parameters.
struct interface Foo {
    var i: Int

    fun test() {
        post {
            print(before(self.i).toString())
        }
    }
}

struct interface Bar: Foo {
    var i: Int

    fun test() {
        post {
            print(before(self.i + 3).toString())
        }
    }
}


struct Test: Bar {
    var i: Int

    init() {
        self.i = 2
    }

    fun test() {
        self.i = 8
    }
}

Would eventually become something like:

struct interface Foo {
    var i: Int
    
    access(all)
    view fun $Foo.test.�exp�0(): AnyStruct {    // Before statements get extracted out to a separate function
        return self.i
    }
    
    access(all)
    view fun $Foo.test.postConditions(_ �exp�0: AnyStruct): Void {  // Post condition accept the before-statement results as parameters
        if !print(�exp�0.toString()) {
            panic("pre/post condition failed")
        }
        return
    }
}

struct interface Bar: Foo {
    var i: Int
    
    access(all)
    view fun $Bar.test.�exp�1(): AnyStruct {
        return self.i + 3
    }
    
    access(all)
    view fun $Bar.test.postConditions(_ �exp�1: AnyStruct): Void {
        if !print(�exp�1.toString()) {
            panic("pre/post condition failed")
        }
        return
    }
}

struct Test: Bar {
    var i: Int
    
    init() {
        self.i = 2
    }
    
    fun test() {
        var $_before0 = self.$Foo.test.�exp�0()  // Implementation would call the before-statement-functions at the start of the implemented function
        var $_before1 = self.$Bar.test.�exp�1()
        self.i = 8
        self.$Foo.test.postConditions($_before0)  // And the results are passed as argments
        self.$Bar.test.postConditions($_before1)
        return
    }
}

  • Targeted PR against master branch
  • Linked to Github issue with discussion and accepted design OR link to spec that describes this work
  • Code follows the standards mentioned here
  • Updated relevant documentation
  • Re-reviewed Files changed in the Github PR explorer
  • Added appropriate labels

@SupunS SupunS added the Feature label Feb 7, 2025
@SupunS SupunS self-assigned this Feb 7, 2025
@SupunS SupunS requested a review from turbolent as a code owner February 7, 2025 19:17
@SupunS SupunS requested a review from jsproz February 7, 2025 19:19
Copy link

github-actions bot commented Feb 7, 2025

Cadence Benchstat comparison

This branch with compared with the base branch onflow:feature/compiler commit d861cc4
The command for i in {1..N}; do go test ./... -run=XXX -bench=. -benchmem -shuffle=on; done was used.
Bench tests were run a total of 7 times on each branch.

Collapsed results for better readability

@turbolent
Copy link
Member

Great work! I haven't looked too deep into the code yet, but how does this handle the case where the post-conditions in the interfaces refer to different expressions, e.g.

struct interface Foo {
    var i: Int

    fun test() {
        post {
            before(self.i) == 2
        }
    }
}

struct interface Bar: Foo {
    var j: Int

    fun test() {
        post {
            before(self.j) == 3
        }
    }
}


struct Test: Bar {
    var i: Int
    var j: Int

    init() {
        self.i = 2
        self.j = 3
    }

    fun test() {
        self.i = 4
        self.j = 5
    }
}

In the the example above the two interfaces referred to the same expression (self.i).

I guess there's no potential for confusion, as the IDs are distinct ($Foo.test.�exp�0 vs $Bar.test.�exp�1), and it's also ensured that the separate compilation ensures confusion even if e.g. Foo gets additional before expressions (I assume $Foo.test.�exp�1, etc.?) How are the IDs generated?

@SupunS
Copy link
Member Author

SupunS commented Feb 7, 2025

but how does this handle the case where the post-conditions in the interfaces refer to different expressions, e.g.

It's pretty much the same. Regardless of the type of the expression, whatever the expression that is inside before(xyz) will become a function of the same interface.
i.e: Turn it into a function which returns xyz

struct interface Foo {
    var i: Int
    
    access(all)
    view fun $Foo.test.�exp�0(): AnyStruct {
        return self.i
    }
    ...
}

struct interface Bar: Foo {
    var j: Int
    
    access(all)
    view fun $Bar.test.exp�1(): AnyStruct {
        return self.j
    }
    ...
}

And then in the post condition, before(xyz) is replaced by the passed-in argument/parameter.

I guess there's no potential for confusion, as the IDs are distinct ($Foo.test.�exp�0 vs $Bar.test.�exp�1), and it's also ensured that the separate compilation ensures confusion even if e.g. Foo gets additional before expressions (I assume $Foo.test.�exp�1, etc.?) How are the IDs generated?

Yes, the IDs are distinct (the counter is per-type). Before-extractor in type-checker generate these counters (�exp�1), the compiler only make it type/method-qualified by prepending the type/method name info to it. Additionally, the type info (MemberAccessInfo) of these invocations (say self.$Foo.test.�exp�0()) points to the correct interface type (Foo) and that is what the compiler use. So the type name prefix in the generated ID is more like a second-level insurance (and to improve debug-ability), but not really a necessity.

The generated complete code for your example is:

struct interface Foo {
    var i: Int
    
    access(all)
    view fun $Foo.test.�exp�0(): AnyStruct {
        return self.i
    }
    
    access(all)
    view fun $Foo.test.postConditions(_ �exp�0: AnyStruct): Void {
        if !(�exp�0 == 2) {
            panic("pre/post condition failed")
        }
        return
    }
}

struct interface Bar: Foo {
    var j: Int
    
    access(all)
    view fun $Bar.test.�exp�1(): AnyStruct {
        return self.j
    }
    
    access(all)
    view fun $Bar.test.postConditions(_ �exp�1: AnyStruct): Void {
        if !(�exp�1 == 3) {
            panic("pre/post condition failed")
        }
        return
    }
}

struct Test: Bar {
    var i: Int
    
    var j: Int
    
    init() {
        self.i = 2
        self.j = 3
    }
    
    fun test() {
        var $_before0 = self.$Foo.test.�exp�0()
        var $_before1 = self.$Bar.test.�exp�1()
        var $_result =  
        self.i = 4
        self.j = 5
        self.$Foo.test.postConditions($_before0)
        self.$Bar.test.postConditions($_before1)
        return
    }
}

@SupunS
Copy link
Member Author

SupunS commented Feb 7, 2025

Happy to walk-through the idea in detail in the next implementation sync

@jsproz
Copy link
Contributor

jsproz commented Feb 7, 2025

It seems like the pre-conditions and the post-conditions should be called in opposite orders.

    fun test() {
        var $_before0 = self.$Foo.test.�exp�0()
        var $_before1 = self.$Bar.test.�exp�1()
        var $_result =  
        self.i = 4
        self.j = 5
        self.$Bar.test.postConditions($_before1)
        self.$Foo.test.postConditions($_before0)
        return
    }

And the order in which they are called should be well documented. I believe that C++ and Swift use different orders for constructors and destructors. Should the pre-conditions of the grandparent be called before or after the parent's pre-conditions?

@jsproz
Copy link
Contributor

jsproz commented Feb 7, 2025

Should the grandparent's post-condition even be called if the parent's condition fails?

@SupunS
Copy link
Member Author

SupunS commented Feb 7, 2025

@jsproz conditions are linearized in a "depth-first pre-ordered" manner. It is already documented under the interface section: https://cadence-lang.org/docs/language/interfaces#linearizing-conditions

Should the grandparent's post-condition even be called if the parent's condition fails?

Given a failed condition would panic the runtime, it will terminate the execution, and would not call any conditions to follow (grandparent's post-condition in this case)

@jsproz
Copy link
Contributor

jsproz commented Feb 7, 2025

The language reference does indicate the post-conditions are executed in the reverse order:

"Similarly, for post-conditions, the same linearization of interfaces would be used, and the post-conditions are executed in the reverse order."

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

Successfully merging this pull request may close these issues.

3 participants