llm
Download
229 lines 8.6 KiBCModified 2026-08-31 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
// Logic for parsing statements

#include <gc.h>
#include <stdbool.h>

#include "../ast.h"
#include "../util.h"
#include "context.h"
#include "controlflow.h"
#include "errors.h"
#include "expressions.h"
#include "files.h"
#include "statements.h"
#include "suffixes.h"
#include "text.h"
#include "types.h"
#include "utils.h"

ast_t *parse_declaration(parse_ctx_t *ctx, const char *pos) {
    const char *start = pos;
    ast_t *var = parse_var(ctx, pos);
    if (!var) return NULL;
    pos = var->end;
    spaces(&pos);
    if (!match(&pos, ":")) return NULL;
    spaces(&pos);
    type_ast_t *type = optional(ctx, &pos, parse_type);
    spaces(&pos);
    ast_t *val = NULL;
    if (match(&pos, "=")) {
        val = optional(ctx, &pos, parse_extended_expr);
        if (!val) {
            if (optional(ctx, &pos, parse_use))
                parser_err(ctx, start, pos, "'use' statements are only allowed at the top level of a file");
            else parser_err(ctx, pos, eol(pos), "This is not a valid expression");
        }
    }
    // A declaration with no value is a bare `x : T`, so an open parenthesis
    // here means this isn't a declaration at all, but a construct that takes
    // a type, like `deserialize:Foo(bytes)`:
    if (val == NULL && *pos == '(') return NULL;
    return NewAST(ctx->file, start, pos, Declare, .var = var, .type = type, .value = val);
}

ast_t *parse_assignment(parse_ctx_t *ctx, const char *pos) {
    const char *start = pos;
    ast_list_t *targets = NULL;
    for (;;) {
        ast_t *lhs = optional(ctx, &pos, parse_term);
        if (!lhs) break;
        targets = new (ast_list_t, .ast = lhs, .next = targets);
        spaces(&pos);
        if (!match(&pos, ",")) break;
        whitespace(ctx, &pos);
    }

    if (!targets) return NULL;

    spaces(&pos);
    if (!match(&pos, "=")) return NULL;
    if (match(&pos, "=")) return NULL; // == comparison

    ast_list_t *values = NULL;
    // The statement ends at its last value, not at wherever the separator scan
    // stopped: trailing spaces inside the span would hide a `# comment` that
    // follows them from anything that looks at what comes after this statement.
    const char *end = pos;
    for (;;) {
        ast_t *rhs = optional(ctx, &pos, parse_extended_expr);
        if (!rhs) break;
        values = new (ast_list_t, .ast = rhs, .next = values);
        end = rhs->end;
        spaces(&pos);
        if (!match(&pos, ",")) break;
        whitespace(ctx, &pos);
    }

    REVERSE_LIST(targets);
    REVERSE_LIST(values);

    return NewAST(ctx->file, start, end, Assign, .targets = targets, .values = values);
}

ast_t *parse_update(parse_ctx_t *ctx, const char *pos) {
    const char *start = pos;
    ast_t *lhs = optional(ctx, &pos, parse_expr);
    if (!lhs) return NULL;
    spaces(&pos);
    ast_e op;
    if (match(&pos, "+=")) op = PlusUpdate;
    else if (match(&pos, "++=")) op = ConcatUpdate;
    else if (match(&pos, "-=")) op = MinusUpdate;
    else if (match(&pos, "*=")) op = MultiplyUpdate;
    else if (match(&pos, "//=")) op = FloorDivideUpdate;
    else if (match(&pos, "/=")) op = DivideUpdate;
    else if (match(&pos, "^=")) op = PowerUpdate;
    else if (match(&pos, "<<=")) op = LeftShiftUpdate;
    else if (match(&pos, "<<<=")) op = UnsignedLeftShiftUpdate;
    else if (match(&pos, ">>=")) op = RightShiftUpdate;
    else if (match(&pos, ">>>=")) op = UnsignedRightShiftUpdate;
    else if (match(&pos, "and=")) op = AndUpdate;
    else if (match(&pos, "or=")) op = OrUpdate;
    else if (match(&pos, "xor=")) op = XorUpdate;
    else return NULL;
    ast_t *rhs = expect(ctx, start, &pos, parse_extended_expr, "I expected an expression here");
    return new (ast_t, .file = ctx->file, .start = start, .end = pos, .tag = op, .__data.PlusUpdate.lhs = lhs,
                .__data.PlusUpdate.rhs = rhs);
}

