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

refactor: add optimization phase #1047

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open

refactor: add optimization phase #1047

wants to merge 21 commits into from

Conversation

jeshecdom
Copy link
Contributor

@jeshecdom jeshecdom commented Nov 15, 2024

Issue

Closes #1449.
Closes #970. (As a byproduct, a test was added).

This PR adds an optimization phase in between the typechecking and FunC code generation phases. Currently, the optimization phase only carries out expression simplification by running the interpreter over all expressions in the AST (later, this will be replaced by the partial evaluator). I did this in preparation for other optimization techniques that will be added in the future; for example, constant propagation will be moved to the optimization phase later.

IMPORTANT: This PR removes calls to the interpreter that were previously being carried out during the FunC code generation phase. The FunC generation phase should NOT be calling constant evaluation (this is a job of the optimization phase).

The PR adds two CLI compiler options:

  • skipTactOptimizationPhase: Default is false. If true, skips the tact optimization phase. Skipping the phase is useful, for example, to carry out tests in which you want to deactivate the interpreter so that you work with the raw program. Simply add the option in your project's json:
"projects": [
    {
      "name": "test",
      "path": "./test.tact",
      "output": "./output",
      "options": { 
          "skipTactOptimizationPhase": true 
      }
    }
  ]
  • dumpCodeBeforeAndAfterTactOptimizationPhase: Default is false. If true, it will produce two files: one with the program before the optimization phase, and another one after the optimization phase, so that you can make a diff on both files and see what the optimization phase did to the program.

NOTE: This PR does not modify the AST one gets from calling getRawAST on the CompilerContext object ctx. The FunC generation part seems to make use of the functions getAllTypes, getAllStaticFunctions and the like. This PR DOES modify the ASTs produced by those functions. The FunC generator part seems to use only the "sources" from getRawAST.

Checklist

  • I have updated CHANGELOG.md
  • I have added tests to demonstrate the contribution is correctly implemented: this usually includes both positive and negative tests, showing the happy path(s) and featuring intentionally broken cases
  • I have run all the tests locally and no test failure was reported
  • I have run the linter, formatter and spellchecker
  • I did not do unrelated and/or undiscussed refactorings

TODOs, like adding the CLI option to activate/deactivate optimization phase.
Added CLI options for skipping optimization phase and for dumping optimized tact code.
Further fixes:
 - subexpressions inside struct instances did not have their types registered correctly.
 - makeValueExpression now receives a SrcInfo object.
