main
Download
231 lines 10.2 KiBCModified 2026-03-14 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
// This file defines how to compile CLI argument parsing

#include "../stdlib/cli.h"
#include "../environment.h"
#include "../stdlib/datatypes.h"
#include "../stdlib/optionals.h"
#include "../stdlib/text.h"
#include "../stdlib/util.h"
#include "../typecheck.h"
#include "../types.h"
#include "compilation.h"

static Text_t get_flag_options(type_t *t, Text_t separator) {
    if (t->tag == BoolType) {
        return Text("yes|no");
    } else if (t->tag == PathType) {
        return Text("path");
    } else if (t->tag == EnumType) {
        Text_t options = EMPTY_TEXT;
        for (tag_t *tag = Match(t, EnumType)->tags; tag; tag = tag->next) {
            if (Match(tag->type, StructType)->fields)
                options = Texts(options, tag->name, " ", get_flag_options(tag->type, separator));
            else options = Texts(options, tag->name);
            if (tag->next) options = Texts(options, separator);
        }
        return options;
    } else if (t->tag == StructType) {
        Text_t options = EMPTY_TEXT;
        for (arg_t *field = Match(t, StructType)->fields; field; field = field->next) {
            options = Texts(options, get_flag_options(field->type, separator));
            if (field->next) options = Texts(options, " ");
        }
        return options;
    } else if (is_numeric_type(t)) {
        return Text("N");
    } else if (t->tag == TextType || t->tag == CStringType) {
        return Text("text");
    } else if (t->tag == ListType) {
        Text_t item_option = get_flag_options(Match(t, ListType)->item_type, separator);
        return Texts(item_option, "1 ", item_option, "2...");
    } else if (t->tag == TableType && Match(t, TableType)->value_type == PRESENT_TYPE) {
        Text_t item_option = get_flag_options(Match(t, TableType)->key_type, separator);
        return Texts(item_option, "1 ", item_option, "2...");
    } else if (t->tag == TableType) {
        Text_t key_option = get_flag_options(Match(t, TableType)->key_type, separator);
        Text_t value_option = get_flag_options(Match(t, TableType)->value_type, separator);
        return Texts(key_option, "1:", value_option, "1 ", key_option, "2:", value_option, "2...");
    } else {
        return Text("value");
    }
}

static OptionalText_t flagify(const char *name, bool prefix) {
    if (!name) return NONE_TEXT;
    Text_t flag = Text$from_str(name);
    flag = Text$replace(flag, Text("_"), Text("-"));
    if (prefix) flag = flag.length == 1 ? Texts("-", flag) : Texts("--", flag);
    return flag;
}

static Text_t generate_usage(env_t *env, type_t *fn_type) {
    DeclareMatch(fn_info, fn_type, FunctionType);
    bool explicit_help_flag = false;
    for (arg_t *arg = fn_info->args; arg; arg = arg->next) {
        if (streq(arg->name, "help")) {
            explicit_help_flag = true;
            break;
        }
    }
    env_t *main_env = fresh_scope(env);
    Text_t usage = explicit_help_flag ? EMPTY_TEXT : Text(" [\x1b[1m--help\x1b[m]");
    for (arg_t *arg = fn_info->args; arg; arg = arg->next) {
        usage = Texts(usage, " ");
        type_t *t = get_arg_type(main_env, arg);
        OptionalText_t flag = flagify(arg->name, arg->default_val != NULL);
        assert(flag.tag != TEXT_NONE);
        Text_t flags = Texts("\x1b[1m", flag, "\x1b[m");
        if (t->tag == BoolType || (t->tag == OptionalType && Match(t, OptionalType)->type->tag == BoolType))
            flags = Texts(flags, "|\x1b[1m--no-", Text$without_prefix(flag, Text("--")), "\x1b[m");
        if (arg->default_val || value_type(t)->tag == BoolType) {
            if (t->tag == BoolType || (t->tag == OptionalType && Match(t, OptionalType)->type->tag == BoolType))
                usage = Texts(usage, "[", flags, "]");
            else if (t->tag == ListType) usage = Texts(usage, "[", flags, " ", get_flag_options(t, Text("|")), "]");
            else if (t->tag == EnumType) usage = Texts(usage, "[", flags, " val]");
            else usage = Texts(usage, "[", flags, " ", get_flag_options(t, Text("|")), "]");
        } else {
            usage = Texts(usage, "\x1b[1m", flag, "\x1b[m");
        }
    }
    return usage;
}

