oas/src/lexer.c
omicron 6f78d26ea1
Some checks failed
Validate the build / validate-build (push) Failing after 35s
Change the n argument of lexer_shift_buffer to size_t from int
2025-04-17 15:12:56 +02:00

557 lines
17 KiB
C

#include "lexer.h"
#include "error.h"
#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <string.h>
error_t *const err_lexer_already_open = &(error_t){
.message =
"Can't open on a lexer object that is already opened. Close it first."};
error_t *const err_lexer_prefix_too_large =
&(error_t){.message = "Prefix too large for internal lexer buffer"};
error_t *const err_lexer_buffer_underrun = &(error_t){
.message = "Buffer does not contain enough characters for lexer_consume_n"};
error_t *const err_lexer_consume_excessive_length =
&(error_t){.message = "Too many valid characters to consume"};
typedef bool (*char_predicate_t)(char);
const char *lexer_token_id_to_cstr(lexer_token_id_t id) {
switch (id) {
case TOKEN_ERROR:
return "TOKEN_ERROR";
case TOKEN_IDENTIFIER:
return "TOKEN_IDENTIFIER";
case TOKEN_DECIMAL:
return "TOKEN_DECIMAL";
case TOKEN_HEXADECIMAL:
return "TOKEN_HEXADECIMAL";
case TOKEN_OCTAL:
return "TOKEN_OCTAL";
case TOKEN_BINARY:
return "TOKEN_BINARY";
case TOKEN_CHAR:
return "TOKEN_CHAR";
case TOKEN_STRING:
return "TOKEN_STRING";
case TOKEN_COLON:
return "TOKEN_COLON";
case TOKEN_COMMA:
return "TOKEN_COMMA";
case TOKEN_LBRACKET:
return "TOKEN_LBRACKET";
case TOKEN_RBRACKET:
return "TOKEN_RBRACKET";
case TOKEN_PLUS:
return "TOKEN_PLUS";
case TOKEN_MINUS:
return "TOKEN_MINUS";
case TOKEN_ASTERISK:
return "TOKEN_ASTERISK";
case TOKEN_DOT:
return "TOKEN_DOT";
case TOKEN_COMMENT:
return "TOKEN_COMMENT";
case TOKEN_NEWLINE:
return "TOKEN_NEWLINE";
case TOKEN_WHITESPACE:
return "TOKEN_WHITESPACE";
}
assert(!"Unreachable, weird token id" && id);
__builtin_unreachable();
}
void lexer_token_print(lexer_token_t *token) {
printf("(%zu, %zu) %s[%d]%s%s\n", token->line_number,
token->character_number, lexer_token_id_to_cstr(token->id),
token->id, token->value ? ": " : "",
token->value ? token->value : "");
if (token->id == TOKEN_ERROR)
printf(" `--> %s\n", token->explanation);
}
void lexer_token_cleanup(lexer_token_t *token) {
free(token->value);
memset(token, 0, sizeof(lexer_token_t));
}
void lexer_close(lexer_t *lex) {
fclose(lex->fp);
memset(lex, 0, sizeof(lexer_t));
}
/**
* Attempts to fill the lexer's internal buffer with more data from the file.
* Only reads data if the buffer isn't already full and the file hasn't reached
* EOF.
*
* @param lex The lexer to fill the buffer for
* @return nullptr on success, an error otherwise (including err_eof if EOF
* reached with empty buffer)
*/
error_t *lexer_fill_buffer(lexer_t *lex) {
if (feof(lex->fp) && lex->buffer_count == 0)
return err_eof;
if (feof(lex->fp))
return nullptr;
if (lex->buffer_count == lexer_buffer_size)
return nullptr;
size_t remaining = lexer_buffer_size - lex->buffer_count;
while (remaining > 0) {
char *buffer = lex->buffer + lex->buffer_count;
size_t n = fread(buffer, 1, remaining, lex->fp);
if (n == 0 && feof(lex->fp))
break;
if (n == 0 && ferror(lex->fp))
return errorf("Read error: %s", strerror(errno));
if (n == 0)
return err_unknown_read_failure;
remaining -= n;
lex->buffer_count += n;
}
return nullptr;
}
error_t *lexer_open(lexer_t *lex, char *path) {
if (lex->fp != nullptr)
return err_lexer_already_open;
lex->fp = fopen(path, "rb");
if (lex->fp == nullptr)
return errorf("Failed to open file '%s': %s", path, strerror(errno));
lex->line_number = 0;
lex->character_number = 0;
lex->buffer_count = 0;
return nullptr;
}
/**
* Shifts the lexer's buffer by n characters, discarding the first n characters
* and moving the remaining characters to the beginning of the buffer.
*
* @param lex The lexer whose buffer to shift
* @param n Number of characters to shift out
*
* @pre There must be at least n characters in the input buffer
*/
void lexer_shift_buffer(lexer_t *lex, size_t n) {
assert(lex->buffer_count >= n);
lex->buffer_count -= n;
memmove(lex->buffer, lex->buffer + n, lex->buffer_count);
}
/**
* Checks if the lexer's buffer starts with the given prefix.
*
* @param lex The lexer to check
* @param prefix The string prefix to check for
* @return true if the buffer starts with the prefix, false otherwise
*/
bool lexer_has_prefix(lexer_t *lex, char *prefix) {
size_t len = strlen(prefix);
if (len > lex->buffer_count)
return false;
return memcmp(lex->buffer, prefix, len) == 0;
}
error_t *lexer_not_implemented(lexer_t *lex, lexer_token_t *token) {
(void)token;
return errorf("Not implemented, character %02x (%c) at (%zu, %zu).\n",
lex->buffer[0], lex->buffer[0], lex->line_number,
lex->character_number);
}
/**
* Consumes exactly n characters from the buffer into the provided output
* buffer.
*
* @param lex The lexer to consume from
* @param len Size of the output buffer
* @param buffer Output buffer to store the consumed characters
* @param n Number of characters to consume
* @return nullptr on success, an error otherwise (err_buffer_underrun if buffer
* contains fewer than n characters)
*/
error_t *lexer_consume_n(lexer_t *lex, const size_t len,
char buffer[static len], const size_t n) {
if (lex->buffer_count < n)
return err_lexer_buffer_underrun;
if (n > len)
return err_lexer_consume_excessive_length;
memcpy(buffer, lex->buffer, n);
lexer_shift_buffer(lex, n);
return nullptr;
}
/**
* Consumes characters from the lexer buffer that satisfy the predicate
* function. Will attempt to refill the buffer if more valid characters are
* available.
*
* @param lex The lexer to consume from
* @param n Maximum number of characters to consume
* @param buffer Output buffer to store consumed characters
* @param is_valid Function that determines if a character should be consumed
* @param n_consumed Output parameter that will contain the number of characters
* consumed
* @return nullptr on success, an error otherwise
*/
error_t *lexer_consume(lexer_t *lex, const size_t n, char buffer[static n],
char_predicate_t is_valid, size_t *n_consumed) {
const size_t buffer_size = n;
bool have_more_characters = false;
*n_consumed = 0;
do {
size_t i = 0;
while (i < lex->buffer_count && i < buffer_size - *n_consumed &&
is_valid(lex->buffer[i])) {
++i;
}
memcpy(buffer + *n_consumed, lex->buffer, i);
lexer_shift_buffer(lex, i);
*n_consumed += i;
error_t *err = lexer_fill_buffer(lex);
if (err == err_eof)
have_more_characters = false;
else if (err)
return err;
else
have_more_characters =
(lex->buffer_count > 0 && is_valid(lex->buffer[0]));
if (have_more_characters && *n_consumed == buffer_size) {
return err_lexer_consume_excessive_length;
}
} while (have_more_characters);
return nullptr;
}
bool is_hexadecimal_character(char c) {
return isdigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
}
bool is_octal_character(char c) {
return c >= '0' && c <= '7';
}
bool is_binary_character(char c) {
return c == '0' || c == '1';
}
bool is_decimal_character(char c) {
return isdigit(c);
}
/**
* Processes a number token (decimal, hexadecimal, octal, or binary).
* Handles number formats with optional size suffixes.
*
* @param lex The lexer to read from
* @param token Output parameter that will be populated with the token
* information
* @return nullptr on success, an error otherwise
*
* @pre There must be at least one character in the input buffer and it should
* be [0-9]
*/
error_t *lexer_next_number(lexer_t *lex, lexer_token_t *token) {
constexpr size_t max_number_length = 128;
size_t so_far = 0;
size_t n = 0;
char buffer[max_number_length + 1] = {};
token->line_number = lex->line_number;
token->character_number = lex->character_number;
char_predicate_t is_valid;
if (lexer_has_prefix(lex, "0x")) {
is_valid = is_hexadecimal_character;
token->id = TOKEN_HEXADECIMAL;
strcpy(buffer, "0x");
so_far = 2;
} else if (lexer_has_prefix(lex, "0o")) {
is_valid = is_octal_character;
token->id = TOKEN_OCTAL;
strcpy(buffer, "0o");
so_far = 2;
} else if (lexer_has_prefix(lex, "0b")) {
token->id = TOKEN_BINARY;
is_valid = is_binary_character;
strcpy(buffer, "0b");
so_far = 2;
} else {
token->id = TOKEN_DECIMAL;
is_valid = is_decimal_character;
so_far = 0;
}
if (so_far > 0) {
lex->character_number += so_far;
lexer_shift_buffer(lex, so_far);
}
error_t *err = lexer_consume(lex, max_number_length - so_far,
buffer + so_far, is_valid, &n);
if (err == err_lexer_consume_excessive_length) {
token->id = TOKEN_ERROR;
token->explanation =
"Number length exceeds the maximum of 128 characters";
}
lex->character_number += n;
so_far += n;
if (n == 0) {
token->id = TOKEN_ERROR;
token->explanation = "Invalid number format";
}
err = lexer_fill_buffer(lex);
if (err != err_eof && err) {
return err;
}
size_t suffix_length = 0;
if (lexer_has_prefix(lex, ":8")) {
suffix_length = 2;
} else if (lexer_has_prefix(lex, ":16")) {
suffix_length = 3;
} else if (lexer_has_prefix(lex, ":32")) {
suffix_length = 3;
} else if (lexer_has_prefix(lex, ":64")) {
suffix_length = 3;
}
if (suffix_length > 0) {
err = lexer_consume_n(lex, max_number_length - so_far, buffer + so_far,
suffix_length);
if (err == err_lexer_consume_excessive_length) {
token->id = TOKEN_ERROR;
token->explanation =
"Number length exceeds the maximum of 128 characters";
} else {
lex->character_number += suffix_length;
}
}
token->value = strdup(buffer);
return nullptr;
}
/**
* Processes a newline token (\n or \r\n).
* Updates the lexer's line and character position tracking.
*
* @param lex The lexer to read from
* @param token Output parameter that will be populated with the token
* information
* @return nullptr on success, an error otherwise
*
* @pre There must be at least on character in the input buffer and it must
* be [\r\n]
*/
error_t *lexer_next_newline(lexer_t *lex, lexer_token_t *token) {
token->line_number = lex->line_number;
token->character_number = lex->character_number;
token->id = TOKEN_NEWLINE;
if (lexer_has_prefix(lex, "\r\n")) {
lexer_shift_buffer(lex, 2);
token->value = strdup("\r\n");
lex->character_number = 0;
lex->line_number += 1;
} else if (lexer_has_prefix(lex, "\n")) {
lexer_shift_buffer(lex, 1);
token->value = strdup("\n");
lex->character_number = 0;
lex->line_number += 1;
} else {
lexer_shift_buffer(lex, 1);
token->id = TOKEN_ERROR;
lex->character_number += 1;
token->value = strdup((char[]){lex->buffer[0]});
token->explanation = "Invalid newline format";
}
return nullptr;
}
bool is_identifier_character(char c) {
return isalnum(c) || c == '_';
}
/**
* Processes an identifier token.
* Identifiers start with a letter or underscore and can contain alphanumeric
* characters or underscores.
*
* @param lex The lexer to read from
* @param token Output parameter that will be populated with the token
* information
* @return nullptr on success, an error otherwise
*
* @pre There must be at least 1 character in the read buffer and it must be
* [a-zA-Z_]
*/
error_t *lexer_next_identifier(lexer_t *lex, lexer_token_t *token) {
constexpr size_t max_identifier_length = 128;
size_t n = 0;
char buffer[max_identifier_length + 1] = {};
token->id = TOKEN_IDENTIFIER;
token->line_number = lex->line_number;
token->character_number = lex->character_number;
error_t *err = lexer_consume(lex, max_identifier_length, buffer,
is_identifier_character, &n);
if (err == err_lexer_consume_excessive_length) {
token->id = TOKEN_ERROR;
token->explanation =
"Identifier length exceeds the maximum of 128 characters";
}
lex->character_number += n;
token->value = strdup(buffer);
return nullptr;
}
error_t *lexer_next_character(lexer_t *lex, lexer_token_t *token) {
return lexer_not_implemented(lex, token);
}
error_t *lexer_next_string(lexer_t *lex, lexer_token_t *token) {
return lexer_not_implemented(lex, token);
}
bool is_whitespace_character(char c) {
return c == ' ' || c == '\t';
}
/**
* Processes a whitespace token (spaces and tabs).
*
* @param lex The lexer to read from
* @param token Output parameter that will be populated with the token
* information
* @return nullptr on success, an error otherwise
*
* @pre There must be at least one character in the buffer and it must be
* [ \t]
*/
error_t *lexer_next_whitespace(lexer_t *lex, lexer_token_t *token) {
constexpr size_t max_whitespace_length = 1024;
size_t n = 0;
char buffer[max_whitespace_length + 1] = {};
token->id = TOKEN_WHITESPACE;
token->line_number = lex->line_number;
token->character_number = lex->character_number;
error_t *err = lexer_consume(lex, max_whitespace_length, buffer,
is_whitespace_character, &n);
if (err == err_lexer_consume_excessive_length) {
token->id = TOKEN_ERROR;
token->explanation =
"Whitespace length exceeds the maximum of 1024 characters";
}
lex->character_number += n;
token->value = strdup(buffer);
return nullptr;
}
bool is_comment_character(char c) {
return c != '\r' && c != '\n';
}
/**
* Processes a comment token (starts with ';' and continues to end of line).
*
* @param lex The lexer to read from
* @param token Output parameter that will be populated with the token
* information
* @return nullptr on success, an error otherwise
*
* @pre There must be at least one character in the buffer and it must be ';'
*/
error_t *lexer_next_comment(lexer_t *lex, lexer_token_t *token) {
constexpr size_t max_comment_length = 1024;
size_t n = 0;
char buffer[max_comment_length + 1] = {};
token->id = TOKEN_COMMENT;
token->line_number = lex->line_number;
token->character_number = lex->character_number;
error_t *err = lexer_consume(lex, max_comment_length, buffer,
is_comment_character, &n);
if (err == err_lexer_consume_excessive_length) {
token->id = TOKEN_ERROR;
token->explanation =
"Comment length exceeds the maximum of 1024 characters";
}
lex->character_number += n;
token->value = strdup(buffer);
return nullptr;
}
error_t *lexer_next(lexer_t *lex, lexer_token_t *token) {
memset(token, 0, sizeof(lexer_token_t));
error_t *err = lexer_fill_buffer(lex);
if (err)
return err;
char first = lex->buffer[0];
if (isalpha(first) || first == '_')
return lexer_next_identifier(lex, token);
if (isdigit(first))
return lexer_next_number(lex, token);
switch (first) {
case '\'':
return lexer_next_character(lex, token);
case '"':
return lexer_next_string(lex, token);
case ' ':
case '\t':
return lexer_next_whitespace(lex, token);
case ';':
return lexer_next_comment(lex, token);
case ':':
token->id = TOKEN_COLON;
break;
case ',':
token->id = TOKEN_COMMA;
break;
case '[':
token->id = TOKEN_LBRACKET;
break;
case ']':
token->id = TOKEN_RBRACKET;
break;
case '+':
token->id = TOKEN_PLUS;
break;
case '-':
token->id = TOKEN_MINUS;
break;
case '*':
token->id = TOKEN_ASTERISK;
break;
case '.':
token->id = TOKEN_DOT;
break;
case '\r':
case '\n':
return lexer_next_newline(lex, token);
default:
token->id = TOKEN_ERROR;
break;
}
token->value = strdup((char[]){first, 0});
lexer_shift_buffer(lex, 1);
token->line_number = lex->line_number;
token->character_number = lex->character_number;
if (token->id == TOKEN_ERROR) {
token->explanation =
"unexpected character during lexing (first of token)";
}
lex->character_number += 1;
return nullptr;
}