1
0
Fork 0
forked from fun/fun

More expression implementations.

This commit is contained in:
Johannes Findeisen 2025-09-14 03:22:22 +02:00
commit 7c735cfdac
4 changed files with 501 additions and 78 deletions

View file

@ -0,0 +1,40 @@
#!/usr/bin/env fun
// Feature test for current Fun parser/VM.
print("=== Feature Test Start ===")
// Typed declarations
number a = 2
number b = 3
number n = 10
string s = "Hello, world!"
boolean flag = true
// Arithmetic and precedence
number sum = a + b * 5 - 1
print(sum) // expect 2 + 3 * 5 - 1 = 16
// Unary operators
number neg = -n
print(neg) // expect -10
number modv = n % 3
print(modv) // expect 1
// Booleans and logical ops
print(!false) // expect 1 (true)
print(true && false) // expect 0 (false)
print(true || false) // expect 1 (true)
// Comparisons and strings
print(n > 5) // expect 1
print(s != "") // expect 1
// Indentation-based nested if blocks (two spaces)
if (n >= 10)
print(n) // expect 10
if (a + b == 5)
print(42) // expect 42
print("=== Feature Test End ===")

View file

@ -4,4 +4,4 @@
* Hello world in Fun
*/
print("Hello, world!")
print("Hello, world!")

View file

@ -7,3 +7,4 @@ print(n)
string s = "Hello, world!"
print(s)

View file

