about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--awk/scheme/scheme/README.md13
-rw-r--r--awk/scheme/scheme/WHAT-NEEDS-FIXING.md56
-rwxr-xr-xawk/scheme/scheme/bin/compiler.awk388
-rwxr-xr-xawk/scheme/scheme/bin/repl4
-rwxr-xr-xawk/scheme/scheme/bin/vm.awk113
-rwxr-xr-xawk/scheme/scheme/test/run_tests.sh14
-rw-r--r--awk/scheme/scheme/test/unit/minimal_closure_env.scm8
-rw-r--r--awk/scheme/scheme/test/unit/minimal_function_persistence.scm2
-rw-r--r--awk/scheme/scheme/test/unit/minimal_global_persistence.scm2
-rw-r--r--js/seed/README.md15
-rwxr-xr-xjs/seed/seed75
-rw-r--r--js/seed/src/app.js8
-rw-r--r--js/seed/src/dev.js4
-rw-r--r--js/seed/src/view.js6
14 files changed, 464 insertions, 244 deletions
diff --git a/awk/scheme/scheme/README.md b/awk/scheme/scheme/README.md
index cf346c7..0b925f4 100644
--- a/awk/scheme/scheme/README.md
+++ b/awk/scheme/scheme/README.md
@@ -100,6 +100,8 @@ Test categories:
 - **Examples**: Demonstration programs
 - **Regression tests**: Edge cases
 
+*Note: The test suite is comprehensive, but some advanced closure/currying and edge-case features are still being debugged. See Known Issues below.*
+
 ## Debugging
 
 ### Enable Debug Mode
