llm
Download
210 lines 11 KiBCModified 2026-09-09 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
// Logic for formatting arguments and argument lists

#include <string.h>

#include "../ast.h"
#include "../stdlib/datatypes.h"
#include "../stdlib/optionals.h"
#include "../stdlib/text.h"
#include "formatter.h"
#include "types.h"
#include "utils.h"

// A parameter can carry a short alias for the command line (`force|f:Bool`),
// which is part of how the parameter is written, so it has to be printed back
// out with the name: dropping it silently deletes a subcommand's short flag.
static Text_t arg_name(arg_ast_t *arg) {
    Text_t name = Text$from_str(arg->name);
    if (arg->alias) name = Texts(name, "|", Text$from_str(arg->alias));
    return name;
}

// Everything written between where the argument before this one stopped and
// where the one after it starts, which is every comment this argument owns.
PUREFUNC static bool arg_has_comment(arg_ast_t *arg, Table_t comments) {
    const char *from = arg->comments_start ?: arg->start;
    const char *to = arg->comments_end ?: arg->end;
    return range_has_comment(from, to, comments);
}

OptionalText_t format_inline_arg(arg_ast_t *arg, Table_t comments) {
    // A comment anywhere in this argument's span can only be written on a line
    // of its own, so the list has to go multi-line rather than drop it.
    if (arg_has_comment(arg, comments)) return NONE_TEXT;
    if (arg->name == NULL && arg->value) return must(bounded_inline(arg->value, comments));
    Text_t code = arg_name(arg);
    if (arg->type) code = Texts(code, ":", must(format_type(arg->type)));
    if (arg->value) code = Texts(code, "=", must(bounded_inline(arg->value, comments)));
    return code;
}

Text_t format_arg_at(arg_ast_t *arg, Table_t comments, Text_t indent, int64_t column) {
    OptionalText_t inline_arg = format_inline_arg(arg, comments);
    if (inline_arg.tag != TEXT_NONE && column + inline_arg.length <= MAX_WIDTH) return inline_arg;
    if (arg->name == NULL && arg->value) return bounded_at(arg->value, comments, indent, column);
    Text_t code = arg_name(arg);
    if (arg->type) code = Texts(code, ":", format_type(arg->type));
    if (arg->value) code = Texts(code, "=", bounded_at(arg->value, comments, indent, column + code.length + 1));
    return code;
}

OptionalText_t format_inline_args(arg_ast_t *args, Table_t comments) {
    Text_t code = EMPTY_TEXT;
    for (arg_ast_t *arg = args; arg; arg = arg->next) {
        // Names sharing a type (`x, y:Int`) are separated like any other pair
        // of arguments; only the type they share is held back until the last
        // of them. format_args() below writes them the same way.
        if (arg->name && arg->next && arg->type == arg->next->type && arg->value == arg->next->value) {
            code = Texts(code, arg_name(arg), ", ");
        } else {
            code = Texts(code, must(format_inline_arg(arg, comments)));
            if (arg->next) code = Texts(code, ", ");
        }
        if (arg->next && range_has_comment(arg->end, arg->next->start, comments)) return NONE_TEXT;
    }
    return code;
}

// Whether these arguments are a plain series of values -- a record literal's
// fields written in order, a call's positional arguments -- rather than the
// named, typed, defaulted list a definition takes. A series of values reads
// like a list, so it wraps like one, filling each line: a line per value says
// there is something to say about each, and ten lines of `no,` say it ten
// times.
PUREFUNC static bool is_positional_series(arg_ast_t *args, Table_t comments) {
    if (args == NULL || args->next == NULL) return false;
    for (arg_ast_t *arg = args; arg; arg = arg->next) {
        if (arg->name != NULL || arg->type != NULL || arg->value == NULL) return false;
        if (arg_has_comment(arg, comments)) return false;
    }
    return true;
}