ast_t *parse_debug_log(parse_ctx_t *ctx, const char *pos) {
    const char *start = pos;
    if (!match(&pos, ">>")) return NULL;
    spaces(&pos);
    ast_t *first_value =
        expect(ctx, start, &pos, parse_statement, "I couldn't parse the expression for this debugging message");
    ast_list_t *values = new (ast_list_t, .ast = first_value, .next = NULL);
    spaces(&pos);
    while (match(&pos, ",")) {
        spaces(&pos);
        ast_t *value = optional(ctx, &pos, parse_statement);
        if (!value) break;
        values = new (ast_list_t, .ast = value, .next = values);
        spaces(&pos);
    }
    REVERSE_LIST(values);
    // whitespace(ctx, &pos);
    return NewAST(ctx->file, start, pos, DebugLog, .values = values);
}

ast_t *parse_assert(parse_ctx_t *ctx, const char *pos) {
    const char *start = pos;
    if (!match_word(&pos, "assert")) return NULL;
    spaces(&pos);
    ast_t *expr = expect(ctx, start, &pos, parse_extended_expr, "I couldn't parse the expression for this assert");
    spaces(&pos);
    ast_t *message = NULL;
    if (match(&pos, ",")) {
        whitespace(ctx, &pos);
        message = expect(ctx, start, &pos, parse_extended_expr, "I couldn't parse the error message for this assert");
    } else {
        pos = expr->end;
    }
    return NewAST(ctx->file, start, pos, Assert, .expr = expr, .message = message);
}

// Extract the plain (non-interpolated) string from a text literal at `*pos`,
// advancing `*pos` past it. Returns NULL if there's no string literal here,
// and errors if the literal contains interpolations.
static const char *parse_plain_text(parse_ctx_t *ctx, const char **pos) {
    const char *start = *pos;
    ast_t *text = parse_text(ctx, *pos, /*allow_interps=*/false);
    if (!text) return NULL;
    *pos = text->end;
    Text_t joined = EMPTY_TEXT;
    for (ast_list_t *chunk = Match(text, TextJoin)->children; chunk; chunk = chunk->next) {
        if (chunk->ast->tag != TextLiteral)
            parser_err(ctx, start, *pos, "I expected a plain text label here, without any interpolations");
        joined = Texts(joined, Match(chunk->ast, TextLiteral)->text);
    }
    return Text$as_c_string(joined);
}

// test "label"
//     <body>
// [fails "msg" | fails_compile "msg"]
ast_t *parse_test(parse_ctx_t *ctx, const char *pos) {
    const char *start = pos;
    if (!match_word(&pos, "test")) return NULL;
    int64_t starting_indent = get_indent(ctx, pos);
    spaces(&pos);

    // No string label => this isn't a test block; let `test` parse as an
    // ordinary identifier instead (soft keyword, non-breaking):
    const char *label = parse_plain_text(ctx, &pos);
    if (!label) return NULL;

    ast_t *body = expect(ctx, start, &pos, parse_block, "I expected an indented body for this test");

    // A `fails`/`fails_compile` clause sets its field non-NULL (a bare clause with no message becomes "", meaning
    // "match any failure"); both left NULL means the test should pass.
    const char *expected_failure = NULL, *expected_compile_error = NULL;

    // Optional trailing outcome clause, dedented back to the test's own indent:
    const char *save = pos;
    whitespace(ctx, &pos);
    if (get_indent(ctx, pos) == starting_indent) {
        if (match_word(&pos, "fails_compile")) {
            spaces(&pos);
            const char *msg = parse_plain_text(ctx, &pos);
            expected_compile_error = msg ? msg : "";
        } else if (match_word(&pos, "fails")) {
            spaces(&pos);
            const char *msg = parse_plain_text(ctx, &pos);
            expected_failure = msg ? msg : "";
        } else {
            pos = save; // no outcome clause; rewind for the next construct
        }
    } else {
        pos = save;
    }

    return NewAST(ctx->file, start, pos, Test, .label = label, .body = body, .expected_failure = expected_failure,
                  .expected_compile_error = expected_compile_error);
}

ast_t *parse_statement(parse_ctx_t *ctx, const char *pos) {
    ast_t *stmt = NULL;
    if ((stmt = parse_declaration(ctx, pos)) || (stmt = parse_debug_log(ctx, pos)) || (stmt = parse_assert(ctx, pos)))
        return stmt;

    if (!(false || (stmt = parse_update(ctx, pos)) || (stmt = parse_assignment(ctx, pos))))
        stmt = parse_extended_expr(ctx, pos);

    for (bool progress = (stmt != NULL); progress;) {
        ast_t *new_stmt;
        progress = false;
        if (stmt->tag == Var) {
            progress =
                (false || (new_stmt = parse_method_call_suffix(ctx, stmt))
                 || (new_stmt = parse_fncall_suffix(ctx, stmt)) || (new_stmt = parse_record_literal_suffix(ctx, stmt)));
        } else if (stmt->tag == FunctionCall || stmt->tag == RecordLiteral) {
            new_stmt = parse_optional_conditional_suffix(ctx, stmt);
            progress = (new_stmt != stmt);
        }

        if (progress) stmt = new_stmt;
    }
    return stmt;
}