-
Notifications
You must be signed in to change notification settings - Fork 122
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
fix: support use of constant in other constant and for struct field default value before declaration #1478
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is a nice solution, I actually like it. It is treating the constants as inlining their definition, as long as they have not been evaluated already. Since evaluation is on demand, it handles cases like this:
const A: Int = B;
const B: Int = foo(1);
function foo(v: Int) {
if (v == 1) {
return 0;
} else {
return A; // The else never executes, so no circular dependence at compile-time
}
}
I was wondering if we could do this process for contract constants, but then I realized that the solution already evaluates contract constants. Note however that contract constants cannot depend on each other, because self
is not allowed in their initializers and only calls to global functions are allowed (which cannot reference the contract's constants). Nevertheless, the solution already handles any dependencies contract constants may have with global constants, which is nice.
Just a couple of minimal comments below. Suggestions from other team members are welcomed.
@jeshecdom Why is |
Looks like this code is not working: contract Master {
const C: Int = 10;
const D: Int = self.C;
get fun foo(a: Int): Int {
if (a == D) {
return 10;
}
return 0;
}
} With this error:
So we need to support this case in the resolver first in order to work with it in the interpreter |
We need to be careful though, because as soon as
One may think that in the above code
would now give |
I think that a contract constant cannot (and shouldn't) depend on a contract field |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I approve these changes, unless @anton-trunov and @verytactical have additional comments.
# Conflicts: # src/test/compilation-failed/tact.config.json
): CompilerContext { | ||
for (const constant of constants) { | ||
if (constant.ast.kind === "constant_def") { | ||
constant.value ??= evalConstantExpression( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm unsure it's even safe to do it here: typechecking is not over, how can we evaluate it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But this function is called only after checkConstants
, where we check types
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mmmm.... @verytactical has a good point. What if the constant initializer is calling a function that has not been typechecked yet? Or is there a guarantee that before constant evaluation all functions have been typechecked?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mmmm.... @verytactical has a good point. What if the constant initializer is calling a function that has not been typechecked yet? Or is there a guarantee that before constant evaluation all functions have been typechecked?
Looks like ctx = initializeConstantsAndDefaultContractAndStructFields(ctx, util);
is the last step in type checking, so this is not the case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is correct that it is the last step in resolveDescriptors
. But then I checked precompile.ts
and noticed that resolveDescriptors
is done before resolveStatements
, which means that functions are actually not type-checked before evaluating constants.
This explains why the tests did not complain about assigning an integer to self
(which has the type of a contract), like self = 100
. I was wandering about that when reading the tests.
If we want to keep this behavior, then the interpreter should not rely on the type-checker and carry out type checks while it interprets the code (we will need to extensively test the interpreter). The other option is to move constant evaluation as the last step in precompile.ts
.
@anton-trunov, we need your opinion on this, because I do not know if some process after resolveDescriptors
in precompile.ts
requires constants to be already evaluated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const eval should be done after typechecking, it's just historically it wasn't the case :)
): CompilerContext { | ||
for (const constant of constants) { | ||
if (constant.ast.kind === "constant_def") { | ||
constant.value ??= evalConstantExpression( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const eval should be done after typechecking, it's just historically it wasn't the case :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const eval tests should not go to the typechecker tests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's add a test with nested function calls foo() -> bar() -> ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, let's add a test like const A: Int = A
this.constantComputationPath.push(name); | ||
constant.value = this.interpretExpression( | ||
constant.ast.initializer, | ||
); | ||
this.constantComputationPath.pop(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since these push
/ pop
operations need to be always balanced, let's define a helper that ensures that
@jeshecdom I don't know any of the nuances of your vision for this interpreter, so maybe this PR is complete nonsense and it should be done other way. I'd be glad to hear any suggestions!
The main idea of this PR is that we recursively (on demand) calculate the value of a constant without depending on the order. The most important part is to handle circular dependencies correctly, otherwise the compiler will crash with an exception due to the depth of the recursion.
Issue
Closes #1477.
Closes #879.
Checklist