Skip to content

Commit

Permalink
refactor: CLI (#1450)
Browse files Browse the repository at this point in the history
  • Loading branch information
verytactical authored Jan 22, 2025
1 parent 70bdea5 commit 132fe4a
Show file tree
Hide file tree
Showing 38 changed files with 1,718 additions and 1,872 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ jest.setup.js
jest.teardown.js
/docs
version.build.ts
bin/*
100 changes: 0 additions & 100 deletions .github/workflows/tact.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,6 @@ jobs:
run: |
yarn fmt:check
- name: Check that tact.config.json adheres to the JSON schema
run: |
yarn lint:schema
- name: Spellcheck code base
run: |
yarn spell
Expand All @@ -116,102 +112,6 @@ jobs:
run: |
yarn link
- name: CLI Test | Check tact --version
if: runner.os != 'Windows'
run: |
tact --version
- name: CLI Test | Compare Tact version from CLI flag `--version` against package.json
if: runner.os != 'Windows'
run: |
if [ "$(tact --version | head -n1)" != "$(jq -r '.version' < package.json)" ];
then false
fi
- name: CLI Test | Check compilation of broken contract doesn't contain stacktrace
if: runner.os != 'Windows'
run: |
output=$(tact src/test/compilation-failed/contracts/const-eval-ascii-empty.tact 2>&1 || true)
if echo "$output" | grep -E ".*at \w+ \(.*"; then
echo "Unexpected stack trace found!"
exit 1
fi
output=$(tact src/test/compilation-failed/contracts/contract-duplicate-bounced-opcode.tact 2>&1 || true)
if echo "$output" | grep -E ".*at \w+ \(.*"; then
echo "Unexpected stack trace found!"
exit 1
fi
- name: CLI Test | Check compilation of broken contract contains stacktrace with --verbose 2 flag
if: runner.os != 'Windows'
run: |
output=$(tact --verbose 2 src/test/compilation-failed/contracts/const-eval-ascii-empty.tact 2>&1 || true)
if ! echo "$output" | grep -E ".*at \w+ \(.*"; then
echo "Expected stack trace not found!"
exit 1
fi
output=$(tact --verbose 2 src/test/compilation-failed/contracts/contract-duplicate-bounced-opcode.tact 2>&1 || true)
if ! echo "$output" | grep -E ".*at \w+ \(.*"; then
echo "Expected stack trace not found!"
exit 1
fi
- name: CLI Test | Check single-contract compilation
if: runner.os != 'Windows'
run: |
tact --check bin/test/success.tact
tact --func bin/test/success.tact
tact bin/test/success.tact
tact --with-decompilation bin/test/success.tact
- name: CLI Test | Check compilation via `--config`
if: runner.os != 'Windows'
run: |
# should output complete results
tact --config bin/test/success.config.json
# should output complete result + decompile binary code
tact --config bin/test/success.config.with.decompilation.json
# should only run the syntax and type checking
tact --config bin/test/success.config.json --check
- name: CLI Test | Check parsing of mutually exclusive flags - 1
if: runner.os != 'Windows'
run: |
! tact --func --check bin/test/success.config.json
- name: CLI Test | Check parsing of mutually exclusive flags - 2
if: runner.os != 'Windows'
run: |
! tact --with-decompilation --check bin/test/success.config.json
- name: CLI Test | Check parsing of mutually exclusive flags - 3
if: runner.os != 'Windows'
run: |
! tact --func --with-decompilation bin/test/success.config.json
- name: CLI Test | Check parsing of a non-existing CLI flag
if: runner.os != 'Windows'
run: |
! tact --nonexistentoption bin/test/success.config.json
- name: CLI Test | tact executable return non-zero exit code if compilation fails
if: runner.os != 'Windows'
run: |
! tact --config bin/test/fail.config.json
- name: CLI Test | Evaluate expression
if: runner.os != 'Windows'
run: |
tact -e '(1 + 2 * (pow(3,4) - 2) << 1 & 0x54 | 33 >> 1) * 2 + 2'
- name: CLI Test | Check TVM disassembler flag
if: runner.os != 'Windows'
run: |
tact bin/test/success.tact
unboc bin/test/success_HelloWorld.code.boc
- name: Test compatibility with tact-template
run: |
git clone https://github.com/tact-lang/tact-template.git
Expand Down
7 changes: 5 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ yarn knip
Tact's command-line interface (CLI) is located in [bin/tact.js](./bin/tact.js).
Tact uses the [meow](https://github.com/sindresorhus/meow) CLI arguments parser.

The main entry point for the Tact CLI is [src/node.ts](./src/node.ts) and [src/pipeline/build.ts](./src/pipeline/build.ts) is the platform-independent compiler driver which contains the high-level compiler pipeline logic described above.
The main entry point for the Tact CLI is [src/cli/tact/index.ts](./src/cli/tact/index.ts) and [src/pipeline/build.ts](./src/pipeline/build.ts) is the platform-independent compiler driver which contains the high-level compiler pipeline logic described above.

The Tact CLI gets Tact settings from a `tact.config.json` file or creates a default config for a single-file compilation mode. The format of `tact.config.json` files is specified in [src/config/configSchema.json](src/config/configSchema.json).

Expand Down Expand Up @@ -164,7 +164,10 @@ The implementation that we have right now is being refactored to produce FunC AS
One can find the end-to-end codegen test spec files in the [src/test/e2e-emulated](./src/test/e2e-emulated/) folder. The test contracts are located in [src/test/e2e-emulated/contracts](./src/test/e2e-emulated/contracts) subfolder. Many of those spec files test various language features in relative isolation.
An important spec file that tests argument passing semantics for functions and assignment semantics for variables is here: [src/test/e2e-emulated/semantics.spec.ts](./src/test/e2e-emulated/semantics.spec.ts).

Note: If you add an end-to-end test contract, you also need to include it into [tact.config.json](src/test/tact.config.json) and run `yarn gen` to compile it and create TypeScript wrappers.
Contracts with `inline` in the name of the file set `experimental.inline` config option to `true`.
Contracts with `external` in the name of the file set `external` config option to `true`.

Note: If you add an end-to-end test contract, you also need to run `yarn gen` to compile it and create TypeScript wrappers.

`yarn gen` also re-compiles test contracts, so it's important to run it when code generation is changed.

Expand Down
199 changes: 1 addition & 198 deletions bin/tact.js
Original file line number Diff line number Diff line change
@@ -1,200 +1,3 @@
#!/usr/bin/env node

const pkg = require("../package.json");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const main = require("../dist/node.js");
const meowModule = import("meow");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { execFileSync } = require("child_process");

void meowModule.then(
/** @param meow {import('meow/build/index')} */
(meow) => {
const cli = meow.default(
`
Usage
$ tact [...flags] (--config CONFIG | FILE)
Flags
-c, --config CONFIG Specify path to config file (tact.config.json)
-p, --projects ...names Build only the specified project name(s) from the config file
-q, --quiet Suppress compiler log output
--with-decompilation Full compilation followed by decompilation of produced binary code
--func Output intermediate FunC code and exit
--check Perform syntax and type checking, then exit
-e, --eval EXPRESSION Evaluate a Tact expression and exit
--verbose LEVEL Set verbosity level (higher = more details), default: 1
-v, --version Print Tact compiler version and exit
-h, --help Display this text and exit
Examples
$ tact --version
${pkg.version}
Learn more about Tact: https://docs.tact-lang.org
Join Telegram group: https://t.me/tactlang
Follow X/Twitter account: https://twitter.com/tact_language`,
{
importMeta: {
url: new URL("file://" + __dirname + __filename).toString(),
},
description: `Command-line utility for the Tact compiler:\n${pkg.description}`,
autoVersion: false,
flags: {
config: {
shortFlag: "c",
type: "string",
isRequired: (flags, _) => {
// Require a config when the projects are specified
// AND version/help are not specified
// AND eval is not specified
return (
flags.projects.length !== 0 &&
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
!flags.version &&
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
!flags.help &&
!flags.eval
);
},
},
projects: {
shortFlag: "p",
type: "string",
isMultiple: true,
},
quiet: { shortFlag: "q", type: "boolean", default: false },
withDecompilation: { type: "boolean", default: false },
func: { type: "boolean", default: false },
check: { type: "boolean", default: false },
eval: { shortFlag: "e", type: "string" },
verbose: { type: "number", default: 1 },
version: { shortFlag: "v", type: "boolean" },
help: { shortFlag: "h", type: "boolean" },
},
allowUnknownFlags: false,
},
);

// Helper function to write less in following checks
const isEmptyConfigAndInput = () => {
return cli.flags.config === undefined && cli.input.length === 0;
};

// Show help regardless of other flags
if (cli.flags.help) {
cli.showHelp(0);
}

// Show version regardless of other flags
if (cli.flags.version) {
console.log(pkg.version);
// if working inside a git repository
// also print the current git commit hash
try {
const gitCommit = execFileSync("git", ["rev-parse", "HEAD"], {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
}).trim();
console.log(`git commit: ${gitCommit}`);
} finally {
process.exit(0);
}
}

// Evaluate expression regardless of other flags
if (cli.flags.eval) {
const result = main.parseAndEvalExpression(cli.flags.eval);
switch (result.kind) {
case "ok":
{
console.log(main.showValue(result.value));
process.exit(0);
}
break;
case "error": {
console.log(result.message);
process.exit(30);
}
}
}

// Disallow specifying both config or Tact source file at the same time
if (cli.flags.config !== undefined && cli.input.length > 0) {
console.log(
"Error: Both config and Tact file can't be simultaneously specified, pick one!",
);
cli.showHelp();
}

// Disallow specifying several exclusive compilation mode flags
const compilationModeFlags = [
cli.flags.check,
cli.flags.func,
cli.flags.withDecompilation,
];
const numOfCompilationModeFlagsSet = compilationModeFlags.filter(
(flag) => flag,
).length;
if (numOfCompilationModeFlagsSet > 1) {
console.log(
"Error: Flags --with-decompilation, --func and --check are mutually exclusive!",
);
cli.showHelp();
}

// Disallow using compilation mode flags without a config or a file specified
if (isEmptyConfigAndInput() && numOfCompilationModeFlagsSet > 0) {
console.log(
"Error: Either config or Tact file have to be specified!",
);
cli.showHelp();
}

// Disallow specifying more than one Tact file
if (cli.input.length > 1) {
console.log(
"Error: Only one Tact file can be specified at a time. If you want more, provide a config!",
);
cli.showHelp();
}

// Show help when all flags and inputs are empty
// Note, that version/help flags are already processed above and don't need to be mentioned here
if (
isEmptyConfigAndInput() &&
numOfCompilationModeFlagsSet === 0 &&
cli.flags.projects.length === 0
) {
cli.showHelp(0);
}

// Compilation mode
const mode = cli.flags.check
? "checkOnly"
: cli.flags.func
? "funcOnly"
: cli.flags.withDecompilation
? "fullWithDecompilation"
: undefined;

// TODO: all flags on the cli should take precedence over flags in the config
// Make a nice model for it in the src/node.ts instead of the current mess
// Consider making overwrites right here or something.

// Main command
void main
.run({
fileName: cli.input.at(0),
configPath: cli.flags.config,
projectNames: cli.flags.projects ?? [],
additionalCliOptions: { mode },
suppressLog: cli.flags.quiet,
verbose: cli.flags.verbose,
})
.then((response) => {
// https://nodejs.org/docs/v20.12.1/api/process.html#exit-codes
process.exit(response.ok ? 0 : 30);
});
},
);
require("../dist/cli/tact/index.js").main();
11 changes: 0 additions & 11 deletions bin/test/fail.config.json

This file was deleted.

3 changes: 0 additions & 3 deletions bin/test/fail.tact

This file was deleted.

12 changes: 0 additions & 12 deletions bin/test/success.config.json

This file was deleted.

12 changes: 0 additions & 12 deletions bin/test/success.config.with.decompilation.json

This file was deleted.

7 changes: 0 additions & 7 deletions bin/test/success.tact

This file was deleted.

Loading

0 comments on commit 132fe4a

Please sign in to comment.