main
Download
864 lines 38.9 KiBCModified 2026-04-14 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864
// This code defines functions for transforming ASTs back into Tomo source text

#include <assert.h>
#include <setjmp.h>
#include <stdbool.h>
#include <stdint.h>
#include <unictype.h>

#include "../ast.h"
#include "../parse/context.h"
#include "../parse/files.h"
#include "../parse/utils.h"
#include "../stdlib/datatypes.h"
#include "../stdlib/integers.h"
#include "../stdlib/optionals.h"
#include "../stdlib/stdlib.h"
#include "../stdlib/text.h"
#include "args.h"
#include "enums.h"
#include "formatter.h"
#include "types.h"
#include "utils.h"

#define fmt_inline(...) must(format_inline_code(__VA_ARGS__))
#define fmt(...) format_code(__VA_ARGS__)

Text_t format_namespace(ast_t *namespace, Table_t comments, Text_t indent) {
    if (unwrap_block(namespace) == NULL) return EMPTY_TEXT;
    return Texts("\n", indent, single_indent, fmt(namespace, comments, Texts(indent, single_indent)));
}

typedef struct {
    Text_t quote, unquote, interp;
} text_opts_t;

PUREFUNC text_opts_t choose_text_options(ast_list_t *chunks) {
    int double_quotes = 0, single_quotes = 0, backticks = 0;
    for (ast_list_t *chunk = chunks; chunk; chunk = chunk->next) {
        if (chunk->ast->tag == TextLiteral) {
            Text_t literal = Match(chunk->ast, TextLiteral)->text;
            if (Text$has(literal, Text("\""))) double_quotes += 1;
            if (Text$has(literal, Text("'"))) single_quotes += 1;
            if (Text$has(literal, Text("`"))) backticks += 1;
        }
    }
    Text_t quote;
    if (double_quotes == 0) quote = Text("\"");
    else if (single_quotes == 0) quote = Text("'");
    else if (backticks == 0) quote = Text("`");
    else quote = Text("\"");

    text_opts_t opts = {.quote = quote, .unquote = quote, .interp = Text("$")};
    return opts;
}

static bool starts_with_id(Text_t text) {
    if (text.length <= 0) return false;
    List_t codepoints = Text$utf32(Text$slice(text, I_small(1), I_small(1)));
    if (codepoints.length <= 0 || codepoints.data == NULL) return false;
    return uc_is_property_xid_continue(*(ucs4_t *)codepoints.data);
}

static OptionalText_t format_inline_text(text_opts_t opts, ast_list_t *chunks, Table_t comments) {
    Text_t code = opts.quote;
    for (ast_list_t *chunk = chunks; chunk; chunk = chunk->next) {
        if (chunk->ast->tag == TextLiteral) {
            Text_t literal = Match(chunk->ast, TextLiteral)->text;
            Text_t segment = Text$escaped(literal, false, Texts(opts.unquote, opts.interp));
            code = Texts(code, segment);
        } else {
            if (chunk->ast->tag == Var
                && (!chunk->next || chunk->next->ast->tag != TextLiteral
                    || !starts_with_id(Match(chunk->next->ast, TextLiteral)->text))) {
                code = Texts(code, opts.interp, fmt_inline(chunk->ast, comments));
            } else {
                code = Texts(code, opts.interp, "(", fmt_inline(chunk->ast, comments), ")");
            }
        }
    }
    return Texts(code, opts.unquote);
}

static Text_t format_text(text_opts_t opts, ast_list_t *chunks, Table_t comments, Text_t indent) {
    Text_t code = EMPTY_TEXT;
    Text_t current_line = EMPTY_TEXT;
    for (ast_list_t *chunk = chunks; chunk; chunk = chunk->next) {
        if (chunk->ast->tag == TextLiteral) {
            Text_t literal = Match(chunk->ast, TextLiteral)->text;
            List_t lines = Text$lines(literal);
            if (lines.length == 0) continue;
            current_line = Texts(current_line, Text$escaped(*(Text_t *)lines.data, false, opts.interp));
            for (int64_t i = 1; i < (int64_t)lines.length; i += 1) {
                add_line(&code, current_line, Texts(indent, single_indent));
                current_line = Text$escaped(*(Text_t *)(lines.data + i * lines.stride), false, opts.interp);
            }
        } else {
            current_line = Texts(current_line, opts.interp, "(", fmt(chunk->ast, comments, indent), ")");
        }
    }
    add_line(&code, current_line, Texts(indent, single_indent));
    code = Texts(opts.quote, "\n", indent, single_indent, code, "\n", indent, opts.unquote);
    return code;
}

