first commit
This commit is contained in:
@@ -0,0 +1,762 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <ctype.h>
|
||||
#include "parse.h"
|
||||
#include "ASTi.h"
|
||||
|
||||
#define UNREACHABLE assert(0)
|
||||
|
||||
#ifndef NULL
|
||||
#define NULL ((void*) 0)
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
|
||||
TPOS = '+',
|
||||
TNEG = '-',
|
||||
|
||||
TADD = '+',
|
||||
TSUB = '-',
|
||||
TMUL = '*',
|
||||
TDIV = '/',
|
||||
TASS = '=',
|
||||
|
||||
TDONE = 256,
|
||||
TINT,
|
||||
TFLOAT,
|
||||
TSTRING,
|
||||
TIDENT,
|
||||
} TokenKind;
|
||||
|
||||
typedef struct Token Token;
|
||||
struct Token {
|
||||
TokenKind kind;
|
||||
Token *prev, *next;
|
||||
int offset, length;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
const char *src;
|
||||
Token *token;
|
||||
BPAlloc *alloc;
|
||||
Error *error;
|
||||
} Context;
|
||||
|
||||
static Node *parse_statement(Context *ctx);
|
||||
static Node *parse_expression(Context *ctx);
|
||||
static Node *parse_expression_statement(Context *ctx);
|
||||
|
||||
static inline _Bool isoper(char c)
|
||||
{
|
||||
return c == '+' ||
|
||||
c == '-' ||
|
||||
c == '*' ||
|
||||
c == '/' ||
|
||||
c == '=';
|
||||
}
|
||||
|
||||
AST *parse(Source *src, BPAlloc *alloc, Error *error)
|
||||
{
|
||||
assert(src != NULL);
|
||||
assert(alloc != NULL);
|
||||
|
||||
const char *str = Source_GetBody(src);
|
||||
int len = Source_GetSize(src);
|
||||
assert(str != NULL);
|
||||
assert(len >= 0);
|
||||
|
||||
AST *ast = BPAlloc_Malloc(alloc, sizeof(AST));
|
||||
|
||||
if(ast == NULL)
|
||||
return NULL;
|
||||
|
||||
Token *head = NULL,
|
||||
*tail = NULL;
|
||||
int i = 0;
|
||||
while(1)
|
||||
{
|
||||
// Skip whitespace and comments.
|
||||
while(i < len && (isspace(str[i]) || str[i] == '#'))
|
||||
{
|
||||
while(i < len && isspace(str[i]))
|
||||
i += 1;
|
||||
|
||||
if(str[i] == '#')
|
||||
{
|
||||
i += 1;
|
||||
|
||||
while(i < len && str[i] != '\n')
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if(i == len)
|
||||
break; // No more tokens left.
|
||||
|
||||
Token *tok = BPAlloc_Malloc(alloc, sizeof(Token)); // Allocate a token.
|
||||
|
||||
if(tok == NULL)
|
||||
{
|
||||
// Error: No memory.
|
||||
Error_Report(error, 1, "No memory");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if(isalpha(str[i]) || str[i] == '_')
|
||||
{
|
||||
tok->kind = TIDENT;
|
||||
tok->offset = i;
|
||||
|
||||
while(i < len && (isalpha(str[i]) || str[i] == '_'))
|
||||
i += 1;
|
||||
|
||||
tok->length = i - tok->offset;
|
||||
}
|
||||
else if(isdigit(str[i]))
|
||||
{
|
||||
tok->kind = TINT;
|
||||
tok->offset = i;
|
||||
|
||||
while(i < len && isdigit(str[i]))
|
||||
i += 1;
|
||||
|
||||
if(i+1 < len && str[i] == '.' && isdigit(str[i+1]))
|
||||
{
|
||||
i += 1; // Consume the dot.
|
||||
|
||||
tok->kind = TFLOAT;
|
||||
|
||||
while(i < len && isdigit(str[i]))
|
||||
i += 1;
|
||||
}
|
||||
|
||||
tok->length = i - tok->offset;
|
||||
}
|
||||
else if(str[i] == '\'' || str[i] == '"')
|
||||
{
|
||||
tok->kind = TSTRING;
|
||||
tok->offset = i;
|
||||
|
||||
char f = str[i];
|
||||
|
||||
i += 1; // Skip the starting quote.
|
||||
|
||||
while(i < len && str[i] != f)
|
||||
i += 1;
|
||||
|
||||
if(i == len)
|
||||
{
|
||||
Error_Report(error, 0, "Source ended inside string literal");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
i += 1; // Consume the ' or ".
|
||||
|
||||
tok->length = i - tok->offset;
|
||||
}
|
||||
else if(isoper(str[i]))
|
||||
{
|
||||
tok->offset = i;
|
||||
|
||||
while(i < len && isoper(str[i]))
|
||||
i += 1;
|
||||
|
||||
tok->length = i - tok->offset;
|
||||
|
||||
// Determine the token
|
||||
|
||||
#define matchop(str, len, const_str) \
|
||||
(len == sizeof(const_str)-1 && !strncmp(str, const_str, sizeof(const_str)-1))
|
||||
|
||||
if(matchop(str + tok->offset, tok->length, "+"))
|
||||
{
|
||||
tok->kind = TADD;
|
||||
}
|
||||
else if(matchop(str + tok->offset, tok->length, "-"))
|
||||
{
|
||||
tok->kind = TSUB;
|
||||
}
|
||||
else if(matchop(str + tok->offset, tok->length, "*"))
|
||||
{
|
||||
tok->kind = TMUL;
|
||||
}
|
||||
else if(matchop(str + tok->offset, tok->length, "/"))
|
||||
{
|
||||
tok->kind = TDIV;
|
||||
}
|
||||
else if(matchop(str + tok->offset, tok->length, "="))
|
||||
{
|
||||
tok->kind = TASS;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not a known operator.
|
||||
tok->kind = str[tok->offset];
|
||||
tok->length = 1;
|
||||
i = tok->offset + 1;
|
||||
}
|
||||
|
||||
#undef matchop
|
||||
}
|
||||
else
|
||||
{
|
||||
tok->kind = str[i];
|
||||
tok->offset = i;
|
||||
tok->length = 1;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
// Append to the token list.
|
||||
if(head)
|
||||
tail->next = tok;
|
||||
else
|
||||
head = tok;
|
||||
tok->prev = tail;
|
||||
tok->next = NULL;
|
||||
tail = tok;
|
||||
}
|
||||
|
||||
{
|
||||
Token *tok = BPAlloc_Malloc(alloc, sizeof(Token)); // Allocate a token.
|
||||
|
||||
if(tok == NULL)
|
||||
{
|
||||
// Error: No memory.
|
||||
Error_Report(error, 1, "No memory");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
tok->kind = TDONE;
|
||||
tok->offset = i;
|
||||
tok->length = 0;
|
||||
|
||||
if(head)
|
||||
tail->next = tok;
|
||||
else
|
||||
head = tok;
|
||||
tok->prev = tail;
|
||||
tok->next = NULL;
|
||||
tail = tok;
|
||||
}
|
||||
|
||||
Context ctx;
|
||||
ctx.src = str;
|
||||
ctx.token = head;
|
||||
ctx.alloc = alloc;
|
||||
ctx.error = error;
|
||||
|
||||
Node *root = parse_statement(&ctx);
|
||||
|
||||
if(root == NULL)
|
||||
return NULL;
|
||||
|
||||
ast->src = src; // Not copying! Be sure to not free the source before the AST!
|
||||
ast->root = root;
|
||||
|
||||
if(ast->src == NULL)
|
||||
return NULL;
|
||||
|
||||
return ast;
|
||||
}
|
||||
|
||||
static Node *parse_statement(Context *ctx)
|
||||
{
|
||||
assert(ctx != NULL);
|
||||
|
||||
switch(ctx->token->kind)
|
||||
{
|
||||
case '*':
|
||||
case '/':
|
||||
case TASS:
|
||||
case TDONE:
|
||||
UNREACHABLE;
|
||||
break;
|
||||
|
||||
case '(':
|
||||
case '+':
|
||||
case '-':
|
||||
case TINT:
|
||||
case TFLOAT:
|
||||
case TSTRING:
|
||||
case TIDENT:
|
||||
return parse_expression_statement(ctx);
|
||||
}
|
||||
|
||||
Error_Report(ctx->error, 0, "Got token \"%.*s\" where the start of a statement was expected",
|
||||
ctx->token->length, ctx->src + ctx->token->offset);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static Node *parse_expression_statement(Context *ctx)
|
||||
{
|
||||
assert(ctx != NULL);
|
||||
|
||||
Node *expr = parse_expression(ctx);
|
||||
|
||||
if(expr == NULL)
|
||||
return NULL;
|
||||
|
||||
assert(ctx->token != NULL);
|
||||
|
||||
if(ctx->token->kind == TDONE)
|
||||
// If the source ended before
|
||||
// the final ";", it's ok.
|
||||
return expr;
|
||||
|
||||
if(ctx->token->kind != ';')
|
||||
{
|
||||
// ERROR: Got something other than
|
||||
// a semicolon at the end of statement.
|
||||
Error_Report(ctx->error, 0, "Got token \"%.*s\" where \";\" was expected", ctx->token->length, ctx->src + ctx->token->offset);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ctx->token = ctx->token->next;
|
||||
assert(ctx->token != NULL);
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
static Node *parse_string_primary_expression(Context *ctx)
|
||||
{
|
||||
assert(ctx != NULL);
|
||||
assert(ctx->token->kind == TSTRING);
|
||||
|
||||
const char *src = ctx->src;
|
||||
int len = ctx->token->offset + ctx->token->length - 1;
|
||||
int i = ctx->token->offset + 1;
|
||||
|
||||
assert(src[i-1] == '"' || src[i-1] == '\'');
|
||||
assert(src[len] == '"' || src[len] == '\'');
|
||||
|
||||
char temp[4096];
|
||||
int temp_used = 0;
|
||||
|
||||
do
|
||||
{
|
||||
int segm_off, segm_len;
|
||||
{
|
||||
segm_off = i;
|
||||
|
||||
while(i < len && src[i] != '\\')
|
||||
i += 1;
|
||||
|
||||
segm_len = i - segm_off;
|
||||
}
|
||||
|
||||
if(temp_used + segm_len >= (int) sizeof(temp))
|
||||
{
|
||||
Error_Report(ctx->error, 1, "String is too big to be rendered inside the fixed size buffer");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
memcpy(temp + temp_used, src + segm_off, segm_len);
|
||||
temp_used += segm_len;
|
||||
|
||||
if(src[i] == '\\')
|
||||
{
|
||||
i += 1; // Consume the \.
|
||||
|
||||
if(temp_used + 1 >= (int) sizeof(temp))
|
||||
{
|
||||
Error_Report(ctx->error, 1, "String is too big to be rendered inside the fixed size buffer");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if(i == len)
|
||||
{
|
||||
// Append the \ as a normal char.
|
||||
temp[temp_used++] = '\\';
|
||||
}
|
||||
else
|
||||
{
|
||||
switch(src[i])
|
||||
{
|
||||
case 'n': temp[temp_used++] = '\n'; break;
|
||||
case 't': temp[temp_used++] = '\t'; break;
|
||||
case 'r': temp[temp_used++] = '\r'; break;
|
||||
|
||||
default:
|
||||
Error_Report(ctx->error, 0, "Invalid escape sequence \\%c", src[i]);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
i += 1; // Consume the char after the \.
|
||||
}
|
||||
}
|
||||
}
|
||||
while(i < len);
|
||||
|
||||
assert(temp_used < (int) sizeof(temp));
|
||||
|
||||
temp[temp_used] = '\0';
|
||||
|
||||
char *copy;
|
||||
int copyl;
|
||||
|
||||
{
|
||||
copy = BPAlloc_Malloc(ctx->alloc, temp_used + 1);
|
||||
|
||||
if(copy == NULL)
|
||||
{
|
||||
Error_Report(ctx->error, 1, "No memory");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
strcpy(copy, temp);
|
||||
copyl = temp_used;
|
||||
}
|
||||
|
||||
StringExprNode *node;
|
||||
{
|
||||
node = BPAlloc_Malloc(ctx->alloc, sizeof(StringExprNode));
|
||||
|
||||
if(node == NULL)
|
||||
{
|
||||
Error_Report(ctx->error, 1, "No memory");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
node->base.base.kind = NODE_EXPR;
|
||||
node->base.base.next = NULL;
|
||||
node->base.base.offset = ctx->token->offset;
|
||||
node->base.base.length = ctx->token->length;
|
||||
node->base.kind = EXPR_STRING;
|
||||
node->val = copy;
|
||||
node->len = copyl;
|
||||
}
|
||||
|
||||
ctx->token = ctx->token->next;
|
||||
assert(ctx->token != NULL);
|
||||
|
||||
return (Node*) node;
|
||||
}
|
||||
|
||||
static Node *parse_primary_expresion(Context *ctx)
|
||||
{
|
||||
assert(ctx != NULL);
|
||||
|
||||
switch(ctx->token->kind)
|
||||
{
|
||||
case '+':
|
||||
case '-':
|
||||
{
|
||||
Token *operator = ctx->token;
|
||||
|
||||
ctx->token = ctx->token->next;
|
||||
assert(ctx->token != NULL);
|
||||
|
||||
Node *operand = parse_primary_expresion(ctx);
|
||||
|
||||
if(operand == NULL)
|
||||
return NULL;
|
||||
|
||||
OperExprNode *temp = BPAlloc_Malloc(ctx->alloc, sizeof(OperExprNode));
|
||||
{
|
||||
if(temp == NULL)
|
||||
return NULL;
|
||||
|
||||
temp->base.base.kind = NODE_EXPR;
|
||||
temp->base.base.next = NULL;
|
||||
temp->base.base.offset = operator->offset;
|
||||
temp->base.base.length = operand->offset + operand->length - operator->offset;
|
||||
temp->base.kind = operator->kind == '+' ? EXPR_POS : EXPR_NEG;
|
||||
temp->head = operand;
|
||||
temp->count = 1;
|
||||
}
|
||||
return (Node*) temp;
|
||||
}
|
||||
|
||||
case '(':
|
||||
{
|
||||
ctx->token = ctx->token->next;
|
||||
|
||||
Node *node = parse_expression(ctx);
|
||||
|
||||
if(node == NULL)
|
||||
return NULL;
|
||||
|
||||
if(ctx->token->kind != ')')
|
||||
{
|
||||
Error_Report(ctx->error, 0, "Missing \")\", after sub-expression");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ctx->token = ctx->token->next;
|
||||
assert(ctx->token != NULL);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
case TINT:
|
||||
{
|
||||
char buffer[64];
|
||||
|
||||
if(ctx->token->length >= (int) sizeof(buffer))
|
||||
{
|
||||
Error_Report(ctx->error, 1, "Integer is too big");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
memcpy(buffer, ctx->src + ctx->token->offset, ctx->token->length);
|
||||
buffer[ctx->token->length] = '\0';
|
||||
|
||||
errno = 0;
|
||||
|
||||
long long int val = strtoll(buffer, NULL, 10);
|
||||
|
||||
if(errno == ERANGE)
|
||||
{
|
||||
Error_Report(ctx->error, 1, "Integer is too big");
|
||||
return NULL;
|
||||
}
|
||||
else assert(errno == 0);
|
||||
|
||||
IntExprNode *node;
|
||||
{
|
||||
node = BPAlloc_Malloc(ctx->alloc, sizeof(IntExprNode));
|
||||
|
||||
if(node == NULL)
|
||||
{
|
||||
Error_Report(ctx->error, 1, "No memory");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
node->base.base.kind = NODE_EXPR;
|
||||
node->base.base.next = NULL;
|
||||
node->base.base.offset = ctx->token->offset;
|
||||
node->base.base.length = ctx->token->length;
|
||||
node->base.kind = EXPR_INT;
|
||||
node->val = val;
|
||||
}
|
||||
|
||||
ctx->token = ctx->token->next;
|
||||
assert(ctx->token != NULL);
|
||||
|
||||
return (Node*) node;
|
||||
}
|
||||
|
||||
case TFLOAT:
|
||||
{
|
||||
char buffer[64];
|
||||
|
||||
if(ctx->token->length >= (int) sizeof(buffer))
|
||||
{
|
||||
Error_Report(ctx->error, 1, "Floating is too big");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
memcpy(buffer, ctx->src + ctx->token->offset, ctx->token->length);
|
||||
buffer[ctx->token->length] = '\0';
|
||||
|
||||
errno = 0;
|
||||
|
||||
double val = strtod(buffer, NULL);
|
||||
|
||||
if(errno == ERANGE)
|
||||
{
|
||||
Error_Report(ctx->error, 1, "Floating is too big");
|
||||
return NULL;
|
||||
}
|
||||
else assert(errno == 0);
|
||||
|
||||
FloatExprNode *node;
|
||||
{
|
||||
node = BPAlloc_Malloc(ctx->alloc, sizeof(FloatExprNode));
|
||||
|
||||
if(node == NULL)
|
||||
{
|
||||
Error_Report(ctx->error, 1, "No memory");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
node->base.base.kind = NODE_EXPR;
|
||||
node->base.base.next = NULL;
|
||||
node->base.base.offset = ctx->token->offset;
|
||||
node->base.base.length = ctx->token->length;
|
||||
node->base.kind = EXPR_FLOAT;
|
||||
node->val = val;
|
||||
}
|
||||
|
||||
ctx->token = ctx->token->next;
|
||||
assert(ctx->token != NULL);
|
||||
|
||||
return (Node*) node;
|
||||
}
|
||||
|
||||
case TSTRING:
|
||||
return parse_string_primary_expression(ctx);
|
||||
|
||||
case TIDENT:
|
||||
{
|
||||
char *copy;
|
||||
int copyl;
|
||||
|
||||
{
|
||||
copy = BPAlloc_Malloc(ctx->alloc, ctx->token->length + 1);
|
||||
|
||||
if(copy == NULL)
|
||||
{
|
||||
Error_Report(ctx->error, 1, "No memory");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
copyl = ctx->token->length;
|
||||
memcpy(copy, ctx->src + ctx->token->offset, copyl);
|
||||
copy[copyl] = '\0';
|
||||
|
||||
}
|
||||
|
||||
IdentExprNode *node;
|
||||
{
|
||||
node = BPAlloc_Malloc(ctx->alloc, sizeof(IdentExprNode));
|
||||
|
||||
if(node == NULL)
|
||||
{
|
||||
Error_Report(ctx->error, 1, "No memory");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
node->base.base.kind = NODE_EXPR;
|
||||
node->base.base.next = NULL;
|
||||
node->base.base.offset = ctx->token->offset;
|
||||
node->base.base.length = ctx->token->length;
|
||||
node->base.kind = EXPR_IDENT;
|
||||
node->val = copy;
|
||||
node->len = copyl;
|
||||
}
|
||||
|
||||
ctx->token = ctx->token->next;
|
||||
assert(ctx->token != NULL);
|
||||
|
||||
return (Node*) node;
|
||||
}
|
||||
|
||||
case TDONE:
|
||||
Error_Report(ctx->error, 1, "Unexpected end of source where a primary expression was expected");
|
||||
return NULL;
|
||||
|
||||
default:
|
||||
Error_Report(ctx->error, 1, "Unexpected token \"%.*s\" where a primary expression was expected", ctx->token->length, ctx->src + ctx->token->offset);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
UNREACHABLE;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static inline _Bool isbinop(Token *tok)
|
||||
{
|
||||
assert(tok != NULL);
|
||||
|
||||
return tok->kind == '+' ||
|
||||
tok->kind == '-' ||
|
||||
tok->kind == '*' ||
|
||||
tok->kind == '/';
|
||||
}
|
||||
|
||||
static inline _Bool isrightassoc(Token *tok)
|
||||
{
|
||||
assert(tok != NULL);
|
||||
|
||||
(void) tok;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline int precedenceof(Token *tok)
|
||||
{
|
||||
assert(tok != NULL);
|
||||
|
||||
switch(tok->kind)
|
||||
{
|
||||
case '+':
|
||||
case '-':
|
||||
return 1;
|
||||
|
||||
case '*':
|
||||
case '/':
|
||||
return 2;
|
||||
|
||||
default:
|
||||
return -10000000;
|
||||
}
|
||||
|
||||
UNREACHABLE;
|
||||
return -100000000;
|
||||
}
|
||||
|
||||
static Node *parse_expression_2(Context *ctx, Node *left_expr, int min_prec)
|
||||
{
|
||||
while(isbinop(ctx->token) && precedenceof(ctx->token) >= min_prec)
|
||||
{
|
||||
Token *op = ctx->token;
|
||||
|
||||
ctx->token = ctx->token->next;
|
||||
assert(ctx->token != NULL);
|
||||
|
||||
Node *right_expr = parse_primary_expresion(ctx);
|
||||
|
||||
if(right_expr == NULL)
|
||||
return NULL;
|
||||
|
||||
while(isbinop(ctx->token) && (precedenceof(ctx->token) > precedenceof(op) || (precedenceof(ctx->token) == precedenceof(op) && isrightassoc(ctx->token))))
|
||||
{
|
||||
right_expr = parse_expression_2(ctx, right_expr, precedenceof(op) + 1);
|
||||
|
||||
if(right_expr == NULL)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
OperExprNode *temp;
|
||||
{
|
||||
temp = BPAlloc_Malloc(ctx->alloc, sizeof(OperExprNode));
|
||||
|
||||
if(temp == NULL)
|
||||
{
|
||||
Error_Report(ctx->error, 1, "No memory");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
temp->base.base.kind = NODE_EXPR;
|
||||
temp->base.base.next = NULL;
|
||||
temp->base.base.offset = left_expr->offset;
|
||||
temp->base.base.length = right_expr->offset + right_expr->length - left_expr->offset;
|
||||
|
||||
switch(op->kind)
|
||||
{
|
||||
case '+': temp->base.kind = EXPR_ADD; break;
|
||||
case '-': temp->base.kind = EXPR_SUB; break;
|
||||
case '*': temp->base.kind = EXPR_MUL; break;
|
||||
case '/': temp->base.kind = EXPR_DIV; break;
|
||||
|
||||
default:
|
||||
UNREACHABLE;
|
||||
break;
|
||||
}
|
||||
|
||||
temp->head = left_expr;
|
||||
temp->head->next = right_expr;
|
||||
temp->count = 2;
|
||||
assert(right_expr->next == NULL);
|
||||
}
|
||||
|
||||
left_expr = (Node*) temp;
|
||||
}
|
||||
|
||||
return left_expr;
|
||||
}
|
||||
|
||||
static Node *parse_expression(Context *ctx)
|
||||
{
|
||||
Node *left_expr = parse_primary_expresion(ctx);
|
||||
|
||||
if(left_expr == NULL)
|
||||
return NULL;
|
||||
|
||||
if(ctx->token->kind == TDONE)
|
||||
return left_expr;
|
||||
|
||||
return parse_expression_2(ctx, left_expr, -1000000000);
|
||||
}
|
||||
Reference in New Issue
Block a user