From 356e10088835b1ef92d8e42d8787e48b82954392 Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Wed, 5 Aug 2026 16:52:52 +0200 Subject: [PATCH] feat(formatter): assign trivia across method bodies --- src/compiler/format_body.cc | 308 +++++++++++++++++++++++++++++ src/compiler/format_body.h | 49 +++++ src/compiler/format_layout.h | 1 + src/compiler/format_trivia.cc | 44 ++++- src/compiler/format_trivia.h | 9 +- tests/ctest/format-body-input.toit | 22 +++ tests/ctest/format-body-test.cc | 235 ++++++++++++++++++++++ 7 files changed, 658 insertions(+), 10 deletions(-) create mode 100644 src/compiler/format_body.cc create mode 100644 src/compiler/format_body.h create mode 100644 tests/ctest/format-body-input.toit create mode 100644 tests/ctest/format-body-test.cc diff --git a/src/compiler/format_body.cc b/src/compiler/format_body.cc new file mode 100644 index 000000000..eac461cbd --- /dev/null +++ b/src/compiler/format_body.cc @@ -0,0 +1,308 @@ +// Copyright (C) 2026 Toit contributors. +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 only. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// The license can be found in the file `LICENSE` in the top level +// directory of this repository. + +#include "format_body.h" + +#include "../top.h" +#include "ast.h" +#include "sources.h" + +#include +#include + +namespace toit { +namespace compiler { + +namespace { + +class BodyLowering { + public: + BodyLowering(Source* source, + LayoutBuilder* layouts, + LogicalOperatorBindings* bindings, + SyntaxProtection* syntax, + TriviaLowering* trivia, + const FormatStyle& style) + : source_(source) + , layouts_(layouts) + , bindings_(bindings) + , syntax_(syntax) + , trivia_(trivia) + , style_(style) {} + + Layout* lower(ast::Sequence* body, int source_from, int source_to) { + ASSERT(body != null); + ASSERT(0 <= source_from && source_from <= source_to); + int source_indentation = 0; + if (!body->expressions().is_empty()) { + int first = start(body->expressions().first()); + source_indentation = first - line_start(first); + } else if (trivia_ != null) { + std::vector comments = trivia_->comments_in(source_from, source_to); + if (!comments.empty()) { + source_indentation = trivia_->comment(comments[0]).original_column; + } + } + Layout* result = + lower_sequence(body, source_from, source_to, source_indentation); + ASSERT(trivia_ == null || + trivia_->all_comments_consumed(source_from, source_to)); + return result; + } + + private: + Source* source_; + LayoutBuilder* layouts_; + LogicalOperatorBindings* bindings_; + SyntaxProtection* syntax_; + TriviaLowering* trivia_; + const FormatStyle& style_; + + int start(ast::Node* node) const { + return source_->offset_in_source(node->full_range().from()); + } + + int end(ast::Node* node) const { + return source_->offset_in_source(node->full_range().to()); + } + + int line_start(int offset) const { + const uint8* text = source_->text(); + while (offset > 0 && text[offset - 1] != '\n' && text[offset - 1] != '\r') { + offset--; + } + return offset; + } + + int line_end(int offset) const { + const uint8* text = source_->text(); + while (offset < source_->size() && text[offset] != '\n' && + text[offset] != '\r') { + offset++; + } + return offset; + } + + int find_syntax(int from, int to, const char* syntax) const { + if (trivia_ != null) return trivia_->find_syntax(from, to, syntax); + int length = std::strlen(syntax); + const uint8* text = source_->text(); + for (int offset = from; offset + length <= to; offset++) { + if (std::memcmp(text + offset, syntax, length) == 0) return offset; + } + return -1; + } + + Layout* join_items(std::vector items) { + if (items.empty()) return layouts_->text(""); + std::vector parts; + for (Layout* item : items) { + if (!parts.empty()) parts.push_back(layouts_->hardline()); + parts.push_back(item); + } + return layouts_->concat(std::move(parts)); + } + + void append_gap_comments(int from, + int to, + int source_indentation, + std::vector* items) { + if (trivia_ == null || from >= to) return; + for (int id : trivia_->comments_in(from, to)) { + const FormatTrivia::Comment& comment = trivia_->comment(id); + // A gap before the next outer sibling is also the syntactic limit of a + // nested sequence. An own-line comment whose indentation is shallower + // than this sequence belongs to that outer gap and must remain + // unconsumed here. + if (!comment.has_code_before && + comment.original_column < source_indentation) + continue; + if (comment.is_line_comment) { + // A `//` with code before it must have been consumed together with + // that statement. At sequence level only a self-contained comment + // line remains, and it behaves like a verbatim statement. + ASSERT(!comment.has_code_before); + items->push_back(trivia_->verbatim_region(comment.from, comment.to)); + continue; + } + if (!comment.has_code_before && !comment.has_code_after) { + items->push_back(trivia_->take_own_line_block(id)); + continue; + } + // Prefix/suffix comments are consumed while their semantic statement is + // lowered. Reaching the surrounding gap means that an attachment shape + // was not modeled; failing here protects the exact-once invariant. + UNREACHABLE(); + } + } + + Layout* lower_sequence(ast::Sequence* sequence, + int from, + int to, + int source_indentation) { + ASSERT(from <= to); + std::vector items; + int cursor = from; + List expressions = sequence->expressions(); + for (int i = 0; i < expressions.length(); i++) { + ast::Expression* expression = expressions[i]; + int expression_start = start(expression); + int expression_limit = + i + 1 < expressions.length() ? start(expressions[i + 1]) : to; + ASSERT(cursor <= expression_start && + expression_start <= expression_limit); + + Layout* statement = lower_statement(expression, expression_limit); + if (trivia_ != null) { + // A block comment on the statement's line but before its first token is + // an attached prefix. Indentation before that comment belongs to the + // containing sequence and is deliberately not copied into the layout. + int prefix_from = std::max(cursor, line_start(expression_start)); + statement = trivia_->take_inline_prefix(statement, prefix_from, + expression_start); + } + append_gap_comments(cursor, expression_start, source_indentation, &items); + items.push_back(statement); + // Keep the complete previous statement in the next gap's source range. + // Comments already assigned inside it are skipped by consumption state; + // an unconsumed, less-indented comment near a nested dedent remains + // visible to this enclosing sequence even when the nested Sequence's + // synthetic range extends to the next token. + cursor = expression_start; + } + append_gap_comments(cursor, to, source_indentation, &items); + return join_items(std::move(items)); + } + + Layout* lower_statement(ast::Expression* expression, int source_limit) { + if (expression->is_If()) { + return lower_if(expression->as_If(), source_limit); + } + if (expression->is_Return()) { + return lower_return(expression->as_Return()); + } + + int expression_start = start(expression); + int expression_end = end(expression); + if (trivia_ != null) { + int line_comment = + trivia_->first_line_comment(expression_start, expression_end, true); + if (line_comment >= 0) { + // For a simple statement the statement is the complete replaceable + // unit on this line, so every byte after its indentation is frozen. + int frozen_end = + std::max(expression_end, trivia_->comment(line_comment).to); + return trivia_->verbatim_region(expression_start, frozen_end); + } + } + return lower_expression(expression, source_, layouts_, bindings_, syntax_, + style_, trivia_) + .layout; + } + + Layout* lower_return(ast::Return* statement) { + int statement_start = start(statement); + int statement_end = end(statement); + if (trivia_ != null) { + int line_comment = + trivia_->first_line_comment(statement_start, statement_end, true); + if (line_comment >= 0) { + int frozen_end = + std::max(statement_end, trivia_->comment(line_comment).to); + return trivia_->verbatim_region(statement_start, frozen_end); + } + } + + Layout* result = layouts_->text("return"); + if (statement->value() != null) { + Layout* value = lower_expression(statement->value(), source_, layouts_, + bindings_, syntax_, style_, trivia_) + .layout; + result = layouts_->concat({result, layouts_->text(" "), value}); + } else if (trivia_ != null) { + result = trivia_->take_inline_suffix(result, statement_end, + line_end(statement_end)); + } + return result; + } + + Layout* lower_if(ast::If* statement, int source_limit) { + ASSERT(statement->no() == null); + ASSERT(statement->yes()->is_Sequence()); + int statement_start = start(statement); + int condition_end = end(statement->expression()); + int colon = find_syntax(condition_end, source_limit, ":"); + ASSERT(colon >= 0); + + Layout* header; + if (trivia_ != null) { + int line_comment = + trivia_->first_line_comment(statement_start, colon + 1, true); + if (line_comment >= 0) { + int frozen_end = std::max(colon + 1, trivia_->comment(line_comment).to); + header = trivia_->verbatim_region(statement_start, frozen_end); + } else { + Layout* condition = + lower_expression(statement->expression(), source_, layouts_, + bindings_, syntax_, style_, trivia_) + .layout; + Layout* colon_layout = layouts_->text(":"); + colon_layout = trivia_->take_inline_suffix(colon_layout, colon + 1, + line_end(colon + 1)); + header = + layouts_->concat({layouts_->text("if "), condition, colon_layout}); + } + } else { + Layout* condition = + lower_expression(statement->expression(), source_, layouts_, + bindings_, syntax_, style_, null) + .layout; + header = layouts_->concat( + {layouts_->text("if "), condition, layouts_->text(":")}); + } + + int statement_indentation = statement_start - line_start(statement_start); + Layout* body = + lower_sequence(statement->yes()->as_Sequence(), colon + 1, source_limit, + statement_indentation + style_.indentation_step); + return layouts_->concat({ + header, + layouts_->indent(style_.indentation_step, + layouts_->concat({layouts_->hardline(), body})), + }); + } +}; + +} // namespace + +Layout* lower_body(ast::Sequence* body, + Source* source, + int source_from, + int source_to, + LayoutBuilder* layouts, + LogicalOperatorBindings* bindings, + SyntaxProtection* syntax, + TriviaLowering* trivia, + const FormatStyle& style) { + ASSERT(source != null); + ASSERT(layouts != null); + ASSERT(bindings != null); + ASSERT(syntax != null); + return BodyLowering(source, layouts, bindings, syntax, trivia, style) + .lower(body, source_from, source_to); +} + +} // namespace compiler +} // namespace toit diff --git a/src/compiler/format_body.h b/src/compiler/format_body.h new file mode 100644 index 000000000..612a353c3 --- /dev/null +++ b/src/compiler/format_body.h @@ -0,0 +1,49 @@ +// Copyright (C) 2026 Toit contributors. +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 only. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// The license can be found in the file `LICENSE` in the top level +// directory of this repository. + +#pragma once + +#include "format_expression.h" +#include "format_trivia.h" + +namespace toit { +namespace compiler { + +namespace ast { +class Sequence; +} + +// Lowers all statements and comments in [source_from, source_to). Explicit +// source bounds are important: a trailing comment is part of the body even +// though no AST node owns it. Nested statement sequences derive smaller bounds +// from their colon and next sibling, which makes indentation ownership +// structural instead of heuristic. +// +// Every comment fully contained in the region must be consumed exactly once. +// Inline block comments become part of a semantic statement/expression; +// own-line comments become statement-like layouts; a line containing `//` +// becomes a verbatim barrier. +Layout* lower_body(ast::Sequence* body, + Source* source, + int source_from, + int source_to, + LayoutBuilder* layouts, + LogicalOperatorBindings* bindings, + SyntaxProtection* syntax, + TriviaLowering* trivia, + const FormatStyle& style = FormatStyle()); + +} // namespace compiler +} // namespace toit diff --git a/src/compiler/format_layout.h b/src/compiler/format_layout.h index 60e9965a8..8179cc4a8 100644 --- a/src/compiler/format_layout.h +++ b/src/compiler/format_layout.h @@ -26,6 +26,7 @@ namespace toit { namespace compiler { struct FormatStyle { + int indentation_step = 2; int continuation_step = 4; }; diff --git a/src/compiler/format_trivia.cc b/src/compiler/format_trivia.cc index 6896b186f..f7948046b 100644 --- a/src/compiler/format_trivia.cc +++ b/src/compiler/format_trivia.cc @@ -47,6 +47,13 @@ bool has_code_before(const uint8* text, int line_start, int comment_start) { return false; } +bool has_code_after(const uint8* text, int comment_end, int line_end) { + for (int i = comment_end; i < line_end; i++) { + if (text[i] != ' ' && text[i] != '\t') return true; + } + return false; +} + } // namespace FormatTrivia::FormatTrivia(Source* source, List comments) @@ -64,13 +71,12 @@ FormatTrivia::FormatTrivia(Source* source, List comments) comments_.push_back({ from, to, - start, - end, from - start, !scanner_comment.is_multiline(), raw.find('\n') != std::string::npos || raw.find('\r') != std::string::npos, has_code_before(text, start, from), + has_code_after(text, to, end), std::move(raw), }); } @@ -121,6 +127,28 @@ const FormatTrivia::Comment& TriviaLowering::comment(int id) const { return trivia_->comments()[id]; } +std::vector TriviaLowering::comments_in(int from, + int to, + bool only_unconsumed) const { + std::vector result; + if (trivia_ == null) return result; + const std::vector& comments = trivia_->comments(); + auto first = std::lower_bound( + comments.begin(), comments.end(), from, + [](const FormatTrivia::Comment& comment, int position) { + return comment.from < position; + }); + int id = static_cast(first - comments.begin()); + for (; id < static_cast(comments.size()); id++) { + const FormatTrivia::Comment& comment = comments[id]; + if (comment.from >= to) break; + if (comment.to > to) continue; + if (only_unconsumed && consumed_[id]) continue; + result.push_back(id); + } + return result; +} + int TriviaLowering::find_syntax(int from, int to, const char* syntax) const { ASSERT(trivia_ != null); int length = std::strlen(syntax); @@ -179,12 +207,11 @@ Layout* TriviaLowering::verbatim_region(int from, int to) { FormatTrivia::Comment region = { from, to, - base_start, - to, base_column, false, raw.find('\n') != std::string::npos, false, + false, std::move(raw), }; for (int id = 0; id < static_cast(trivia_->comments().size()); id++) { @@ -277,9 +304,12 @@ Layout* TriviaLowering::take_own_line_block(int id) { return exact_multiline_text(comment, comment.original_column); } -bool TriviaLowering::all_comments_consumed() const { - for (bool consumed : consumed_) { - if (!consumed) return false; +bool TriviaLowering::all_comments_consumed(int from, int to) const { + if (trivia_ == null) return true; + for (int id = 0; id < static_cast(trivia_->comments().size()); id++) { + const FormatTrivia::Comment& comment = trivia_->comments()[id]; + if (comment.from < from || comment.to > to) continue; + if (!consumed_[id]) return false; } return true; } diff --git a/src/compiler/format_trivia.h b/src/compiler/format_trivia.h index 5957585e5..275935cb0 100644 --- a/src/compiler/format_trivia.h +++ b/src/compiler/format_trivia.h @@ -34,12 +34,11 @@ class FormatTrivia { struct Comment { int from; int to; - int line_start; - int line_end; int original_column; bool is_line_comment; bool spans_lines; bool has_code_before; + bool has_code_after; std::string text; }; @@ -66,6 +65,10 @@ class TriviaLowering { // that unit and shifts its lines by the unit's original base indentation. int first_line_comment(int from, int to, bool include_trailing_line) const; const FormatTrivia::Comment& comment(int id) const; + // Returns source-ordered comment ids fully contained in [from, to). + std::vector comments_in(int from, + int to, + bool only_unconsumed = true) const; int find_syntax(int from, int to, const char* syntax) const; Layout* verbatim_region(int from, int to); @@ -80,7 +83,7 @@ class TriviaLowering { // belongs to the containing statement/argument/element list. Layout* take_own_line_block(int id); - bool all_comments_consumed() const; + bool all_comments_consumed(int from, int to) const; private: const FormatTrivia* trivia_; diff --git a/tests/ctest/format-body-input.toit b/tests/ctest/format-body-input.toit new file mode 100644 index 000000000..3967d07ae --- /dev/null +++ b/tests/ctest/format-body-input.toit @@ -0,0 +1,22 @@ +// Copyright (C) 2026 Toit contributors. +// Use of this source code is governed by a Zero-Clause BSD license that can +// be found in the tests/LICENSE file. + +sample x: + x + 1 // Keep every byte. + if not x: + // Do nothing. + if x: // Keep header bytes. + return 1 + // This belongs to the outer body. + if x: + x + /* Avoid joining + the two lines. */ + return 499 + x/*attached*/ + return x // Keep return bytes. + /* Last body comment. */ + +main: + sample true diff --git a/tests/ctest/format-body-test.cc b/tests/ctest/format-body-test.cc new file mode 100644 index 000000000..e36bcfd9c --- /dev/null +++ b/tests/ctest/format-body-test.cc @@ -0,0 +1,235 @@ +// Copyright (C) 2026 Toit contributors. +// Use of this source code is governed by a Zero-Clause BSD license that can +// be found in the tests/LICENSE file. + +#include +#include + +#include +#include +#include + +#include "../../src/compiler/ast.h" +#include "../../src/compiler/diagnostic.h" +#include "../../src/compiler/filesystem_local.h" +#include "../../src/compiler/format_body.h" +#include "../../src/compiler/parser.h" +#include "../../src/compiler/scanner.h" +#include "../../src/compiler/symbol_canonicalizer.h" +#include "../../src/top.h" +#include "format-test-source.h" + +namespace toit { +namespace compiler { + +struct ParsedBody { + std::unique_ptr source; + std::unique_ptr symbols; + std::unique_ptr trivia; + ast::Sequence* body; + int from; + int to; +}; + +static ParsedBody parse_body(const std::string& body, SourceManager* sources) { + std::string program = "sample x:\n "; + for (char c : body) { + if (c == '\n') { + program += "\n "; + } else { + program += c; + } + } + program += '\n'; + + ParsedBody result; + result.source.reset(new FormatTestSource(program)); + result.symbols.reset(new SymbolCanonicalizer()); + NullDiagnostics diagnostics(sources); + Scanner scanner(result.source.get(), result.symbols.get(), &diagnostics); + Parser parser(result.source.get(), &scanner, &diagnostics); + ast::Unit* unit = parser.parse_unit(); + if (diagnostics.encountered_error() || unit->declarations().length() != 1) { + fprintf(stderr, "Failed to parse rendered body\n"); + exit(1); + } + ast::Method* method = unit->declarations()[0]->as_Method(); + if (method == null || method->body() == null) exit(1); + result.trivia.reset( + new FormatTrivia(result.source.get(), scanner.comments())); + result.body = method->body(); + result.from = static_cast(std::strlen("sample x:")); + result.to = result.source->size(); + return result; +} + +static ast::Expression* peel_parentheses(ast::Expression* expression) { + while (expression->is_Parenthesis()) { + expression = expression->as_Parenthesis()->expression(); + } + return expression; +} + +static bool same_symbol(Symbol left, Symbol right) { + return std::strcmp(left.c_str(), right.c_str()) == 0; +} + +static bool equivalent(ast::Expression* left, ast::Expression* right) { + if (left == null || right == null) return left == right; + left = peel_parentheses(left); + right = peel_parentheses(right); + if (left->is_Identifier() && right->is_Identifier()) { + return same_symbol(left->as_Identifier()->data(), + right->as_Identifier()->data()); + } + if (left->is_LiteralInteger() && right->is_LiteralInteger()) { + return same_symbol(left->as_LiteralInteger()->data(), + right->as_LiteralInteger()->data()); + } + if (left->is_Unary() && right->is_Unary()) { + ast::Unary* left_unary = left->as_Unary(); + ast::Unary* right_unary = right->as_Unary(); + return left_unary->kind() == right_unary->kind() && + left_unary->prefix() == right_unary->prefix() && + equivalent(left_unary->expression(), right_unary->expression()); + } + if (left->is_Binary() && right->is_Binary()) { + ast::Binary* left_binary = left->as_Binary(); + ast::Binary* right_binary = right->as_Binary(); + return left_binary->kind() == right_binary->kind() && + equivalent(left_binary->left(), right_binary->left()) && + equivalent(left_binary->right(), right_binary->right()); + } + if (left->is_Call() && right->is_Call()) { + ast::Call* left_call = left->as_Call(); + ast::Call* right_call = right->as_Call(); + if (!equivalent(left_call->target(), right_call->target()) || + left_call->arguments().length() != right_call->arguments().length()) { + return false; + } + for (int i = 0; i < left_call->arguments().length(); i++) { + if (!equivalent(left_call->arguments()[i], right_call->arguments()[i])) + return false; + } + return true; + } + if (left->is_Return() && right->is_Return()) { + return equivalent(left->as_Return()->value(), right->as_Return()->value()); + } + if (left->is_If() && right->is_If()) { + ast::If* left_if = left->as_If(); + ast::If* right_if = right->as_If(); + return equivalent(left_if->expression(), right_if->expression()) && + equivalent(left_if->yes(), right_if->yes()) && + equivalent(left_if->no(), right_if->no()); + } + if (left->is_Sequence() && right->is_Sequence()) { + List left_expressions = + left->as_Sequence()->expressions(); + List right_expressions = + right->as_Sequence()->expressions(); + if (left_expressions.length() != right_expressions.length()) return false; + for (int i = 0; i < left_expressions.length(); i++) { + if (!equivalent(left_expressions[i], right_expressions[i])) return false; + } + return true; + } + return false; +} + +static void expect(const char* expected, const std::string& actual) { + if (actual == expected) return; + fprintf(stderr, "Expected:\n---\n%s\n---\nActual:\n---\n%s\n---\n", expected, + actual.c_str()); + exit(1); +} + +static std::string render_body(ast::Sequence* body, + Source* source, + int from, + int to, + int preferred_width, + const FormatTrivia* trivia) { + LayoutBuilder layouts; + LogicalOperatorBindings bindings; + SyntaxProtection syntax; + TriviaLowering trivia_lowering(trivia, &layouts); + Layout* layout = lower_body(body, source, from, to, &layouts, &bindings, + &syntax, &trivia_lowering); + SelectedPlan selected = select_layout(layout, preferred_width); + WhitespaceEdits edits = FormatRepairs::propose(bindings, selected); + if (!selected.apply(edits)) { + fprintf(stderr, "Body repairs conflicted\n"); + exit(1); + } + FinalPlan final = std::move(selected).freeze(); + return render_plan(final, syntax.resolve(final)); +} + +static void test_body(Source* source, + ast::Unit* unit, + SourceManager* sources, + const FormatTrivia* trivia) { + ASSERT(unit->declarations().length() == 2); + ast::Method* sample = unit->declarations()[0]->as_Method(); + ast::Method* main = unit->declarations()[1]->as_Method(); + ASSERT(sample != null && sample->body() != null && main != null); + int from = source->offset_in_source(sample->name_or_dot()->full_range().to()); + const uint8* text = source->text(); + while (from < source->size() && text[from] != ':') from++; + ASSERT(from < source->size()); + from++; + int to = source->offset_in_source(main->full_range().from()); + + const char* expected = + "x + 1 // Keep every byte.\n" + "if not x:\n" + " // Do nothing.\n" + "if x: // Keep header bytes.\n" + " return 1\n" + "// This belongs to the outer body.\n" + "if x:\n" + " x\n" + " /* Avoid joining\n" + " the two lines. */\n" + " return 499\n" + "x/*attached*/\n" + "return x // Keep return bytes.\n" + "/* Last body comment. */"; + std::string rendered = + render_body(sample->body(), source, from, to, 20, trivia); + expect(expected, rendered); + + ParsedBody reparsed = parse_body(rendered, sources); + if (!equivalent(sample->body(), reparsed.body)) { + fprintf(stderr, "Rendered body changed the AST\n"); + exit(1); + } + expect(rendered.c_str(), + render_body(reparsed.body, reparsed.source.get(), reparsed.from, + reparsed.to, 20, reparsed.trivia.get())); +} + +} // namespace compiler +} // namespace toit + +int main(int argc, char** argv) { + toit::throwing_new_allowed = true; + ASSERT(argc == 2); + + toit::compiler::FilesystemLocal filesystem; + toit::compiler::SourceManager sources(&filesystem); + toit::compiler::NullDiagnostics diagnostics(&sources); + toit::compiler::SourceManager::LoadResult loaded = + sources.load_file(argv[1], toit::compiler::Package::invalid()); + ASSERT(loaded.status == toit::compiler::SourceManager::LoadResult::OK); + + toit::compiler::SymbolCanonicalizer symbols; + toit::compiler::Scanner scanner(loaded.source, &symbols, &diagnostics); + toit::compiler::Parser parser(loaded.source, &scanner, &diagnostics); + toit::compiler::ast::Unit* unit = parser.parse_unit(); + ASSERT(!diagnostics.encountered_error()); + toit::compiler::FormatTrivia trivia(loaded.source, scanner.comments()); + toit::compiler::test_body(loaded.source, unit, &sources, &trivia); + return 0; +}