"dumpOptimizedTactCode": {
"type": "boolean",
"default": false,
"description": "False by default. If set to true, dumps the code produced by the Tact code optimization phase. In case the optimization phase is skipped, this option is ignored."
Copy link
Contributor

Choose a reason for hiding this comment

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

"Smooth" (in math sense) behavior would be to dump the code anyway, yet unoptimized. "Dump the optimized code" is just another separate optional compilation step that happens to be in the list after "optimize" step.

@@ -960,7 +960,7 @@ export class Interpreter {
if (foundContractConst.value !== undefined) {
return foundContractConst.value;
} else {
throwErrorConstEval(
throwNonFatalErrorConstEval(
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry I don't understand this change, and I suspect a future reader wouldn't either.

When do we throw non-fatal errors in general?

Copy link
Member

@novusnota novusnota Nov 26, 2024

Choose a reason for hiding this comment

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

When do we throw non-fatal errors in general?

When we want to stop the constant evaluation, but continue compiling the rest of the contract. Like, when not being able to compute something at compile-time with ability to fallback to run-time implementation for that something.

Copy link
Contributor

Choose a reason for hiding this comment

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

stopEvaluation()?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, but the current pipeline kinda relies on throwing and catching errors and not on errors as values ¯\_(ツ)_/¯

return ctx;
}

export function dump_tact_code(ctx: CompilerContext, file: string) {
Copy link
Contributor

Choose a reason for hiding this comment

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

It's odd that the function calls prettyPrint several times.

prettyPrint is supposed to print the whole AST node, whatever its type is.

Either we're missing another type of AST node, or this is actually something in the lines of prettyPrint({ kind: 'module', ... }).

program += `${prettyPrint(t.ast)}\n\n`;
}

writeFileSync(file, program, {
Copy link
Contributor

Choose a reason for hiding this comment

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

writeFile does w by default, doesn't it?

Also *Sync functions are synchronous and stop the whole JS virtual machine until they're done with what they were doing. It's fine to use a few of these in tests or in build scripts, but I think they can never be used from any of src/**/*.ts.

Eventually we might want to read several files during compilation, and in case of Web IDE probably even download those files over wire. *Sync is impossible to emulate in web (barring some unfortunate deprecated APIs that make the whole browser tab unresponsive), and there'll be no easy way to unSync the code in the future.

This should probably use import fs from "node:fs/promises"

@@ -0,0 +1,184 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`expression-simplification should fail expression simplification for interpreter-called-when-no-contract 1`] = `
Copy link
Contributor

Choose a reason for hiding this comment

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

Very, very wise use of Jest snapshots.

I was thinking to investigate how to do this, but you've already done it. Thanks!

case "constant_def": {
const newCode = newConstantsMap.get(
idText(decl.name),
)!.ast;
Copy link
Contributor

Choose a reason for hiding this comment

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

!

case "field_decl": {
const newCode = newFieldsMap.get(
idText(decl.name),
)!.ast;
Copy link
Contributor

Choose a reason for hiding this comment

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

!

}

newTypes.set(t.name, {
...t,
Copy link
Contributor

@verytactical verytactical Nov 26, 2024

Choose a reason for hiding this comment

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

Spread operator typechecks incorrectly if used on a union type. All the fields that are not modified by the code should be listed explicitly.

The only valid use-case for spread operator is when the type of an object is Record<string, ...>. Otherwise the program might compile, but work incorrectly.

return { stmts: newStatements, ctx: ctx };
}

function process_statement(
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a visitor, it's not unique to optimizer, and likely should go into ast.ts.

// Register the new expression in the context
ctx = registerAllSubExpTypes(ctx, newExpr, getExpType(ctx, expr));
} catch (_) {
// This means that transforming the value into an AST node is
Copy link
Contributor

Choose a reason for hiding this comment

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

We could have any kind of exception here, including stack overflow exception.

If there is a specific kind of exception that should be handled here, there has to be an instanceof check, and all other exceptions have to be rethrown up the stack.

Pending changes related to Value type. These will be addressed once Value type gets refactored in issue #1190.
@anton-trunov
Copy link
Member

I'm wondering what is going on with this PR and a bit surprised with the lack of progress.

@jeshecdom
Copy link
Contributor Author

It required the refactor of the Value type, but now that it is solved, I will finish it ASAP.

@jeshecdom jeshecdom changed the title fix: Issue #970: interpreter called only when contract present refactor: Add optimization phase Jan 21, 2025
@jeshecdom jeshecdom marked this pull request as ready for review January 21, 2025 04:25
@jeshecdom jeshecdom requested a review from a team as a code owner January 21, 2025 04:25
@anton-trunov anton-trunov changed the title refactor: Add optimization phase refactor: add optimization phase Jan 21, 2025
Copy link
Member

@anton-trunov anton-trunov left a comment

Choose a reason for hiding this comment

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

The first batch of review comments

package.json Outdated
@@ -95,7 +95,7 @@
"@typescript-eslint/parser": "^7.0.2",
"ajv-cli": "^5.0.0",
"cross-env": "^7.0.3",
"cspell": "^8.8.3",
"cspell": "^8.16.0",
Copy link
Member

Choose a reason for hiding this comment

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

irrelevant change

@@ -51,6 +51,16 @@
"default": false,
"description": "False by default. If set to true, enables generation of a getter the information on the interfaces provided by the contract.\n\nRead more about supported interfaces: https://docs.tact-lang.org/ref/evolution/OTP-001."
},
"skipTactOptimizationPhase": {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"skipTactOptimizationPhase": {
"skipPartialEval": {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renaming it to skipPartialEval would imply that the optimization phase only includes partial evaluation, but it will also include other steps, like constant propagation in the future (and probably other techniques as well). Is there a way to keep the "skipTactOptimizationPhase" for the entire phase, but to include sub-options for each of the steps in the optimization phase?

Copy link
Member

Choose a reason for hiding this comment

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

We need to come up with a plan for those optimizations and design a config for those then

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 guess it is enough to have an option for each step inside the optimization phase and avoid the complication of having an option for the entire phase.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We need to come up with a plan for those optimizations and design a config for those then

Yeah. All right. I will change the option to skipPartialEval, and once we have a plan, I will carry out the changes in a separate issue/PR.

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'll open an issue-task regarding this.

export function dumpTactCode(ast: AstModule, file: string) {
const program = prettyPrint(ast);

void writeFile(file, program);
Copy link
Member

Choose a reason for hiding this comment

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

you cannot use Node's file system here as it will break when Tact runs in the browser, use VFS instead

"default": false,
"description": "False by default. If set to true, skips the Tact code optimization phase."
},
"dumpCodeBeforeAndAfterTactOptimizationPhase": {
Copy link
Member

Choose a reason for hiding this comment

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

This needs to be split into two options, dumpAst and dumpAfterPartialEval

yarn.lock Outdated
Copy link
Member

Choose a reason for hiding this comment

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

undo this change

if (config.options?.dumpCodeBeforeAndAfterTactOptimizationPhase) {
dumpTactCode(
optimizationCtx.originalAst,
config.output + `/${config.name}-unoptimized-tact-dump.tact`,
Copy link
Member

Choose a reason for hiding this comment

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

use path.join instead of +

if (config.options?.dumpCodeBeforeAndAfterTactOptimizationPhase) {
dumpTactCode(
optimizationCtx.modifiedAst,
config.output + `/${config.name}-optimized-tact-dump.tact`,
Copy link
Member

Choose a reason for hiding this comment

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

use path.join instead of +

import { prepareAstForOptimization } from "../optimization-phase";

describe("expression-simplification", () => {
for (const r of loadCases(__dirname + "/success/")) {
Copy link
Member

Choose a reason for hiding this comment

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

use path.join instead of +

expect(getAllExpressionTypes(optCtx.ctx)).toMatchSnapshot();
});
}
for (const r of loadCases(__dirname + "/failed/")) {
Copy link
Member

Choose a reason for hiding this comment

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

use path.join instead of +

} else {
throwInternalCompilerError(`kind ${ast.kind} is not a receiver`);
}
}
Copy link
Member

Choose a reason for hiding this comment

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

all ensureX functions need to go to ast/...

Copy link
Member

Choose a reason for hiding this comment

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

We should not unfreeze the AST. Produce a new AST instead.

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.

Add optimization phase Call interpreter even if a contract is not present in the module
4 participants