@@ -109,6 +111,11 @@ DEBUG=1 ./scheme program.scm        # File with debug
 echo "(+ 1 2)" | DEBUG=1 ./scheme   # Single expression
 ```
 
+**S-expression Debugging:**
+```bash
+DEBUG_SEXPR=1 ./scheme program.scm  # Enable detailed S-expression parsing/codegen debug output
+```
+
 ### Debug Output Shows
 - Token parsing and expression tree construction
 - VM instruction generation
@@ -218,4 +225,8 @@ echo "5 3 + ." | awk -f forth.awk  # => Result: N:8
 - `bin/vm.awk` - Virtual machine implementation
 - `bin/repl` - Interactive REPL
 - `test/` - Comprehensive test suite
-- `scratch/forth/` - Forth implementation for VM validation
\ No newline at end of file
+- `scratch/forth/` - Forth implementation for VM validation
+
+## Known Issues
+
+- Some advanced closure/currying and higher-order function tests are still failing and are under active investigation. See WHAT-NEEDS-FIXING.md for current status and debugging plan.
diff --git a/awk/scheme/scheme/WHAT-NEEDS-FIXING.md b/awk/scheme/scheme/WHAT-NEEDS-FIXING.md
new file mode 100644
index 0000000..99dbfd8
--- /dev/null
+++ b/awk/scheme/scheme/WHAT-NEEDS-FIXING.md
@@ -0,0 +1,56 @@
+# What Needs Fixing
+
+## Current State (as of latest debugging)
+
+- **Testing Infrastructure:**
+  - Test runner and environment variable handling are now robust; DEBUG/DEBUG_SEXPR work as intended.
+  - Most tests pass (37/51), including basic, integration, and many higher-order function tests.
+  - 14 tests still fail, primarily those involving advanced closure/currying, nested lambdas, and some edge-case scoping.
+
+- **Recent Fixes:**
+  - S-expression splitting and body handling for define/lambda/let are robust and debugged.
+  - Compiler now emits correct closure construction (CAPTURE_ENV, PUSH_CONST CLOSURE) before RETURN in lambdas.
+  - Function application parsing and codegen now handle curried and higher-order calls recursively.
+  - Test runner bug with environment variable passing (DEBUG=1) is fixed.
+
+- **Current Closure/Currying Bug:**
+  - Minimal closure/currying tests (e.g., minimal_closure_env, minimal_function_persistence) still fail.
+  - Symptom: After a chain of curried calls, the final value is a number, but the VM/compiler emits an extra CALL, causing 'Undefined function: N:99' or similar errors.
+  - The compiler always emits CALL for function applications, even when the result is not a function.
+  - The test runner workaround (just printing the result) is not sufficient, as the compiler still emits CALL for the last value.
+
+- **Other Failing Patterns:**
+  - Some advanced function, closure, and scoping tests (advanced_functions, closures, lambdas, higher_order_functions) still fail, likely due to the same root issue: incorrect codegen or VM handling for returned closures and final values.
+
+## What Has Been Ruled Out
+- For all currently failing tests and with current debug evidence, S-expression splitting and body parsing are robust and not the source of closure/currying bugs.
+- The VM constructs and returns closures correctly when the codegen is correct; no VM-level closure bugs are currently indicated.
+- The test runner and shell environment are not the source of the remaining failures in the current setup.
+
+## Next Steps: Plan for Closure/Currying/Function Application Bugs
+
+1. **Targeted Debugging of Failing Closure/Curried Tests:**
+   - Focus on minimal_closure_env, minimal_function_persistence, closures, and advanced_functions.
+   - Use DEBUG_SEXPR=1 and DEBUG=1 to trace codegen and VM execution for these tests.
+   - Confirm exactly where the extra CALL is emitted and why.
+
+2. **Compiler Codegen Review:**
+   - Review compile_primitive_call and related code to ensure CALL is only emitted when the result is expected to be a function.
+   - Consider a minimal patch: at the top-level, avoid emitting CALL for the final value if it is not a function.
+   - Document with TODOs where a more robust, type-aware solution would go.
+
+3. **VM/Runtime Review:**
+   - Confirm that the VM leaves the correct value on the stack after each function call, and does not attempt to call non-functions.
+   - Add debug output if needed to trace stack state after each CALL.
+
+4. **Test and Iterate:**
+   - After each fix, re-run the minimal closure/currying tests and confirm progress.
+   - Once minimal tests pass, move to more complex closure and higher-order function tests.
+
+5. **Document Findings:**
+   - Update this file after each major fix or discovery, so the next debugging session has a clear starting point.
+
+## Goal (Restated)
+- Ensure closure/currying/function application codegen and VM logic are correct for all cases, including nested and returned lambdas.
+- Eliminate extra CALLs for non-function values at the top level.
+- Systematically fix all remaining failing tests by following the above plan. 
\ No newline at end of file
diff --git a/awk/scheme/scheme/bin/compiler.awk b/awk/scheme/scheme/bin/compiler.awk
index f36f497..864b19c 100755
--- a/awk/scheme/scheme/bin/compiler.awk
+++ b/awk/scheme/scheme/bin/compiler.awk
@@ -22,6 +22,14 @@
 # - Stack clearing between expressions to prevent argument pollution
 
 BEGIN {
+
+    # Debug mode configuration
+    # FIXME: Don't leave in, trigger on flag
+    print "[DEBUG_SEXPR] BEGIN block reached" > "/dev/stderr"
+    print "[DEBUG_SEXPR] ENVIRON[\"DEBUG_SEXPR\"] = " ENVIRON["DEBUG_SEXPR"] > "/dev/stderr"
+    DEBUG_SEXPR = (ENVIRON["DEBUG_SEXPR"] == "1") ? 1 : 0
+    if (DEBUG_SEXPR) print "[DEBUG_SEXPR] DEBUG_SEXPR is enabled" > "/dev/stderr"
+
     # Compiler maintains internal state for code generation
     curr_token = ""      # Current token being processed by lexer
     input_buffer = ""    # Buffer for input text being tokenized
@@ -33,6 +41,7 @@ BEGIN {
     # Unlike JS process.env, this is automatically available in awk
     DEBUG = (ENVIRON["DEBUG"] == "1") ? 1 : 0
     error_flag = 0  # Set to 1 if any error occurs
+    DEBUG_SEXPR = (ENVIRON["DEBUG_SEXPR"] == "1") ? 1 : 0
 }
 
 # Debug logging helper function
@@ -46,6 +55,7 @@ function debug(msg) {
 # This is awk's main input processing loop - every line from stdin/files goes here
 # In JS, you'd need to explicitly read lines from a stream
 {
+    if (DEBUG_SEXPR) print "[DEBUG_SEXPR] Reading line: [" $0 "]" > "/dev/stderr"
     if (program != "") program = program "\n"
     program = program $0  # $0 is the current line being processed
 }
@@ -61,6 +71,8 @@ END {
     debug("END block: error_flag=" error_flag)
     # If error_flag was set, exit 1
     if (error_flag) exit 1
+    debug("END block: exiting")
+    exit 0
 }
 
 # Splits input into individual Scheme expressions
@@ -335,32 +347,43 @@ function parse_list(result, expr) {
 # - Supporting both simple calls and complex nested expressions
 #
 # Handles nested expressions correctly by tracking parenthesis nesting
-function split_expr(expr, i, len, c, op, args, paren_count) {
-    # AWK FEATURE: length(string) returns the length of a string
-    # Unlike JS string.length, this is a function call, not a property
+function split_expr(expr, i, len, c, op, args, paren_count, j, c2) {
     len = length(expr)
     paren_count = 0
-    
-    for (i = 1; i <= len; i++) {
-        c = substr(expr, i, 1)
-        if (c == " " && paren_count == 0) {
-            op = substr(expr, 1, i - 1)
-            args = substr(expr, i + 1)
-            break
+    op = ""
+    if (substr(expr, 1, 1) == "(") {
+        # Operator is a parenthesized expression
+        paren_count = 1
+        for (i = 2; i <= len; i++) {
+            c = substr(expr, i, 1)
+            if (c == "(") paren_count++
+            if (c == ")") paren_count--
+            if (paren_count == 0) {
+                op = substr(expr, 1, i)
+                # Skip any whitespace after the closing paren
+                j = i + 1
+                while (j <= len && (substr(expr, j, 1) == " " || substr(expr, j, 1) == "\t")) j++
+                args = substr(expr, j)
+                break
+            }
+        }
+    } else {
+        for (i = 1; i <= len; i++) {
+            c = substr(expr, i, 1)
+            if (c == " " && paren_count == 0) {
+                op = substr(expr, 1, i - 1)
+                args = substr(expr, i + 1)
+                break
+            }
+            if (c == "(") paren_count++
+            if (c == ")") paren_count--
+        }
+        if (!op) {
+            op = expr
+            args = ""
         }
-        if (c == "(") paren_count++
-        if (c == ")") paren_count--
-    }
-    
-    if (!op) {
-        op = expr
-        args = ""
     }
-    
     debug("Split expr: op=" op " args=" args)
-    # AWK FEATURE: SUBSEP is a built-in variable used as separator for array indices
-    # When you use array[key1,key2], awk internally stores it as array[key1 SUBSEP key2]
-    # This allows multi-dimensional arrays in awk (though they're really single-dimensional)
     return op SUBSEP args
 }
 
@@ -778,16 +801,20 @@ function compile_primitive_call(op, args, arg_array, nargs, i) {
         print "CALL"
     }
     else {
-        # Function call for user-defined functions
+        # Function call for user-defined functions or higher-order/callable expressions
         debug("Function call: " op)
         # First compile arguments
         for (i = 1; i <= nargs; i++) {
             compile_expr(arg_array[i])
         }
-        # Then look up the function name
-        print "LOOKUP " op
-        # Get the actual function name
-        print "GET_VALUE"
+        # If the operator is a parenthesized expression, recursively compile it
+        if (substr(op, 1, 1) == "(") {
+            if (DEBUG_SEXPR) print "[DEBUG_SEXPR] compile_primitive_call: compiling operator expr: [" op "]" > "/dev/stderr"
+            compile_expr(op)
+        } else {
+            print "LOOKUP " op
+            print "GET_VALUE"
+        }
         # Call the function
         print "CALL"
     }
@@ -836,55 +863,34 @@ function split_bindings(bindings, binding_array, count, current, paren_count, i,
 }
 
 # Compiles let expressions (local variable bindings)
-function compile_let(args, bindings, body, binding_array, nbindings, i, var, val, binding_parts) {
+function compile_let(args, bindings, body, binding_array, nbindings, i, var, val, binding_parts, sexprs, nsexprs, j, expr) {
     # Split into bindings and body
     if (substr(args, 1, 1) != "(") error("Malformed let expression")
-    
-    # Find matching closing parenthesis for bindings
     paren_count = 1
     i = 2
-    # AWK FEATURE: length(string) returns the length of a string
-    # Unlike JS string.length, this is a function call, not a property
     while (paren_count > 0 && i <= length(args)) {
         if (substr(args, i, 1) == "(") paren_count++
         if (substr(args, i, 1) == ")") paren_count--
         i++
     }
     if (paren_count > 0) error("Unmatched parenthesis in let bindings")
-    
-    bindings = substr(args, 2, i - 3)  # Remove outer parentheses
+    bindings = substr(args, 2, i - 3)
     body = substr(args, i)
-    
-    # Trim whitespace from body
     sub(/^[ \t\n]+/, "", body)
     sub(/[ \t\n]+$/, "", body)
-    
     debug("Let bindings: " bindings)
     debug("Let body: " body)
-    
-    # Compile each binding
     nbindings = split_bindings(bindings, binding_array)
     for (i = 1; i <= nbindings; i++) {
         debug("Processing binding: " binding_array[i])
-        
-        # Find the variable name (everything up to the first space)
         var = binding_array[i]
         sub(/ .*$/, "", var)
-        
-        # Find the value (everything after the first space)
         val = binding_array[i]
         sub(/^[^ ]+ /, "", val)
-        
         debug("Binding var: " var " val: " val)
-        
-        # Compile the value
         if (substr(val, 1, 1) == "(") {
-            # Handle lambda or other compound expressions
             if (substr(val, 2, 6) == "lambda") {
-                # This is a lambda expression
-                # Pass the entire lambda expression to compile_lambda
                 compile_lambda(val)
-                # Store the function name in the environment
                 print "STORE " var
             } else {
                 compile_expr(val)
@@ -895,122 +901,115 @@ function compile_let(args, bindings, body, binding_array, nbindings, i, var, val
             print "STORE " var
         }
     }
-    
-    # Compile the body
-    compile_expr(body)
-    
-    # Clean up bindings AFTER evaluating body
+    # --- Robust multi-expression let body support ---
+    nsexprs = split_sexpressions(body, sexprs)
+    if (DEBUG_SEXPR) {
+        printf("[DEBUG_SEXPR] compile_let: splitting body, found %d expressions\n", nsexprs) > "/dev/stderr"
+        for (j = 1; j <= nsexprs; j++) {
+            printf("[DEBUG_SEXPR]   %d: [%s]\n", j, sexprs[j]) > "/dev/stderr"
+        }
+    }
+    for (j = 1; j <= nsexprs; j++) {
+        expr = sexprs[j]
+        sub(/^[ \t\n]+/, "", expr)
+        sub(/[ \t\n]+$/, "", expr)
+        if (DEBUG_SEXPR) printf("[DEBUG_SEXPR]   let body expr: [%s]\n", expr) > "/dev/stderr"
+        compile_expr(expr)
+    }
+    # --- End robust let body support ---
     for (i = nbindings; i >= 1; i--) {
         print "POP_ENV"
     }
 }
 
 # Compiles define expressions (function/variable definitions)
-function compile_define(args, name, params, body, param_array, nparams, i, paren_start, paren_end) {
-    # Set flag for global definition
+function compile_define(args, name, params, body, param_array, nparams, i, paren_start, paren_end, sexprs, nsexprs, j, expr, is_define, last_body_idx) {
+    if (DEBUG_SEXPR) print "[DEBUG_SEXPR] compile_define called with args: [" args "]" > "/dev/stderr"
     print "PUSH_CONST B:1"
-    print "STORE from_define"  # Must match exactly what vm_store checks for
-    
-    # Find the function name (everything up to the first space)
+    print "STORE from_define"
     i = index(args, " ")
     if (i == 0) error("Malformed define expression")
     name = substr(args, 1, i - 1)
     args = substr(args, i + 1)
-
-    # Check if it's a function or variable definition
-    # A function definition has the form: (define name (param1 param2) body)
-    # A variable definition has the form: (define name value)
     debug("compile_define: args = [" args "]")
     if (substr(args, 1, 1) == "(") {
-        # Check if this is a function definition or a variable definition with function call
-        # Look for the pattern: (name ...) where name is not a function call
-        # For (list 1), we need to check if the first word after ( is a function name
-        paren_count = 0
-        i = 1
-        first_word = ""
-        while (i <= length(args)) {
+        paren_count = 1
+        close_paren = 0
+        for (i = 2; i <= length(args); i++) {
             c = substr(args, i, 1)
             if (c == "(") paren_count++
             if (c == ")") paren_count--
-            if (c == " " && paren_count == 1) {
-                # Found a space inside the first parentheses
-                # Check if the first word is a function name (like list, cons, etc.)
-                if (first_word == "list" || first_word == "cons" || first_word == "append" || 
-                    first_word == "reverse" || first_word == "member" || first_word == "length" ||
-                    first_word == "car" || first_word == "cdr" || first_word == "cadr" || 
-                    first_word == "caddr" || first_word == "list-ref" || first_word == "list-tail" ||
-                    first_word == "null?" || first_word == "pair?" || first_word == "lambda" ||
-                    first_word == "map" || first_word == "filter") {
-                    # This is a variable definition with function call value
-                    debug("Defining variable with function call value: " name " with value: " args)
-                    compile_expr(args)  # Compile the value
-                    print "STORE " name  # Store the variable
-                    return
+            if (paren_count == 0) { close_paren = i; break }
+        }
+        if (close_paren == 0) error("Unmatched parenthesis in define")
+        rest = substr(args, close_paren + 1)
+        sub(/^[ \t\n]+/, "", rest)
+        if (rest == "") {
+            if (DEBUG_SEXPR) print "[DEBUG_SEXPR] variable definition (parenthesized value): [" name "] value: [" args "]" > "/dev/stderr"
+            compile_expr(args)
+            print "STORE " name
+            return
+        } else {
+            params = substr(args, 2, close_paren - 2)
+            body = rest
+            if (DEBUG_SEXPR) print "[DEBUG_SEXPR] function definition: [" name "] params: [" params "] body: [" body "]" > "/dev/stderr"
+            print "LABEL " name
+            nparams = split(params, param_array, " ")
+            for (i = 1; i <= nparams; i++) {
+                print "STORE " param_array[i]
+            }
+            nsexprs = split_sexpressions(body, sexprs)
+            if (DEBUG_SEXPR) {
+                printf("[DEBUG_SEXPR] compile_define: splitting body, found %d expressions\n", nsexprs) > "/dev/stderr"
+                for (j = 1; j <= nsexprs; j++) {
+                    printf("[DEBUG_SEXPR]   %d: [%s]\n", j, sexprs[j]) > "/dev/stderr"
+                }
+            }
+            last_body_idx = 0
+            for (j = 1; j <= nsexprs; j++) {
+                expr = sexprs[j]
+                sub(/^[ \t\n]+/, "", expr)
+                sub(/[ \t\n]+$/, "", expr)
+                is_define = (substr(expr, 2, 6) == "define")
+                if (is_define) {
+                    if (DEBUG_SEXPR) printf("[DEBUG_SEXPR]   local define: [%s]\n", expr) > "/dev/stderr"
+                    compile_expr(expr)
                 } else {
-                    # This is a function definition with parameters
-                    break
+                    last_body_idx = j
                 }
             }
-            if (paren_count == 1 && c != "(") {
-                first_word = first_word c
+            for (j = 1; j <= nsexprs; j++) {
+                expr = sexprs[j]
+                sub(/^[ \t\n]+/, "", expr)
+                sub(/[ \t\n]+$/, "", expr)
+                is_define = (substr(expr, 2, 6) == "define")
+                if (!is_define && expr != "") {
+                    if (DEBUG_SEXPR) printf("[DEBUG_SEXPR]   body expr: [%s]\n", expr) > "/dev/stderr"
+                    compile_expr(expr)
+                }
             }
-            i++
-        }
-        
-        # It's a function definition
-        paren_count = 1
-        i = 2
-        while (paren_count > 0 && i <= length(args)) {
-            if (substr(args, i, 1) == "(") paren_count++
-            if (substr(args, i, 1) == ")") paren_count--
-            i++
-        }
-        if (paren_count > 0) error("Unmatched parenthesis in parameter list")
-
-        params = substr(args, 2, i - 3)  # Remove parentheses
-        body = substr(args, i + 1)
-
-        # Create function label
-        print "LABEL " name
-
-        # Process parameters
-        # AWK FEATURE: split(string, array, separator) splits string into array elements
-        # Unlike JS string.split() which returns an array, this populates an existing array
-        nparams = split(params, param_array, " ")
-        for (i = 1; i <= nparams; i++) {
-            print "STORE " param_array[i]
-        }
-
-        # Compile function body
-        compile_expr(body)
-
-        # Clean up parameters and return
-        for (i = nparams; i >= 1; i--) {
-            print "POP_ENV"
+            for (i = nparams; i >= 1; i--) {
+                print "POP_ENV"
+            }
+            print "RETURN"
+            return
         }
-        print "RETURN"
     } else {
-        # Variable definition
-        debug("Defining variable: " name " with value: " args)
-        compile_expr(args)  # Compile the value
-        print "STORE " name  # Store the variable
+        if (DEBUG_SEXPR) print "[DEBUG_SEXPR] variable definition: [" name "] value: [" args "]" > "/dev/stderr"
+        compile_expr(args)
+        print "STORE " name
     }
 }
 
 # Compiles lambda expressions (anonymous functions)
-function compile_lambda(args, params, body, param_array, nparams, i, lambda_name) {
-    # Generate a unique name for the lambda function
+function compile_lambda(args, params, body, param_array, nparams, i, lambda_name, expr, op, rest, sexprs, nsexprs, j, is_define, last_body_idx) {
+    if (DEBUG_SEXPR) print "[DEBUG_SEXPR] compile_lambda called" > "/dev/stderr"
     lambda_name = "__lambda_" next_label++
-    
     debug("compile_lambda: args = [" args "]")
-    
-    # Handle both full lambda expression and just the arguments part
     if (substr(args, 1, 7) == "(lambda") {
         debug("compile_lambda: detected full lambda expression")
-        # Full lambda expression: (lambda (params) body)
-        args = substr(args, 8)  # Remove "(lambda"
+        args = substr(args, 8)
         debug("compile_lambda: after removing (lambda: [" args "]")
-        # Find the closing parenthesis of the entire lambda expression
         paren_count = 0
         i = 1
         while (i <= length(args)) {
@@ -1018,66 +1017,70 @@ function compile_lambda(args, params, body, param_array, nparams, i, lambda_name
             if (c == "(") paren_count++
             if (c == ")") {
                 paren_count--
-                if (paren_count == -1) break  # Found the closing paren of the lambda
+                if (paren_count == -1) break
             }
             i++
         }
         if (paren_count != -1) error("Unmatched parenthesis in lambda")
-        args = substr(args, 1, i - 1)  # Remove the closing parenthesis
+        args = substr(args, 1, i - 1)
         debug("compile_lambda: after removing closing paren: [" args "]")
-        # Remove leading whitespace
         sub(/^[ \t\n]+/, "", args)
         debug("compile_lambda: after removing leading whitespace: [" args "]")
     }
-    
-    # Split into parameters and body
     if (substr(args, 1, 1) != "(") error("Malformed lambda expression")
-    
-    # Find matching closing parenthesis for parameters
     paren_count = 1
     i = 2
-    # AWK FEATURE: length(string) returns the length of a string
-    # Unlike JS string.length, this is a function call, not a property
     while (paren_count > 0 && i <= length(args)) {
         if (substr(args, i, 1) == "(") paren_count++
         if (substr(args, i, 1) == ")") paren_count--
         i++
     }
     if (paren_count > 0) error("Unmatched parenthesis in lambda parameters")
-    
-    params = substr(args, 2, i - 3)  # Remove parentheses
+    params = substr(args, 2, i - 3)
     body = substr(args, i)
-    
-    # Trim whitespace from body
     sub(/^[ \t\n]+/, "", body)
     sub(/[ \t\n]+$/, "", body)
-    
     debug("Lambda parameters: " params)
     debug("Lambda body: " body)
-    
-    # Create function label
+    if (DEBUG_SEXPR) print "[DEBUG_SEXPR] body to split_sexpressions: [" body "]" > "/dev/stderr"
     print "LABEL " lambda_name
-    
-    # Process parameters
-    # AWK FEATURE: split(string, array, separator) splits string into array elements
-    # Unlike JS string.split() which returns an array, this populates an existing array
     nparams = split(params, param_array, " ")
     for (i = 1; i <= nparams; i++) {
         print "STORE " param_array[i]
     }
-    
-    # Compile function body
-    compile_expr(body)
-    
-    # Clean up parameters and return
+    nsexprs = split_sexpressions(body, sexprs)
+    if (DEBUG_SEXPR) {
+        printf("[DEBUG_SEXPR] compile_lambda: processing %d expressions\n", nsexprs) > "/dev/stderr"
+    }
+    last_body_idx = 0
+    for (j = 1; j <= nsexprs; j++) {
+        expr = sexprs[j]
+        sub(/^[ \t\n]+/, "", expr)
+        sub(/[ \t\n]+$/, "", expr)
+        is_define = (substr(expr, 2, 6) == "define")
+        if (is_define) {
+            if (DEBUG_SEXPR) printf("[DEBUG_SEXPR]   local define: [%s]\n", expr) > "/dev/stderr"
+            compile_expr(expr)
+        } else {
+            last_body_idx = j
+        }
+    }
+    for (j = 1; j <= nsexprs; j++) {
+        expr = sexprs[j]
+        sub(/^[ \t\n]+/, "", expr)
+        sub(/[ \t\n]+$/, "", expr)
+        is_define = (substr(expr, 2, 6) == "define")
+        if (!is_define && expr != "") {
+            if (DEBUG_SEXPR) printf("[DEBUG_SEXPR]   body expr: [%s]\n", expr) > "/dev/stderr"
+            compile_expr(expr)
+        }
+    }
     for (i = nparams; i >= 1; i--) {
         print "POP_ENV"
     }
-    print "RETURN"
-    
-    # Create closure that captures current environment
     print "CAPTURE_ENV " lambda_name
     print "PUSH_CONST CLOSURE:" lambda_name ":ENV_ID"
+    print "RETURN"
 }
 
 # Compile if expression: (if condition then-expr else-expr)
@@ -1304,6 +1307,7 @@ function compile_not(args,    expr) {
 
 # Main expression compiler - dispatches based on expression type
 function compile_expr(expr,    split_result, op, args) {
+    if (DEBUG_SEXPR) print "[DEBUG_SEXPR] compile_expr called with expr: [" expr "]" > "/dev/stderr"
     debug("Compiling expression: " expr)
     
     # Handle empty expressions
@@ -1346,7 +1350,7 @@ function compile_expr(expr,    split_result, op, args) {
         return
     }
     
-    # Handle variable lookup
+    # Handle variable lookup (only if not a parenthesized expression)
     if (expr ~ /^[a-zA-Z_][a-zA-Z0-9_?-]*$/) {
         print "LOOKUP " expr
         return
@@ -1358,7 +1362,7 @@ function compile_expr(expr,    split_result, op, args) {
         split_result = split_expr(expr)
         op = substr(split_result, 1, index(split_result, SUBSEP) - 1)
         args = substr(split_result, index(split_result, SUBSEP) + 1)
-        
+        if (DEBUG_SEXPR) print "[DEBUG_SEXPR] split_expr op: [" op "] args: [" args "]" > "/dev/stderr"
         if (op == "define") {
             compile_define(args)
         } else if (op == "let") {
@@ -1389,4 +1393,58 @@ function error(msg) {
     print "Error: " msg > "/dev/stderr"
     error_flag = 1
     exit 1
-}
\ No newline at end of file
+}
+
+# Split a string into top-level S-expressions (returns count, fills sexpr_array)
+function split_sexpressions(str, sexpr_array, i, c, in_string, paren_count, current, n, in_comment) {
+    if (DEBUG_SEXPR) print "[DEBUG_SEXPR] split_sexpressions called" > "/dev/stderr"
+    in_string = 0
+    paren_count = 0
+    current = ""
+    n = 0
+    in_comment = 0
+    i = 1
+    while (i <= length(str)) {
+        c = substr(str, i, 1)
+        # Skip whitespace
+        if (!in_string && (c == " " || c == "\t" || c == "\n")) { i++; continue; }
+        # Skip comments (start with ; to end of line)
+        if (!in_string && c == ";") {
+            while (i <= length(str) && substr(str, i, 1) != "\n") i++;
+            i++; continue;
+        }
+        # Parse S-expression
+        current = ""
+        if (c == "(") {
+            paren_count = 0
+            while (i <= length(str)) {
+                c = substr(str, i, 1)
+                current = current c
+                if (c == "\"") in_string = !in_string
+                if (!in_string && c == "(") paren_count++
+                if (!in_string && c == ")") paren_count--
+                i++
+                if (paren_count == 0 && !in_string) break
+            }
+            sexpr_array[++n] = current
+        } else {
+            # Parse an atom
+            while (i <= length(str)) {
+                c = substr(str, i, 1)
+                if (!in_string && (c == " " || c == "\t" || c == "\n" || c == "(" || c == ")" || c == ";")) break;
+                if (c == "\"") in_string = !in_string
+                current = current c
+                i++
+            }
+            if (current != "") sexpr_array[++n] = current
+        }
+    }
+    if (DEBUG_SEXPR) {
+        printf("[DEBUG_SEXPR] split_sexpressions: found %d expressions\n", n) > "/dev/stderr"
+        for (i = 1; i <= n; i++) {
+            printf("[DEBUG_SEXPR]   %d: [%s]\n", i, sexpr_array[i]) > "/dev/stderr"
+        }
+        print "[DEBUG_SEXPR] split_sexpressions returning" > "/dev/stderr"
+    }
+    return n
+}
diff --git a/awk/scheme/scheme/bin/repl b/awk/scheme/scheme/bin/repl
index 1c290d1..0f1a049 100755
--- a/awk/scheme/scheme/bin/repl
+++ b/awk/scheme/scheme/bin/repl
@@ -116,6 +116,10 @@ if [ "$#" -gt 0 ]; then
     debug "Reading file: $1"
     file_content=$(cat "$1")
     debug "File content: $file_content"
+    # TODO: Workaround for curried/closure tests: just print the result of the last expression.
+    # This avoids emitting an extra CALL for the final value if it is not a function.
+    # A more robust solution would be to have the compiler analyze the top-level expression and only emit CALLs for function results,
+    # or to have the VM detect and print non-function results at the top level.
     evaluate_expression "$file_content"
     exit_code=$?
     cleanup "keep_state"  # Keep state after file execution
diff --git a/awk/scheme/scheme/bin/vm.awk b/awk/scheme/scheme/bin/vm.awk
index 7e68e92..33a52a2 100755
--- a/awk/scheme/scheme/bin/vm.awk
+++ b/awk/scheme/scheme/bin/vm.awk
@@ -297,20 +297,16 @@ function getClosureEnvId(closure_val) {
 
 # Environment capture for closures
 function captureEnvironment(env_id, i) {
-    debug("Capturing environment with ID: " env_id)
+    if (DEBUG) print "[DEBUG_CLOSURE] Capturing environment with ID: " env_id > "/dev/stderr"
     closure_env_sizes[env_id] = env_size
-
-    # Copy current environment to closure environment
     for (i = 0; i < env_size; i++) {
-        # Only capture non-global variables
         if (env_name[i] !~ /^__global_/) {
             closure_env_names[env_id, i] = env_name[i]
             closure_env_vals[env_id, i] = env_val[i]
-            debug("Captured: " env_name[i] " = " env_val[i])
+            if (DEBUG) print "[DEBUG_CLOSURE]   Captured: " env_name[i] " = " env_val[i] > "/dev/stderr"
         }
     }
-
-    debug("Captured environment size: " closure_env_sizes[env_id])
+    if (DEBUG) print "[DEBUG_CLOSURE] Captured environment size: " closure_env_sizes[env_id] > "/dev/stderr"
 }
 
 # VM instruction to capture environment
@@ -334,14 +330,14 @@ function vm_capture_env(func_name) {
 
 # Environment restoration for closures
 function pushClosureEnvironment(env_id, i) {
-    debug("Pushing closure environment: " env_id)
+    if (DEBUG) print "[DEBUG_CLOSURE] Pushing closure environment: " env_id > "/dev/stderr"
     if (env_id in closure_env_sizes) {
         for (i = 0; i < closure_env_sizes[env_id]; i++) {
             if ((env_id, i) in closure_env_names) {
                 env_name[env_size] = closure_env_names[env_id, i]
                 env_val[env_size] = closure_env_vals[env_id, i]
                 env_size++
-                debug("Restored: " closure_env_names[env_id, i] " = " closure_env_vals[env_id, i])
+                if (DEBUG) print "[DEBUG_CLOSURE]   Restored: " closure_env_names[env_id, i] " = " closure_env_vals[env_id, i] > "/dev/stderr"
             }
         }
     }
@@ -931,7 +927,7 @@ function vm_define_function(name, start_pc) {
 }
 
 # Function call implementation
-function vm_call_function(code_lines, j, saved_pc, saved_env_size, arg, param_name) {
+function vm_call_function(code_lines, j, saved_pc, saved_env_size, arg, param_name, param_names, nparams, k) {
     # Get function name from stack
     func_name = pop()
     debug("Calling function: " func_name)
@@ -1152,62 +1148,73 @@ function vm_call_function(code_lines, j, saved_pc, saved_env_size, arg, param_na
     saved_pc = pc
     saved_env_size = env_size
 
-    # AWK FEATURE: split(string, array, separator) splits string into array elements
-    # Unlike JS string.split() which returns an array, this populates an existing array
     split(FUNCTIONS[func_name], code_lines, "\n")
 
-    # Check if this is a parameterized function
-    if (code_lines[1] ~ /^STORE /) {
-        # This is a parameterized function (lambda)
-        # Get parameter name from STORE instruction
-        param_name = substr(code_lines[1], 7)
-        debug("Found parameter name: " param_name)
-        debug("FUNCTION_PARAM: " param_name)
-
-        # Get argument from stack
-        arg = pop()
-        debug("Function argument: " arg)
-        debug("FUNCTION_ARG: " arg)
-
-        # Create new environment frame
-        debug("Creating new environment frame at size: " env_size)
-        env_name[env_size] = param_name
-        env_val[env_size] = arg
-        env_size++
-        debug("FUNCTION_ENV_STORE: " param_name " = " arg " at index " (env_size-1))
-
-        # Execute function code directly, skipping STORE and POP_ENV instructions
-        for (j = 2; j <= length(code_lines); j++) {
+    # --- Multi-parameter function support ---
+    # Collect all leading STORE instructions as parameter names
+    nparams = 0
+    for (j = 1; j <= length(code_lines); j++) {
+        if (code_lines[j] ~ /^STORE /) {
+            nparams++
+            param_names[nparams] = substr(code_lines[j], 7)
+        } else {
+            break
+        }
+    }
+    if (nparams > 0) {
+        debug("Function parameters: " nparams)
+        # Pop arguments in reverse order so first argument is bound to first param
+        for (k = nparams; k >= 1; k--) {
+            arg = pop()
+            param_name = param_names[k]
+            debug("Binding argument to parameter: " param_name " = " arg)
+            env_name[env_size] = param_name
+            env_val[env_size] = arg
+            env_size++
+            debug("FUNCTION_ENV_STORE: " param_name " = " arg " at index " (env_size-1))
+        }
+        # Execute function code, skipping STORE and POP_ENV instructions
+        for (j = nparams + 1; j <= length(code_lines); j++) {
             if (code_lines[j] != "" && code_lines[j] != "POP_ENV") {
                 debug("Executing function instruction: " code_lines[j])
                 execute(code_lines[j])
             }
         }
-
-        # Clean up parameter
-        vm_pop_env()
-
-        # Return to caller
+        # Clean up parameters
+        for (k = 1; k <= nparams; k++) {
+            vm_pop_env()
+        }
         debug("Function completed, returning to PC: " saved_pc)
-        pc = saved_pc
-        return
-    } else {
-        # This is a built-in function or non-parameterized function
-        debug("Calling non-parameterized function: " func_name)
-
-        # Execute all function code directly
-        for (j in code_lines) {
-            if (code_lines[j] != "") {
-                debug("Executing function instruction: " code_lines[j])
-                execute(code_lines[j])
+        if (DEBUG) {
+            if (stack_ptr > 0) {
+                debug("[DEBUG_VM] Value on stack after CALL: " stack[stack_ptr-1])
+            } else {
+                debug("[DEBUG_VM] Stack is empty after CALL")
             }
         }
-
-        # Return to caller
-        debug("Function completed, returning to PC: " saved_pc)
         pc = saved_pc
         return
     }
+    # --- End multi-parameter support ---
+
+    # This is a built-in function or non-parameterized function
+    debug("Calling non-parameterized function: " func_name)
+    for (j in code_lines) {
+        if (code_lines[j] != "") {
+            debug("Executing function instruction: " code_lines[j])
+            execute(code_lines[j])
+        }
+    }
+    debug("Function completed, returning to PC: " saved_pc)
+    if (DEBUG) {
+        if (stack_ptr > 0) {
+            debug("[DEBUG_VM] Value on stack after CALL: " stack[stack_ptr-1])
+        } else {
+            debug("[DEBUG_VM] Stack is empty after CALL")
+        }
+    }
+    pc = saved_pc
+    return
 }
 
 # Function call with argument count implementation
diff --git a/awk/scheme/scheme/test/run_tests.sh b/awk/scheme/scheme/test/run_tests.sh
index 4e7d181..b32d57b 100755
--- a/awk/scheme/scheme/test/run_tests.sh
+++ b/awk/scheme/scheme/test/run_tests.sh
@@ -68,13 +68,23 @@ run_test() {
     
     if [ "$DEBUG" = "1" ]; then
         # Run with debug output
-        output=$(DEBUG=1 "$SCHEME_BIN" "$test_file" 2>&1)
+        output=$(env DEBUG=1 timeout 15s stdbuf -oL "$SCHEME_BIN" "$test_file" 2>&1)
         exit_code=$?
     else
         # Run normally
-        output=$("$SCHEME_BIN" "$test_file" 2>&1)
+        output=$(timeout 15s stdbuf -oL "$SCHEME_BIN" "$test_file" 2>&1)
         exit_code=$?
     fi
+
+    # Check for timeout (exit code 124)
+    if [ $exit_code -eq 124 ]; then
+        print_status "FAIL" "$test_name (timeout)"
+        FAILED_TESTS=$((FAILED_TESTS + 1))
+        FAILED_TEST_NAMES+=("$rel_test_file")
+        echo "Error: Test timed out after 15 seconds."
+        echo
+        return 1
+    fi
     
     # Check if test passed (exit code 0)
     if [ $exit_code -eq 0 ]; then
diff --git a/awk/scheme/scheme/test/unit/minimal_closure_env.scm b/awk/scheme/scheme/test/unit/minimal_closure_env.scm
new file mode 100644
index 0000000..a7de816
--- /dev/null
+++ b/awk/scheme/scheme/test/unit/minimal_closure_env.scm
@@ -0,0 +1,8 @@
+(define make-outer
+  (lambda (x)
+    (lambda (y)
+      (let ((x (+ x y)))
+        (lambda (z) (+ x y z))))))
+
+(define closure1 ((make-outer 10) 5))
+(closure1 2) 
\ No newline at end of file
diff --git a/awk/scheme/scheme/test/unit/minimal_function_persistence.scm b/awk/scheme/scheme/test/unit/minimal_function_persistence.scm
new file mode 100644
index 0000000..c6825e4
--- /dev/null
+++ b/awk/scheme/scheme/test/unit/minimal_function_persistence.scm
@@ -0,0 +1,2 @@
+(define add2 (lambda (x) (+ x 2)))
+(add2 40) 
\ No newline at end of file
diff --git a/awk/scheme/scheme/test/unit/minimal_global_persistence.scm b/awk/scheme/scheme/test/unit/minimal_global_persistence.scm
new file mode 100644
index 0000000..7b75b28
--- /dev/null
+++ b/awk/scheme/scheme/test/unit/minimal_global_persistence.scm
@@ -0,0 +1,2 @@
+(define foo 42)
+foo 
\ No newline at end of file
diff --git a/js/seed/README.md b/js/seed/README.md
index 8159cb3..981ca7d 100644
--- a/js/seed/README.md
+++ b/js/seed/README.md
@@ -1,6 +1,6 @@
 # Seed: Minimal FRP/TEA Web App Starter Kit
 
-This is an opinionated, minimal starting point for browser-native web apps using a functional, Elm-style architecture (FRP/TEA) and only browser APIs. No frameworks, no build step, just ES modules.
+This is an opinionated, hopefully simple starting point for browser-native web apps using a functional, Elm-style architecture (FRP/TEA) and only browser APIs. No rulers, no kings, no frameworks, no build step, only ES modules.
 
 ## Architecture
 - **state.js**: App state definition and helpers
@@ -15,14 +15,9 @@ This is an opinionated, minimal starting point for browser-native web apps using
 - **View**: Pure function `(state) => html`
 - **Entrypoint**: Handles events, dispatches actions, triggers re-render
 
-## Why?
-- Simple, testable, and maintainable
-- No dependencies
-- Encourages functional, declarative code
-
 ## How to Extend and Use This Template
 
-This template is designed to be a flexible, opinionated starting point for any browser-native app.
+This template is designed to be a flexible, opinionated starting point for any kinda app, especially proofs of concept, toys, and prototypes.
 
 ### Key Files to Extend
 - **src/state.js**: Define the app's state shape and any helper functions for cloning or initializing state.
@@ -69,9 +64,9 @@ Suppose you want to add a button that increments a counter:
      ```
 
 ### Tips
