aboutsummaryrefslogtreecommitdiff
path: root/src/formatter
diff options
context:
space:
mode:
Diffstat (limited to 'src/formatter')
-rw-r--r--src/formatter/args.c57
-rw-r--r--src/formatter/args.h11
-rw-r--r--src/formatter/enums.c51
-rw-r--r--src/formatter/enums.h11
-rw-r--r--src/formatter/formatter.c884
-rw-r--r--src/formatter/formatter.h13
-rw-r--r--src/formatter/types.c45
-rw-r--r--src/formatter/types.h8
-rw-r--r--src/formatter/utils.c154
-rw-r--r--src/formatter/utils.h30
10 files changed, 1264 insertions, 0 deletions
diff --git a/src/formatter/args.c b/src/formatter/args.c
new file mode 100644
index 00000000..997a1e39
--- /dev/null
+++ b/src/formatter/args.c
@@ -0,0 +1,57 @@
+// Logic for formatting arguments and argument lists
+
+#include "../ast.h"
+#include "../stdlib/datatypes.h"
+#include "../stdlib/optionals.h"
+#include "../stdlib/text.h"
+#include "formatter.h"
+#include "types.h"
+#include "utils.h"
+
+OptionalText_t format_inline_arg(arg_ast_t *arg, Table_t comments) {
+ if (range_has_comment(arg->start, arg->end, comments)) return NONE_TEXT;
+ if (arg->name == NULL && arg->value) return must(format_inline_code(arg->value, comments));
+ Text_t code = Text$from_str(arg->name);
+ if (arg->type) code = Texts(code, ":", must(format_type(arg->type)));
+ if (arg->value) code = Texts(code, "=", must(format_inline_code(arg->value, comments)));
+ return code;
+}
+
+Text_t format_arg(arg_ast_t *arg, Table_t comments, Text_t indent) {
+ OptionalText_t inline_arg = format_inline_arg(arg, comments);
+ if (inline_arg.length >= 0 && inline_arg.length <= MAX_WIDTH) return inline_arg;
+ if (arg->name == NULL && arg->value) return format_code(arg->value, comments, indent);
+ Text_t code = Text$from_str(arg->name);
+ if (arg->type) code = Texts(code, ":", format_type(arg->type));
+ if (arg->value) code = Texts(code, "=", format_code(arg->value, comments, indent));
+ 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) {
+ if (arg->name && arg->next && arg->type == arg->next->type && arg->value == arg->next->value) {
+ code = Texts(code, Text$from_str(arg->name), ",");
+ } 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;
+}
+
+Text_t format_args(arg_ast_t *args, Table_t comments, Text_t indent) {
+ OptionalText_t inline_args = format_inline_args(args, comments);
+ if (inline_args.length >= 0 && inline_args.length <= MAX_WIDTH) return inline_args;
+ Text_t code = EMPTY_TEXT;
+ for (arg_ast_t *arg = args; arg; arg = arg->next) {
+ if (arg->name && arg->next && arg->type == arg->next->type && arg->value == arg->next->value) {
+ code = Texts(code, Text$from_str(arg->name), ",");
+ } else {
+ code = Texts(code, "\n", indent, single_indent, format_arg(arg, comments, Texts(indent, single_indent)));
+ if (args->next) code = Texts(code, ",");
+ }
+ }
+ return code;
+}
diff --git a/src/formatter/args.h b/src/formatter/args.h
new file mode 100644
index 00000000..c902684b
--- /dev/null
+++ b/src/formatter/args.h
@@ -0,0 +1,11 @@
+// Logic for formatting arguments and argument lists
+
+#pragma once
+
+#include "../ast.h"
+#include "../stdlib/datatypes.h"
+
+OptionalText_t format_inline_arg(arg_ast_t *arg, Table_t comments);
+Text_t format_arg(arg_ast_t *arg, Table_t comments, Text_t indent);
+OptionalText_t format_inline_args(arg_ast_t *args, Table_t comments);
+Text_t format_args(arg_ast_t *args, Table_t comments, Text_t indent);
diff --git a/src/formatter/enums.c b/src/formatter/enums.c
new file mode 100644
index 00000000..893f055b
--- /dev/null
+++ b/src/formatter/enums.c
@@ -0,0 +1,51 @@
+// Logic for formatting enums and enum tags
+
+#include "../ast.h"
+#include "../stdlib/datatypes.h"
+#include "../stdlib/optionals.h"
+#include "../stdlib/text.h"
+#include "args.h"
+#include "utils.h"
+
+OptionalText_t format_inline_tag(tag_ast_t *tag, Table_t comments) {
+ if (range_has_comment(tag->start, tag->end, comments)) return NONE_TEXT;
+ Text_t code = Text$from_str(tag->name);
+ if (tag->fields || tag->secret) {
+ code = Texts(code, "(", must(format_inline_args(tag->fields, comments)));
+ if (tag->secret) code = Texts(code, "; secret");
+ code = Texts(code, ")");
+ }
+ return code;
+}
+
+Text_t format_tag(tag_ast_t *tag, Table_t comments, Text_t indent) {
+ OptionalText_t inline_tag = format_inline_tag(tag, comments);
+ if (inline_tag.length >= 0) return inline_tag;
+ Text_t code = Text$from_str(tag->name);
+ if (tag->fields || tag->secret) {
+ code = Texts(code, "(", format_args(tag->fields, comments, Texts(indent, single_indent)));
+ if (tag->secret) code = Texts(code, "; secret");
+ code = Texts(code, ")");
+ }
+ return code;
+}
+
+OptionalText_t format_inline_tags(tag_ast_t *tags, Table_t comments) {
+ Text_t code = EMPTY_TEXT;
+ for (; tags; tags = tags->next) {
+ code = Texts(code, must(format_inline_tag(tags, comments)));
+ if (tags->next) code = Texts(code, ", ");
+ if (tags->next && range_has_comment(tags->end, tags->next->start, comments)) return NONE_TEXT;
+ }
+ return code;
+}
+
+Text_t format_tags(tag_ast_t *tags, Table_t comments, Text_t indent) {
+ OptionalText_t inline_tags = format_inline_tags(tags, comments);
+ if (inline_tags.length >= 0) return inline_tags;
+ Text_t code = EMPTY_TEXT;
+ for (; tags; tags = tags->next) {
+ add_line(&code, Texts(format_tag(tags, comments, indent), ","), indent);
+ }
+ return code;
+}
diff --git a/src/formatter/enums.h b/src/formatter/enums.h
new file mode 100644
index 00000000..e7233df4
--- /dev/null
+++ b/src/formatter/enums.h
@@ -0,0 +1,11 @@
+// Logic for formatting enums and enum tags
+
+#pragma once
+
+#include "../ast.h"
+#include "../stdlib/datatypes.h"
+
+OptionalText_t format_inline_tag(tag_ast_t *tag, Table_t comments);
+Text_t format_tag(tag_ast_t *tag, Table_t comments, Text_t indent);
+OptionalText_t format_inline_tags(tag_ast_t *tags, Table_t comments);
+Text_t format_tags(tag_ast_t *tags, Table_t comments, Text_t indent);
diff --git a/src/formatter/formatter.c b/src/formatter/formatter.c
new file mode 100644
index 00000000..b68b3874
--- /dev/null
+++ b/src/formatter/formatter.c
@@ -0,0 +1,884 @@
+// 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) {
+ List_t codepoints = Text$utf32_codepoints(Text$slice(text, I_small(1), I_small(1)));
+ 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 < 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 Extend:
+ /*inline*/ case FunctionDef:
+ /*inline*/ case ConvertDef:
+ /*inline*/ case DocTest:
+ /*inline*/ case Extern:
+ 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:
+ /*inline*/ case Set: {
+ ast_list_t *items = ast->tag == List ? Match(ast, List)->items : Match(ast, Set)->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 ast->tag == List ? Texts("[", code, "]") : 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);
+ return Texts(fmt_inline(entry->key, comments), "=", fmt_inline(entry->value, 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 Optional: {
+ ast_t *val = Match(ast, Optional)->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 Deserialize: {
+ DeclareMatch(deserialize, ast, Deserialize);
+ return Texts("deserialize(", fmt_inline(deserialize->value, comments), " -> ", format_type(deserialize->type),
+ ")");
+ }
+ /*inline*/ case Use: {
+ DeclareMatch(use, ast, Use);
+ // struct {
+ // ast_t *var;
+ // const char *path;
+ // enum { USE_LOCAL, USE_MODULE, USE_SHARED_OBJECT, USE_HEADER, USE_C_CODE, USE_ASM } what;
+ // } 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.length >= 0 && 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(")"));
+ 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));
+ return Texts(code, Text$has(code, Text("\n")) ? Texts("\n", indent, ")") : Text(")"),
+ format_namespace(def->namespace, comments, indent));
+ }
+ /*multiline*/ case LangDef: {
+ DeclareMatch(def, ast, LangDef);
+ return Texts("lang ", Text$from_str(def->name), format_namespace(def->namespace, comments, indent));
+ }
+ /*multiline*/ case Extend: {
+ DeclareMatch(extend, ast, Extend);
+ return Texts("lang ", Text$from_str(extend->name), format_namespace(extend->body, comments, indent));
+ }
+ /*multiline*/ case Extern: {
+ DeclareMatch(ext, ast, Extern);
+ return Texts("extern ", Text$from_str(ext->name), " : ", format_type(ext->type));
+ }
+ /*multiline*/ case Defer:
+ return Texts("defer ", format_namespace(Match(ast, Defer)->body, comments, indent));
+ /*multiline*/ case List:
+ /*multiline*/ case Set: {
+ if (inlined_fits) return inlined;
+ ast_list_t *items = ast->tag == List ? Match(ast, List)->items : Match(ast, Set)->items;
+ Text_t code = ast->tag == List ? Text("[") : 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 ast->tag == List ? Texts(code, "\n", indent, "]") : 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);
+ return Texts(fmt(entry->key, comments, indent), "=", fmt(entry->value, 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 Optional: {
+ if (inlined_fits) return inlined;
+ ast_t *val = Match(ast, Optional)->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.length >= 0);
+ 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 DocTest: {
+ DeclareMatch(test, ast, DocTest);
+ Text_t expr = fmt(test->expr, comments, indent);
+ Text_t code = Texts(">> ", expr);
+ if (test->expected) {
+ Text_t expected = fmt(test->expected, comments, indent);
+ code = Texts(code, "\n", indent, "= ", expected);
+ }
+ 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 Deserialize: {
+ if (inlined_fits) return inlined;
+ DeclareMatch(deserialize, ast, Deserialize);
+ return Texts("deserialize(", fmt(deserialize->value, comments, indent), " -> ", format_type(deserialize->type),
+ ")");
+ }
+ /*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;
+}
diff --git a/src/formatter/formatter.h b/src/formatter/formatter.h
new file mode 100644
index 00000000..a8f9013a
--- /dev/null
+++ b/src/formatter/formatter.h
@@ -0,0 +1,13 @@
+// This code defines functions for transforming ASTs back into Tomo source text
+
+#pragma once
+
+#include <stdbool.h>
+
+#include "../ast.h"
+#include "../stdlib/datatypes.h"
+
+Text_t format_file(const char *path);
+Text_t format_code(ast_t *ast, Table_t comments, Text_t indentation);
+Text_t format_namespace(ast_t *namespace, Table_t comments, Text_t indent);
+OptionalText_t format_inline_code(ast_t *ast, Table_t comments);
diff --git a/src/formatter/types.c b/src/formatter/types.c
new file mode 100644
index 00000000..e52faf70
--- /dev/null
+++ b/src/formatter/types.c
@@ -0,0 +1,45 @@
+// Logic for formatting types
+
+#include "../ast.h"
+#include "../stdlib/datatypes.h"
+#include "../stdlib/stdlib.h"
+#include "../stdlib/text.h"
+#include "args.h"
+#include "formatter.h"
+
+Text_t format_type(type_ast_t *type) {
+ switch (type->tag) {
+ case VarTypeAST: return Text$from_str(Match(type, VarTypeAST)->name);
+ case PointerTypeAST: {
+ DeclareMatch(ptr, type, PointerTypeAST);
+ return Texts(ptr->is_stack ? Text("&") : Text("@"), format_type(ptr->pointed));
+ }
+ case ListTypeAST: {
+ return Texts("[", format_type(Match(type, ListTypeAST)->item), "]");
+ }
+ case SetTypeAST: {
+ return Texts("|", format_type(Match(type, SetTypeAST)->item), "|");
+ }
+ case TableTypeAST: {
+ DeclareMatch(table, type, TableTypeAST);
+ Text_t code = Texts("{", format_type(table->key), "=", format_type(table->value));
+ if (table->default_value) {
+ OptionalText_t val = format_inline_code(table->default_value, (Table_t){});
+ assert(val.length >= 0);
+ code = Texts(code, "; default=", val);
+ }
+ return Texts(code, "}");
+ }
+ case FunctionTypeAST: {
+ DeclareMatch(func, type, FunctionTypeAST);
+ Text_t code = Texts("func(", format_inline_args(func->args, (Table_t){}));
+ if (func->ret) code = Texts(code, func->args ? Text(" -> ") : Text("-> "), format_type(func->ret));
+ return Texts(code, ")");
+ }
+ case OptionalTypeAST: {
+ return Texts(format_type(Match(type, OptionalTypeAST)->type), "?");
+ }
+ case UnknownTypeAST:
+ default: fail("Invalid Type AST");
+ }
+}
diff --git a/src/formatter/types.h b/src/formatter/types.h
new file mode 100644
index 00000000..2571f880
--- /dev/null
+++ b/src/formatter/types.h
@@ -0,0 +1,8 @@
+// Logic for formatting types
+
+#pragma once
+
+#include "../ast.h"
+#include "../stdlib/datatypes.h"
+
+Text_t format_type(type_ast_t *type);
diff --git a/src/formatter/utils.c b/src/formatter/utils.c
new file mode 100644
index 00000000..bbe74d7f
--- /dev/null
+++ b/src/formatter/utils.c
@@ -0,0 +1,154 @@
+// This file defines utility functions for autoformatting code
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "../ast.h"
+#include "../parse/context.h"
+#include "../stdlib/datatypes.h"
+#include "../stdlib/optionals.h"
+#include "../stdlib/tables.h"
+#include "../stdlib/text.h"
+#include "formatter.h"
+
+const Text_t single_indent = Text(" ");
+
+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;
+}
+
+bool range_has_comment(const char *start, const char *end, Table_t comments) {
+ OptionalText_t comment = next_comment(comments, &start, end);
+ return (comment.length >= 0);
+}
+
+CONSTFUNC int suggested_blank_lines(ast_t *first, ast_t *second) {
+ if (second == NULL) return 0;
+
+ for (;;) {
+ if (first->tag == Declare && Match(first, Declare)->value) {
+ first = Match(first, Declare)->value;
+ } else if (first->tag == DocTest && Match(first, DocTest)->expr && Match(first, DocTest)->expected == NULL) {
+ first = Match(first, DocTest)->expr;
+ } else break;
+ }
+
+ for (;;) {
+ if (second->tag == Declare && Match(second, Declare)->value) {
+ second = Match(second, Declare)->value;
+ } else if (second->tag == DocTest && Match(second, DocTest)->expr && Match(second, DocTest)->expected == NULL) {
+ second = Match(second, DocTest)->expr;
+ } else break;
+ }
+
+ switch (first->tag) {
+ case If:
+ case When:
+ case Repeat:
+ case While:
+ case For:
+ case Block:
+ case Defer:
+ case ConvertDef:
+ case FunctionDef:
+ case Lambda:
+ case StructDef:
+ case EnumDef:
+ case LangDef:
+ case Extend: return 1;
+ case Use: {
+ if (second->tag != Use) return 1;
+ break;
+ }
+ case Declare: {
+ DeclareMatch(decl, first, Declare);
+ if (decl->value) return suggested_blank_lines(decl->value, second);
+ break;
+ }
+ case Assign: {
+ DeclareMatch(assign, first, Assign);
+ for (ast_list_t *val = assign->values; val; val = val->next) {
+ if (suggested_blank_lines(val->ast, second) > 0) return 1;
+ }
+ break;
+ }
+ default: break;
+ }
+
+ switch (second->tag) {
+ case If:
+ case When:
+ case Repeat:
+ case While:
+ case For:
+ case Block:
+ case Defer:
+ case ConvertDef:
+ case FunctionDef:
+ case Lambda:
+ case StructDef:
+ case EnumDef:
+ case LangDef:
+ case Extend: return 1;
+ default: break;
+ }
+ 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;
+}
+
+OptionalText_t termify_inline(ast_t *ast, Table_t comments) {
+ if (range_has_comment(ast->start, ast->end, comments)) return NONE_TEXT;
+ switch (ast->tag) {
+ case BINOP_CASES:
+ case Not:
+ case Negative:
+ case HeapAllocate:
+ case StackReference: return parenthesize(format_inline_code(ast, comments), EMPTY_TEXT);
+ default: return format_inline_code(ast, comments);
+ }
+}
+
+Text_t termify(ast_t *ast, Table_t comments, Text_t indent) {
+ switch (ast->tag) {
+ case BINOP_CASES:
+ case Not:
+ case Negative:
+ case HeapAllocate:
+ case StackReference: return parenthesize(format_code(ast, comments, indent), indent);
+ default: return format_inline_code(ast, comments);
+ }
+}
diff --git a/src/formatter/utils.h b/src/formatter/utils.h
new file mode 100644
index 00000000..880da0a9
--- /dev/null
+++ b/src/formatter/utils.h
@@ -0,0 +1,30 @@
+// This file defines utility functions for autoformatting code
+
+#pragma once
+
+#include <stdbool.h>
+
+#include "../ast.h"
+#include "../stdlib/datatypes.h"
+#include "../stdlib/optionals.h"
+
+#define MAX_WIDTH 100
+
+#define must(expr) \
+ ({ \
+ OptionalText_t _expr = expr; \
+ if (_expr.length < 0) return NONE_TEXT; \
+ (Text_t) _expr; \
+ })
+
+extern const Text_t single_indent;
+
+void add_line(Text_t *code, Text_t line, Text_t indent);
+OptionalText_t next_comment(Table_t comments, const char **pos, const char *end);
+bool range_has_comment(const char *start, const char *end, Table_t comments);
+CONSTFUNC int suggested_blank_lines(ast_t *first, ast_t *second);
+Text_t indent_code(Text_t code);
+Text_t parenthesize(Text_t code, Text_t indent);
+CONSTFUNC ast_t *unwrap_block(ast_t *ast);
+OptionalText_t termify_inline(ast_t *ast, Table_t comments);
+Text_t termify(ast_t *ast, Table_t comments, Text_t indent);