@ -5,6 +5,34 @@
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <stdarg.h>
/* ---- parser error state ---- */
static int g_has_error = 0;
static size_t g_err_pos = 0;
static char g_err_msg[256];
static void parser_fail(size_t pos, const char *fmt, ...) {
g_has_error = 1;
g_err_pos = pos;
va_list ap;
va_start(ap, fmt);
vsnprintf(g_err_msg, sizeof(g_err_msg), fmt, ap);
va_end(ap);
}
static void calc_line_col(const char *src, size_t len, size_t pos, int *out_line, int *out_col) {
int line = 1, col = 1;
size_t limit = pos < len ? pos : len;
for (size_t i = 0; i < limit; ++i) {
if (src[i] == '\n') { line++; col = 1; }
else { col++; }
}
if (out_line) *out_line = line;
if (out_col) *out_col = col;
}
/* --------------------------- */
static char *read_file_all(const char *path, size_t *out_len) {
FILE *f = fopen(path, "rb");
@ -182,17 +210,34 @@ static int sym_index(const char *name) {
if (strcmp(G.names[i], name) == 0) return i;
}
if (G.count >= VM_MAX_GLOBALS) {
fprintf(stderr, "Parser error: too many globals (max %d)\n", VM_MAX_GLOBALS);
exit(1);
parser_fail(0, "Too many globals (max %d)", VM_MAX_GLOBALS);
return 0;
}
G.names[G.count] = strdup(name);
return G.count++;
}
/* emit_expression compiles: string | number | true | false | identifier */
static int emit_expression(Bytecode *bc, const char *src, size_t len, size_t *pos) {
/* forward declaration so helpers can recurse */
static int emit_expression(Bytecode *bc, const char *src, size_t len, size_t *pos);
/* primary: (expr) | string | number | true/false | identifier */
static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) {
skip_spaces(src, len, pos);
/* parenthesized */
if (*pos < len && src[*pos] == '(') {
(*pos)++; /* '(' */
if (!emit_expression(bc, src, len, pos)) {
parser_fail(*pos, "Expected expression after '('");
return 0;
}
if (!consume_char(src, len, pos, ')')) {
parser_fail(*pos, "Expected ')'");
return 0;
}
return 1;
}
/* string */
char *s = parse_string_literal_any_quote(src, len, pos);
if (s) {
@ -231,102 +276,424 @@ static int emit_expression(Bytecode *bc, const char *src, size_t len, size_t *po
return 0;
}
/* unary: '!' unary | '-' unary | primary */
static int emit_unary(Bytecode *bc, const char *src, size_t len, size_t *pos) {
skip_spaces(src, len, pos);
if (*pos < len && src[*pos] == '!') {
(*pos)++;
if (!emit_unary(bc, src, len, pos)) {
parser_fail(*pos, "Expected expression after '!'");
return 0;
}
bytecode_add_instruction(bc, OP_NOT, 0);
return 1;
}
if (*pos < len && src[*pos] == '-') {
(*pos)++;
/* unary minus -> 0 - expr */
int ci = bytecode_add_constant(bc, make_int(0));
bytecode_add_instruction(bc, OP_LOAD_CONST, ci);
if (!emit_unary(bc, src, len, pos)) {
parser_fail(*pos, "Expected expression after unary '-'");
return 0;
}
bytecode_add_instruction(bc, OP_SUB, 0);
return 1;
}
return emit_primary(bc, src, len, pos);
}
/* multiplicative: unary (('*' | '/' | '%') unary)* */
static int emit_multiplicative(Bytecode *bc, const char *src, size_t len, size_t *pos) {
if (!emit_unary(bc, src, len, pos)) return 0;
for (;;) {
skip_spaces(src, len, pos);
if (*pos < len && src[*pos] == '*') {
(*pos)++;
if (!emit_unary(bc, src, len, pos)) { parser_fail(*pos, "Expected expression after '*'"); return 0; }
bytecode_add_instruction(bc, OP_MUL, 0);
continue;
}
if (*pos < len && src[*pos] == '/') {
(*pos)++;
if (!emit_unary(bc, src, len, pos)) { parser_fail(*pos, "Expected expression after '/'"); return 0; }
bytecode_add_instruction(bc, OP_DIV, 0);
continue;
}
if (*pos < len && src[*pos] == '%') {
(*pos)++;
if (!emit_unary(bc, src, len, pos)) { parser_fail(*pos, "Expected expression after '%'"); return 0; }
bytecode_add_instruction(bc, OP_MOD, 0);
continue;
}
break;
}
return 1;
}
/* additive: multiplicative (('+' | '-') multiplicative)* */
static int emit_additive(Bytecode *bc, const char *src, size_t len, size_t *pos) {
if (!emit_multiplicative(bc, src, len, pos)) return 0;
for (;;) {
skip_spaces(src, len, pos);
if (*pos < len && src[*pos] == '+') {
(*pos)++;
if (!emit_multiplicative(bc, src, len, pos)) { parser_fail(*pos, "Expected expression after '+'"); return 0; }
bytecode_add_instruction(bc, OP_ADD, 0);
continue;
}
if (*pos < len && src[*pos] == '-') {
(*pos)++;
if (!emit_multiplicative(bc, src, len, pos)) { parser_fail(*pos, "Expected expression after '-'"); return 0; }
bytecode_add_instruction(bc, OP_SUB, 0);
continue;
}
break;
}
return 1;
}
/* relational: additive (('<' | '<=' | '>' | '>=') additive)* */
static int emit_relational(Bytecode *bc, const char *src, size_t len, size_t *pos) {
if (!emit_additive(bc, src, len, pos)) return 0;
for (;;) {
skip_spaces(src, len, pos);
if (*pos + 1 < len && src[*pos] == '<' && src[*pos + 1] == '=') {
*pos += 2;
if (!emit_additive(bc, src, len, pos)) { parser_fail(*pos, "Expected expression after '<='"); return 0; }
bytecode_add_instruction(bc, OP_LTE, 0);
continue;
}
if (*pos + 1 < len && src[*pos] == '>' && src[*pos + 1] == '=') {
*pos += 2;
if (!emit_additive(bc, src, len, pos)) { parser_fail(*pos, "Expected expression after '>='"); return 0; }
bytecode_add_instruction(bc, OP_GTE, 0);
continue;
}
if (*pos < len && src[*pos] == '<') {
(*pos)++;
if (!emit_additive(bc, src, len, pos)) { parser_fail(*pos, "Expected expression after '<'"); return 0; }
bytecode_add_instruction(bc, OP_LT, 0);
continue;
}
if (*pos < len && src[*pos] == '>') {
(*pos)++;
if (!emit_additive(bc, src, len, pos)) { parser_fail(*pos, "Expected expression after '>'"); return 0; }
bytecode_add_instruction(bc, OP_GT, 0);
continue;
}
break;
}
return 1;
}
/* equality: relational (('==' | '!=') relational)* */
static int emit_equality(Bytecode *bc, const char *src, size_t len, size_t *pos) {
if (!emit_relational(bc, src, len, pos)) return 0;
for (;;) {
skip_spaces(src, len, pos);
if (*pos + 1 < len && src[*pos] == '=' && src[*pos + 1] == '=') {
*pos += 2;
if (!emit_relational(bc, src, len, pos)) { parser_fail(*pos, "Expected expression after '=='"); return 0; }
bytecode_add_instruction(bc, OP_EQ, 0);
continue;
}
if (*pos + 1 < len && src[*pos] == '!' && src[*pos + 1] == '=') {
*pos += 2;
if (!emit_relational(bc, src, len, pos)) { parser_fail(*pos, "Expected expression after '!='"); return 0; }
bytecode_add_instruction(bc, OP_NEQ, 0);
continue;
}
break;
}
return 1;
}
/* logical AND: equality ( '&&' equality )* */
static int emit_and_expr(Bytecode *bc, const char *src, size_t len, size_t *pos) {
if (!emit_equality(bc, src, len, pos)) return 0;
for (;;) {
skip_spaces(src, len, pos);
if (*pos + 1 < len && src[*pos] == '&' && src[*pos + 1] == '&') {
*pos += 2;
if (!emit_equality(bc, src, len, pos)) { parser_fail(*pos, "Expected expression after '&&'"); return 0; }
bytecode_add_instruction(bc, OP_AND, 0);
continue;
}
break;
}
return 1;
}
/* logical OR: and_expr ( '||' and_expr )* */
static int emit_or_expr(Bytecode *bc, const char *src, size_t len, size_t *pos) {
if (!emit_and_expr(bc, src, len, pos)) return 0;
for (;;) {
skip_spaces(src, len, pos);
if (*pos + 1 < len && src[*pos] == '|' && src[*pos + 1] == '|') {
*pos += 2;
if (!emit_and_expr(bc, src, len, pos)) { parser_fail(*pos, "Expected expression after '||'"); return 0; }
bytecode_add_instruction(bc, OP_OR, 0);
continue;
}
break;
}
return 1;
}
/* top-level expression */
static int emit_expression(Bytecode *bc, const char *src, size_t len, size_t *pos) {
return emit_or_expr(bc, src, len, pos);
}
/*
* Very small compiler:
* - Optionally skips: fun <ident>() { ... } wrapper.
* - Emits PRINT for print(expr) and supports assignments: ident = expr.
* - Supports literals: strings, integers, booleans; identifiers as globals.
* Indentation-aware compiler (2 spaces):
* - Optional shebang and a single fun <ident>() { ... } wrapper.
* - Statements supported: print(expr), ident = expr, and if <expr> with an indented block.
* - Literals: strings, integers, booleans; identifiers are globals.
* - Ends with HALT.
*/
/* line/indent utilities */
static void skip_to_eol(const char *src, size_t len, size_t *pos) {
while (*pos < len && src[*pos] != '\n') (*pos)++;
if (*pos < len && src[*pos] == '\n') (*pos)++;
}
static int read_line_start(const char *src, size_t len, size_t *pos, int *out_indent) {
while (*pos < len) {
size_t p = *pos;
int spaces = 0;
while (p < len && src[p] == ' ') { spaces++; p++; }
if (p < len && src[p] == '\t') {
parser_fail(p, "Tabs are forbidden for indentation");
return 0;
}
if (p >= len) { *pos = p; return 0; }
if (src[p] == '\n') { /* empty line */ p++; *pos = p; continue; }
/* comment-only line? */
if (p + 1 < len && src[p] == '/' && src[p + 1] == '/') {
/* skip entire line */
while (p < len && src[p] != '\n') p++;
if (p < len && src[p] == '\n') p++;
*pos = p;
continue;
}
if (spaces % 2 != 0) {
parser_fail(p, "Indentation must be multiples of two spaces");
return 0;
}
*out_indent = spaces / 2;
*pos = p; /* point to first code char */
return 1;
}
return 0;
}
/* forward decl */
static void parse_block(Bytecode *bc, const char *src, size_t len, size_t *pos, int current_indent);
/* parse and emit a single simple (non-if) statement on the current line */
static void parse_simple_statement(Bytecode *bc, const char *src, size_t len, size_t *pos) {
size_t local_pos = *pos;
char *name = NULL;
if (read_identifier_into(src, len, &local_pos, &name)) {
/* typed declarations: number|string|boolean <ident> (= expr)? */
if (strcmp(name, "number") == 0 || strcmp(name, "string") == 0 || strcmp(name, "boolean") == 0) {
int is_number = (strcmp(name, "number") == 0);
int is_string = (strcmp(name, "string") == 0);
int is_boolean = (strcmp(name, "boolean") == 0);
free(name);
/* read variable name */
char *varname = NULL;
skip_spaces(src, len, &local_pos);
if (!read_identifier_into(src, len, &local_pos, &varname)) {
parser_fail(local_pos, "Expected identifier after type declaration");
return;
}
int gi = sym_index(varname);
free(varname);
skip_spaces(src, len, &local_pos);
if (local_pos < len && src[local_pos] == '=') {
local_pos++; /* '=' */
if (emit_expression(bc, src, len, &local_pos)) {
bytecode_add_instruction(bc, OP_STORE_GLOBAL, gi);
} else {
parser_fail(local_pos, "Expected initializer expression after '='");
return;
}
} else {
/* default initialize if no '=' given */
int ci = -1;
if (is_number || is_boolean) {
ci = bytecode_add_constant(bc, make_int(is_boolean ? 0 : 0));
} else if (is_string) {
ci = bytecode_add_constant(bc, make_string(""));
}
if (ci >= 0) {
bytecode_add_instruction(bc, OP_LOAD_CONST, ci);
bytecode_add_instruction(bc, OP_STORE_GLOBAL, gi);
}
}
*pos = local_pos;
skip_to_eol(src, len, pos);
return;
}
if (strcmp(name, "print") == 0) {
free(name);
skip_spaces(src, len, &local_pos);
(void)consume_char(src, len, &local_pos, '(');
if (emit_expression(bc, src, len, &local_pos)) {
(void)consume_char(src, len, &local_pos, ')');
bytecode_add_instruction(bc, OP_PRINT, 0);
} else {
(void)consume_char(src, len, &local_pos, ')');
}
*pos = local_pos;
skip_to_eol(src, len, pos);
return;
}
/* assignment or simple call */
int gi = sym_index(name);
free(name);
skip_spaces(src, len, &local_pos);
if (local_pos < len && src[local_pos] == '=') {
local_pos++; /* '=' */
if (emit_expression(bc, src, len, &local_pos)) {
bytecode_add_instruction(bc, OP_STORE_GLOBAL, gi);
}
*pos = local_pos;
skip_to_eol(src, len, pos);
return;
} else if (local_pos < len && src[local_pos] == '(') {
/* simple call form (argument ignored/emitted but no CALL yet) */
local_pos++; /* '(' */
(void)emit_expression(bc, src, len, &local_pos);
(void)consume_char(src, len, &local_pos, ')');
*pos = local_pos;
skip_to_eol(src, len, pos);
return;
} else {
/* bare identifier line */
*pos = local_pos;
skip_to_eol(src, len, pos);
return;
}
}
/* fallback: print(expr) without identifier read (unlikely) */
if (starts_with(src, len, *pos, "print")) {
*pos += 5;
skip_spaces(src, len, pos);
(void)consume_char(src, len, pos, '(');
if (emit_expression(bc, src, len, pos)) {
(void)consume_char(src, len, pos, ')');
bytecode_add_instruction(bc, OP_PRINT, 0);
} else {
(void)consume_char(src, len, pos, ')');
}
skip_to_eol(src, len, pos);
return;
}
/* unknown token: report error */
parser_fail(*pos, "Unknown token at start of statement");
}
/* parse a block with lines at indentation >= current_indent; stop at dedent */
static void parse_block(Bytecode *bc, const char *src, size_t len, size_t *pos, int current_indent) {
while (*pos < len) {
if (g_has_error) return;
size_t line_start = *pos;
int indent = 0;
if (!read_line_start(src, len, pos, &indent)) {
/* EOF or error */
return;
}
if (indent < current_indent) {
/* dedent -> let caller handle this line */
*pos = line_start;
return;
}
if (indent > current_indent) {
/* nested block without a header (tolerate by parsing it and continuing) */
parse_block(bc, src, len, pos, indent);
continue;
}
/* at same indent -> parse statement */
if (starts_with(src, len, *pos, "if")) {
*pos += 2;
/* require at least one space before condition if present */
skip_spaces(src, len, pos);
/* condition: a single expression (booleans/ident/number/string) */
if (!emit_expression(bc, src, len, pos)) {
/* no condition -> treat as false */
int ci = bytecode_add_constant(bc, make_int(0));
bytecode_add_instruction(bc, OP_LOAD_CONST, ci);
}
/* end of condition: ignore any trailing until EOL */
skip_to_eol(src, len, pos);
/* conditional jump over the block */
int jmp_false = bytecode_add_instruction(bc, OP_JUMP_IF_FALSE, 0);
/* parse nested block if next line is indented */
size_t after_if = *pos;
int next_indent = 0;
if (read_line_start(src, len, pos, &next_indent)) {
if (next_indent > current_indent) {
/* parse body at increased indent */
parse_block(bc, src, len, pos, next_indent);
} else {
/* empty if-body; keep *pos at start of that line (already set) */
}
} else {
/* EOF -> empty body */
}
/* patch jump to here */
bytecode_set_operand(bc, jmp_false, bc->instr_count);
continue;
}
/* otherwise: simple statement on this line */
parse_simple_statement(bc, src, len, pos);
}
}
static Bytecode *compile_minimal(const char *src, size_t len) {
Bytecode *bc = bytecode_new();
size_t pos = 0;
skip_shebang_if_present(src, len, &pos);
// Optionally consume a single function wrapper: fun <ident>() { ... }
/* Optional: consume a single function wrapper line: fun <ident>() { ... } */
skip_comments(src, len, &pos);
skip_ws(src, len, &pos);
if (starts_with(src, len, pos, "fun")) {
pos += 3;
skip_comments(src, len, &pos);
skip_ws(src, len, &pos);
skip_identifier(src, len, &pos); // function name
skip_identifier(src, len, &pos); /* name */
skip_comments(src, len, &pos);
skip_ws(src, len, &pos);
(void)consume_char(src, len, &pos, '(');
(void)consume_char(src, len, &pos, ')');
skip_comments(src, len, &pos);
skip_ws(src, len, &pos);
(void)consume_char(src, len, &pos, '{'); // tolerate braces
(void)consume_char(src, len, &pos, '{'); /* tolerate */
/* move to next line start after wrapper */
skip_to_eol(src, len, &pos);
}
// Parse simple statements until EOF or closing brace
while (pos < len) {
skip_comments(src, len, &pos);
skip_ws(src, len, &pos);
if (pos < len && src[pos] == '}') { pos++; break; }
if (pos >= len) break;
// read an identifier (for 'print' or assignment), or handle print directly
size_t stmt_start = pos;
char *name = NULL;
if (read_identifier_into(src, len, &pos, &name)) {
// print(expr)
if (strcmp(name, "print") == 0) {
free(name);
skip_spaces(src, len, &pos);
(void)consume_char(src, len, &pos, '(');
if (emit_expression(bc, src, len, &pos)) {
(void)consume_char(src, len, &pos, ')');
bytecode_add_instruction(bc, OP_PRINT, 0);
} else {
// nothing to print; still try to close ')'
(void)consume_char(src, len, &pos, ')');
}
continue;
}
// assignment: ident = expr
int gi = sym_index(name);
free(name);
skip_spaces(src, len, &pos);
if (pos < len && src[pos] == '=') {
pos++; // '='
if (emit_expression(bc, src, len, &pos)) {
bytecode_add_instruction(bc, OP_STORE_GLOBAL, gi);
}
// end of statement (consume to end of line or semicolon-like chars)
} else if (pos < len && src[pos] == '(') {
// Call to non-builtin: not supported yet; consume a simple ()-arg for now
pos++; // '('
// best effort: parse a single expression and drop it
(void)emit_expression(bc, src, len, &pos);
(void)consume_char(src, len, &pos, ')');
// No CALL opcode emitted yet
} else {
// bare identifier line -> ignore for now
}
continue;
}
// 'print' without having read identifier (unlikely) - fallback
if (starts_with(src, len, pos, "print")) {
pos += 5;
skip_spaces(src, len, &pos);
(void)consume_char(src, len, &pos, '(');
if (emit_expression(bc, src, len, &pos)) {
(void)consume_char(src, len, &pos, ')');
bytecode_add_instruction(bc, OP_PRINT, 0);
} else {
(void)consume_char(src, len, &pos, ')');
}
continue;
}
// Unknown/unsupported token: advance to avoid infinite loop
if (stmt_start == pos) pos++;
}
/* parse the top-level block at indent 0 */
parse_block(bc, src, len, &pos, 0);
bytecode_add_instruction(bc, OP_HALT, 0);
return bc;
@ -339,7 +706,22 @@ Bytecode *parse_file_to_bytecode(const char *path) {
fprintf(stderr, "Error: cannot read file: %s\n", path);
return NULL;
}
/* reset error state */
g_has_error = 0;
g_err_pos = 0;
g_err_msg[0] = '\0';
Bytecode *bc = compile_minimal(src, len);
if (g_has_error) {
int line = 1, col = 1;
calc_line_col(src, len, g_err_pos, &line, &col);
fprintf(stderr, "Parse error %s:%d:%d: %s\n", path ? path : "<input>", line, col, g_err_msg);
if (bc) bytecode_free(bc);
free(src);
return NULL;
}
free(src);
return bc;
}