-- Keep all state transitions in `update.js` for predictability.
-- Keep all DOM rendering in `view.js` for clarity.
-- Use the `postRender` hook for accessibility or focus management.
+- Keep all state transitions in `update.js`.
+- Keep all DOM rendering in `view.js`.
+- Use the `postRender` hook for accessibility or focus management stuff.
 - Add new features by extending state, update, view, and wiring up events in `app.js`.
 
 ---
diff --git a/js/seed/seed b/js/seed/seed
new file mode 100755
index 0000000..15276e7
--- /dev/null
+++ b/js/seed/seed
@@ -0,0 +1,75 @@
+#!/bin/bash
+
+set -euo pipefail
+
+# Usage: seed plant
+
+if [ "$#" -ne 1 ] || [ "$1" != "plant" ]; then
+  echo "Usage: $0 plant"
+  exit 1
+fi
+
+if [ "$EUID" -eq 0 ]; then
+  echo "Do not run this script as root."
+  exit 1
+fi
+
+if ! command -v git >/dev/null 2>&1; then
+  echo "Warning: git is not installed. You won't be able to initialize a git repo."
+fi
+
+SRC_DIR="$(cd "$(dirname "$0")" && pwd)"
+
+read -rp "Enter new project name: " DEST_DIR
+
+if [ -z "$DEST_DIR" ]; then
+  echo "Project name cannot be empty."
+  exit 1
+fi
+
+DEST_PATH="$PWD/$DEST_DIR"
+
+if [ -e "$DEST_PATH" ]; then
+  echo "Error: '$DEST_PATH' already exists."
+  exit 1
+fi
+
+cleanup() {
+  if [ -d "$DEST_PATH" ]; then
+    echo "Cleaning up partial directory..."
+    rm -rf "$DEST_PATH"
+  fi
+}
+trap cleanup INT TERM
+
+echo "Copying seed template to '$DEST_PATH'..."
+mkdir "$DEST_PATH"
+
+if command -v rsync >/dev/null 2>&1; then
+  rsync -a --exclude='.git' --exclude='seed' "$SRC_DIR/" "$DEST_PATH/"
+else
+  cp -r "$SRC_DIR/"* "$DEST_PATH/"
+  cp -r "$SRC_DIR/".* "$DEST_PATH/" 2>/dev/null || true
+  rm -rf "$DEST_PATH/.git" "$DEST_PATH/seed"
+fi
+
+cd "$DEST_PATH"
+
+# Optionally, update README
+if [ -f README.md ]; then
+  sed -i '' "1s/.*/# $DEST_DIR/" README.md 2>/dev/null || sed -i "1s/.*/# $DEST_DIR/" README.md
+fi
+
+echo "Initialized new project in '$DEST_PATH'."
+
+read -rp "Do you want to initialize a git repository? (y/n): " INIT_GIT
+if [[ "$INIT_GIT" =~ ^[Yy]$ ]]; then
+  git init
+  echo "Git repository initialized."
+else
+  echo "Skipping git initialization."
+fi
+
+echo "Next steps:"
+echo "  cd \"$DEST_PATH\""
+echo "  and tend to the seed you've planted..." 
\ No newline at end of file
diff --git a/js/seed/src/app.js b/js/seed/src/app.js
index 34b4579..49ad9d1 100644
--- a/js/seed/src/app.js
+++ b/js/seed/src/app.js
@@ -155,11 +155,3 @@ function setHistoryPointer(idx) {
     updateHistoryInfo();
   }
 }
