llm
Download
123 lines 5.3 KiBCModified 2026-09-02 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
// Logic for parsing numbers

#include <ctype.h>
#include <gc.h>
#include <stdarg.h>
#include <stdbool.h>
#include <string.h>

#include <unictype.h>
#include <uniname.h>

#include "../ast.h"
#include "../stdlib/integers.h"
#include "../stdlib/number.h"
#include "context.h"
#include "errors.h"
#include "utils.h"

// A numeric literal's trailing scale, which the parser folds into the value:
// nothing else in the compiler needs to know it was written that way.
enum { NUM_PLAIN, NUM_PERCENT, NUM_DEGREES };

// The exact value a numeric literal denotes. Built from the written digits
// rather than a double, so `3.15` is 63/20 and not the nearest double to it,
// and scaled exactly by any suffix: `%` is a division by 100, and `deg` a
// multiplication by pi/180, which, pi being exact here, stays exact.
static Num_t num_literal_value(parse_ctx_t *ctx, const char *pos, const char *digits, int suffix) {
    Num_t n = number_from_decimal(digits);
    if (number_is_error(n)) parser_err(ctx, pos, pos, "I couldn't parse this number");
    switch (suffix) {
    case NUM_PERCENT: return number_div(n, number_from_int(100));
    case NUM_DEGREES: return number_mul(n, number_div(number_pi(), number_from_int(180)));
    default: return n;
    }
}

ast_t *parse_int(parse_ctx_t *ctx, const char *pos) {
    const char *start = pos;
    if (!isdigit(*pos)) return NULL;
    if (match(&pos, "0x")) { // Hex
        pos += span_of(pos, "0123456789abcdefABCDEF_");
    } else if (match(&pos, "0b")) { // Binary
        pos += span_of(pos, "01_");
    } else if (match(&pos, "0o")) { // Octal
        pos += span_of(pos, "01234567_");
    } else { // Decimal
        pos += span_of(pos, "0123456789_");
    }
    char *str = GC_MALLOC_ATOMIC((size_t)(pos - start) + 1);
    memset(str, 0, (size_t)(pos - start) + 1);
    for (char *src = (char *)start, *dest = str; src < pos; ++src) {
        if (*src != '_') *(dest++) = *src;
    }

    if (match(&pos, "e") || match(&pos, "f")) // floating point literal
        return NULL;

    // `50%` and `90deg` are numeric literals, not integers: both scale the
    // written digits by an exact factor (1/100, and pi/180 respectively).
    if (match(&pos, "%"))
        return NewAST(ctx->file, start, pos, Num, .n = num_literal_value(ctx, start, str, NUM_PERCENT),
                      .str = strndup_bounded(start, (size_t)(pos - start)));
    else if (match(&pos, "deg"))
        return NewAST(ctx->file, start, pos, Num, .n = num_literal_value(ctx, start, str, NUM_DEGREES),
                      .str = strndup_bounded(start, (size_t)(pos - start)));

    OptionalInt_t i = Int$from_str(str);
    if (i.small == 0) parser_err(ctx, start, pos, "I couldn't parse this integer");
    return NewAST(ctx->file, start, pos, Int, .i = i, .str = strndup_bounded(start, (size_t)(pos - start)));
}

ast_t *parse_num(parse_ctx_t *ctx, const char *pos) {
    const char *start = pos;
    if (!isdigit(*pos) && *pos != '.') return NULL;
    else if (*pos == '.' && !isdigit(pos[1])) return NULL;

    size_t len = span_of(pos, "0123456789_");
    // Digits followed by `.identifier` are an Int with a method/field, not a
    // Num: `12.sqrt()` calls sqrt on the integer 12. But a SECOND dot ends
    // that reading: `12..round()` is the Num `12.` with `.round()` called
    // on it, since `12.` followed by a path literal would be nonsense.
    if (pos[len] == '.' && pos[len + 1] != '.' && is_xid_start_next(pos + len + 1)) return NULL;
    else if (pos[len] == '.') len += 1 + span_of(pos + len + 1, "0123456789");
    else if (pos[len] != 'e' && pos[len] != 'f' && pos[len] != '%') return NULL;
    if (pos[len] == 'e') {
        len += 1;
        if (pos[len] == '-' || pos[len] == '+') len += 1;
        len += span_of(pos + len, "0123456789_");
    }
    char *buf = GC_MALLOC_ATOMIC(len + 1);
    memset(buf, 0, len + 1);
    for (char *src = (char *)pos, *dest = buf; src < pos + len; ++src) {
        if (*src != '_') *(dest++) = *src;
    }
    pos += len;

    int suffix = NUM_PLAIN;
    if (match(&pos, "%")) suffix = NUM_PERCENT;
    else if (match(&pos, "deg")) suffix = NUM_DEGREES;

    return NewAST(ctx->file, start, pos, Num, .n = num_literal_value(ctx, start, buf, suffix),
                  .str = strndup_bounded(start, (size_t)(pos - start)));
}

// Fold a leading `-` into a numeric literal, so `-128` is a single Int literal
// rather than a negation applied to `128`: only a literal can be compiled
// straight to a sized target, so `Int8(-128)` fits where negating an `Int8(128)`
// would not. Returns NULL when there's no literal for the sign to fold into.
ast_t *negate_literal(parse_ctx_t *ctx, const char *start, ast_t *literal) {
    // The sign is only part of the literal when it's written against the
    // digits. Written apart from them, as in `- -1` or `- 2`, it stays a
    // negation applied to a literal, which is what it looks like.
    if (!isdigit((unsigned char)start[1]) && start[1] != '.') return NULL;
    switch (literal->tag) {
    case Int:
        return NewAST(ctx->file, start, literal->end, Int, .i = Int$negative(Match(literal, Int)->i),
                      .str = strndup_bounded(start, (size_t)(literal->end - start)));
    case Num:
        return NewAST(ctx->file, start, literal->end, Num, .n = number_neg(Match(literal, Num)->n),
                      .str = strndup_bounded(start, (size_t)(literal->end - start)));
    default: return NULL;
    }
}