OptionalText_t format_inline_code(ast_t *ast, Table_t comments) {
    if (range_has_comment(ast->start, ast->end, comments)) return NONE_TEXT;
    switch (ast->tag) {
    /*inline*/ case Unknown:
        fail("Invalid AST");
    /*inline*/ case Block: {
        ast_list_t *statements = Match(ast, Block)->statements;
        if (statements == NULL) return Text("pass");
        else if (statements->next == NULL) return fmt_inline(statements->ast, comments);
        else return NONE_TEXT;
    }
    /*inline*/ case StructDef:
    /*inline*/ case EnumDef:
    /*inline*/ case LangDef:
    /*inline*/ case FunctionDef:
    /*inline*/ case ConvertDef:
    /*inline*/ case DebugLog:
        return NONE_TEXT;
    /*inline*/ case Assert: {
        DeclareMatch(assert, ast, Assert);
        Text_t expr = fmt_inline(assert->expr, comments);
        if (!assert->message) return Texts("assert ", expr);
        Text_t message = fmt_inline(assert->message, comments);
        return Texts("assert ", expr, ", ", message);
    }
    /*inline*/ case Defer:
        return Texts("defer ", fmt_inline(Match(ast, Defer)->body, comments));
    /*inline*/ case Lambda: {
        DeclareMatch(lambda, ast, Lambda);
        Text_t code = Texts("func(", format_inline_args(lambda->args, comments));
        if (lambda->ret_type)
            code = Texts(code, lambda->args ? Text(" -> ") : Text("-> "), format_type(lambda->ret_type));
        code = Texts(code, ") ", fmt_inline(lambda->body, comments));
        return Texts(code);
    }
    /*inline*/ case If: {
        DeclareMatch(if_, ast, If);

        Text_t if_condition = if_->condition->tag == Not
                                  ? Texts("unless ", fmt_inline(Match(if_->condition, Not)->value, comments))
                                  : Texts("if ", fmt_inline(if_->condition, comments));

        if (if_->else_body == NULL && if_->condition->tag != Declare) {
            ast_t *stmt = unwrap_block(if_->body);
            if (!stmt) return Texts("pass ", if_condition);
            switch (stmt->tag) {
            case Return:
            case Skip:
            case Stop: return Texts(fmt_inline(stmt, comments), " ", if_condition);
            default: break;
            }
        }

        Text_t code = Texts(if_condition, " then ", fmt_inline(if_->body, comments));
        if (if_->else_body) code = Texts(code, " else ", fmt_inline(if_->else_body, comments));
        return code;
    }
    /*inline*/ case When: {
        DeclareMatch(when, ast, When);
        Text_t code = Texts("when ", fmt_inline(when->subject, comments));
        for (when_clause_t *clause = when->clauses; clause; clause = clause->next) {
            code = Texts(code, " is ", fmt_inline(clause->pattern, comments));
            while (clause->next && clause->next->body == clause->body) {
                clause = clause->next;
                code = Texts(code, ", ", fmt_inline(clause->pattern, comments));
            }
            code = Texts(code, " then ", fmt_inline(clause->body, comments));
        }
        if (when->else_body) code = Texts(code, " else ", fmt_inline(when->else_body, comments));
        return code;
    }
    /*inline*/ case Repeat:
        return Texts("repeat ", fmt_inline(Match(ast, Repeat)->body, comments));
    /*inline*/ case While: {
        DeclareMatch(loop, ast, While);
        return Texts("while ", fmt_inline(loop->condition, comments), " do ", fmt_inline(loop->body, comments));
    }
    /*inline*/ case For: {
        DeclareMatch(loop, ast, For);
        Text_t code = Text("for ");
        for (ast_list_t *var = loop->vars; var; var = var->next) {
            code = Texts(code, fmt_inline(var->ast, comments));
            if (var->next) code = Texts(code, ", ");
        }
        code = Texts(code, " in ", fmt_inline(loop->iter, comments), " do ", fmt_inline(loop->body, comments));
        if (loop->empty) code = Texts(code, " else ", fmt_inline(loop->empty, comments));
        return code;
    }
    /*inline*/ case Comprehension: {
        DeclareMatch(comp, ast, Comprehension);
        Text_t code = Texts(fmt_inline(comp->expr, comments), " for ");
        for (ast_list_t *var = comp->vars; var; var = var->next) {
            code = Texts(code, fmt_inline(var->ast, comments));
            if (var->next) code = Texts(code, ", ");
        }
        code = Texts(code, " in ", fmt_inline(comp->iter, comments));
        if (comp->filter) code = Texts(code, " if ", fmt_inline(comp->filter, comments));
        return code;
    }
    /*inline*/ case List: {
        ast_list_t *items = Match(ast, List)->items;
        Text_t code = EMPTY_TEXT;
        for (ast_list_t *item = items; item; item = item->next) {
            code = Texts(code, fmt_inline(item->ast, comments));
            if (item->next) code = Texts(code, ", ");
        }
        return Texts("[", code, "]");
    }
    /*inline*/ case Table: {
        DeclareMatch(table, ast, Table);
        Text_t code = EMPTY_TEXT;
        for (ast_list_t *entry = table->entries; entry; entry = entry->next) {
            code = Texts(code, fmt_inline(entry->ast, comments));
            if (entry->next) code = Texts(code, ", ");
        }
        if (table->fallback) code = Texts(code, "; fallback=", fmt_inline(table->fallback, comments));
        if (table->default_value) code = Texts(code, "; default=", fmt_inline(table->default_value, comments));
        return Texts("{", code, "}");
    }
    /*inline*/ case TableEntry: {
        DeclareMatch(entry, ast, TableEntry);
        if (entry->value) return Texts(fmt_inline(entry->key, comments), ": ", fmt_inline(entry->value, comments));
        else return Texts(fmt_inline(entry->key, comments));
    }
    /*inline*/ case Declare: {
        DeclareMatch(decl, ast, Declare);
        Text_t code = fmt_inline(decl->var, comments);
        if (decl->type) code = Texts(code, " : ", format_type(decl->type));
        if (decl->value) code = Texts(code, decl->type ? Text(" = ") : Text(" := "), fmt_inline(decl->value, comments));
        return code;
    }
    /*inline*/ case Assign: {
        DeclareMatch(assign, ast, Assign);
        Text_t code = EMPTY_TEXT;
        for (ast_list_t *target = assign->targets; target; target = target->next) {
            code = Texts(code, fmt_inline(target->ast, comments));
            if (target->next) code = Texts(code, ", ");
        }
        code = Texts(code, " = ");
        for (ast_list_t *value = assign->values; value; value = value->next) {
            code = Texts(code, fmt_inline(value->ast, comments));
            if (value->next) code = Texts(code, ", ");
        }
        return code;
    }
    /*inline*/ case Pass:
        return Text("pass");
    /*inline*/ case Return: {
        ast_t *value = Match(ast, Return)->value;
        return value ? Texts("return ", fmt_inline(value, comments)) : Text("return");
    }
    /*inline*/ case Not: {
        ast_t *val = Match(ast, Not)->value;
        return Texts("not ", must(termify_inline(val, comments)));
    }
    /*inline*/ case Negative: {
        ast_t *val = Match(ast, Negative)->value;
        return Texts("-", must(termify_inline(val, comments)));
    }
    /*inline*/ case HeapAllocate: {
        ast_t *val = Match(ast, HeapAllocate)->value;
        return Texts("@", must(termify_inline(val, comments)));
    }
    /*inline*/ case StackReference: {
        ast_t *val = Match(ast, StackReference)->value;
        return Texts("&", must(termify_inline(val, comments)));
    }
    /*inline*/ case NonOptional: {
        ast_t *val = Match(ast, NonOptional)->value;
        return Texts(must(termify_inline(val, comments)), "!");
    }
    /*inline*/ case FieldAccess: {
        DeclareMatch(access, ast, FieldAccess);
        return Texts(must(termify_inline(access->fielded, comments)), ".", Text$from_str(access->field));
    }
    /*inline*/ case Index: {
        DeclareMatch(index, ast, Index);
        Text_t indexed = must(termify_inline(index->indexed, comments));
        if (index->index) return Texts(indexed, "[", fmt_inline(index->index, comments), "]");
        else return Texts(indexed, "[]");
    }
    /*inline*/ case TextJoin: {
        text_opts_t opts = choose_text_options(Match(ast, TextJoin)->children);
        Text_t ret = must(format_inline_text(opts, Match(ast, TextJoin)->children, comments));
        const char *lang = Match(ast, TextJoin)->lang;
        return lang ? Texts("$", Text$from_str(lang), ret) : ret;
    }
    /*inline*/ case InlineCCode: {
        DeclareMatch(c_code, ast, InlineCCode);
        Text_t code = c_code->type_ast ? Texts("C_code:", format_type(c_code->type_ast)) : Text("C_code");
        text_opts_t opts = {.quote = Text("`"), .unquote = Text("`"), .interp = Text("@")};
        return Texts(code, must(format_inline_text(opts, Match(ast, InlineCCode)->chunks, comments)));
    }
    /*inline*/ case TextLiteral: { fail("Something went wrong, we shouldn't be formatting text literals directly"); }
    /*inline*/ case Path: {
        return Texts("(", Text$escaped(Text$from_str(Match(ast, Path)->path), false, Text("()")), ")");
    }
    /*inline*/ case Stop: {
        const char *target = Match(ast, Stop)->target;
        return target ? Texts("stop ", Text$from_str(target)) : Text("stop");
    }
    /*inline*/ case Skip: {
        const char *target = Match(ast, Skip)->target;
        return target ? Texts("skip ", Text$from_str(target)) : Text("skip");
    }
    /*inline*/ case Min:
    /*inline*/ case Max: {
        Text_t lhs = fmt_inline(ast->tag == Min ? Match(ast, Min)->lhs : Match(ast, Max)->lhs, comments);
        Text_t rhs = fmt_inline(ast->tag == Min ? Match(ast, Min)->rhs : Match(ast, Max)->rhs, comments);
        ast_t *key = ast->tag == Min ? Match(ast, Min)->key : Match(ast, Max)->key;
        return Texts(lhs, key ? fmt_inline(key, comments) : (ast->tag == Min ? Text(" _min_ ") : Text(" _max_ ")), rhs);
    }
    /*inline*/ case Reduction: {
        DeclareMatch(reduction, ast, Reduction);
        if (reduction->key) {
            return Texts("(", fmt_inline(reduction->key, comments), ": ", fmt_inline(reduction->iter, comments));
        } else {
            return Texts("(", Text$from_str(binop_info[reduction->op].operator), ": ",
                         fmt_inline(reduction->iter, comments));
        }
    }
    /*inline*/ case None:
        return Text("none");
    /*inline*/ case Bool:
        return Match(ast, Bool)->b ? Text("yes") : Text("no");
    /*inline*/ case Int: {
        OptionalText_t source = ast_source(ast);
        return source.length > 0 ? source : Text$from_str(Match(ast, Int)->str);
    }
    /*inline*/ case Num: {
        OptionalText_t source = ast_source(ast);
        return source.length > 0 ? source : Text$from_str(String(Match(ast, Num)->n));
    }
    /*inline*/ case Var:
        return Text$from_str(Match(ast, Var)->name);
    /*inline*/ case FunctionCall: {
        DeclareMatch(call, ast, FunctionCall);
        return Texts(fmt_inline(call->fn, comments), "(", must(format_inline_args(call->args, comments)), ")");
    }
    /*inline*/ case MethodCall: {
        DeclareMatch(call, ast, MethodCall);
        Text_t self = fmt_inline(call->self, comments);
        if (is_binary_operation(call->self) || call->self->tag == Negative || call->self->tag == Not)
            self = parenthesize(self, EMPTY_TEXT);
        return Texts(self, ".", Text$from_str(call->name), "(", must(format_inline_args(call->args, comments)), ")");
    }
    /*inline*/ case BINOP_CASES: {
        binary_operands_t operands = BINARY_OPERANDS(ast);
        const char *op = binop_info[ast->tag].operator;

        Text_t lhs = fmt_inline(operands.lhs, comments);
        Text_t rhs = fmt_inline(operands.rhs, comments);

        if (is_update_assignment(ast)) {
            return Texts(lhs, " ", Text$from_str(op), " ", rhs);
        }

        if (is_binary_operation(operands.lhs) && op_tightness[operands.lhs->tag] < op_tightness[ast->tag])
            lhs = parenthesize(lhs, EMPTY_TEXT);
        if (is_binary_operation(operands.rhs) && op_tightness[operands.rhs->tag] < op_tightness[ast->tag])
            rhs = parenthesize(rhs, EMPTY_TEXT);

        Text_t space = op_tightness[ast->tag] >= op_tightness[Multiply] ? EMPTY_TEXT : Text(" ");
        return Texts(lhs, space, Text$from_str(binop_info[ast->tag].operator), space, rhs);
    }
    /*inline*/ case Use: {
        DeclareMatch(use, ast, Use);
        return Texts("use ", use->path);
    }
    /*inline*/ case ExplicitlyTyped:
        fail("Explicitly typed AST nodes are only meant to be used internally.");
    default: {
        fail("Formatting not implemented for: ", ast_to_sexp(ast));
    }
    }
}