static Text_t generate_help(env_t *env, type_t *fn_type) {
    DeclareMatch(fn_info, fn_type, FunctionType);
    env_t *main_env = fresh_scope(env);
    Text_t help_text = EMPTY_TEXT;

    for (arg_t *arg = fn_info->args; arg; arg = arg->next) {
        help_text = Texts(help_text, "\n");
        type_t *t = get_arg_type(main_env, arg);
        OptionalText_t flag = flagify(arg->name, true);
        assert(flag.tag != TEXT_NONE);
        OptionalText_t alias_flag = flagify(arg->alias, true);
        Text_t flags = Texts("\x1b[33;1m", flag, "\x1b[m");
        if (alias_flag.tag != TEXT_NONE) flags = Texts("\x1b[33;1m", alias_flag, "\x1b[0;2m,\x1b[m ", flags);
        if (t->tag == BoolType || (t->tag == OptionalType && Match(t, OptionalType)->type->tag == BoolType))
            flags = Texts(flags, "|\x1b[33;1m--no-", Text$without_prefix(flag, Text("--")), "\x1b[m");
        if (t->tag == BoolType || (t->tag == OptionalType && Match(t, OptionalType)->type->tag == BoolType))
            help_text = Texts(help_text, "  ", flags);
        else
            help_text = Texts(help_text, "  ", flags, " \x1b[1;34m", get_flag_options(t, Text("\x1b[m | \x1b[1;34m")),
                              "\x1b[m");

        if (arg->comment.length > 0) help_text = Texts(help_text, " \x1b[3m", arg->comment, "\x1b[m");
        if (arg->default_val) {
            Text_t default_text =
                Text$from_strn(arg->default_val->start, (size_t)(arg->default_val->end - arg->default_val->start));
            help_text = Texts(help_text, " \x1b[2m(default:", default_text, ")\x1b[m");
        }
    }
    return help_text;
}