-
-function handleSliderChange(e) {
-  setHistoryPointer(Number(e.target.value));
-}
-
-function handleStepperChange(e) {
-  setHistoryPointer(Number(e.target.value));
-} 
\ No newline at end of file
diff --git a/js/seed/src/dev.js b/js/seed/src/dev.js
index ee1a6e7..173fc3c 100644
--- a/js/seed/src/dev.js
+++ b/js/seed/src/dev.js
@@ -1,8 +1,8 @@
 // devMode.js
-// Minimal, single-file dev mode with scriptable console API
+// Simpleish, single-file dev mode with interactive console API
 
 /**
- * Initialize dev mode: exposes a scriptable API for stepping through state history.
+ * Initialize dev mode: exposes an API for stepping through state history.
  * @param {object} opts
  * @param {function} opts.getState - returns current app state
  * @param {function} opts.setState - sets app state
diff --git a/js/seed/src/view.js b/js/seed/src/view.js
index 5feef6e..4c6e680 100644
--- a/js/seed/src/view.js
+++ b/js/seed/src/view.js
@@ -4,10 +4,10 @@
 /**
  * Pure view functions for the application.
  *
- * Why pure functions returning HTML strings?
+ * Why pure functions returning HTML strings? Because Elm does it, tbh.
  * - Keeps rendering logic stateless and easy to test.
- * - Ensures the UI is always a direct function of state, avoiding UI bugs from incremental DOM updates.
- * - Using template literals is minimal and browser-native, with no dependencies.
+ * - Ensures the UI is always a direct function of state, which should in theory totally avoid bugs from incremental DOM updates.
+ * - Using template literals is minimal and browser-native, with no dependencies, and is fun.
  *
  * Why escape output?
  * - Prevents XSS and ensures all user/content data is safely rendered.