I had a cool idea for method call semantics. voila, custom methods!
authorcassowarii <cassowary@cassowary.me>
Sun, 12 Jul 2026 20:33:00 +0000 (13:33 -0700)
committercassowarii <cassowary@cassowary.me>
Sun, 12 Jul 2026 20:33:00 +0000 (13:33 -0700)
14 files changed:
sample/project_euler/pe03.sa
sample/project_euler/pe04.sa
sample/project_euler/pe06.sa
src/compile/analyze.c
src/compile/ir.c
src/lib/lib.c
src/lib/module.h
src/lib/module/global.c
src/lib/module/string.c [new file with mode: 0644]
src/parse/ast.h
src/parse/lexer.c
src/parse/parser.c
src/vm/exec.c
src/vm/exec.h

index 2c4c002..295bdfc 100644 (file)
@@ -20,7 +20,7 @@ def factors num {
 }
 
 def prime? num {
-  factors(num).length == 0
+  factors(num).length() == 0
 }
 
 println max_positive factors(600851475143).filter => num { prime? num }
index f9f8418..48bd320 100644 (file)
@@ -1,5 +1,6 @@
 def is_palindrome num {
-  num.to_string == num.to_string.split.reverse.join
+  let string_num = num | string::from
+  string_num == string_num.split().reverse().join()
 }
 
 let max = 0
index 51aad0e..8e2715c 100644 (file)
@@ -10,12 +10,8 @@ def square num {
   num * num
 }
 