public
Text_t compile_cli_arg_call(env_t *env, ast_t *ast, Text_t fn_name, type_t *fn_type) {
    DeclareMatch(fn_info, fn_type, FunctionType);

    Text_t code = EMPTY_TEXT;
    OptionalText_t usage = ast_metadata(ast, "USAGE");
    if (usage.tag == TEXT_NONE) usage = generate_usage(env, fn_type);

    OptionalText_t help = ast_metadata(ast, "HELP");
    if (help.tag == TEXT_NONE) help = generate_help(env, fn_type);

    code = Texts(code,
                 "Text_t usage = Texts(Text(\"\\x1b[1mUsage:\\x1b[m \"), "
                 "Text$from_str(argv[0]), Text(\" \")",
                 usage.length == 0 ? EMPTY_TEXT : Texts(", Text(", quoted_text(usage), ")"), ");\n",
                 "Text_t help = Texts(usage, Text(\"\\n\"", quoted_text(help), "\"\\n\"));\n");

    for (arg_t *arg = fn_info->args; arg; arg = arg->next) {
        code = Texts(code, compile_declaration(arg->type, Texts("_$", Text$from_str(arg->name))), " = ",
                     compile_empty(arg->type), ";\n");
    }

    OptionalText_t version = ast_metadata(ast, "VERSION");
    if (version.tag == TEXT_NONE) version = Text("0.0.1");
    Text_t version_code = Text$quoted(version, false, Text("\""));
    code = Texts(code, "cli_arg_t cli_args[] = {\n");
    for (arg_t *arg = fn_info->args; arg; arg = arg->next) {
        code = Texts(code, "{", quoted_text(Text$replace(Text$from_str(arg->name), Text("_"), Text("-"))), ", &",
                     Texts("_$", Text$from_str(arg->name)), ", ", compile_type_info(arg->type),
                     arg->default_val ? Text("") : Text(", .required=true"),
                     arg->alias ? Texts(", .short_flag=", quoted_text(Text$from_str(arg->alias)),
                                        "[0]") // TODO: escape char properly
                                : Text(""),

                     "},\n");
    }
    code = Texts(code, "};\n");
    code = Texts(code, "tomo_parse_args(argc, argv, usage, help, ", version_code,
                 ", sizeof(cli_args)/sizeof(cli_args[0]), cli_args);\n");

    // Lazily initialize default values to prevent side effects
    int64_t i = 0;
    for (arg_t *arg = fn_info->args; arg; arg = arg->next) {
        if (arg->default_val) {
            Text_t default_val;
            if (arg->type) {
                default_val = compile_to_type(env, arg->default_val, arg->type);
            } else {
                default_val = compile(env, arg->default_val);
            }
            code = Texts(code, "if (!cli_args[", i, "].populated) ", Texts("_$", Text$from_str(arg->name)), " = ",
                         default_val, ";\n");
        }
        i += 1;
    }

    code = Texts(code, fn_name, "(");
    for (arg_t *arg = fn_info->args; arg; arg = arg->next) {
        code = Texts(code, Texts("_$", arg->name));
        if (arg->next) code = Texts(code, ", ");
    }
    code = Texts(code, ");\n");
    return code;
}

public
Text_t compile_manpage(Text_t program, ast_t *ast, arg_t *args) {
    OptionalText_t user_manpage = ast_metadata(ast, "MANPAGE");
    if (user_manpage.tag != TEXT_NONE) {
        return user_manpage;
    }

    OptionalText_t synopsys = ast_metadata(ast, "MANPAGE_SYNOPSYS");
    OptionalText_t description = ast_metadata(ast, "MANPAGE_DESCRIPTION");
    Text_t date = Text(""); // TODO: use date
    Text_t man = Texts(".\\\" Automatically generated by Tomo\n"
                       ".TH \"",
                       Text$upper(program, Text("C")), "\" \"1\" \"", date,
                       "\" \"\" \"\"\n"
                       ".SH NAME\n",
                       program, " \\- ", synopsys.tag == TEXT_NONE ? Text("a Tomo program") : synopsys, "\n");

    if (description.tag != TEXT_NONE) {
        man = Texts(man, ".SH DESCRIPTION\n", description, "\n");
    }

    man = Texts(man, ".SH OPTIONS\n");
    for (arg_t *arg = args; arg; arg = arg->next) {
        OptionalText_t flag = flagify(arg->name, true);
        assert(flag.tag != TEXT_NONE);
        Text_t flags = Texts("\\f[B]", flag, "\\f[R]");
        if (arg->alias) flags = Texts(flags, ", \\f[B]", flagify(arg->alias, true), "\\f[R]");
        if (non_optional(arg->type)->tag == BoolType)
            flags = Texts(flags, " | \\f[B]--no-", Text$without_prefix(flag, Text("--")), "\\f[R]");

        man = Texts(man, "\n.TP\n", flags);
        if (non_optional(arg->type)->tag != BoolType) {
            Text_t options = Texts("\\f[I]", get_flag_options(arg->type, Text("\\f[R] | \\f[I]")), "\\f[R]");
            man = Texts(man, " ", options);
        }

        if (arg->comment.length > 0) {
            man = Texts(man, "\n", arg->comment);
        }
    }

    return man;
}