PUREFUNC static int64_t trailing_line_len(Text_t text) {
    TextIter_t state = NEW_TEXT_ITER_STATE(text);
    int64_t len = 0;
    for (int64_t i = text.length - 1; i >= 0; i--) {
        int32_t g = Text$get_grapheme_fast(&state, i);
        if (g == '\n' || g == '\r') break;
        len += 1;
    }
    return len;
}

Text_t format_code(ast_t *ast, Table_t comments, Text_t indent) {
    OptionalText_t inlined = format_inline_code(ast, comments);
    bool inlined_fits = (inlined.tag != TEXT_NONE && indent.length + inlined.length <= MAX_WIDTH);

    switch (ast->tag) {
    /*multiline*/ case Unknown:
        fail("Invalid AST");
    /*multiline*/ case Block: {
        Text_t code = EMPTY_TEXT;
        bool gap_before_comment = false;
        const char *comment_pos = ast->start;
        for (ast_list_t *stmt = Match(ast, Block)->statements; stmt; stmt = stmt->next) {
            for (OptionalText_t comment;
                 (comment = next_comment(comments, &comment_pos, stmt->ast->start)).length > 0;) {
                if (gap_before_comment) {
                    add_line(&code, Text(""), indent);
                    gap_before_comment = false;
                }
                add_line(&code, Text$trim(comment, Text(" \t\r\n"), false, true), indent);
            }

            if (stmt->ast->tag == Block) {
                add_line(&code,
                         Texts("do\n", indent, single_indent, fmt(stmt->ast, comments, Texts(indent, single_indent))),
                         indent);
            } else {
                add_line(&code, fmt(stmt->ast, comments, indent), indent);
            }
            comment_pos = stmt->ast->end;

            if (stmt->next) {
                int suggested_blanks = suggested_blank_lines(stmt->ast, stmt->next->ast);
                for (int blanks = suggested_blanks; blanks > 0; blanks--)
                    add_line(&code, Text(""), indent);
                gap_before_comment = (suggested_blanks == 0);
            } else gap_before_comment = true;
        }

        for (OptionalText_t comment; (comment = next_comment(comments, &comment_pos, ast->end)).length > 0;) {
            if (gap_before_comment) {
                add_line(&code, Text(""), indent);
                gap_before_comment = false;
            }
            add_line(&code, Text$trim(comment, Text(" \t\r\n"), false, true), indent);
        }
        return code;
    }
    /*multiline*/ case If: {
        DeclareMatch(if_, ast, If);
        Text_t code = if_->condition->tag == Not
                          ? Texts("unless ", fmt(Match(if_->condition, Not)->value, comments, indent))
                          : Texts("if ", fmt(if_->condition, comments, indent));

        code = Texts(code, "\n", indent, single_indent, fmt(if_->body, comments, Texts(indent, single_indent)));
        if (if_->else_body) {
            if (if_->else_body->tag != If) {
                code = Texts(code, "\n", indent, "else\n", indent, single_indent,
                             fmt(if_->else_body, comments, Texts(indent, single_indent)));
            } else {
                code = Texts(code, "\n", indent, "else ", fmt(if_->else_body, comments, indent));
            }
        }
        return code;
    }
    /*multiline*/ case When: {
        DeclareMatch(when, ast, When);
        Text_t code = Texts("when ", fmt(when->subject, comments, indent));
        for (when_clause_t *clause = when->clauses; clause; clause = clause->next) {
            code = Texts(code, "\n", indent, "is ", fmt(clause->pattern, comments, indent));
            while (clause->next && clause->next->body == clause->body) {
                clause = clause->next;
                code = Texts(code, ", ", fmt(clause->pattern, comments, indent));
            }
            code = Texts(code, format_namespace(clause->body, comments, indent));
        }
        if (when->else_body)
            code = Texts(code, "\n", indent, "else", format_namespace(when->else_body, comments, indent));
        return code;
    }
    /*multiline*/ case Repeat: {
        return Texts("repeat\n", indent, single_indent,
                     fmt(Match(ast, Repeat)->body, comments, Texts(indent, single_indent)));
    }
    /*multiline*/ case While: {
        DeclareMatch(loop, ast, While);
        return Texts("while ", fmt(loop->condition, comments, indent), "\n", indent, single_indent,
                     fmt(loop->body, comments, Texts(indent, single_indent)));
    }
    /*multiline*/ case For: {
        DeclareMatch(loop, ast, For);
        Text_t code = Text("for ");
        for (ast_list_t *var = loop->vars; var; var = var->next) {
            code = Texts(code, fmt(var->ast, comments, indent));
            if (var->next) code = Texts(code, ", ");
        }
        code = Texts(code, " in ", fmt(loop->iter, comments, indent), format_namespace(loop->body, comments, indent));
        if (loop->empty) code = Texts(code, "\n", indent, "else", format_namespace(loop->empty, comments, indent));
        return code;
    }
    /*multiline*/ case Comprehension: {
        if (inlined_fits) return inlined;
        DeclareMatch(comp, ast, Comprehension);
        Text_t code = Texts("(", fmt(comp->expr, comments, indent));
        if (code.length >= MAX_WIDTH) code = Texts(code, "\n", indent, "for ");
        else code = Texts(code, " for ");

        for (ast_list_t *var = comp->vars; var; var = var->next) {
            code = Texts(code, fmt(var->ast, comments, indent));
            if (var->next) code = Texts(code, ", ");
        }

        code = Texts(code, " in ", fmt(comp->iter, comments, indent));

        if (comp->filter) {
            if (code.length >= MAX_WIDTH) code = Texts(code, "\n", indent, "if ");
            else code = Texts(code, " if ");
            code = Texts(code, fmt(comp->filter, comments, indent));
        }
        return code;
    }
    /*multiline*/ case FunctionDef: {
        DeclareMatch(func, ast, FunctionDef);
        Text_t code = Texts("func ", fmt(func->name, comments, indent), "(", format_args(func->args, comments, indent));
        if (func->ret_type) code = Texts(code, func->args ? Text(" -> ") : Text("-> "), format_type(func->ret_type));
        if (func->cache) code = Texts(code, "; cache=", fmt(func->cache, comments, indent));
        if (func->is_inline) code = Texts(code, "; inline");
        code = Texts(code, Text$has(code, Text("\n")) ? Texts("\n", indent, ")") : Text(")"), "\n", indent,
                     single_indent, fmt(func->body, comments, Texts(indent, single_indent)));
        return Texts(code);
    }
    /*multiline*/ case Lambda: {
        if (inlined_fits) return inlined;
        DeclareMatch(lambda, ast, Lambda);
        Text_t code = Texts("func(", format_args(lambda->args, comments, indent));
        if (lambda->ret_type)
            code = Texts(code, lambda->args ? Text(" -> ") : Text("-> "), format_type(lambda->ret_type));
        code = Texts(code, Text$has(code, Text("\n")) ? Texts("\n", indent, ")") : Text(")"), "\n", indent,
                     single_indent, fmt(lambda->body, comments, Texts(indent, single_indent)));
        return Texts(code);
    }
    /*multiline*/ case ConvertDef: {
        DeclareMatch(convert, ast, ConvertDef);
        Text_t code = Texts("convert (", format_args(convert->args, comments, indent));
        if (convert->ret_type)
            code = Texts(code, convert->args ? Text(" -> ") : Text("-> "), format_type(convert->ret_type));
        if (convert->cache) code = Texts(code, "; cache=", fmt(convert->cache, comments, indent));
        if (convert->is_inline) code = Texts(code, "; inline");
        code = Texts(code, Text$has(code, Text("\n")) ? Texts("\n", indent, ")") : Text(")"), "\n", indent,
                     single_indent, fmt(convert->body, comments, Texts(indent, single_indent)));
        return Texts(code);
    }
    /*multiline*/ case StructDef: {
        DeclareMatch(def, ast, StructDef);
        Text_t args = format_args(def->fields, comments, indent);
        Text_t code = Texts("struct ", Text$from_str(def->name), "(", args);
        if (def->secret) code = Texts(code, "; secret");
        if (def->external) code = Texts(code, "; external");
        if (def->opaque) code = Texts(code, "; opaque");
        code = Texts(code, Text$has(code, Text("\n")) ? Texts("\n", indent, ")") : Text(")"));
        const char *comment_pos = ast->start;
        for (OptionalText_t comment; (comment = next_comment(comments, &comment_pos, ast->end)).length > 0;) {
            add_line(&code, Text$trim(comment, Text(" \t\r\n"), false, true), Texts(indent, single_indent));
        }
        return Texts(code, format_namespace(def->namespace, comments, indent));
    }
    /*multiline*/ case EnumDef: {
        DeclareMatch(def, ast, EnumDef);
        Text_t code = Texts("enum ", Text$from_str(def->name), "(", format_tags(def->tags, comments, indent));
        code = Texts(code, Text$has(code, Text("\n")) ? Texts("\n", indent, ")") : Text(")"));
        const char *comment_pos = ast->start;
        for (OptionalText_t comment; (comment = next_comment(comments, &comment_pos, ast->end)).length > 0;) {
            add_line(&code, Text$trim(comment, Text(" \t\r\n"), false, true), Texts(indent, single_indent));
        }
        return Texts(code, format_namespace(def->namespace, comments, indent));
    }
    /*multiline*/ case LangDef: {
        DeclareMatch(def, ast, LangDef);
        Text_t code = Texts("lang ", Text$from_str(def->name));
        const char *comment_pos = ast->start;
        for (OptionalText_t comment; (comment = next_comment(comments, &comment_pos, ast->end)).length > 0;) {
            add_line(&code, Text$trim(comment, Text(" \t\r\n"), false, true), Texts(indent, single_indent));
        }
        return Texts(code, format_namespace(def->namespace, comments, indent));
    }
    /*multiline*/ case Defer:
        return Texts("defer ", format_namespace(Match(ast, Defer)->body, comments, indent));
    /*multiline*/ case List: {
        if (inlined_fits) return inlined;
        ast_list_t *items = Match(ast, List)->items;
        Text_t code = Text("[");
        const char *comment_pos = ast->start;
        for (ast_list_t *item = items; item; item = item->next) {
            for (OptionalText_t comment;
                 (comment = next_comment(comments, &comment_pos, item->ast->start)).length > 0;) {
                add_line(&code, Text$trim(comment, Text(" \t\r\n"), false, true), Texts(indent, single_indent));
            }
            Text_t item_text = fmt(item->ast, comments, Texts(indent, single_indent));
            if (Text$ends_with(code, Text(","), NULL)) {
                if (!Text$has(item_text, Text("\n")) && trailing_line_len(code) + 1 + item_text.length + 1 <= MAX_WIDTH)
                    code = Texts(code, " ", item_text, ",");
                else code = Texts(code, "\n", indent, single_indent, item_text, ",");
            } else {
                add_line(&code, Texts(item_text, ","), Texts(indent, single_indent));
            }
        }
        for (OptionalText_t comment; (comment = next_comment(comments, &comment_pos, ast->end)).length > 0;) {
            add_line(&code, Text$trim(comment, Text(" \t\r\n"), false, true), Texts(indent, single_indent));
        }
        return Texts(code, "\n", indent, "]");
    }
    /*multiline*/ case Table: {
        if (inlined_fits) return inlined;
        DeclareMatch(table, ast, Table);
        Text_t code = Texts("{");
        const char *comment_pos = ast->start;
        for (ast_list_t *entry = table->entries; entry; entry = entry->next) {
            for (OptionalText_t comment;
                 (comment = next_comment(comments, &comment_pos, entry->ast->start)).length > 0;) {
                add_line(&code, Text$trim(comment, Text(" \t\r\n"), false, true), Texts(indent, single_indent));
            }

            Text_t entry_text = fmt(entry->ast, comments, Texts(indent, single_indent));
            if (Text$ends_with(code, Text(","), NULL)) {
                if (!Text$has(entry_text, Text("\n"))
                    && trailing_line_len(code) + 1 + entry_text.length + 1 <= MAX_WIDTH)
                    code = Texts(code, " ", entry_text, ",");
                else code = Texts(code, "\n", indent, single_indent, entry_text, ",");
            } else {
                add_line(&code, Texts(entry_text, ","), Texts(indent, single_indent));
            }

            add_line(&code, Texts(entry_text, ","), Texts(indent, single_indent));
        }
        for (OptionalText_t comment; (comment = next_comment(comments, &comment_pos, ast->end)).length > 0;) {
            add_line(&code, Text$trim(comment, Text(" \t\r\n"), false, true), Texts(indent, single_indent));
        }

        if (table->fallback)
            code = Texts(code, ";\n", indent, single_indent, "fallback=", fmt(table->fallback, comments, indent));

        if (table->default_value)
            code = Texts(code, ";\n", indent, single_indent, "default=", fmt(table->default_value, comments, indent));

        return Texts(code, "\n", indent, "}");
    }
    /*multiline*/ case TableEntry: {
        if (inlined_fits) return inlined;
        DeclareMatch(entry, ast, TableEntry);
        if (entry->value) return Texts(fmt(entry->key, comments, indent), ": ", fmt(entry->value, comments, indent));
        else return Texts(fmt(entry->key, comments, indent));
    }
    /*multiline*/ case Declare: {
        if (inlined_fits) return inlined;
        DeclareMatch(decl, ast, Declare);
        Text_t code = fmt(decl->var, comments, indent);
        if (decl->type) code = Texts(code, " : ", format_type(decl->type));
        if (decl->value)
            code = Texts(code, decl->type ? Text(" = ") : Text(" := "), fmt(decl->value, comments, indent));
        return code;
    }
    /*multiline*/ case Assign: {
        if (inlined_fits) return inlined;
        DeclareMatch(assign, ast, Assign);
        Text_t code = EMPTY_TEXT;
        for (ast_list_t *target = assign->targets; target; target = target->next) {
            code = Texts(code, fmt(target->ast, comments, indent));
            if (target->next) code = Texts(code, ", ");
        }
        code = Texts(code, " = ");
        for (ast_list_t *value = assign->values; value; value = value->next) {
            code = Texts(code, fmt(value->ast, comments, indent));
            if (value->next) code = Texts(code, ", ");
        }
        return code;
    }
    /*multiline*/ case Pass:
        return Text("pass");
    /*multiline*/ case Return: {
        if (inlined_fits) return inlined;
        ast_t *value = Match(ast, Return)->value;
        return value ? Texts("return ", fmt(value, comments, indent)) : Text("return");
    }
    /*inline*/ case Not: {
        if (inlined_fits) return inlined;
        ast_t *val = Match(ast, Not)->value;
        if (is_binary_operation(val)) return Texts("not ", termify(val, comments, indent));
        else return Texts("not ", fmt(val, comments, indent));
    }
    /*inline*/ case Negative: {
        if (inlined_fits) return inlined;
        ast_t *val = Match(ast, Negative)->value;
        if (is_binary_operation(val)) return Texts("-", termify(val, comments, indent));
        else return Texts("-", fmt(val, comments, indent));
    }
    /*multiline*/ case HeapAllocate: {
        if (inlined_fits) return inlined;
        ast_t *val = Match(ast, HeapAllocate)->value;
        return Texts("@", termify(val, comments, indent), "");
    }
    /*multiline*/ case StackReference: {
        if (inlined_fits) return inlined;
        ast_t *val = Match(ast, StackReference)->value;
        return Texts("&(", termify(val, comments, indent), ")");
    }
    /*multiline*/ case NonOptional: {
        if (inlined_fits) return inlined;
        ast_t *val = Match(ast, NonOptional)->value;
        return Texts(termify(val, comments, indent), "!");
    }
    /*multiline*/ case FieldAccess: {
        if (inlined_fits) return inlined;
        DeclareMatch(access, ast, FieldAccess);
        return Texts(termify(access->fielded, comments, indent), ".", Text$from_str(access->field));
    }
    /*multiline*/ case Index: {
        if (inlined_fits) return inlined;
        DeclareMatch(index, ast, Index);
        if (index->index)
            return Texts(termify(index->indexed, comments, indent), "[", fmt(index->index, comments, indent), "]");
        else return Texts(termify(index->indexed, comments, indent), "[]");
    }
    /*multiline*/ case TextJoin: {
        if (inlined_fits) return inlined;

        text_opts_t opts = choose_text_options(Match(ast, TextJoin)->children);
        Text_t ret = format_text(opts, Match(ast, TextJoin)->children, comments, indent);
        const char *lang = Match(ast, TextJoin)->lang;
        return lang ? Texts("$", Text$from_str(lang), ret) : ret;
    }
    /*multiline*/ case InlineCCode: {
        DeclareMatch(c_code, ast, InlineCCode);
        if (inlined_fits && c_code->type != NULL) return inlined;
        Text_t code = c_code->type_ast ? Texts("C_code:", format_type(c_code->type_ast)) : Text("C_code");
        text_opts_t opts = {.quote = Text("`"), .unquote = Text("`"), .interp = Text("@")};
        return Texts(code, format_text(opts, Match(ast, InlineCCode)->chunks, comments, indent));
    }
    /*multiline*/ case TextLiteral: { fail("Something went wrong, we shouldn't be formatting text literals directly"); }
    /*multiline*/ case Path: {
        assert(inlined.length > 0);
        return inlined;
    }
    /*multiline*/ case Min:
    /*multiline*/ case Max: {
        if (inlined_fits) return inlined;
        Text_t lhs = termify(ast->tag == Min ? Match(ast, Min)->lhs : Match(ast, Max)->lhs, comments, indent);
        Text_t rhs = termify(ast->tag == Min ? Match(ast, Min)->rhs : Match(ast, Max)->rhs, comments, indent);
        ast_t *key = ast->tag == Min ? Match(ast, Min)->key : Match(ast, Max)->key;
        Text_t op = key ? fmt(key, comments, indent) : (ast->tag == Min ? Text("_min_") : Text("_max_"));
        return Texts(lhs, " ", op, " ", rhs);
    }
    /*multiline*/ case Reduction: {
        if (inlined_fits) return inlined;
        DeclareMatch(reduction, ast, Reduction);
        if (reduction->key) {
            return Texts("(", fmt(reduction->key, comments, Texts(indent, single_indent)), ": ",
                         fmt(reduction->iter, comments, Texts(indent, single_indent)));
        } else {
            return Texts("(", binop_info[reduction->op].operator, ": ",
                         fmt(reduction->iter, comments, Texts(indent, single_indent)));
        }
    }
    /*multiline*/ case Stop:
    /*multiline*/ case Skip:
    /*multiline*/ case None:
    /*multiline*/ case Bool:
    /*multiline*/ case Int:
    /*multiline*/ case Num:
    /*multiline*/ case Var: {
        assert(inlined.tag != TEXT_NONE);
        return inlined;
    }
    /*multiline*/ case FunctionCall: {
        if (inlined_fits) return inlined;
        DeclareMatch(call, ast, FunctionCall);
        Text_t args = format_args(call->args, comments, indent);
        return Texts(fmt(call->fn, comments, indent), "(", args,
                     Text$has(args, Text("\n")) ? Texts("\n", indent) : EMPTY_TEXT, ")");
    }
    /*multiline*/ case MethodCall: {
        if (inlined_fits) return inlined;
        DeclareMatch(call, ast, MethodCall);
        Text_t args = format_args(call->args, comments, indent);
        return Texts(termify(call->self, comments, indent), ".", Text$from_str(call->name), "(", args,
                     Text$has(args, Text("\n")) ? Texts("\n", indent) : EMPTY_TEXT, ")");
    }
    /*multiline*/ case DebugLog: {
        DeclareMatch(debug, ast, DebugLog);
        Text_t code = Texts(">> ");
        for (ast_list_t *value = debug->values; value; value = value->next) {
            Text_t expr = fmt(value->ast, comments, indent);
            code = Texts(code, expr);
            if (value->next) code = Texts(code, ", ");
        }
        return code;
    }
    /*multiline*/ case Assert: {
        DeclareMatch(assert, ast, Assert);
        Text_t expr = fmt(assert->expr, comments, indent);
        if (!assert->message) return Texts("assert ", expr);
        Text_t message = fmt(assert->message, comments, indent);
        return Texts("assert ", expr, ", ", message);
    }
    /*multiline*/ case BINOP_CASES: {
        if (inlined_fits) return inlined;
        binary_operands_t operands = BINARY_OPERANDS(ast);
        const char *op = binop_info[ast->tag].operator;
        Text_t lhs = fmt(operands.lhs, comments, indent);
        Text_t rhs = fmt(operands.rhs, comments, indent);

        if (is_update_assignment(ast)) {
            return Texts(lhs, " ", Text$from_str(op), " ", rhs);
        }

        if (is_binary_operation(operands.lhs) && op_tightness[operands.lhs->tag] < op_tightness[ast->tag])
            lhs = parenthesize(lhs, indent);
        if (is_binary_operation(operands.rhs) && op_tightness[operands.rhs->tag] < op_tightness[ast->tag])
            rhs = parenthesize(rhs, indent);

        Text_t space = op_tightness[ast->tag] >= op_tightness[Multiply] ? EMPTY_TEXT : Text(" ");
        return Texts(lhs, space, Text$from_str(binop_info[ast->tag].operator), space, rhs);
    }
    /*multiline*/ case Use: {
        assert(inlined.length > 0);
        return inlined;
    }
    /*multiline*/ case ExplicitlyTyped:
        fail("Explicitly typed AST nodes are only meant to be used internally.");
    default: {
        if (inlined_fits) return inlined;
        fail("Formatting not implemented for: ", ast_to_sexp(ast));
    }
    }
}

