123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298 | // This file defines utility functions for autoformatting code
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include "../ast.h"
#include "../parse/context.h"
#include "../stdlib/datatypes.h"
#include "../stdlib/files.h"
#include "../stdlib/table.h"
#include "../stdlib/text.h"
#include "formatter.h"
#include "utils.h"
const Text_t single_indent = Text(" ");
// How many columns the last line of this text takes up, which is where
// anything appended to it would start.
PUREFUNC 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;
}
void add_flag(flag_list_t *flags, bool present, Text_t name) {
if (!present) return;
assert(flags->count < (int)(sizeof(flags->items) / sizeof(flags->items[0])));
flags->items[flags->count++] = name;
}
Text_t inline_flags(flag_list_t flags) {
Text_t code = EMPTY_TEXT;
for (int i = 0; i < flags.count; i++)
code = Texts(code, "; ", flags.items[i]);
return code;
}
void add_line(Text_t *code, Text_t line, Text_t indent) {
if (code->length == 0) {
*code = line;
} else {
if (line.length > 0) *code = Texts(*code, "\n", indent, line);
else *code = Texts(*code, "\n");
}
}
OptionalText_t next_comment(Table_t comments, const char **pos, const char *end) {
for (const char *p = *pos; p < end; p++) {
const char **comment_end = Table$get(comments, &p, parse_comments_info);
if (comment_end) {
*pos = *comment_end;
return Text$from_strn(p, (size_t)(*comment_end - p));
}
}
return NONE_TEXT;
}
// The comments between two positions, one per line at `indent`, with the
// blank lines the author left between them kept.
Text_t comment_range(const char **pos, const char *end, Text_t indent, Table_t comments) {
Text_t ret = EMPTY_TEXT;
const char *prev = NULL;
for (OptionalText_t comment; (comment = next_comment(comments, pos, end)).length > 0;) {
if (prev) {
for (const char *p = prev + 1; p < *pos; p++) {
if (*p == '\n') {
ret = Text$concat(ret, Text("\n"));
break;
}
}
}
add_line(&ret, Text$trim(comment, Text(" \t\r\n"), false, true), indent);
prev = *pos;
}
return ret;
}
bool range_has_comment(const char *start, const char *end, Table_t comments) {
OptionalText_t comment = next_comment(comments, &start, end);
return (comment.tag != TEXT_NONE);
}
// The position just past the next `close` delimiter, stepping over comments so
// a `}` or `)` inside one doesn't count. Callers use it to resume comment
// scanning after a bracketed list whose comments were already written out with
// its items. The scan is blind to string literals, so it must start past the
// last item's end -- from there to the delimiter the source can only hold
// commas, whitespace, `; flag` names, and comments.
const char *after_close_delimiter(const char *pos, const char *end, char close, Table_t comments) {
while (pos < end) {
const char **comment_end = Table$get(comments, &pos, parse_comments_info);
if (comment_end) {
pos = *comment_end;
continue;
}
if (*pos == close) return pos + 1;
pos++;
}
return pos;
}
// True if the source between two positions contains a blank line: a line with
// nothing but whitespace on it. This is how the formatter decides where the
// author put blank lines, so that formatting can preserve them.
PUREFUNC
bool has_blank_line(const char *start, const char *end) {
bool seen_newline = false, only_space = true;
for (const char *p = start; p < end; p++) {
if (*p == '\n') {
if (seen_newline && only_space) return true;
seen_newline = true;
only_space = true;
} else if (*p != ' ' && *p != '\t' && *p != '\r') {
only_space = false;
}
}
return false;
}
// The column the line holding `pos` is indented to.
PUREFUNC static int64_t line_indent(const char *file, const char *pos) {
const char *line = pos;
while (line > file && line[-1] != '\n')
line--;
int64_t col = 0;
for (const char *p = line; p < pos && (*p == ' ' || *p == '\t'); p++)
col += 1;
return col;
}
// Where a run of source actually stops. A block consumes the whitespace after
// itself as it parses, so a definition's span runs past the blank lines below
// it and over any comment written there. That is how a comment at the top level
// came to be indented into the namespace above it, and how the blank line the
// author left in front of that comment disappeared. Nothing written to the left
// of `own` on a line of its own was ever part of this.
PUREFUNC static const char *content_end_at(ast_t *ast, int64_t own) {
const char *file = ast->file->text;
const char *p = ast->end;
for (;;) {
while (p > ast->start && (p[-1] == ' ' || p[-1] == '\t' || p[-1] == '\r' || p[-1] == '\n'))
p--;
if (p <= ast->start) return p;
const char *line = p;
while (line > file && line[-1] != '\n')
line--;
if (line <= ast->start) return p;
int64_t col = 0;
const char *text = line;
for (; *text == ' ' || *text == '\t'; text++)
col += 1;
if (*text != '#' || col >= own) return p;
p = line;
}
}
// A statement owns what is written below it and indented past it.
PUREFUNC const char *content_end(ast_t *ast) {
return content_end_at(ast, line_indent(ast->file->text, ast->start) + 1);
}
// A block owns what is written at the column its own statements sit at.
PUREFUNC const char *block_content_end(ast_t *block) {
ast_list_t *first = Match(block, Block)->statements;
if (first == NULL) return block->end;
return content_end_at(block, line_indent(block->file->text, first->ast->start));
}
// How many blank lines belong between two consecutive statements. The
// formatter keeps the blank lines the author wrote (collapsing runs of them
// down to one) and only insists on a blank line of its own after a function
// definition. The other rule -- a blank line after a body that nests two
// levels deep -- depends on how the statement came out once formatted, so it
// lives in the block formatter rather than here.
PUREFUNC int suggested_blank_lines(ast_t *first, ast_t *second) {
if (first == NULL || second == NULL) return 0;
if (has_blank_line(content_end(first), second->start)) return 1;
switch (first->tag) {
case FunctionDef:
case ConvertDef: return 1;
default: return 0;
}
}
Text_t indent_code(Text_t code) {
if (code.length <= 0) return code;
return Texts(single_indent, Text$replace(code, Text("\n"), Texts("\n", single_indent)));
}
Text_t parenthesize(Text_t code, Text_t indent) {
if (Text$has(code, Text("\n"))) return Texts("(\n", indent, indent_code(code), "\n", indent, ")");
else return Texts("(", code, ")");
}
CONSTFUNC ast_t *unwrap_block(ast_t *ast) {
if (ast == NULL) return NULL;
while (ast->tag == Block && Match(ast, Block)->statements && Match(ast, Block)->statements->next == NULL) {
ast = Match(ast, Block)->statements->ast;
}
if (ast->tag == Block && Match(ast, Block)->statements == NULL) return NULL;
return ast;
}
// Whether the final line of `code` is indented further than `indent`, i.e. the
// text ends inside an indented block rather than back at its own level.
PUREFUNC bool ends_deeper_than(Text_t code, Text_t indent) {
List_t lines = Text$lines(code);
if (lines.length <= 1) return false;
Text_t last = *(Text_t *)(lines.data + ((int64_t)lines.length - 1) * lines.stride);
Text_t body = Text$trim(last, Text(" \t"), true, false);
return (int64_t)last.length - (int64_t)body.length > (int64_t)indent.length;
}
// Whether this expression needs parentheses to be a self-contained term that
// a suffix or prefix can attach to. Anything is_operation() covers does (which
// takes in a literal with a folded `-`: `(-2).abs()` is not `-2.abs()`), and
// so do the prefixed and multi-clause forms below.
static PUREFUNC bool needs_parens_as_term(ast_t *ast) {
if (is_operation(ast)) return true;
switch (ast->tag) {
case Not:
case HeapAllocate:
case If:
case Lambda:
case Match:
case StackReference: return true;
default: return false;
}
}
// An inline `if`/`match` runs on through whatever follows it, so wherever the
// surrounding syntax has to resume afterwards -- an argument list's comma, a
// table entry's `:`, the value of a `return`, which the postfix `x if c` would
// otherwise claim -- it needs parentheses to end where it means to.
OptionalText_t bounded_inline(ast_t *ast, Table_t comments) {
if (ast->tag == If || ast->tag == Match) return parenthesize(must(format_inline_code(ast, comments)), EMPTY_TEXT);
return format_inline_code(ast, comments);
}
// The same, for a sub-expression that has to be written across several lines.
// The block form runs on just as the one-line form does: the `,` after a list
// item, or a table entry's `:`, lands inside the last clause of a bare
// `if`/`match` rather than after it.
Text_t bounded_at(ast_t *ast, Table_t comments, Text_t indent, int64_t column) {
// The parentheses take a column of their own on the line.
if (ast->tag == If || ast->tag == Match)
return parenthesize(format_code_at(ast, comments, indent, column + 1), indent);
return format_code_at(ast, comments, indent, column);
}
OptionalText_t termify_inline(ast_t *ast, Table_t comments) {
if (range_has_comment(ast->start, ast->end, comments)) return NONE_TEXT;
if (needs_parens_as_term(ast)) return parenthesize(must(format_inline_code(ast, comments)), EMPTY_TEXT);
return format_inline_code(ast, comments);
}
// Whether a `.` written straight against this expression would be hard to
// read. A numeric literal that carries its own decimal point is: `2.` and
// `.sqrt()` run together into `2..sqrt()`, which reads as a range, and
// `4.2.ceil()` leaves the eye nowhere to stop. The parser takes both -- this
// is about reading them, so it asks how the literal was written rather than
// what it evaluates to.
static PUREFUNC bool needs_parens_before_dot(ast_t *ast) {
const char *str = ast->tag == Num ? Match(ast, Num)->str : ast->tag == Int ? Match(ast, Int)->str : NULL;
return str != NULL && strchr(str, '.') != NULL;
}
// A receiver with a `.` written against it: `x.field`, `x.method()`. It is a
// term like any other suffix's, and a decimal literal besides.
OptionalText_t dotted_inline(ast_t *ast, Table_t comments) {
if (needs_parens_before_dot(ast)) return parenthesize(must(format_inline_code(ast, comments)), EMPTY_TEXT);
return termify_inline(ast, comments);
}
Text_t dotted_at(ast_t *ast, Table_t comments, Text_t indent, int64_t column) {
if (needs_parens_before_dot(ast)) return parenthesize(format_code_at(ast, comments, indent, column + 1), indent);
return termify_at(ast, comments, indent, column);
}
Text_t termify_at(ast_t *ast, Table_t comments, Text_t indent, int64_t column) {
if (needs_parens_as_term(ast)) return parenthesize(format_code_at(ast, comments, indent, column + 1), indent);
Text_t inlined = format_inline_code(ast, comments);
if (inlined.tag != TEXT_NONE && column + inlined.length <= MAX_WIDTH) return inlined;
// A rendering spread over several lines that ends back at its own level
// closed with a delimiter of its own -- `[`...`]`, `f(`...`)` -- and is a
// term already. One that ends deeper trailed off into an indented block,
// and needs the parentheses to say where it stopped.
Text_t code = format_code_at(ast, comments, indent, column);
return ends_deeper_than(code, indent) ? parenthesize(code, indent) : code;
}
|