number method

void number()

Scans a number literal (integer or floating-point).

Implementation

void number() {
  start = current; // Ensure start is set correctly before advancing
  bool startsWithDot = peek() == '.';
  bool isFloat = startsWithDot;
  bool hasExponent = false;
  bool hasFractionalDigits = false;
  // 1. Consume leading dot if present
  if (startsWithDot) {
    advance(); // Consume '.'
    // Must be followed by digits *or* 'e'/'E' eventually for validity check later
    if (!isDigit(peek()) && peek().toLowerCase() != 'e') {
      // This was just a DOT token
      current = start + 1; // Reset position to only include the dot
      addToken(TokenType.DOT);
      return; // Not a number
    }
  }

  // 2. Consume integer part (if not starting with dot)
  if (!startsWithDot) {
    // --- Handle Prefixed Integers (0x, 0b, 0o) ---
    if (peek() == '0' && current == start && current + 1 < source.length) {
      String nextChar = source[current + 1].toLowerCase();
      if (nextChar == 'x' || nextChar == 'b' || nextChar == 'o') {
        advance(); // Consume '0'
        advance(); // Consume 'x', 'b', or 'o'
        _scanPrefixedInteger(nextChar);
        return; // Prefixed integer handled
      }
      // Fall through for '0.' or '0' followed by other digits (decimal)
    }
    // --- Consume Decimal Integer Part ---
    while (isDigit(peek())) {
      advance();
    }
  }

  // 3. Consume optional fractional part
  //    This runs if we didn't start with a dot AND a dot follows the integer part,
  //    OR if we *did* start with a dot (because we need to consume the digits after it).
  if (peek() == '.') {
    if (startsWithDot) {
      // We already consumed the starting dot. This would be a second dot.
      throw LexerError(
        line,
        currentColumn(),
        "Invalid syntax: multiple decimal points in number.",
      );
    }
    isFloat = true;
    advance(); // Consume '.'
  }
  if (isFloat) {
    // Consume digits if it's a float (either startsWithDot or dotSeenAfterInteger)
    int fractionStart = current;
    while (isDigit(peek())) {
      advance();
    }
    if (current > fractionStart) {
      hasFractionalDigits = true;
    }
    // If startsWithDot is true, we MUST have found some fractional digits OR an exponent later
    if (startsWithDot &&
        !hasFractionalDigits &&
        peek().toLowerCase() != 'e') {
      // Example: "." followed by non-digit, non-'e'
      throw LexerError(
        line,
        start - lineStart + 1,
        "Invalid number format: lone decimal point.",
      );
    }
  }

  // 4. Consume optional exponent part
  if (peek().toLowerCase() == 'e') {
    // Ensure something came before 'e'
    if (current == start || (startsWithDot && !hasFractionalDigits)) {
      // Handles cases like "e5" or ".e5"
      throw LexerError(
        line,
        start - lineStart + 1,
        "Invalid number format: exponent must follow digits or decimal point with digits.",
      );
    }
    isFloat = true;
    hasExponent = true;
    advance(); // Consume 'e' or 'E'
    if (peek() == '+' || peek() == '-') {
      advance();
    } // Optional sign
    int exponentStart = current;
    while (isDigit(peek())) {
      advance();
    }
    if (current == exponentStart) {
      throw LexerError(
        line,
        currentColumn(),
        "Invalid scientific notation: exponent lacks digits.",
      );
    }
  }

  // 5. Check for invalid characters after number
  if (!isAtEnd() && !" \n\t\r+-*/%<>=&|~^!:()[]{},#".contains(peek())) {
    throw LexerError(line, currentColumn(), "Invalid decimal literal.");
  }

  // --- Parse and validate the Literal Value ---
  String numberString = source.substring(start, current);

  // Python allows "1." or ".5" but not just "."
  // Our logic now prevents just "." from reaching here.
  // Validate ".e5" - should fail because no digits after initial dot before 'e'
  if (startsWithDot && !hasFractionalDigits && !hasExponent) {
    // This means we only parsed "." which should have been handled earlier.
    // This code path likely indicates an error in the logic above.
    // However, let's treat it as just a DOT token if it occurs.
    current = start + 1; // Reset current to only include the dot
    addToken(TokenType.DOT);
    return;
  }

  Object literalValue;

  try {
    // If it has '.', 'e', or started with '.', treat as float
    if (isFloat) {
      literalValue = PyNum.double(double.parse(numberString));
    } else {
      literalValue = PyNum.bigInt(BigInt.parse(numberString));
    }
  } catch (e) {
    String formatType = isFloat ? "floating-point" : "integer";
    throw LexerError(
      line,
      start - lineStart + 1,
      "Invalid $formatType number format: '$numberString'",
    );
  }
  addToken(TokenType.NUMBER, literalValue);
}