Text_t format_file(const char *path) {
    file_t *file = load_file(path);
    if (!file) return EMPTY_TEXT;

    jmp_buf on_err;
    if (setjmp(on_err) != 0) {
        return Text$from_str(file->text);
    }
    parse_ctx_t ctx = {
        .file = file,
        .on_err = &on_err,
        .comments = {},
    };

    const char *pos = file->text;
    if (match(&pos, "#!")) // shebang
        some_not(&pos, "\r\n");

    whitespace(&ctx, &pos);
    ast_t *ast = parse_file_body(&ctx, pos);
    if (!ast) return Text$from_str(file->text);
    pos = ast->end;
    whitespace(&ctx, &pos);
    if (pos < file->text + file->len && *pos != '\0') {
        return Text$from_str(file->text);
    }

    const char *fmt_pos = file->text;
    Text_t code = EMPTY_TEXT;
    for (OptionalText_t comment; (comment = next_comment(ctx.comments, &fmt_pos, ast->start)).length > 0;) {
        code = Texts(code, Text$trim(comment, Text(" \t\r\n"), false, true), "\n");
    }
    code = Texts(code, fmt(ast, ctx.comments, EMPTY_TEXT));
    for (OptionalText_t comment; (comment = next_comment(ctx.comments, &fmt_pos, ast->start)).length > 0;) {
        code = Texts(code, Text$trim(comment, Text(" \t\r\n"), false, true), "\n");
    }
    return code;
}