-def map f {
-  => l { l.map(f) }
-}
-
 def sum_squares list {
-  list.map(square) | sum
+  list | _.map(square) | sum
 }
 
 def square_sum list {
index a83a507..7eac78a 100644 (file)
@@ -20,6 +20,7 @@ i32 sbAst_count_pipe_underscores(sbAst node) {
     case AST_NODE_NEXT:
     case AST_NODE_MULTIVAL:
     case AST_NODE_FUNCCALL:
+    case AST_NODE_DOT:
     case AST_NODE_HASHENTRY:
     case AST_NODE_WHILE:
     case AST_NODE_REPEAT:
index 31e96a8..88807fe 100644 (file)
@@ -903,7 +903,7 @@ static sbIrVariable *compile_ast_var(hIrChunk ck, sbAst node) {
   }
 }
 
-static sbIrExpr *compile_ast_list(hIrChunk ck, sbAst node) {
+static sbIrExpr *compile_ast_list_after(hIrChunk ck, sbAst node, sbIrExpr *eol) {
   sbAst considering = node;
   sbIrExpr *list = IR_EMPTY_LIST;
   sbIrExpr **place_here = &list;
@@ -914,9 +914,16 @@ static sbIrExpr *compile_ast_list(hIrChunk ck, sbAst node) {
     place_here = &(*place_here)->list.next;
     considering = considering->seq.left;
   }
+
+  *place_here = eol;
+
   return list;
 }
 
+static sbIrExpr *compile_ast_list(hIrChunk ck, sbAst node) {
+  return compile_ast_list_after(ck, node, IR_EMPTY_LIST);
+}
+
 static sbIrExpr *compile_ast_hash(hIrChunk ck, sbAst node) {
   sbAst considering = node;
   sbIrExpr *list = NULL;
@@ -1006,13 +1013,22 @@ static sbIrExpr *compile_ast_expr(hIrChunk ck, sbAst node, flag list_context) {
   if (node == NO_NODE) return NULL;
 
   if (node->type == AST_NODE_FUNCCALL) {
-    sbIrExpr *called = compile_ast_expr(ck, node->seq.left, FALSE);
-    sbIrExpr *params = compile_ast_list(ck, node->seq.right);
-    return expr_call(ck, called, params);
-  } else if (node->type == AST_NODE_METHODCALL) {
+    if (node->seq.left->type == AST_NODE_DOT) {
+      /* For a.b(c,d,e), we can optimize a bit to avoid allocating closures
+       * for built-in types that get methods called on them */
+      sbIrExpr *called = compile_ast_expr(ck, node->seq.left->seq.left, FALSE);
+      sbIrExpr *method = expr_list(ck, compile_ast_expr(ck, node->seq.left->seq.right, FALSE));
+      sbIrExpr *params = compile_ast_list_after(ck, node->seq.right, method);
+      return expr_send(ck, called, params);
+    } else {
+      sbIrExpr *called = compile_ast_expr(ck, node->seq.left, FALSE);
+      sbIrExpr *params = compile_ast_list(ck, node->seq.right);
+      return expr_call(ck, called, params);
+    }
+  } else if (node->type == AST_NODE_DOT) {
     sbIrExpr *target = compile_ast_expr(ck, node->seq.left, FALSE);
-    sbIrExpr *message = compile_ast_list(ck, node->seq.right);
-    return expr_send(ck, target, message);
+    sbIrExpr *param = expr_list(ck, compile_ast_expr(ck, node->seq.right, FALSE));
+    return expr_call(ck, target, param);
   } else if (node->type == AST_VAL_FUNC) {
     sbIrChunk *func = compile_ast_function(ck->program, node->seq.left, node->seq.right);
     return expr_func(ck, func);
index 23e0f48..2f293d2 100644 (file)
@@ -7,6 +7,8 @@
 #include "lib/method.h"
 #include "lib/module.h"
 
+sbCFuncStatus please_call_again(hVm vm, flag init);
+
 void sbLib_sys_init() {
   sbLib_create_sentinels();
 
@@ -38,6 +40,7 @@ void sbLib_resolve_method(hVm vm) {
     /* TODO this may become not true */
     PANIC("method name must be symbol!");
   }
+  hSymbol method_sym = method_name_val->symbol;
 
   /* subtract 1 because the method name is itself a param */
   usize num_params = argc->integer - 1;
@@ -55,15 +58,32 @@ void sbLib_resolve_method(hVm vm) {
     case IT_FLOAT:
       table_to_use = &g_float_methods;
       break;
-    default:
-      PANIC("Have not implemented this method table yet! (%lld)", (long long)target->type);
   }
 
-  sbLibMethod f = sbLibTable_find_method(table_to_use, method_name_val->symbol);
-  if (f) {
-    f(vm, target, num_params);
+  if (table_to_use == NULL) {
+    /* did not find built in method table */
+    if (target->type <= 0) {
+      /* for built in type, just fail, it's not done yet */
+      PANIC("Have not implemented this method table yet! (%lld)", (long long)target->type);
+    } else {
+      /* for a user defined type, try to retrieve its method: we'll need to jump back into VM for this */
+      /* convert stack position ...params :methodname (argc+1) target --> ...params argc :methodname 1 target,
+       * and then call twice */
+      sbVm_push_immediate(vm, &HVINT(num_params));
+      sbVm_push_immediate(vm, &HVSYM(method_sym));
+      sbVm_push_immediate(vm, target); /* if target points to xstack, need to push before it gets overwritten */
+      sbVm_push_immediate(vm, &HVINT(1));
+      sbVm_swap(vm); /* now put target on top of stack */
+      sbVm_call_c_func(vm, please_call_again);
+    }
   } else {
-    PANIC("invalid method name for type %lld: %s", (long long)target->type, sbSymbol_name(method_name_val->symbol));
+    /* built in type that has a method table defined: find method */
+    sbLibMethod f = sbLibTable_find_method(table_to_use, method_name_val->symbol);
+    if (f) {
+      f(vm, target, num_params);
+    } else {
+      PANIC("invalid method name for type %lld: %s", (long long)target->type, sbSymbol_name(method_name_val->symbol));
+    }
   }
 }
 
@@ -79,3 +99,29 @@ void sbLib_resolve_global(hVm vm) {
     PANIC("name '%s' is not defined", sbSymbol_name(target->symbol));
   }
 }
+
+/* --- */
+
+/* function that resolves methods from BC_SEND for non intrinsic types (aka functions) */
+/* we have something like a.b(c,d,e), we need to call twice to get result of a(:b)(c,d,e) */
+/* resolve_method should have set us up so we can just call in twice */
+sbCFuncStatus please_call_again(hVm vm, flag init) {
+  hV *target = sbVm_pop(vm);
+  if (target->type <= 0) {
+    PANIC("i have not implemented partial methods for builtin types yet! i'll get to it");
+  }
+
+  if (init) {
+    /* first time around: call to dispatch method */
+    sbVm_call_func(vm, target);
+
+    /* please call us again if you have any questions*/
+    return CFUNC_NEXT;
+  } else {
+    /* second time around: call method on its parameters */
+    sbVm_transfer_to_func(vm, target);
+
+    /* please forget we ever existed */
+    return CFUNC_TAILCALL;
+  }
+}
index 65d2e3e..1b824cf 100644 (file)
@@ -1,6 +1,9 @@
 extern sbLibTable g_global_module;
+extern sbLibTable g_math_module;
 extern sbLibTable g_list_module;
+extern sbLibTable g_string_module;
 
 void sbLib_loadmodule_global();
 void sbLib_loadmodule_math();
 void sbLib_loadmodule_list();
+void sbLib_loadmodule_string();
index 52922e3..9600aee 100644 (file)
@@ -13,10 +13,6 @@ sbCFuncStatus println_cfunc(hVm vm, flag init);
 
 sbLibTable g_global_module;
 
-sbLibTable g_string_namespace;
-
-extern sbLibTable g_math_module;
-
 static void print(hVm vm, usize argc) {
   sbVm_push_immediate(vm, &HVINT(argc));
   sbVm_call_c_func(vm, print_cfunc);
@@ -32,9 +28,8 @@ void sbLib_loadmodule_global() {
   REGISTER_VALUE(&g_global_module, "print", &HVBUILTIN(print));
   REGISTER_VALUE(&g_global_module, "println", &HVBUILTIN(println));
 
-  sbLibTable_initialize(&g_string_namespace, 16, FALSE);
-  REGISTER_VALUE(&g_string_namespace, "convert", &HVSYM(S_OP_TO_STRING));
-  REGISTER_VALUE(&g_global_module, "string", &HVMODULE(&g_string_namespace));
+  sbLib_loadmodule_string();
+  REGISTER_VALUE(&g_global_module, "string", &HVMODULE(&g_string_module));
 
   sbLib_loadmodule_list();
   REGISTER_VALUE(&g_global_module, "list", &HVMODULE(&g_list_module));
diff --git a/src/lib/module/string.c b/src/lib/module/string.c
new file mode 100644 (file)
index 0000000..7a7750d
--- /dev/null
@@ -0,0 +1,51 @@
+#include "common.h"
+
+#include "lib/table.h"
+#include "lib/sentinel.h"
+#include "data/list.h"
+#include "data/string.h"
+#include "data/symbol.h"
+#include "data/integer.h"
+#include "vm/exec.h"
+
+sbCFuncStatus from_cfunc(hVm vm, flag init);
+
+sbLibTable g_string_module;
+
+static void from(hVm vm, usize argc) {
+  if (argc != 1) {
+    PANIC("string::from takes 1 argument");
+  }
+
+  sbVm_call_c_func(vm, from_cfunc);
+}
+
+void sbLib_loadmodule_string() {
+  sbLibTable_initialize(&g_string_module, 16, FALSE);
+  REGISTER_VALUE(&g_string_module, "from", &HVBUILTIN(from));
+  REGISTER_VALUE(&g_string_module, "convert", &HVSYM(S_OP_TO_STRING));
+}
+
+/* --- */
+
+sbCFuncStatus from_cfunc(hVm vm, flag init) {
+  if (!init) {
+    /* get value of previous to_string */
+    hV *value = sbVm_peek(vm, 0);
+    /* TODO: We should probably have some kind of 'implicit convert to string'
+     * thing that checks that it's really a string and throws if not, that we
+     * can call from multiple places  */
+    /* also this should have some kind of like default value if it doesn't work */
+    if (value->type != IT_STRING) {
+      PANIC("string::convert needs to return a string");
+    }
+    return CFUNC_END;
+  } else {
+    sbVm_push_immediate(vm, &HVSYM(S_OP_TO_STRING));  /* ... target string::convert */
+    sbVm_swap(vm);                                    /* ... string::convert target */
+    sbVm_push_immediate(vm, &HVINT(1));               /* ... string::convert target 1 */
+    sbVm_swap(vm);                                    /* ... string::convert 1 target */
+    sbLib_resolve_method(vm);
+    return CFUNC_NEXT;
+  }
+}
index 8ad0257..c870e7a 100644 (file)
@@ -37,7 +37,7 @@ typedef enum sbAstType {
   AST_NODE_NEXT,
   AST_NODE_MULTIVAL,
   AST_NODE_FUNCCALL,
-  AST_NODE_METHODCALL,
+  AST_NODE_DOT,
   AST_NODE_SEND,
   AST_NODE_RETURN,
   AST_NODE_ELLIPSIS,
index 268042c..db364ae 100644 (file)
@@ -291,17 +291,6 @@ static void stack_token(hLexer lx, sbLexToken token) {
     }
 }
 
-static void unstack_one_invisible_parenthesis(hLexer lx) {
-    if (lx->brackets_stack.size == 0) return;
-
-    char top = brackets_stack_top(lx);
-    if (top == 'G' || top == 'H') {
-        sbLexToken invisible_rparen = { .type = T_RPAREN, .invisible = 1 };
-        enqueue_output_token(lx, invisible_rparen);
-        brackets_stack_pop(lx);
-    }
-}
-
 static void unstack_invisible_parentheses_of_type(hLexer lx, char type) {
     /* this happens when a real bracket closes, and also at the end of a line */
     if (lx->brackets_stack.size == 0) return;
@@ -451,44 +440,7 @@ static void compute_next_token(hLexer lx) {
             space_offset = 1;
         }
 
-        if (token.type == T_BACKSQUIGARROW) {
-            /* <~ always gets a ( after it to include its parameters, unless there's a ( immediately
-             * after it with no space. (*with* a space, we accept stuff like a <~ (b), c, d which
-             * seems likely to be more common than things like a <~(b, c, d) which is kinda wonky
-             * to begin with (probably people should just write (a <~ b, c, d) but idk) */
-            if (input_peek_ahead(lx, 0).type != T_LPAREN) {
-              enqueue_output_token(lx, invisible_lparen);
-            }
-        } else if (lx->last_token_seen.type == T_DOT && token.type == T_IDENTIFIER) {
-            /* an identifier after a dot always gets an invisible parentheses after it,
-             * unless there is a visible parentheses immediately after it. */
-            if (input_peek_ahead(lx, 0).type != T_LPAREN) {
-                enqueue_output_token(lx, invisible_lparen);
-
-                sbTokenType next_type = input_peek_ahead(lx, space_offset).type;
-                sbTokenType next_next_type = input_peek_ahead(lx, space_offset + 1).type;
-
-                if (maybe_can_start_expression(next_type)) {
-                    /* this is something like 'a.b( +c' or 'a.b( + c'. if there is a space
-                     * after the operator, or not before the operator, add an invisible ),
-                     * otherwise don't because that's the 'a.b( +c' case */
-                    if (next_next_type == T_SPACE || space_offset == 0) {
-                        unstack_one_invisible_parenthesis(lx);
-                    }
-                } else if (!can_only_start_expression(next_type, is_in_brace_terminated_state(lx))) {
-                    /* ok, so this means that we just inserted a ( in some situation like
-                     * 'a.b(.c' or 'a.b(, c' or 'a.b( % 3' -- in this situation, we need to also
-                     * add a matching invisible right parenthesis immediately. */
-
-                    /* In brace-terminated state, { doesn't count as can_only_start_expression.
-                     * So this applies before { as well: normally 'a.b {' --> 'a.b({', but
-                     * inside block headers 'a.b {' --> 'a.b() {' */
-                    unstack_one_invisible_parenthesis(lx);
-                }
-                /* if can_only_start_expression(next_type), then we don't want to add an invisible
-                 * right parenthesis because it must be a parameter. */
-            }
-        } else if (input_peek_ahead(lx, 0).type == T_SPACE || insert_paren_no_space) {
+        if (input_peek_ahead(lx, 0).type == T_SPACE || insert_paren_no_space) {
             /* suspicious... tell me more */
             if (can_only_start_expression(input_peek_ahead(lx, space_offset).type, is_in_brace_terminated_state(lx))) {
                 /* ah ! yes. insert a magic ( into the stream. */
index bc47632..5774da3 100644 (file)
@@ -616,30 +616,13 @@ static sbAst parse_expr(hParser pr, u8 min_precedence) {
       /* . and -> parse an identifier to their right as a symbol that will
        * name the method to be called. to call a method whose name isn't
        * a symbol, you need a.[expr] or a->[expr] */
-      sbAst method_name = NO_NODE;
-      if (expect(pr, T_LBRACKET)) {
-        method_name = parse_expr(pr, 0);
-        if (!expect(pr, T_RBRACKET)) return syntax_error(pr);
-      } else {
-        method_name = parse_name_as_sym(pr);
-      }
-      if (method_name == NO_NODE) return syntax_error(pr);
-      rhs = seq_node(pr, AST_NODE_MULTIVAL, NO_NODE, method_name);
-      if (expect(pr, T_LPAREN)) {
-        rhs = parse_comma_exprs(pr, rhs);
-        if (!expect(pr, T_RPAREN)) return syntax_error(pr);
-      }
-      ast_type = AST_NODE_METHODCALL;
+      rhs = parse_name_as_sym(pr);
+      if (rhs == NO_NODE) return syntax_error(pr);
+      ast_type = AST_NODE_DOT;
       if (op.type == T_ARROW) {
         /* (whatever)->x is rewritten as (*whatever).x */
         lhs = unop_node(pr, AST_OP_DEREF, lhs);
       }
-    } else if (op.type == T_BACKSQUIGARROW) {
-      /* a <~ b, c, d can have multiple comma things on the right side */
-      if (!expect(pr, T_LPAREN)) return syntax_error(pr);
-      ast_type = AST_NODE_METHODCALL;
-      rhs = parse_comma_exprs(pr, NULL);
-      if (!expect(pr, T_RPAREN)) return syntax_error(pr);
     } else if (op.type == T_PAAMAYIM_NEKUDOTAYIM) {
       /* :: is similar to . and -> above, will parse the thing to the right
        * as a symbol unless using the syntax a::[expr] */
index bc2a549..9f8639a 100644 (file)
@@ -82,6 +82,12 @@ void sbVm_call_func(hVm vm, hV *func) {
   call_block(vm, func->type, func->closure);
 }
 
+void sbVm_transfer_to_func(hVm vm, hV *func) {
+  /* tail call */
+  return_from_block(vm);
+  call_block(vm, func->type, func->closure);
+}
+
 void sbVm_call_c_func(hVm vm, sbRuntimeCFunc func) {
   sbVmStackFrame frame = {
     .return_addr = vm->ip,
index 7229c9b..6041238 100644 (file)
@@ -39,6 +39,7 @@
 typedef enum {
   CFUNC_END,
   CFUNC_NEXT,
+  CFUNC_TAILCALL,
 } sbCFuncStatus;
 
 typedef sbCFuncStatus (*sbRuntimeCFunc)(hVm, flag init);
@@ -102,6 +103,8 @@ void sbVm_swap(hVm vm);
 
 void sbVm_call_func(hVm vm, hV *func);
 
+void sbVm_transfer_to_func(hVm vm, hV *func);
+
 void sbVm_call_c_func(hVm vm, sbRuntimeCFunc func);
 
 void sbVm_request_var_space(hVm vm, usize amount);