Text_t format_args_at(arg_ast_t *args, Table_t comments, Text_t indent, int64_t column) {
    OptionalText_t inline_args = format_inline_args(args, comments);
    if (inline_args.tag != TEXT_NONE && column + inline_args.length <= MAX_WIDTH) return inline_args;

    if (is_positional_series(args, comments)) {
        Text_t code = EMPTY_TEXT;
        Text_t arg_indent = Texts(indent, single_indent);
        bool prev_wrapped = false;
        for (arg_ast_t *arg = args; arg; arg = arg->next) {
            Text_t arg_code = format_arg_at(arg, comments, arg_indent, (int64_t)arg_indent.length);
            bool wrapped = Text$has(arg_code, Text("\n"));
            // As for a list item: an argument that ends inside an indented
            // block is separated by the newline rather than by a comma.
            Text_t comma = ends_deeper_than(arg_code, arg_indent) ? EMPTY_TEXT : Text(",");
            // Neither this argument nor the one before it may have taken more
            // than a line. Filling in after one that did leaves the value
            // trailing off the delimiter that closed it, as in `}, 3 * x,`.
            if (Text$ends_with(code, Text(","), NULL) && !wrapped && !prev_wrapped
                && trailing_line_len(code) + 1 + arg_code.length + 1 <= MAX_WIDTH)
                code = Texts(code, " ", arg_code, comma);
            else code = Texts(code, "\n", arg_indent, arg_code, comma);
            prev_wrapped = wrapped;
        }
        return code;
    }

    Text_t code = EMPTY_TEXT;
    Text_t arg_indent = Texts(indent, single_indent);
    for (arg_ast_t *arg = args; arg; arg = arg->next) {
        // Arguments that share a type and default (`x, y:Int`) are written on
        // one line, so the comments above any of them belong above that line.
        arg_ast_t *first = arg;
        Text_t names = EMPTY_TEXT;
        while (arg->name && arg->type && arg->next && arg->type == arg->next->type && arg->value == arg->next->value) {
            names = Texts(names, arg_name(arg), ", ");
            arg = arg->next;
        }
        // Read out of the source rather than off the argument: the text on the
        // argument is the lines joined into one paragraph, which is what a
        // command-line description wants and not what was written here.
        const char *pos = first->comments_start ?: first->start;
        Text_t leading = comment_range(&pos, arg->start, arg_indent, comments);

        code = Texts(code, "\n", arg_indent);
        if (leading.length > 0) code = Texts(code, leading, "\n", arg_indent);
        code = Texts(code, names);
        // Names sharing a type sit in front of the argument on its line.
        Text_t arg_code = format_arg_at(arg, comments, arg_indent, arg_indent.length + names.length);
        // The separating comma goes on the same line as the end of the
        // argument, which only works if that line is the argument's own level;
        // when the argument ends inside an indented block, the newline is the
        // separator instead.
        code = Texts(code, arg_code, ends_deeper_than(arg_code, arg_indent) ? EMPTY_TEXT : Text(","));
        pos = arg->end;
        Text_t trailing = arg->comments_end ? comment_range(&pos, arg->comments_end, arg_indent, comments) : EMPTY_TEXT;
        if (trailing.length > 0) code = Texts(code, " ", trailing);
    }
    return code;
}

// Everything between a definition's delimiters: the arguments, the return type
// if it has one, and the flags if it has any.
//
// Whether the arguments fit on one line is a question about all of that, so the
// two of those and both delimiters come out of the same budget: a definition
// can break on its return type or its flags alone, with room to spare on its
// arguments.
//
// When it does break, the return type and the flags each take a line of their
// own. Left on the last argument's line they would sit after the comma that
// separates that argument from the next, and read as more arguments.
Text_t format_bracketed_args(arg_ast_t *args, Text_t ret_type, flag_list_t flags, Text_t tail_comment, Table_t comments,
                             Text_t indent, int64_t column, const char *open, const char *close) {
    // A return type stands apart from the arguments it follows; a flag carries
    // the `;` that separates it already.
    Text_t flat_flags = inline_flags(flags);
    bool gap = ret_type.length > 0 && args != NULL;
    int64_t around = ret_type.length + flat_flags.length + (int64_t)(strlen(open) + strlen(close)) + (gap ? 1 : 0);
    // A comment written here has no line of its own on the one-line form, so
    // carrying one is itself a reason to break -- and the parameters have to
    // break with it, or the first of them is left on the opening delimiter's
    // line with nothing to separate it from the return type below.
    bool commented = Text$has(ret_type, Text("#")) || tail_comment.length > 0;
    Text_t arg_code = format_args_at(args, comments, indent, commented ? MAX_WIDTH : column + around);
    if (Text$has(arg_code, Text("\n")) || commented) {
        Text_t inner_indent = Texts(indent, single_indent);
        if (ret_type.length > 0) arg_code = Texts(arg_code, "\n", inner_indent, ret_type);
        for (int i = 0; i < flags.count; i++)
            arg_code = Texts(arg_code, "\n", inner_indent, "; ", flags.items[i]);
        if (tail_comment.length > 0) arg_code = Texts(arg_code, "\n", inner_indent, tail_comment);
        return Texts(open, arg_code, "\n", indent, close);
    }
    return Texts(open, arg_code, gap ? Text(" ") : EMPTY_TEXT, ret_type, flat_flags, close);
}

// Shared by parenthesized calls and braced record literals, which differ only
// in their delimiters.
static Text_t format_delimited_args(arg_ast_t *args, Table_t comments, Text_t indent, int64_t column, const char *open,
                                    const char *close) {
    // Written on one line this is `open`, the arguments, and `close`, so the
    // two delimiters take a column each. The paths below pass that same budget
    // on, since what they measure against it is the one-line form too.
    OptionalText_t inline_args = format_inline_args(args, comments);
    if (inline_args.tag != TEXT_NONE && column + 2 + inline_args.length <= MAX_WIDTH)
        return Texts(open, inline_args, close);

    // A lone argument normally hugs the delimiters, but not when it carries
    // comments: only format_args() below writes those out, so hugging here
    // would drop them.
    if (args && args->next == NULL && !arg_has_comment(args, comments)) {
        Text_t arg_code = format_arg_at(args, comments, indent, column + 2);
        // It can't hug either when its last line sits deeper than the call
        // itself (a lambda body, an `if`): a closing paren tacked onto the end
        // of an indented block reads as a second statement on that line and
        // doesn't parse.
        if (!ends_deeper_than(arg_code, indent)) return Texts(open, arg_code, close);
    }

    return Texts(open, format_args_at(args, comments, indent, column + 2), "\n", indent, close);
}

Text_t format_fncall_at(arg_ast_t *args, Table_t comments, Text_t indent, int64_t column) {
    return format_delimited_args(args, comments, indent, column, "(", ")");
}

Text_t format_record_literal_at(arg_ast_t *args, Table_t comments, Text_t indent, int64_t column) {
    return format_delimited_args(args, comments, indent, column, "{", "}");
}