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 inherited functions and conditions through delegation #3734

Merged
merged 19 commits into from
Jan 29, 2025

Conversation

SupunS
Copy link
Member

@SupunS SupunS commented Jan 24, 2025

This PR also includes the changes added in #3727, #3728, #3729, #3731. i.e: Builds on top of the foundation of those previous PRs.

Description

Generate code for interfaces separately, and avoids copying over the code for default functions and inherited function pre/post conditions.

Default functions

Instead, injects a delegator function, which would call into the interface's default method.

struct interface IA {
    fun test(): Int {
        return 42
    }
}

struct Test: IA {}

becomes:

struct interface IA {
    fun test(): Int {
        return 42
    }
}

struct Test: IA {
    fun test(): Int {
        return parent.test()  // At codegen, this becomes a static function invocation
    }
}

Function conditions

A function condition that is defined in an interface method gets extracted out to a separate method. Then any implementation would call into this generated/synthetic function as part of their pre/post conditions.
e.g:

struct interface A {
    access(all) fun test(_ a: Int): Int {
        pre { a > 10: "a must be larger than 10" }
    }
}

struct interface B: A {
    access(all) fun test(_ a: Int): Int
}

struct C: B {
    fun test(_ a: Int): Int {
        return a + 3
    }
}

becomes

struct interface A {    
    access(all) fun test(_ a: Int): Int {}

    // condition gets extracted out as a function
    access(all) view fun $A.test.preConditions(_ a: Int): Void {
        if !(a > 10) {
            panic("a must be larger than 10")
        }
        return
    }
}

struct interface B: A {
    access(all) fun test(_ a: Int): Int {}
}

struct C: B {
    fun test(_ a: Int): Int {
        parent.$A.test.preConditions(a)    // calls into the inherited pre-condition function (a static function invocation)
        return a + 3
    }
}

TODO:

  • Handle result variable inside conditions.
  • Handle situations where a default function with conditions gets overridden.

  • 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 requested a review from turbolent as a code owner January 24, 2025 00:59
@SupunS SupunS self-assigned this Jan 24, 2025
@SupunS SupunS force-pushed the supun/generate-code-for-interfaces branch from 269e3d6 to 2a13f25 Compare January 24, 2025 16:02
@SupunS SupunS changed the base branch from supun/improve-copied-code-compilation to feature/compiler January 24, 2025 16:09
@SupunS SupunS changed the title [Compiler+VM POC] Generate code for interfaces separately [Compiler+VM POC] Support inherited functions and conditions through delegation Jan 24, 2025
Copy link

github-actions bot commented Jan 24, 2025

Cadence Benchstat comparison

This branch with compared with the base branch onflow:feature/compiler commit 3a904fd
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

@SupunS SupunS force-pushed the supun/generate-code-for-interfaces branch from aed29ca to 5ded59f Compare January 24, 2025 16:29
@SupunS SupunS changed the title [Compiler+VM POC] Support inherited functions and conditions through delegation [Compiler] Support inherited functions and conditions through delegation Jan 24, 2025
@SupunS SupunS force-pushed the supun/generate-code-for-interfaces branch from 5c1c4c0 to 47355ef Compare January 24, 2025 19:55
@SupunS SupunS force-pushed the supun/generate-code-for-interfaces branch from 47355ef to b0570bb Compare January 24, 2025 19:58
@SupunS SupunS requested a review from jsproz January 24, 2025 20:46
@bluesign
Copy link
Contributor

Out of curiosity. Why not like this ?

struct interface A {    
    access(all) fun test(_ a: Int): Int {}

    // condition gets extracted out as a function
    access(all) view fun $A.test.preConditions(_ a: Int): Void {
        if !(a > 10) {
            panic("a must be larger than 10")
        }
        return
    }
}

struct interface B: A {

    access(all) fun test(_ a: Int): Int {}

    access(all) view fun $B.test.preConditions(_ a: Int): Void {
        parent.$A.test.preConditions(a) 
    }

}

struct C: B {
    fun test(_ a: Int): Int {
        parent.$B.test.preConditions(a)    // calls into the inherited pre-condition function (a static function invocation)
        return a + 3
    }
}

@SupunS
Copy link
Member Author

SupunS commented Jan 27, 2025

@bluesign That's a good question! I initially thought about it too, and It's possible to do as what you've suggested. However, if there are multiple interface conformances for C (say X, Y, Z), and if more than one of them inherits from A, (i.e: if there are more than one path to A in the hierarchy) then by chaining like that would end up invoking the function-condition multiple times (from the different paths).

The way it works now is, all those different paths gets flattened at compile time (the same way the checker does), and only one path would be preserved.

I need to document these somewhere.

@turbolent
Copy link
Member

@SupunS Does that mean that if B.test would have a precondition, then C.test would have two pre-condition function calls, B.test and A.test (the linearization of all conditions)?

Copy link
Member

@turbolent turbolent left a comment

Choose a reason for hiding this comment

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

Great work! 👏

I like this separate compilation approach a lot more compared to the copying of code on source-level.

As for the desugaring: It currently requires quite a lot of code to implement a transformation, I wonder if we can use a form of "template", where we parse Cadence source code and then have a function produce a copy of the AST of it, where a particular node in the tree is replaced with a parameter value.

For example, something like (pseudo-code):

type TemplateFunc func(args map[string]ast.Expression) []ast.Statements

var conditionTemplate = newTemplate(
    `if (!$test) { panic($message) }`,
    "$test",
    "$message",
)

func newTemplate(source string, params ...string) TemplateFunc {
   return func(args map[string]ast.Expression) []ast.Statements {
       // parse source to AST
       // replace identifiers which have $ prefix with args
       // return replaced AST 
   }
}

bbq/opcode/instructions.yml Show resolved Hide resolved
@SupunS
Copy link
Member Author

SupunS commented Jan 28, 2025

@SupunS Does that mean that if B.test would have a precondition, then C.test would have two pre-condition function calls, B.test and A.test (the linearization of all conditions)?

Yes, correct. For example, for a complex inheritance chain like:

cadence/bbq/vm/test/vm_test.go

Lines 2590 to 2628 in b0570bb

t.Run("pre conditions order", func(t *testing.T) {
t.Parallel()
code := `struct A: B {
access(all) fun test() {
pre { print("A") }
}
}
struct interface B: C, D {
access(all) fun test() {
pre { print("B") }
}
}
struct interface C: E, F {
access(all) fun test() {
pre { print("C") }
}
}
struct interface D: F {
access(all) fun test() {
pre { print("D") }
}
}
struct interface E {
access(all) fun test() {
pre { print("E") }
}
}
struct interface F {
access(all) fun test() {
pre { print("F") }
}
}

The eventual concrete type's function would look like below (this is the actual desugared-tree pretty-printed):

struct A: B {
    access(all)
    fun test() {
        self.$B.test.preConditions()
        self.$C.test.preConditions()
        self.$E.test.preConditions()
        self.$F.test.preConditions()
        self.$D.test.preConditions()
        if !print("A") {
            panic("pre/post condition failed")
        }
        return
    }
}

@SupunS
Copy link
Member Author

SupunS commented Jan 29, 2025

Regarding the templating, that's a good idea! Agree that we can simplify these synthetic nodes generation through some templating mechanism 👌

@SupunS SupunS merged commit 6ce5b49 into feature/compiler Jan 29, 2025
9 checks passed
@SupunS SupunS deleted the supun/generate-code-for-interfaces branch January 29, 2025 22:15
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