goon

goon

https://git.tonybtw.com/goon.git git://git.tonybtw.com/goon.git

added arrow functions, ranges, and map built-in

Commit
d9406f8ed30c39a5e281619b5c299b38f4d840f8
Parent
95edc99
Author
tonybanters <tonyoutoften@gmail.com>
Date
2026-01-06 07:37:07

Diff

diff --git a/src/goon.c b/src/goon.c
index de279c6..5819317 100644
--- a/src/goon.c
+++ b/src/goon.c
@@ -20,7 +20,9 @@ typedef enum {
     TOK_COLON,
     TOK_QUESTION,
     TOK_SPREAD,
+    TOK_DOTDOT,
     TOK_DOT,
+    TOK_ARROW,
     TOK_INT,
     TOK_STRING,
     TOK_IDENT,
@@ -45,37 +47,113 @@ typedef struct {
     const char *src;
     size_t pos;
     size_t len;
+    size_t line;
+    size_t col;
+    size_t line_start;
+    size_t token_start;
     Token current;
     char *error;
+    size_t error_line;
+    size_t error_col;
 } Lexer;
 
 static void lexer_init(Lexer *lex, const char *src) {
     lex->src = src;
     lex->pos = 0;
     lex->len = strlen(src);
+    lex->line = 1;
+    lex->col = 1;
+    lex->line_start = 0;
+    lex->token_start = 0;
     lex->current.type = TOK_EOF;
     lex->current.data.string = NULL;
     lex->error = NULL;
+    lex->error_line = 0;
+    lex->error_col = 0;
+}
+
+static void lexer_set_error(Lexer *lex, const char *msg) {
+    if (lex->error) free(lex->error);
+    lex->error = strdup(msg);
+    lex->error_line = lex->line;
+    lex->error_col = lex->col;
+}
+
+typedef struct {
+    size_t pos;
+    size_t line;
+    size_t col;
+    size_t line_start;
+    size_t token_start;
+    Token current;
+} Lexer_State;
+
+static void lexer_save(Lexer *lex, Lexer_State *state) {
+    state->pos = lex->pos;
+    state->line = lex->line;
+    state->col = lex->col;
+    state->line_start = lex->line_start;
+    state->token_start = lex->token_start;
+    state->current = lex->current;
+    if (lex->current.type == TOK_STRING || lex->current.type == TOK_IDENT) {
+        state->current.data.string = strdup(lex->current.data.string);
+    }
+}
+
+static void lexer_restore(Lexer *lex, Lexer_State *state) {
+    if (lex->current.type == TOK_STRING || lex->current.type == TOK_IDENT) {
+        free(lex->current.data.string);
+    }
+    lex->pos = state->pos;
+    lex->line = state->line;
+    lex->col = state->col;
+    lex->line_start = state->line_start;
+    lex->token_start = state->token_start;
+    lex->current = state->current;
+}
+
+static void lexer_state_free(Lexer_State *state) {
+    if (state->current.type == TOK_STRING || state->current.type == TOK_IDENT) {
+        free(state->current.data.string);
+        state->current.data.string = NULL;
+    }
+}
+
+static void lexer_advance(Lexer *lex) {
+    if (lex->pos < lex->len) {
+        if (lex->src[lex->pos] == '\n') {
+            lex->line++;
+            lex->col = 1;
+            lex->pos++;
+            lex->line_start = lex->pos;
+        } else {
+            lex->col++;
+            lex->pos++;
+        }
+    }
 }
 
 static void lexer_skip_whitespace(Lexer *lex) {
     while (lex->pos < lex->len) {
         char c = lex->src[lex->pos];
         if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
-            lex->pos++;
+            lexer_advance(lex);
         } else if (c == '/' && lex->pos + 1 < lex->len && lex->src[lex->pos + 1] == '/') {
-            lex->pos += 2;
+            lexer_advance(lex);
+            lexer_advance(lex);
             while (lex->pos < lex->len && lex->src[lex->pos] != '\n') {
-                lex->pos++;
+                lexer_advance(lex);
             }
         } else if (c == '/' && lex->pos + 1 < lex->len && lex->src[lex->pos + 1] == '*') {
-            lex->pos += 2;
+            lexer_advance(lex);
+            lexer_advance(lex);
             while (lex->pos + 1 < lex->len) {
                 if (lex->src[lex->pos] == '*' && lex->src[lex->pos + 1] == '/') {
-                    lex->pos += 2;
+                    lexer_advance(lex);
+                    lexer_advance(lex);
                     break;
                 }
-                lex->pos++;
+                lexer_advance(lex);
             }
         } else {
             break;
@@ -106,6 +184,7 @@ static bool lexer_next(Lexer *lex) {
     }
 
     lexer_skip_whitespace(lex);
+    lex->token_start = lex->pos;
 
     if (lex->pos >= lex->len) {
         lex->current.type = TOK_EOF;
@@ -114,33 +193,48 @@ static bool lexer_next(Lexer *lex) {
 
     char c = lex->src[lex->pos];
 
-    if (c == '{') { lex->current.type = TOK_LBRACE; lex->pos++; return true; }
-    if (c == '}') { lex->current.type = TOK_RBRACE; lex->pos++; return true; }
-    if (c == '[') { lex->current.type = TOK_LBRACKET; lex->pos++; return true; }
-    if (c == ']') { lex->current.type = TOK_RBRACKET; lex->pos++; return true; }
-    if (c == '(') { lex->current.type = TOK_LPAREN; lex->pos++; return true; }
-    if (c == ')') { lex->current.type = TOK_RPAREN; lex->pos++; return true; }
-    if (c == ';') { lex->current.type = TOK_SEMICOLON; lex->pos++; return true; }
-    if (c == ',') { lex->current.type = TOK_COMMA; lex->pos++; return true; }
-    if (c == '=') { lex->current.type = TOK_EQUALS; lex->pos++; return true; }
-    if (c == ':') { lex->current.type = TOK_COLON; lex->pos++; return true; }
-    if (c == '?') { lex->current.type = TOK_QUESTION; lex->pos++; return true; }
+    if (c == '{') { lex->current.type = TOK_LBRACE; lexer_advance(lex); return true; }
+    if (c == '}') { lex->current.type = TOK_RBRACE; lexer_advance(lex); return true; }
+    if (c == '[') { lex->current.type = TOK_LBRACKET; lexer_advance(lex); return true; }
+    if (c == ']') { lex->current.type = TOK_RBRACKET; lexer_advance(lex); return true; }
+    if (c == '(') { lex->current.type = TOK_LPAREN; lexer_advance(lex); return true; }
+    if (c == ')') { lex->current.type = TOK_RPAREN; lexer_advance(lex); return true; }
+    if (c == ';') { lex->current.type = TOK_SEMICOLON; lexer_advance(lex); return true; }
+    if (c == ',') { lex->current.type = TOK_COMMA; lexer_advance(lex); return true; }
+    if (c == '=' && lex->pos + 1 < lex->len && lex->src[lex->pos + 1] == '>') {
+        lex->current.type = TOK_ARROW;
+        lexer_advance(lex);
+        lexer_advance(lex);
+        return true;
+    }
+    if (c == '=') { lex->current.type = TOK_EQUALS; lexer_advance(lex); return true; }
+    if (c == ':') { lex->current.type = TOK_COLON; lexer_advance(lex); return true; }
+    if (c == '?') { lex->current.type = TOK_QUESTION; lexer_advance(lex); return true; }
 
     if (c == '.' && lex->pos + 2 < lex->len &&
         lex->src[lex->pos + 1] == '.' && lex->src[lex->pos + 2] == '.') {
         lex->current.type = TOK_SPREAD;
-        lex->pos += 3;
+        lexer_advance(lex);
+        lexer_advance(lex);
+        lexer_advance(lex);
+        return true;
+    }
+
+    if (c == '.' && lex->pos + 1 < lex->len && lex->src[lex->pos + 1] == '.') {
+        lex->current.type = TOK_DOTDOT;
+        lexer_advance(lex);
+        lexer_advance(lex);
         return true;
     }
 
     if (c == '.') {
         lex->current.type = TOK_DOT;
-        lex->pos++;
+        lexer_advance(lex);
         return true;
     }
 
     if (c == '"') {
-        lex->pos++;
+        lexer_advance(lex);
         size_t buf_size = 256;
         size_t buf_len = 0;
         char *buf = malloc(buf_size);
@@ -149,7 +243,7 @@ static bool lexer_next(Lexer *lex) {
         while (lex->pos < lex->len && lex->src[lex->pos] != '"') {
             char ch = lex->src[lex->pos];
             if (ch == '\\' && lex->pos + 1 < lex->len) {
-                lex->pos++;
+                lexer_advance(lex);
                 ch = lex->src[lex->pos];
                 switch (ch) {
                     case 'n': ch = '\n'; break;
@@ -167,16 +261,16 @@ static bool lexer_next(Lexer *lex) {
                 if (!buf) return false;
             }
             buf[buf_len++] = ch;
-            lex->pos++;
+            lexer_advance(lex);
         }
 
         if (lex->pos >= lex->len) {
             free(buf);
-            lex->error = strdup("unterminated string");
+            lexer_set_error(lex, "unterminated string");
             return false;
         }
 
-        lex->pos++;
+        lexer_advance(lex);
         buf[buf_len] = '\0';
         lex->current.type = TOK_STRING;
         lex->current.data.string = buf;
@@ -187,12 +281,12 @@ static bool lexer_next(Lexer *lex) {
         int sign = 1;
         if (c == '-') {
             sign = -1;
-            lex->pos++;
+            lexer_advance(lex);
         }
         int64_t val = 0;
         while (lex->pos < lex->len && isdigit(lex->src[lex->pos])) {
             val = val * 10 + (lex->src[lex->pos] - '0');
-            lex->pos++;
+            lexer_advance(lex);
         }
         lex->current.type = TOK_INT;
         lex->current.data.integer = val * sign;
@@ -202,7 +296,7 @@ static bool lexer_next(Lexer *lex) {
     if (is_ident_start(c)) {
         size_t start = lex->pos;
         while (lex->pos < lex->len && is_ident_char(lex->src[lex->pos])) {
-            lex->pos++;
+            lexer_advance(lex);
         }
         char *ident = strdup_range(lex->src + start, lex->pos - start);
 
@@ -247,7 +341,7 @@ static bool lexer_next(Lexer *lex) {
         return true;
     }
 
-    lex->error = strdup("unexpected character");
+    lexer_set_error(lex, "unexpected character");
     return false;
 }
 
@@ -319,6 +413,21 @@ Goon_Value *goon_record(Goon_Ctx *ctx) {
     return val;
 }
 
+static Goon_Value *goon_lambda(Goon_Ctx *ctx, char **params, size_t param_count, const char *body, Goon_Binding *env) {
+    Goon_Value *val = alloc_value(ctx);
+    if (!val) return NULL;
+    val->type = GOON_LAMBDA;
+    val->data.lambda.params = malloc(param_count * sizeof(char *));
+    if (!val->data.lambda.params && param_count > 0) return NULL;
+    for (size_t i = 0; i < param_count; i++) {
+        val->data.lambda.params[i] = strdup(params[i]);
+    }
+    val->data.lambda.param_count = param_count;
+    val->data.lambda.body = strdup(body);
+    val->data.lambda.env = env;
+    return val;
+}
+
 bool goon_is_nil(Goon_Value *val) {
     return val == NULL || val->type == GOON_NIL;
 }
@@ -456,6 +565,62 @@ typedef struct {
 
 static Goon_Value *parse_expr(Parser *p);
 
+static Goon_Value *call_lambda(Goon_Ctx *ctx, Goon_Value *fn, Goon_Value **args, size_t argc) {
+    if (!fn || fn->type != GOON_LAMBDA) return goon_nil(ctx);
+    if (argc != fn->data.lambda.param_count) return goon_nil(ctx);
+
+    Goon_Binding *old_env = ctx->env;
+    ctx->env = fn->data.lambda.env;
+
+    for (size_t i = 0; i < argc; i++) {
+        define(ctx, fn->data.lambda.params[i], args[i]);
+    }
+
+    Lexer body_lex;
+    lexer_init(&body_lex, fn->data.lambda.body);
+    Parser body_parser;
+    body_parser.ctx = ctx;
+    body_parser.lex = &body_lex;
+
+    if (!lexer_next(&body_lex)) {
+        ctx->env = old_env;
+        return goon_nil(ctx);
+    }
+
+    Goon_Value *result = parse_expr(&body_parser);
+
+    ctx->env = old_env;
+    return result ? result : goon_nil(ctx);
+}
+
+static Goon_Value *builtin_map(Goon_Ctx *ctx, Goon_Value **args, size_t argc) {
+    if (argc != 2) return goon_nil(ctx);
+    Goon_Value *list = args[0];
+    Goon_Value *fn = args[1];
+
+    if (!list || list->type != GOON_LIST) return goon_nil(ctx);
+    if (!fn || (fn->type != GOON_LAMBDA && fn->type != GOON_BUILTIN)) return goon_nil(ctx);
+
+    Goon_Value *result = goon_list(ctx);
+
+    for (size_t i = 0; i < list->data.list.len; i++) {
+        Goon_Value *item = list->data.list.items[i];
+        Goon_Value *mapped;
+
+        if (fn->type == GOON_LAMBDA) {
+            Goon_Value *fn_args[1] = { item };
+            mapped = call_lambda(ctx, fn, fn_args, 1);
+        } else {
+            Goon_Value *fn_args[1] = { item };
+            mapped = fn->data.builtin(ctx, fn_args, 1);
+        }
+
+        goon_list_push(ctx, result, mapped);
+    }
+
+    return result;
+}
+
 static Goon_Value *interpolate_string(Goon_Ctx *ctx, const char *str) {
     size_t len = strlen(str);
     size_t buf_size = len * 2 + 1;
@@ -540,7 +705,7 @@ static Goon_Value *parse_record(Parser *p) {
         }
 
         if (p->lex->current.type != TOK_IDENT) {
-            p->lex->error = strdup("expected field name");
+            lexer_set_error(p->lex, "expected field name");
             return NULL;
         }
 
@@ -553,7 +718,7 @@ static Goon_Value *parse_record(Parser *p) {
         }
 
         if (p->lex->current.type != TOK_EQUALS) {
-            p->lex->error = strdup("expected = after field name");
+            lexer_set_error(p->lex, "expected = after field name");
             free(key);
             return NULL;
         }
@@ -574,7 +739,7 @@ static Goon_Value *parse_record(Parser *p) {
     }
 
     if (p->lex->current.type != TOK_RBRACE) {
-        p->lex->error = strdup("expected }");
+        lexer_set_error(p->lex, "expected }");
         return NULL;
     }
 
@@ -596,6 +761,25 @@ static Goon_Value *parse_list(Parser *p) {
                     goon_list_push(p->ctx, list, spread_val->data.list.items[i]);
                 }
             }
+        } else if (p->lex->current.type == TOK_INT) {
+            int64_t start = p->lex->current.data.integer;
+            if (!lexer_next(p->lex)) return NULL;
+
+            if (p->lex->current.type == TOK_DOTDOT) {
+                if (!lexer_next(p->lex)) return NULL;
+                if (p->lex->current.type != TOK_INT) {
+                    lexer_set_error(p->lex, "expected integer after ..");
+                    return NULL;
+                }
+                int64_t end = p->lex->current.data.integer;
+                if (!lexer_next(p->lex)) return NULL;
+
+                for (int64_t i = start; i <= end; i++) {
+                    goon_list_push(p->ctx, list, goon_int(p->ctx, i));
+                }
+            } else {
+                goon_list_push(p->ctx, list, goon_int(p->ctx, start));
+            }
         } else {
             Goon_Value *item = parse_expr(p);
             if (!item) return NULL;
@@ -608,7 +792,7 @@ static Goon_Value *parse_list(Parser *p) {
     }
 
     if (p->lex->current.type != TOK_RBRACKET) {
-        p->lex->error = strdup("expected ]");
+        lexer_set_error(p->lex, "expected ]");
         return NULL;
     }
 
@@ -620,14 +804,14 @@ static Goon_Value *parse_import(Parser *p) {
     if (!lexer_next(p->lex)) return NULL;
 
     if (p->lex->current.type != TOK_LPAREN) {
-        p->lex->error = strdup("expected ( after import");
+        lexer_set_error(p->lex,"expected ( after import");
         return NULL;
     }
 
     if (!lexer_next(p->lex)) return NULL;
 
     if (p->lex->current.type != TOK_STRING) {
-        p->lex->error = strdup("expected string path in import");
+        lexer_set_error(p->lex,"expected string path in import");
         return NULL;
     }
 
@@ -635,7 +819,7 @@ static Goon_Value *parse_import(Parser *p) {
     if (!lexer_next(p->lex)) { free(path); return NULL; }
 
     if (p->lex->current.type != TOK_RPAREN) {
-        p->lex->error = strdup("expected ) after import path");
+        lexer_set_error(p->lex,"expected ) after import path");
         free(path);
         return NULL;
     }
@@ -666,7 +850,7 @@ static Goon_Value *parse_import(Parser *p) {
 
     FILE *f = fopen(full_path, "r");
     if (!f) {
-        p->lex->error = strdup("could not open import file");
+        lexer_set_error(p->lex,"could not open import file");
         return NULL;
     }
 
@@ -729,7 +913,7 @@ static Goon_Value *parse_call(Parser *p, const char *name) {
     }
 
     if (p->lex->current.type != TOK_RPAREN) {
-        p->lex->error = strdup("expected )");
+        lexer_set_error(p->lex,"expected )");
         return NULL;
     }
 
@@ -739,6 +923,36 @@ static Goon_Value *parse_call(Parser *p, const char *name) {
         return fn->data.builtin(p->ctx, args, argc);
     }
 
+    if (fn && fn->type == GOON_LAMBDA) {
+        if (argc != fn->data.lambda.param_count) {
+            lexer_set_error(p->lex, "wrong number of arguments");
+            return NULL;
+        }
+
+        Goon_Binding *old_env = p->ctx->env;
+        p->ctx->env = fn->data.lambda.env;
+
+        for (size_t i = 0; i < argc; i++) {
+            define(p->ctx, fn->data.lambda.params[i], args[i]);
+        }
+
+        Lexer body_lex;
+        lexer_init(&body_lex, fn->data.lambda.body);
+        Parser body_parser;
+        body_parser.ctx = p->ctx;
+        body_parser.lex = &body_lex;
+
+        if (!lexer_next(&body_lex)) {
+            p->ctx->env = old_env;
+            return NULL;
+        }
+
+        Goon_Value *result = parse_expr(&body_parser);
+
+        p->ctx->env = old_env;
+        return result ? result : goon_nil(p->ctx);
+    }
+
     return goon_nil(p->ctx);
 }
 
@@ -787,7 +1001,7 @@ static Goon_Value *parse_primary(Parser *p) {
                 while (p->lex->current.type == TOK_DOT) {
                     if (!lexer_next(p->lex)) return NULL;
                     if (p->lex->current.type != TOK_IDENT) {
-                        p->lex->error = strdup("expected field name after .");
+                        lexer_set_error(p->lex,"expected field name after .");
                         return NULL;
                     }
                     char *field = p->lex->current.data.string;
@@ -812,14 +1026,85 @@ static Goon_Value *parse_primary(Parser *p) {
             return parse_import(p);
 
         case TOK_LPAREN: {
-            if (!lexer_next(p->lex)) return NULL;
-            Goon_Value *val = parse_expr(p);
-            if (p->lex->current.type != TOK_RPAREN) {
-                p->lex->error = strdup("expected )");
-                return NULL;
+            Lexer_State saved;
+            lexer_save(p->lex, &saved);
+
+            if (!lexer_next(p->lex)) { lexer_state_free(&saved); return NULL; }
+
+            char *params[16];
+            size_t param_count = 0;
+            bool is_lambda = true;
+
+            if (p->lex->current.type == TOK_RPAREN) {
+                if (!lexer_next(p->lex)) { lexer_state_free(&saved); return NULL; }
+                is_lambda = (p->lex->current.type == TOK_ARROW);
+            } else if (p->lex->current.type == TOK_IDENT) {
+                while (is_lambda && param_count < 16) {
+                    if (p->lex->current.type != TOK_IDENT) {
+                        is_lambda = false;
+                        break;
+                    }
+                    params[param_count++] = strdup(p->lex->current.data.string);
+                    if (!lexer_next(p->lex)) {
+                        for (size_t i = 0; i < param_count; i++) free(params[i]);
+                        lexer_state_free(&saved);
+                        return NULL;
+                    }
+                    if (p->lex->current.type == TOK_COMMA) {
+                        if (!lexer_next(p->lex)) {
+                            for (size_t i = 0; i < param_count; i++) free(params[i]);
+                            lexer_state_free(&saved);
+                            return NULL;
+                        }
+                    } else if (p->lex->current.type == TOK_RPAREN) {
+                        if (!lexer_next(p->lex)) {
+                            for (size_t i = 0; i < param_count; i++) free(params[i]);
+                            lexer_state_free(&saved);
+                            return NULL;
+                        }
+                        is_lambda = (p->lex->current.type == TOK_ARROW);
+                        break;
+                    } else {
+                        is_lambda = false;
+                        break;
+                    }
+                }
+            } else {
+                is_lambda = false;
+            }
+
+            if (is_lambda) {
+                lexer_state_free(&saved);
+                if (!lexer_next(p->lex)) {
+                    for (size_t i = 0; i < param_count; i++) free(params[i]);
+                    return NULL;
+                }
+                size_t body_start = p->lex->token_start;
+                Goon_Value *body_val = parse_expr(p);
+                if (!body_val) {
+                    for (size_t i = 0; i < param_count; i++) free(params[i]);
+                    return NULL;
+                }
+                size_t body_end = p->lex->token_start;
+                char *body_src = strdup_range(p->lex->src + body_start, body_end - body_start);
+
+                Goon_Value *lambda = goon_lambda(p->ctx, params, param_count, body_src, p->ctx->env);
+                free(body_src);
+                for (size_t i = 0; i < param_count; i++) free(params[i]);
+                return lambda;
+            } else {
+                for (size_t i = 0; i < param_count; i++) free(params[i]);
+                lexer_restore(p->lex, &saved);
+
+                if (!lexer_next(p->lex)) return NULL;
+                Goon_Value *val = parse_expr(p);
+                if (p->lex->current.type != TOK_RPAREN) {
+                    lexer_set_error(p->lex, "expected )");
+                    return NULL;
+                }
+                if (!lexer_next(p->lex)) return NULL;
+                return val;
             }
-            if (!lexer_next(p->lex)) return NULL;
-            return val;
         }
 
         default:
@@ -832,7 +1117,7 @@ static Goon_Value *parse_expr(Parser *p) {
         if (!lexer_next(p->lex)) return NULL;
 
         if (p->lex->current.type != TOK_IDENT) {
-            p->lex->error = strdup("expected identifier after let");
+            lexer_set_error(p->lex,"expected identifier after let");
             return NULL;
         }
 
@@ -845,7 +1130,7 @@ static Goon_Value *parse_expr(Parser *p) {
         }
 
         if (p->lex->current.type != TOK_EQUALS) {
-            p->lex->error = strdup("expected = in let binding");
+            lexer_set_error(p->lex,"expected = in let binding");
             free(name);
             return NULL;
         }
@@ -858,9 +1143,11 @@ static Goon_Value *parse_expr(Parser *p) {
         define(p->ctx, name, value);
         free(name);
 
-        if (p->lex->current.type == TOK_SEMICOLON) {
-            if (!lexer_next(p->lex)) return NULL;
+        if (p->lex->current.type != TOK_SEMICOLON) {
+            lexer_set_error(p->lex, "expected ; after let binding");
+            return NULL;
         }
+        if (!lexer_next(p->lex)) return NULL;
 
         return value;
     }
@@ -872,7 +1159,7 @@ static Goon_Value *parse_expr(Parser *p) {
         if (!cond) return NULL;
 
         if (p->lex->current.type != TOK_THEN) {
-            p->lex->error = strdup("expected 'then' after if condition");
+            lexer_set_error(p->lex,"expected 'then' after if condition");
             return NULL;
         }
 
@@ -882,7 +1169,7 @@ static Goon_Value *parse_expr(Parser *p) {
         if (!then_val) return NULL;
 
         if (p->lex->current.type != TOK_ELSE) {
-            p->lex->error = strdup("expected 'else' after then branch");
+            lexer_set_error(p->lex,"expected 'else' after then branch");
             return NULL;
         }
 
@@ -904,7 +1191,7 @@ static Goon_Value *parse_expr(Parser *p) {
         if (!then_val) return NULL;
 
         if (p->lex->current.type != TOK_COLON) {
-            p->lex->error = strdup("expected : in ternary");
+            lexer_set_error(p->lex,"expected : in ternary");
             return NULL;
         }
 
@@ -919,15 +1206,37 @@ static Goon_Value *parse_expr(Parser *p) {
     return val;
 }
 
+static void clear_error(Goon_Ctx *ctx) {
+    if (ctx->error.message) { free(ctx->error.message); ctx->error.message = NULL; }
+    if (ctx->error.file) { free(ctx->error.file); ctx->error.file = NULL; }
+    if (ctx->error.source_line) { free(ctx->error.source_line); ctx->error.source_line = NULL; }
+    ctx->error.line = 0;
+    ctx->error.col = 0;
+}
+
+static char *get_source_line(const char *src, size_t line_start) {
+    const char *start = src + line_start;
+    const char *end = start;
+    while (*end && *end != '\n') end++;
+    return strdup_range(start, end - start);
+}
+
 Goon_Ctx *goon_create(void) {
     Goon_Ctx *ctx = malloc(sizeof(Goon_Ctx));
     if (!ctx) return NULL;
     ctx->env = NULL;
     ctx->values = NULL;
     ctx->fields = NULL;
-    ctx->error = NULL;
+    ctx->error.message = NULL;
+    ctx->error.file = NULL;
+    ctx->error.line = 0;
+    ctx->error.col = 0;
+    ctx->error.source_line = NULL;
     ctx->base_path = NULL;
     ctx->userdata = NULL;
+
+    goon_register(ctx, "map", builtin_map);
+
     return ctx;
 }
 
@@ -949,6 +1258,16 @@ void goon_destroy(Goon_Ctx *ctx) {
             free(v->data.string);
         } else if (v->type == GOON_LIST && v->data.list.items) {
             free(v->data.list.items);
+        } else if (v->type == GOON_LAMBDA) {
+            if (v->data.lambda.params) {
+                for (size_t i = 0; i < v->data.lambda.param_count; i++) {
+                    free(v->data.lambda.params[i]);
+                }
+                free(v->data.lambda.params);
+            }
+            if (v->data.lambda.body) {
+                free(v->data.lambda.body);
+            }
         }
         free(v);
         v = next;
@@ -962,7 +1281,7 @@ void goon_destroy(Goon_Ctx *ctx) {
         f = next;
     }
 
-    if (ctx->error) free(ctx->error);
+    clear_error(ctx);
     if (ctx->base_path) free(ctx->base_path);
     free(ctx);
 }
@@ -985,6 +1304,30 @@ void goon_register(Goon_Ctx *ctx, const char *name, Goon_Builtin_Fn fn) {
 
 static Goon_Value *last_result = NULL;
 
+static void set_error_from_lexer(Goon_Ctx *ctx, Lexer *lex, const char *source) {
+    clear_error(ctx);
+    if (lex->error) {
+        ctx->error.message = lex->error;
+        lex->error = NULL;
+    }
+    ctx->error.line = lex->error_line > 0 ? lex->error_line : lex->line;
+    ctx->error.col = lex->error_col > 0 ? lex->error_col : lex->col;
+    if (ctx->base_path) {
+        ctx->error.file = strdup(ctx->base_path);
+    }
+    size_t line_start = lex->line_start;
+    if (lex->error_line > 0 && lex->error_line < lex->line) {
+        const char *p = source;
+        size_t cur_line = 1;
+        while (*p && cur_line < lex->error_line) {
+            if (*p == '\n') cur_line++;
+            p++;
+        }
+        line_start = p - source;
+    }
+    ctx->error.source_line = get_source_line(source, line_start);
+}
+
 bool goon_load_string(Goon_Ctx *ctx, const char *source) {
     Lexer lex;
     lexer_init(&lex, source);
@@ -994,8 +1337,7 @@ bool goon_load_string(Goon_Ctx *ctx, const char *source) {
     parser.lex = &lex;
 
     if (!lexer_next(&lex)) {
-        if (ctx->error) free(ctx->error);
-        ctx->error = lex.error;
+        set_error_from_lexer(ctx, &lex, source);
         return false;
     }
 
@@ -1004,10 +1346,7 @@ bool goon_load_string(Goon_Ctx *ctx, const char *source) {
     while (lex.current.type != TOK_EOF) {
         Goon_Value *expr = parse_expr(&parser);
         if (!expr) {
-            if (lex.error) {
-                if (ctx->error) free(ctx->error);
-                ctx->error = lex.error;
-            }
+            set_error_from_lexer(ctx, &lex, source);
             return false;
         }
         last_result = expr;
@@ -1019,8 +1358,9 @@ bool goon_load_string(Goon_Ctx *ctx, const char *source) {
 bool goon_load_file(Goon_Ctx *ctx, const char *path) {
     FILE *f = fopen(path, "r");
     if (!f) {
-        if (ctx->error) free(ctx->error);
-        ctx->error = strdup("could not open file");
+        clear_error(ctx);
+        ctx->error.message = strdup("could not open file");
+        ctx->error.file = strdup(path);
         return false;
     }
 
@@ -1031,8 +1371,8 @@ bool goon_load_file(Goon_Ctx *ctx, const char *path) {
     char *source = malloc(size + 1);
     if (!source) {
         fclose(f);
-        if (ctx->error) free(ctx->error);
-        ctx->error = strdup("out of memory");
+        clear_error(ctx);
+        ctx->error.message = strdup("out of memory");
         return false;
     }
 
@@ -1048,7 +1388,34 @@ bool goon_load_file(Goon_Ctx *ctx, const char *path) {
 }
 
 const char *goon_get_error(Goon_Ctx *ctx) {
-    return ctx->error;
+    return ctx->error.message;
+}
+
+const Goon_Error *goon_get_error_info(Goon_Ctx *ctx) {
+    if (!ctx->error.message) return NULL;
+    return &ctx->error;
+}
+
+void goon_error_print(const Goon_Error *err) {
+    if (!err || !err->message) return;
+
+    fprintf(stderr, "error: %s\n", err->message);
+
+    if (err->file && err->line > 0) {
+        fprintf(stderr, "  --> %s:%zu:%zu\n", err->file, err->line, err->col);
+    } else if (err->line > 0) {
+        fprintf(stderr, "  --> %zu:%zu\n", err->line, err->col);
+    }
+
+    if (err->source_line) {
+        fprintf(stderr, "   |\n");
+        fprintf(stderr, "%3zu| %s\n", err->line, err->source_line);
+        fprintf(stderr, "   |");
+        for (size_t i = 0; i < err->col; i++) {
+            fprintf(stderr, " ");
+        }
+        fprintf(stderr, "^\n");
+    }
 }
 
 Goon_Value *goon_eval_result(Goon_Ctx *ctx) {
diff --git a/src/goon.h b/src/goon.h
index 016db8a..c3bc179 100644
--- a/src/goon.h
+++ b/src/goon.h
@@ -15,11 +15,13 @@ typedef enum {
     GOON_LIST,
     GOON_RECORD,
     GOON_BUILTIN,
+    GOON_LAMBDA,
 } Goon_Type;
 
 typedef struct Goon_Value Goon_Value;
 typedef struct Goon_Ctx Goon_Ctx;
 typedef struct Goon_Record_Field Goon_Record_Field;
+typedef struct Goon_Binding Goon_Binding;
 
 typedef Goon_Value *(*Goon_Builtin_Fn)(Goon_Ctx *ctx, Goon_Value **args, size_t argc);
 
@@ -45,6 +47,12 @@ struct Goon_Value {
             Goon_Record_Field *fields;
         } record;
         Goon_Builtin_Fn builtin;
+        struct {
+            char **params;
+            size_t param_count;
+            char *body;
+            Goon_Binding *env;
+        } lambda;
     } data;
 };
 
@@ -54,11 +62,19 @@ typedef struct Goon_Binding {
     struct Goon_Binding *next;
 } Goon_Binding;
 
+typedef struct {
+    char *message;
+    char *file;
+    size_t line;
+    size_t col;
+    char *source_line;
+} Goon_Error;
+
 struct Goon_Ctx {
     Goon_Binding *env;
     Goon_Value *values;
     Goon_Record_Field *fields;
-    char *error;
+    Goon_Error error;
     char *base_path;
     void *userdata;
 };
@@ -75,6 +91,8 @@ bool goon_load_file(Goon_Ctx *ctx, const char *path);
 bool goon_load_string(Goon_Ctx *ctx, const char *source);
 
 const char *goon_get_error(Goon_Ctx *ctx);
+const Goon_Error *goon_get_error_info(Goon_Ctx *ctx);
+void goon_error_print(const Goon_Error *err);
 
 Goon_Value *goon_nil(Goon_Ctx *ctx);
 Goon_Value *goon_bool(Goon_Ctx *ctx, bool val);
diff --git a/src/main.c b/src/main.c
index 0729d18..46a3970 100644
--- a/src/main.c
+++ b/src/main.c
@@ -28,8 +28,12 @@ static int cmd_eval(const char *path, bool pretty) {
     }
 
     if (!goon_load_file(ctx, path)) {
-        const char *err = goon_get_error(ctx);
-        fprintf(stderr, "error: %s\n", err ? err : "unknown error");
+        const Goon_Error *err = goon_get_error_info(ctx);
+        if (err) {
+            goon_error_print(err);
+        } else {
+            fprintf(stderr, "error: unknown error\n");
+        }
         goon_destroy(ctx);
         return 1;
     }
@@ -53,8 +57,12 @@ static int cmd_check(const char *path) {
     }
 
     if (!goon_load_file(ctx, path)) {
-        const char *err = goon_get_error(ctx);
-        fprintf(stderr, "error: %s\n", err ? err : "unknown error");
+        const Goon_Error *err = goon_get_error_info(ctx);
+        if (err) {
+            goon_error_print(err);
+        } else {
+            fprintf(stderr, "error: unknown error\n");
+        }
         goon_destroy(ctx);
         return 1;
     }