about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--awk/scheme/scheme/WHAT-NEEDS-FIXING.md71
-rwxr-xr-xawk/scheme/scheme/bin/compiler.awk302
-rw-r--r--awk/uxn/.gitignore3
-rw-r--r--awk/uxn/README.md0
-rw-r--r--awk/uxn/awk/uxnasm.awk916
-rw-r--r--awk/uxn/ref/uxnasm.c481
-rwxr-xr-xawk/uxn/test/runner.sh105
-rw-r--r--awk/uxn/test/tal/brk.tal1
-rw-r--r--awk/uxn/test/tal/brk_test.tal1
-rw-r--r--awk/uxn/test/tal/brk_with_data.tal1
-rw-r--r--awk/uxn/test/tal/bunnymark.tal221
-rw-r--r--awk/uxn/test/tal/life.tal221
-rw-r--r--awk/uxn/test/tal/opctest.tal492
-rw-r--r--awk/uxn/test/tal/proper.tal1
-rw-r--r--awk/uxn/test/tal/simple.tal1
-rw-r--r--awk/uxn/test/tal/simple2.tal1
-rw-r--r--awk/uxn/test/tal/simple3.tal1
17 files changed, 2596 insertions, 223 deletions
diff --git a/awk/scheme/scheme/WHAT-NEEDS-FIXING.md b/awk/scheme/scheme/WHAT-NEEDS-FIXING.md
index 99dbfd8..f8b5a0c 100644
--- a/awk/scheme/scheme/WHAT-NEEDS-FIXING.md
+++ b/awk/scheme/scheme/WHAT-NEEDS-FIXING.md
@@ -3,54 +3,47 @@
 ## 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.
+  - Test runner and environment variable handling are robust; DEBUG/DEBUG_SEXPR work as intended.
+  - Only 6/51 tests pass; 45 fail, including basic, integration, closure, and higher-order function tests.
+  - Most failures are silent (no assertion errors/output), indicating expressions are not being evaluated or CALLs are mis-emitted.
+
+- **Recent Fixes and Attempts:**
+  - Refactored top-level CALL emission logic in the compiler to match idiomatic Scheme/Lisp behavior:
+    - Now, CALL is only emitted for top-level compound expressions whose first symbol is not a special form (define, let, lambda, if, cond, and, or, not).
+    - Special forms are handled by the compiler, not as function calls.
+  - Helper function added to extract the first symbol from a compound expression string.
+  - Type-tracking logic for top-level CALL emission has been removed for simplicity and robustness.
+  - This approach is modeled after working reference implementations (e.g., [maryrosecook/littlelisp](https://raw.githubusercontent.com/maryrosecook/littlelisp/refs/heads/master/littlelisp.js)).
+
+- **Current Symptoms:**
+  - Many tests still fail, including basic arithmetic, map, closures, and higher-order functions.
+  - Failures are mostly silent: no assertion errors, no output, suggesting expressions are not being evaluated or results are not printed.
+  - Some improvement: a few more tests pass compared to previous attempts, but the majority still fail.
 
 ## 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
+- The VM and test runner are not the source of the remaining failures.
+- S-expression parsing and body handling are robust and not the source of the bug.
+- The new CALL emission logic is more correct, but not sufficient to fix all test failures.
+- The bug is not due to missing CALLs for assert/display/user-defined function calls at the top level.
 
-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.
+## Next Steps: Plan for Remaining Bugs
 
-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.
+1. **Targeted Debugging of Failing Tests:**
+   - Focus on a representative failing test (e.g., basic_numeric_operations, closures, or a simple assert).
+   - Inspect the generated assembly and VM output for these tests to confirm whether CALL is being emitted and executed as expected.
+   - Check for missing PRINT/DISPLAY or incorrect stack state after CALL.
 
-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.
+2. **Special Form Handling Review:**
+   - Ensure that all special forms (let, lambda, if, cond, etc.) are handled correctly and do not result in spurious CALLs or missed evaluation.
+   - Confirm that nested expressions within special forms are compiled and evaluated as expected.
 
-4. **Test and Iterate:**
-   - After each fix, re-run the minimal closure/currying tests and confirm progress.
+3. **Test and Iterate:**
+   - After each fix, re-run the minimal and representative tests to confirm progress.
    - Once minimal tests pass, move to more complex closure and higher-order function tests.
 
-5. **Document Findings:**
+4. **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.
+- Ensure top-level and nested expression evaluation is correct for all cases, including special forms, closures, and function applications.
 - 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 864b19c..dec4c22 100755
--- a/awk/scheme/scheme/bin/compiler.awk
+++ b/awk/scheme/scheme/bin/compiler.awk
@@ -87,14 +87,11 @@ END {
 function split_expressions(prog, current, paren_count, i, c, expr, cleaned, lines, n, line, in_string, out, j) {
     current = ""
     paren_count = 0
-    # Improved comment removal: process line by line
     n = split(prog, lines, "\n")
     out = ""
     for (j = 1; j <= n; j++) {
         line = lines[j]
-        # Skip lines that start with ';' (comments)
         if (line ~ /^[ \t]*;/) continue
-        # Remove inline comments, but not inside strings
         in_string = 0
         cleaned_line = ""
         for (i = 1; i <= length(line); i++) {
@@ -103,101 +100,104 @@ function split_expressions(prog, current, paren_count, i, c, expr, cleaned, line
             if (!in_string && c == ";") break
             cleaned_line = cleaned_line c
         }
-        # Append cleaned line
         out = out cleaned_line "\n"
     }
     cleaned = out
     debug("Cleaned program: [" cleaned "]")
     if (cleaned == "") return
-    
     if (cleaned == "") return
-    
-    # Parse expressions by tracking parenthesis nesting and string literals
-    # This approach ensures that parentheses inside strings don't affect
-    # expression boundaries, and that comments are properly handled
-    # AWK FEATURE: length(string) returns the length of a string
-    # Unlike JS string.length, this is a function call, not a property
-    in_string = 0  # Track if we're inside a string literal
-    
+    in_string = 0
     for (i = 1; i <= length(cleaned); i++) {
         c = substr(cleaned, i, 1)
-        
-        # Handle string literals
         if (c == "\"" && !in_string) {
             in_string = 1
             if (paren_count == 0) current = ""
         } else if (c == "\"" && in_string) {
             in_string = 0
         }
-        
         if (c == "(" && !in_string) {
             if (paren_count == 0) current = ""
             paren_count++
         }
-        
         current = current c
-        
         if (c == ")" && !in_string) {
             paren_count--
             if (paren_count == 0) {
-                # Complete expression found - compile it
                 expr = current
                 sub(/^\s+/, "", expr)
                 sub(/\s+$/, "", expr)
-                
                 debug("Processing expression: [" expr "]")
-                program = expr  # Set for parser
+                program = expr
+                expr_str = expr
                 expr = parse_expr()
-                compile_expr(expr)
-                # Clear stack between expressions to prevent pollution
-                print "CLEAR_STACK"  # Clear stack between expressions
+                if (substr(expr_str, 1, 1) == "(") {
+                    op = extract_first_symbol(expr_str)
+                    if (op != "define" && op != "let" && op != "lambda" && op != "if" && op != "cond" && op != "and" && op != "or" && op != "not") {
+                        compile_expr(expr)
+                        print "CALL"
+                    } else {
+                        compile_expr(expr)
+                    }
+                } else {
+                    compile_expr(expr)
+                }
+                print "CLEAR_STACK"
                 current = ""
             }
         }
-        
-        # Handle atomic expressions (not in parentheses or strings)
         if (paren_count == 0 && !in_string && c == " " && current != "") {
-            # We've reached a space after an atomic expression
             expr = current
             sub(/^\s+/, "", expr)
             sub(/\s+$/, "", expr)
-            
             if (expr != "") {
                 debug("Processing atomic expression: [" expr "]")
-                program = expr  # Set for parser
+                program = expr
+                expr_str = expr
                 expr = parse_expr()
-                compile_expr(expr)
-                # Clear stack between expressions to prevent pollution
-                print "CLEAR_STACK"  # Clear stack between expressions
+                if (substr(expr_str, 1, 1) == "(") {
+                    op = extract_first_symbol(expr_str)
+                    if (op != "define" && op != "let" && op != "lambda" && op != "if" && op != "cond" && op != "and" && op != "or" && op != "not") {
+                        compile_expr(expr)
+                        print "CALL"
+                    } else {
+                        compile_expr(expr)
+                    }
+                } else {
+                    compile_expr(expr)
+                }
+                print "CLEAR_STACK"
             }
             current = ""
         }
     }
-    
-    # Handle the last expression if it's atomic
     if (paren_count == 0 && !in_string && current != "") {
         expr = current
         sub(/^\s+/, "", expr)
         sub(/\s+$/, "", expr)
-        
         if (expr != "") {
             debug("Processing final atomic expression: [" expr "]")
-            program = expr  # Set for parser
+            program = expr
+            expr_str = expr
             expr = parse_expr()
-            compile_expr(expr)
-            # Clear stack after the final expression
+            if (substr(expr_str, 1, 1) == "(") {
+                op = extract_first_symbol(expr_str)
+                if (op != "define" && op != "let" && op != "lambda" && op != "if" && op != "cond" && op != "and" && op != "or" && op != "not") {
+                    compile_expr(expr)
+                    print "CALL"
+                } else {
+                    compile_expr(expr)
+                }
+            } else {
+                compile_expr(expr)
+            }
             print "CLEAR_STACK"
         }
     }
-    
-    # Check for incomplete expressions
     if (paren_count > 0) {
         debug("paren_count at end of split_expressions: " paren_count)
         error("Unmatched opening parentheses - incomplete expression")
         exit 1
     }
-    
-    # Add final HALT instruction
     print "HALT"
 }
 
@@ -448,35 +448,23 @@ function compile_primitive_call(op, args, arg_array, nargs, i) {
     debug("Primitive call: op=" op " args=" args)
     nargs = split_args(args, arg_array)
     
-    # Check if this is a lambda function call
-    # AWK FEATURE: ~ is the regex match operator (like /pattern/.test() in JS)
-    # The pattern is a regex literal, not a string
     if (op ~ /^\(lambda /) {
-        # This is a lambda function call
-        # First compile all arguments
         for (i = 1; i <= nargs; i++) {
             compile_expr(arg_array[i])
         }
-        
-        # Then compile the lambda function (this will push the function name)
         compile_expr(op)
-        
-        # Call the function - the lambda name is now on top of stack
         print "CALL"
-        return
+        return "function"
     }
-    
-    # Then emit appropriate operation
     if (op == "+") {
-        # Compile arguments
         for (i = 1; i <= nargs; i++) {
             compile_expr(arg_array[i])
         }
         for (i = 1; i < nargs; i++)
             print "ADD"
+        return "value"
     }
     else if (op == "-") {
-        # Compile arguments
         for (i = 1; i <= nargs; i++) {
             compile_expr(arg_array[i])
         }
@@ -486,14 +474,15 @@ function compile_primitive_call(op, args, arg_array, nargs, i) {
         }
         for (i = 1; i < nargs; i++)
             print "SUB"
+        return "value"
     }
     else if (op == "*") {
-        # Compile arguments
         for (i = 1; i <= nargs; i++) {
             compile_expr(arg_array[i])
         }
         for (i = 1; i < nargs; i++)
             print "MUL"
+        return "value"
     }
     else if (op == "/") {
         if (nargs < 2) error("/ requires at least 2 arguments")
@@ -502,6 +491,7 @@ function compile_primitive_call(op, args, arg_array, nargs, i) {
         }
         for (i = 1; i < nargs; i++)
             print "DIV"
+        return "value"
     }
     else if (op == "modulo" || op == "%") {
         if (nargs != 2) error("modulo requires 2 arguments")
@@ -511,6 +501,7 @@ function compile_primitive_call(op, args, arg_array, nargs, i) {
         print "LOOKUP modulo"
         print "GET_VALUE"
         print "CALL"
+        return "value"
     }
     else if (op == "expt") {
         if (nargs != 2) error("expt requires 2 arguments")
@@ -520,6 +511,7 @@ function compile_primitive_call(op, args, arg_array, nargs, i) {
         print "LOOKUP expt"
         print "GET_VALUE"
         print "CALL"
+        return "value"
     }
     else if (op == "abs") {
         if (nargs != 1) error("abs requires 1 argument")
@@ -527,6 +519,7 @@ function compile_primitive_call(op, args, arg_array, nargs, i) {
         print "LOOKUP abs"
         print "GET_VALUE"
         print "CALL"
+        return "value"
     }
     else if (op == "min") {
         if (nargs != 2) error("min requires 2 arguments")
@@ -536,6 +529,7 @@ function compile_primitive_call(op, args, arg_array, nargs, i) {
         print "LOOKUP min"
         print "GET_VALUE"
         print "CALL"
+        return "value"
     }
     else if (op == "max") {
         if (nargs != 2) error("max requires 2 arguments")
@@ -545,42 +539,43 @@ function compile_primitive_call(op, args, arg_array, nargs, i) {
         print "LOOKUP max"
         print "GET_VALUE"
         print "CALL"
+        return "value"
     }
     else if (op == "cons") {
         if (nargs != 2) error("cons requires 2 arguments")
-        # Compile arguments
         for (i = 1; i <= nargs; i++) {
             compile_expr(arg_array[i])
         }
         print "CONS"
+        return "value"
     }
     else if (op == "car") {
         if (nargs != 1) error("car requires 1 argument")
-        # Compile argument
         compile_expr(arg_array[1])
         print "CAR"
+        return "value"
     }
     else if (op == "cdr") {
         if (nargs != 1) error("cdr requires 1 argument")
-        # Compile argument
         compile_expr(arg_array[1])
         print "CDR"
+        return "value"
     }
     else if (op == "<") {
         if (nargs != 2) error("< requires 2 arguments")
-        # Compile arguments
         for (i = 1; i <= nargs; i++) {
             compile_expr(arg_array[i])
         }
         print "LT"
+        return "value"
     }
     else if (op == "=") {
         if (nargs != 2) error("= requires 2 arguments")
-        # Compile arguments
         for (i = 1; i <= nargs; i++) {
             compile_expr(arg_array[i])
         }
         print "EQ"
+        return "value"
     }
     # Standard library functions
     else if (op == "null?") {
@@ -589,6 +584,7 @@ function compile_primitive_call(op, args, arg_array, nargs, i) {
         print "LOOKUP null?"
         print "GET_VALUE"
         print "CALL"
+        return "value"
     }
     else if (op == "pair?") {
         if (nargs != 1) error("pair? requires 1 argument")
@@ -596,6 +592,7 @@ function compile_primitive_call(op, args, arg_array, nargs, i) {
         print "LOOKUP pair?"
         print "GET_VALUE"
         print "CALL"
+        return "value"
     }
     else if (op == "number?") {
         if (nargs != 1) error("number? requires 1 argument")
@@ -603,6 +600,7 @@ function compile_primitive_call(op, args, arg_array, nargs, i) {
         print "LOOKUP number?"
         print "GET_VALUE"
         print "CALL"
+        return "value"
     }
     else if (op == "string?") {
         if (nargs != 1) error("string? requires 1 argument")
@@ -610,6 +608,7 @@ function compile_primitive_call(op, args, arg_array, nargs, i) {
         print "LOOKUP string?"
         print "GET_VALUE"
         print "CALL"
+        return "value"
     }
     else if (op == "boolean?") {
         if (nargs != 1) error("boolean? requires 1 argument")
@@ -617,6 +616,7 @@ function compile_primitive_call(op, args, arg_array, nargs, i) {
         print "LOOKUP boolean?"
         print "GET_VALUE"
         print "CALL"
+        return "value"
     }
     else if (op == "symbol?") {
         if (nargs != 1) error("symbol? requires 1 argument")
@@ -624,6 +624,7 @@ function compile_primitive_call(op, args, arg_array, nargs, i) {
         print "LOOKUP symbol?"
         print "GET_VALUE"
         print "CALL"
+        return "value"
     }
     else if (op == "zero?") {
         if (nargs != 1) error("zero? requires 1 argument")
@@ -631,68 +632,7 @@ function compile_primitive_call(op, args, arg_array, nargs, i) {
         print "LOOKUP zero?"
         print "GET_VALUE"
         print "CALL"
-    }
-    else if (op == "positive?") {
-        if (nargs != 1) error("positive? requires 1 argument")
-        compile_expr(arg_array[1])
-        print "LOOKUP positive?"
-        print "GET_VALUE"
-        print "CALL"
-    }
-    else if (op == "negative?") {
-        if (nargs != 1) error("negative? requires 1 argument")
-        compile_expr(arg_array[1])
-        print "LOOKUP negative?"
-        print "GET_VALUE"
-        print "CALL"
-    }
-    else if (op == "length") {
-        if (nargs != 1) error("length requires 1 argument")
-        compile_expr(arg_array[1])
-        print "LOOKUP length"
-        print "GET_VALUE"
-        print "CALL"
-    }
-    else if (op == "cadr") {
-        if (nargs != 1) error("cadr requires 1 argument")
-        compile_expr(arg_array[1])
-        print "LOOKUP cadr"
-        print "GET_VALUE"
-        print "CALL"
-    }
-    else if (op == "caddr") {
-        if (nargs != 1) error("caddr requires 1 argument")
-        compile_expr(arg_array[1])
-        print "LOOKUP caddr"
-        print "GET_VALUE"
-        print "CALL"
-    }
-    else if (op == "list-ref") {
-        if (nargs != 2) error("list-ref requires 2 arguments")
-        for (i = 1; i <= nargs; i++) {
-            compile_expr(arg_array[i])
-        }
-        print "LOOKUP list-ref"
-        print "GET_VALUE"
-        print "CALL"
-    }
-    else if (op == "list-tail") {
-        if (nargs != 2) error("list-tail requires 2 arguments")
-        for (i = 1; i <= nargs; i++) {
-            compile_expr(arg_array[i])
-        }
-        print "LOOKUP list-tail"
-        print "GET_VALUE"
-        print "CALL"
-    }
-    else if (op == "append") {
-        if (nargs != 2) error("append requires 2 arguments")
-        for (i = 1; i <= nargs; i++) {
-            compile_expr(arg_array[i])
-        }
-        print "LOOKUP append"
-        print "GET_VALUE"
-        print "CALL"
+        return "value"
     }
     else if (op == "list") {
         for (i = 1; i <= nargs; i++) {
@@ -701,6 +641,7 @@ function compile_primitive_call(op, args, arg_array, nargs, i) {
         print "LOOKUP list"
         print "GET_VALUE"
         print "CALL_WITH_ARGS " nargs
+        return "value"
     }
     else if (op == "reverse") {
         if (nargs != 1) error("reverse requires 1 argument")
@@ -710,6 +651,7 @@ function compile_primitive_call(op, args, arg_array, nargs, i) {
         print "LOOKUP reverse"
         print "GET_VALUE"
         print "CALL"
+        return "value"
     }
     else if (op == "member") {
         if (nargs != 2) error("member requires 2 arguments")
@@ -719,6 +661,7 @@ function compile_primitive_call(op, args, arg_array, nargs, i) {
         print "LOOKUP member"
         print "GET_VALUE"
         print "CALL"
+        return "value"
     }
     else if (op == "map") {
         if (nargs != 2) error("map requires 2 arguments")
@@ -728,6 +671,7 @@ function compile_primitive_call(op, args, arg_array, nargs, i) {
         print "LOOKUP map"
         print "GET_VALUE"
         print "CALL"
+        return "value"
     }
     else if (op == "filter") {
         if (nargs != 2) error("filter requires 2 arguments")
@@ -737,14 +681,15 @@ function compile_primitive_call(op, args, arg_array, nargs, i) {
         print "LOOKUP filter"
         print "GET_VALUE"
         print "CALL"
+        return "value"
     }
-    # String operations
     else if (op == "string-length") {
         if (nargs != 1) error("string-length requires 1 argument")
         compile_expr(arg_array[1])
         print "LOOKUP string-length"
         print "GET_VALUE"
         print "CALL"
+        return "value"
     }
     else if (op == "string-append") {
         if (nargs < 2) error("string-append requires at least 2 arguments")
@@ -754,60 +699,23 @@ function compile_primitive_call(op, args, arg_array, nargs, i) {
         print "LOOKUP string-append"
         print "GET_VALUE"
         print "CALL_WITH_ARGS " nargs
+        return "value"
     }
-    else if (op == "string-ref") {
-        if (nargs != 2) error("string-ref requires 2 arguments")
-        for (i = 1; i <= nargs; i++) {
-            compile_expr(arg_array[i])
-        }
-        print "LOOKUP string-ref"
-        print "GET_VALUE"
-        print "CALL"
-    }
-    else if (op == "substring") {
-        if (nargs != 3) error("substring requires 3 arguments")
-        for (i = 1; i <= nargs; i++) {
-            compile_expr(arg_array[i])
-        }
-        print "LOOKUP substring"
-        print "GET_VALUE"
-        print "CALL"
-    }
-    else if (op == "string=?") {
-        if (nargs != 2) error("string=? requires 2 arguments")
+    else if (op == "assert" || op == "display" || op == "error" || op == "print") {
         for (i = 1; i <= nargs; i++) {
             compile_expr(arg_array[i])
         }
-        print "LOOKUP string=?"
-        print "GET_VALUE"
-        print "CALL"
-    }
-    else if (op == "string<?") {
-        if (nargs != 2) error("string<? requires 2 arguments")
-        for (i = 1; i <= nargs; i++) {
-            compile_expr(arg_array[i])
-        }
-        print "LOOKUP string<?"
-        print "GET_VALUE"
-        print "CALL"
-    }
-    else if (op == "string>?") {
-        if (nargs != 2) error("string>? requires 2 arguments")
-        for (i = 1; i <= nargs; i++) {
-            compile_expr(arg_array[i])
-        }
-        print "LOOKUP string>?"
+        print "LOOKUP " op
         print "GET_VALUE"
         print "CALL"
+        return "function"
     }
     else {
         # 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])
         }
-        # 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)
@@ -815,8 +723,8 @@ function compile_primitive_call(op, args, arg_array, nargs, i) {
             print "LOOKUP " op
             print "GET_VALUE"
         }
-        # Call the function
         print "CALL"
+        return "function"
     }
 }
 
@@ -863,8 +771,7 @@ 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, sexprs, nsexprs, j, expr) {
-    # Split into bindings and body
+function compile_let(args, bindings, body, binding_array, nbindings, i, var, val, binding_parts, sexprs, nsexprs, j, expr, last_type) {
     if (substr(args, 1, 1) != "(") error("Malformed let expression")
     paren_count = 1
     i = 2
@@ -901,7 +808,6 @@ function compile_let(args, bindings, body, binding_array, nbindings, i, var, val
             print "STORE " var
         }
     }
-    # --- 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"
@@ -909,17 +815,18 @@ function compile_let(args, bindings, body, binding_array, nbindings, i, var, val
             printf("[DEBUG_SEXPR]   %d: [%s]\n", j, sexprs[j]) > "/dev/stderr"
         }
     }
+    last_type = "value"
     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)
+        last_type = compile_expr(expr)
     }
-    # --- End robust let body support ---
     for (i = nbindings; i >= 1; i--) {
         print "POP_ENV"
     }
+    return last_type
 }
 
 # Compiles define expressions (function/variable definitions)
@@ -1306,54 +1213,54 @@ function compile_not(args,    expr) {
 }
 
 # Main expression compiler - dispatches based on expression type
-function compile_expr(expr,    split_result, op, args) {
+function compile_expr(expr,    split_result, op, args, result_type) {
     if (DEBUG_SEXPR) print "[DEBUG_SEXPR] compile_expr called with expr: [" expr "]" > "/dev/stderr"
     debug("Compiling expression: " expr)
     
     # Handle empty expressions
     if (expr == "") {
         debug("Skipping empty expression")
-        return
+        return "value"
     }
     
     # Handle comment lines
     if (expr ~ /^[ \t]*;;/ || expr ~ /^[ \t]*;/) {
         debug("Skipping comment line: [" expr "]")
-        return
+        return "value"
     }
     
     # Handle string literals
     if (substr(expr, 1, 1) == "\"") {
         compile_string(expr)
-        return
+        return "value"
     }
     
     # Handle numeric literals
     if (expr ~ /^-?[0-9]+$/) {
         compile_number(expr)
-        return
+        return "value"
     }
     
     # Handle nil constant
     if (expr == "nil") {
         print "PUSH_CONST NIL:"
-        return
+        return "value"
     }
     
     # Handle boolean literals
     if (expr == "#t") {
         print "PUSH_CONST B:1"
-        return
+        return "value"
     }
     if (expr == "#f") {
         print "PUSH_CONST B:0"
-        return
+        return "value"
     }
     
     # Handle variable lookup (only if not a parenthesized expression)
     if (expr ~ /^[a-zA-Z_][a-zA-Z0-9_?-]*$/) {
         print "LOOKUP " expr
-        return
+        return "value"
     }
     
     # Handle compound expressions (lists)
@@ -1365,27 +1272,37 @@ function compile_expr(expr,    split_result, op, args) {
         if (DEBUG_SEXPR) print "[DEBUG_SEXPR] split_expr op: [" op "] args: [" args "]" > "/dev/stderr"
         if (op == "define") {
             compile_define(args)
+            return "value"
         } else if (op == "let") {
-            compile_let(args)
+            result_type = compile_let(args)
+            return result_type
         } else if (op == "lambda") {
             compile_lambda(args)
+            return "function"
         } else if (op == "if") {
             compile_if(args)
+            # TODO: Could be value or function, but usually value
+            return "value"
         } else if (op == "cond") {
             compile_cond(args)
+            # TODO: Could be value or function, but usually value
+            return "value"
         } else if (op == "and") {
             compile_and(args)
+            return "value"
         } else if (op == "or") {
             compile_or(args)
+            return "value"
         } else if (op == "not") {
             compile_not(args)
+            return "value"
         } else {
-            compile_primitive_call(op, args)
+            return compile_primitive_call(op, args)
         }
-        return
     }
     
     error("Unknown expression type: " expr)
+    return "value"
 }
 
 # Error reporting helper
@@ -1448,3 +1365,20 @@ function split_sexpressions(str, sexpr_array, i, c, in_string, paren_count, curr
     }
     return n
 }
+
+# Helper: Extract first symbol from a compound expression string
+function extract_first_symbol(expr_str, op) {
+    # Assumes expr_str starts with '('
+    op = ""
+    i = 2
+    # Skip whitespace after '('
+    while (i <= length(expr_str) && (substr(expr_str, i, 1) == " " || substr(expr_str, i, 1) == "\t")) i++
+    # Read until next whitespace or ')'
+    while (i <= length(expr_str)) {
+        c = substr(expr_str, i, 1)
+        if (c == " " || c == "\t" || c == ")") break
+        op = op c
+        i++
+    }
+    return op
+}
diff --git a/awk/uxn/.gitignore b/awk/uxn/.gitignore
new file mode 100644
index 0000000..f71ddea
--- /dev/null
+++ b/awk/uxn/.gitignore
@@ -0,0 +1,3 @@
+**/out/
+**/uxnasm
+
diff --git a/awk/uxn/README.md b/awk/uxn/README.md
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/awk/uxn/README.md
diff --git a/awk/uxn/awk/uxnasm.awk b/awk/uxn/awk/uxnasm.awk
new file mode 100644
index 0000000..cfcdd00
--- /dev/null
+++ b/awk/uxn/awk/uxnasm.awk
@@ -0,0 +1,916 @@
+#!/usr/bin/awk -f
+
+# Uxntal Assembler in AWK - Two-Pass Implementation
+
+BEGIN {
+    # Constants
+    PAGE = 256
+    MAX_LABELS = 1024
+    MAX_REFS = 4096
+    
+    # Global state
+    ptr = PAGE
+    data_length = PAGE
+    
+    # Label and reference tracking
+    labels_len = 0
+    refs_len = 0
+    macros_len = 0
+    
+    # Device tracking
+    devices_len = 0
+    last_padding = ""
+    last_size = 0
+    current_device = ""
+    
+    # Lambda tracking
+    lambda_len = 0
+    lambda_ptr = 0
+    
+    # Opcode table
+    split("LIT INC POP NIP SWP ROT DUP OVR EQU NEQ GTH LTH JMP JCN JSR STH LDZ STZ LDR STR LDA STA DEI DEO ADD SUB MUL DIV AND ORA EOR SFT", ops)
+    
+    # Utility strings
+    hexad = "0123456789abcdef"
+    
+    # Check arguments
+    if (ARGC < 3) {
+        printf "usage: uxnasm.awk [-v] input.tal output.rom\n" > "/dev/stderr"
+        exit 1
+    }
+    
+    if (ARGC == 3 && substr(ARGV[1], 1, 2) == "-v") {
+        printf "Uxnasm.awk\n"
+        exit 0
+    }
+    
+    if (ARGC != 3) {
+        printf "usage: uxnasm.awk [-v] input.tal output.rom\n" > "/dev/stderr"
+        exit 1
+    }
+    
+    input_file = ARGV[1]
+    output_file = ARGV[2]
+    
+    # Remove output file from ARGV so AWK doesn't try to read it
+    ARGV[2] = ""
+    
+    # Two-pass assembly
+    if (!pass1_collect_symbols(input_file)) {
+        exit 1
+    }
+    
+    if (!pass2_generate_code(input_file)) {
+        exit 1
+    }
+    
+    if (!build_output(output_file)) {
+        exit 1
+    }
+    
+    printf "Assembled %s in %d bytes(%.2f%% used), %d labels, %d refs.\n",
+           output_file, data_length - PAGE, (data_length - PAGE) / 652.80, labels_len, refs_len
+}
+
+# Utility functions
+function remove_comments(line,    result, i, c, depth) {
+    # Remove comments from a line
+    # Comments are delimited by ( and ) and can be nested
+    result = ""
+    depth = 0
+    
+    for (i = 1; i <= length(line); i++) {
+        c = substr(line, i, 1)
+        if (c == "(") {
+            depth++
+        } else if (c == ")") {
+            depth--
+        } else if (depth == 0) {
+            result = result c
+        }
+    }
+    
+    # Trim whitespace
+    gsub(/^[ \t]+|[ \t]+$/, "", result)
+    return result
+}
+
+function shex(s,    d, n, c, i) {
+    n = 0
+    for (i = 1; i <= length(s); i++) {
+        c = substr(s, i, 1)
+        d = index(hexad, c) - 1
+        if (d < 0) return -1
+        n = n * 16 + d
+    }
+    return n
+}
+
+function ishex(x) {
+    return shex(x) >= 0
+}
+
+function findopcode(s,    i, m, base, c) {
+    # Special case for BRK
+    if (s == "BRK") return 0
+    
+    for (i = 1; i <= 32; i++) {
+        if (substr(ops[i], 1, 3) == substr(s, 1, 3)) {
+            base = i - 1
+            if (i == 1) base = base + 128  # LIT special case
+            
+            m = 4
+            while (m <= length(s)) {
+                c = substr(s, m, 1)
+                if (c == "2")
+                    base = base + 32
+                else if (c == "r")
+                    base = base + 64
+                else if (c == "k")
+                    base = base + 128
+                else
+                    return -1
+                m++
+            }
+            return base
+        }
+    }
+    return -1
+}
+
+function isopc(x) {
+    return findopcode(x) >= 0 || x == "BRK"
+}
+
+function makelabel(name, setscope) {
+    if (labels_len >= MAX_LABELS) {
+        printf "Labels limit exceeded\n" > "/dev/stderr"
+        return 0
+    }
+    
+    labels_len++
+    labels[labels_len, "name"] = name
+    labels[labels_len, "addr"] = ptr
+    labels[labels_len, "refs"] = 0
+    
+    printf "DEBUG: Created label '%s' at addr %d\n", name, ptr > "/dev/stderr"
+    
+    return 1
+}
+
+function findlabel(name,    i) {
+    for (i = 1; i <= labels_len; i++) {
+        if (labels[i, "name"] == name) {
+            return i
+        }
+    }
+    return 0
+}
+
+function makeref(label, rune, addr) {
+    if (refs_len >= MAX_REFS) {
+        printf "References limit exceeded\n" > "/dev/stderr"
+        return 0
+    }
+    
+    refs_len++
+    refs[refs_len, "name"] = label
+    refs[refs_len, "rune"] = rune
+    refs[refs_len, "addr"] = addr
+    
+    return 1
+}
+
+function makedevice(name, base_addr) {
+    if (devices_len >= MAX_LABELS) {
+        printf "Devices limit exceeded\n" > "/dev/stderr"
+        return 0
+    }
+    
+    devices_len++
+    devices[devices_len, "name"] = name
+    devices[devices_len, "base"] = base_addr
+    devices[devices_len, "fields_len"] = 0
+    
+    return 1
+}
+
+function adddevicefield(device_name, field_name, size) {
+    # Find device
+    for (i = 1; i <= devices_len; i++) {
+        if (devices[i, "name"] == device_name) {
+            devices[i, "fields_len"]++
+            devices[i, "fields", devices[i, "fields_len"], "name"] = field_name
+            devices[i, "fields", devices[i, "fields_len"], "size"] = size
+            return 1
+        }
+    }
+    return 0
+}
+
+function finddevicefield(device_name, field_name,    i, j) {
+    for (i = 1; i <= devices_len; i++) {
+        if (devices[i, "name"] == device_name) {
+            addr = devices[i, "base"]
+            for (j = 1; j <= devices[i, "fields_len"]; j++) {
+                if (devices[i, "fields", j, "name"] == field_name) {
+                    return addr
+                }
+                addr += devices[i, "fields", j, "size"]
+            }
+        }
+    }
+    return -1
+}
+
+# --- PASS 1: Symbol/Label Collection ---
+function pass1_collect_symbols(filename) {
+    ptr = PAGE
+    data_length = PAGE
+    
+    while ((getline < filename) > 0) {
+        pass1_process_line($0)
+    }
+    close(filename)
+    
+    return 1
+}
+
+function pass1_process_line(line_text,    tokens, i, token, j) {
+    line_text = remove_comments(line_text)
+    if (line_text == "") return 1
+    
+    # Custom tokenization to handle quoted strings properly
+    tokens_len = 0
+    i = 1
+    while (i <= length(line_text)) {
+        c = substr(line_text, i, 1)
+        if (c == " " || c == "\t") {
+            i++
+            continue
+        }
+        
+        if (c == "\"") {
+            # Handle quoted string - capture everything until closing quote
+            token = "\""
+            i++
+            while (i <= length(line_text) && substr(line_text, i, 1) != "\"") {
+                token = token substr(line_text, i, 1)
+                i++
+            }
+            if (i <= length(line_text)) {
+                token = token "\""
+                i++
+            }
+            tokens[++tokens_len] = token
+        } else {
+            # Regular token - capture until whitespace
+            token = ""
+            while (i <= length(line_text) && substr(line_text, i, 1) != " " && substr(line_text, i, 1) != "\t") {
+                token = token substr(line_text, i, 1)
+                i++
+            }
+            if (token != "") {
+                tokens[++tokens_len] = token
+            }
+        }
+    }
+    
+    # Combine - tokens with / (like -Screen/pixel)
+    for (i = 1; i < tokens_len; i++) {
+        if (tokens[i] == "-" && index(tokens[i+1], "/") > 0) {
+            tokens[i] = tokens[i] tokens[i+1]
+            for (j = i + 1; j < tokens_len; j++) {
+                tokens[j] = tokens[j+1]
+            }
+            tokens_len--
+        }
+    }
+    
+    for (i = 1; i <= tokens_len; i++) {
+        token = tokens[i]
+        printf "DEBUG: pass1 processing token: '%s'\n", token > "/dev/stderr"
+        if (!pass1_parse_token(token)) {
+            printf "ERROR: Failed to parse token '%s' at line %d\n", token, line_number > "/dev/stderr"
+            return 0
+        }
+    }
+    return 1
+}
+
+function pass1_parse_token(w) {
+    
+                # Skip standalone tokens
+            if (w == ">" || w == "-") {
+                return 1
+            }
+    
+    # Handle device definitions and labels
+    if (substr(w, 1, 1) == "@") {
+        printf "DEBUG: Processing @ token: %s\n", w > "/dev/stderr"
+        # Check if this is a macro definition (labels starting with @<)
+        printf "DEBUG: Checking macro condition: substr(w, 2, 1)='%s', substr(w, length(w), 1)='%s'\n", substr(w, 2, 1), substr(w, length(w), 1) > "/dev/stderr"
+        printf "DEBUG: Condition check: '%s' == '<' && '%s' == '>' = %s\n", substr(w, 2, 1), substr(w, length(w), 1), (substr(w, 2, 1) == "<" && substr(w, length(w), 1) == ">") > "/dev/stderr"
+        if (substr(w, 2, 1) == "<" && substr(w, length(w), 1) == ">") {
+            printf "DEBUG: Found macro definition: %s\n", w > "/dev/stderr"
+            makemacro(substr(w, 3, length(w) - 3))
+            return 1
+        }
+        
+        # Check if this is a device definition (has base address)
+        if (last_padding != "" && current_device == "") {
+            makedevice(substr(w, 2), shex(last_padding))
+            current_device = substr(w, 2)
+            last_padding = ""  # Reset after device definition
+        } else {
+            makelabel(substr(w, 2), 1)
+        }
+        return 1
+    }
+    
+    # Handle device fields
+    if (substr(w, 1, 1) == "&") {
+        if (current_device != "") {
+            adddevicefield(current_device, substr(w, 2), last_size)
+        } else {
+            makelabel(w, 0)
+        }
+        return 1
+    }
+    
+    # Skip brackets and control flow
+    if (substr(w, 1, 1) == "[" || substr(w, 1, 1) == "]" || w == "{") {
+        return 1
+    }
+    
+    # Handle lambda labels
+    if (w == "}") {
+        makelabel(makelambda(lambda_len++))
+        return 1
+    }
+    
+    # Handle padding and size
+    if (substr(w, 1, 1) == "|") {
+        last_padding = substr(w, 2)
+        # Set pointer based on padding value (no writing, just positioning)
+        if (last_padding == "0000") {
+            ptr = 0
+        } else if (last_padding == "0100") {
+            ptr = PAGE
+        } else {
+            ptr = shex(last_padding)
+        }
+        return 1
+    }
+    if (substr(w, 1, 1) == "$") {
+        last_size = shex(substr(w, 2))
+        # Advance pointer by size (no writing, just positioning)
+        ptr += last_size
+        return 1
+    }
+    
+    # Handle references (just collect them, don't resolve yet)
+    if (substr(w, 1, 1) == "_") {
+        makeref(substr(w, 2), substr(w, 1, 1), ptr)
+        ptr++
+        return 1
+    }
+    if (substr(w, 1, 1) == ",") {
+        makeref(substr(w, 2), substr(w, 1, 1), ptr + 1)
+        ptr += 2
+        return 1
+    }
+    if (substr(w, 1, 1) == "-") {
+        # Check if this is a device field reference
+        if (index(substr(w, 2), "/") > 0) {
+            # Device field reference - just advance pointer (will be resolved in pass2)
+            ptr++
+        } else {
+            makeref(substr(w, 2), substr(w, 1, 1), ptr)
+            ptr++
+        }
+        return 1
+    }
+    if (substr(w, 1, 1) == ".") {
+        # Check if this is a device field reference
+        if (index(substr(w, 2), "/") > 0) {
+            # Device field reference - just advance pointer
+            ptr += 2
+        } else {
+            makeref(substr(w, 2), substr(w, 1, 1), ptr + 1)
+            ptr += 2
+        }
+        return 1
+    }
+    if (substr(w, 1, 1) == "=") {
+        makeref(substr(w, 2), substr(w, 1, 1), ptr)
+        ptr += 2
+        return 1
+    }
+    if (substr(w, 1, 1) == ";") {
+        makeref(substr(w, 2), substr(w, 1, 1), ptr + 1)
+        ptr += 3
+        return 1
+    }
+    if (substr(w, 1, 1) == "?") {
+        makeref(substr(w, 2), substr(w, 1, 1), ptr + 1)
+        ptr += 3
+        return 1
+    }
+    if (substr(w, 1, 1) == "!") {
+        makeref(substr(w, 2), substr(w, 1, 1), ptr + 1)
+        ptr += 3
+        return 1
+    }
+    
+    # Handle hex literals (with # prefix or raw hex values)
+    if (substr(w, 1, 1) == "#") {
+        if (length(substr(w, 2)) > 2) {
+            ptr += 3  # LIT2 + 2 bytes
+        } else {
+            ptr += 2  # LIT + 1 byte
+        }
+        return 1
+    }
+    
+    # Handle raw hex values (like font data)
+    if (ishex(w)) {
+        if (length(w) > 2) {
+            ptr += 3  # LIT2 + 2 bytes
+        } else {
+            ptr += 2  # LIT + 1 byte
+        }
+        return 1
+    }
+    
+    # Handle opcodes
+    if (isopc(w)) {
+        ptr++
+        return 1
+    }
+    
+    # Handle strings
+    if (substr(w, 1, 1) == "\"") {
+        ptr += length(substr(w, 2))
+        return 1
+    }
+    
+    # Handle macro definitions (labels starting with @<)
+    if (substr(w, 1, 1) == "@" && substr(w, 2, 1) == "<" && substr(w, length(w), 1) == ">") {
+        makemacro(substr(w, 3, length(w) - 3))
+        return 1
+    }
+    
+    # Handle macro calls (tokens starting with <)
+    if (substr(w, 1, 1) == "<" && substr(w, length(w), 1) == ">") {
+        # Just advance pointer in pass1, will be expanded in pass2
+        ptr += 1  # Placeholder - actual size depends on macro content
+        return 1
+    }
+    
+    # Handle unknown tokens as label references (fallback)
+    makeref(w, " ", ptr + 1)
+    ptr += 3  # LIT2 + 2 bytes
+    return 1
+}
+
+# --- PASS 2: Code Generation ---
+function pass2_generate_code(filename) {
+    ptr = PAGE
+    data_length = PAGE
+    
+    while ((getline < filename) > 0) {
+        pass2_process_line($0)
+    }
+    close(filename)
+    
+    return 1
+}
+
+function pass2_process_line(line_text,    tokens, i, token, j) {
+    line_text = remove_comments(line_text)
+    if (line_text == "") return 1
+    
+    # Custom tokenization to handle quoted strings properly
+    tokens_len = 0
+    i = 1
+    while (i <= length(line_text)) {
+        c = substr(line_text, i, 1)
+        if (c == " " || c == "\t") {
+            i++
+            continue
+        }
+        
+        if (c == "\"") {
+            # Handle quoted string - capture everything until closing quote
+            token = "\""
+            i++
+            while (i <= length(line_text) && substr(line_text, i, 1) != "\"") {
+                token = token substr(line_text, i, 1)
+                i++
+            }
+            if (i <= length(line_text)) {
+                token = token "\""
+                i++
+            }
+            tokens[++tokens_len] = token
+        } else {
+            # Regular token - capture until whitespace
+            token = ""
+            while (i <= length(line_text) && substr(line_text, i, 1) != " " && substr(line_text, i, 1) != "\t") {
+                token = token substr(line_text, i, 1)
+                i++
+            }
+            if (token != "") {
+                tokens[++tokens_len] = token
+            }
+        }
+    }
+    
+    # Combine - tokens with / (like -Screen/pixel)
+    for (i = 1; i < tokens_len; i++) {
+        if (tokens[i] == "-" && index(tokens[i+1], "/") > 0) {
+            tokens[i] = tokens[i] tokens[i+1]
+            for (j = i + 1; j < tokens_len; j++) {
+                tokens[j] = tokens[j+1]
+            }
+            tokens_len--
+        }
+    }
+    
+    for (i = 1; i <= tokens_len; i++) {
+        token = tokens[i]
+        if (!pass2_parse_token(token)) {
+            printf "ERROR: Failed to parse token '%s' at line %d\n", token, line_number > "/dev/stderr"
+            return 0
+        }
+    }
+    return 1
+}
+
+function pass2_parse_token(w) {
+    printf "DEBUG: pass2_parse_token processing '%s'\n", w > "/dev/stderr"
+    
+    # Skip standalone tokens (but not device field references)
+    if (w == ">") {
+        return 1
+    }
+    
+    # Handle labels (just skip, already collected in pass 1)
+    if (substr(w, 1, 1) == "@" || substr(w, 1, 1) == "&") {
+        return 1
+    }
+    
+    # Skip brackets and control flow
+    if (substr(w, 1, 1) == "[" || substr(w, 1, 1) == "]" || w == "{") {
+        return 1
+    }
+    
+    # Handle lambda labels (just skip, already collected in pass 1)
+    if (w == "}") {
+        return 1
+    }
+    
+    # Handle padding
+    if (substr(w, 1, 1) == "|") {
+        # Set pointer based on padding value (no writing, just positioning)
+        padding_val = substr(w, 2)
+        if (padding_val == "0000") {
+            ptr = 0
+        } else if (padding_val == "0100") {
+            ptr = PAGE
+        } else {
+            ptr = shex(padding_val)
+        }
+        return 1
+    }
+    if (substr(w, 1, 1) == "$") {
+        # Advance pointer by size (no writing, just positioning)
+        size_val = shex(substr(w, 2))
+        ptr += size_val
+        return 1
+    }
+    
+    # Handle references (resolve them now)
+    if (substr(w, 1, 1) == "_") {
+        resolve_ref(substr(w, 2), substr(w, 1, 1), ptr) && writebyte(0xff)
+        return 1
+    }
+    if (substr(w, 1, 1) == ",") {
+        resolve_ref(substr(w, 2), substr(w, 1, 1), ptr + 1) && writebyte(128) && writebyte(0xff)
+        return 1
+    }
+    if (substr(w, 1, 1) == "-") {
+        # Device field reference
+        if (index(substr(w, 2), "/") > 0) {
+            resolve_device_ref(substr(w, 2), ptr)
+            writebyte(0xff)
+        } else {
+            resolve_ref(substr(w, 2), substr(w, 1, 1), ptr)
+            writebyte(0xff)
+        }
+        return 1
+    }
+    if (substr(w, 1, 1) == ".") {
+        # Check if this is a device field reference
+        if (index(substr(w, 2), "/") > 0) {
+            resolve_device_ref(substr(w, 2), ptr + 1) && writebyte(128) && writebyte(0xff)
+        } else {
+            resolve_ref(substr(w, 2), substr(w, 1, 1), ptr + 1) && writebyte(128) && writebyte(0xff)
+        }
+        return 1
+    }
+    if (substr(w, 1, 1) == "=") {
+        resolve_ref(substr(w, 2), substr(w, 1, 1), ptr) && writeshort(0xffff)
+        return 1
+    }
+    if (substr(w, 1, 1) == ";") {
+        resolve_ref(substr(w, 2), substr(w, 1, 1), ptr + 1) && writebyte(160) && writeshort(0xffff)
+        return 1
+    }
+    if (substr(w, 1, 1) == "?") {
+        resolve_ref(substr(w, 2), substr(w, 1, 1), ptr + 1) && writebyte(32) && writeshort(0xffff)
+        return 1
+    }
+    if (substr(w, 1, 1) == "!") {
+        resolve_ref(substr(w, 2), substr(w, 1, 1), ptr + 1) && writebyte(64) && writeshort(0xffff)
+        return 1
+    }
+    
+    # Handle hex literals (with # prefix or raw hex values)
+    if (substr(w, 1, 1) == "#") {
+        writehex(w)
+        return 1
+    }
+    
+    # Handle raw hex values (like font data)
+    if (ishex(w)) {
+        writehex(w)
+        return 1
+    }
+    
+    # Handle opcodes
+    if (isopc(w)) {
+        writebyte(findopcode(w))
+        return 1
+    }
+    
+    # Handle string literals
+    if (substr(w, 1, 1) == "\"") {
+        writestring(substr(w, 2, length(w) - 2))
+        return 1
+    }
+    
+    # Handle macro calls (tokens starting with <)
+    if (substr(w, 1, 1) == "<" && substr(w, length(w), 1) == ">") {
+        expandmacro(substr(w, 2, length(w) - 2))
+        return 1
+    }
+    
+    # Handle unknown tokens as label references (fallback)
+    printf "DEBUG: Unknown token '%s' treated as label reference\n", w > "/dev/stderr"
+    makeref(w, " ", ptr + 1)
+    ptr += 3  # LIT2 + 2 bytes
+    return 1
+}
+
+function resolve_ref(label, rune, addr,    l, rel) {
+    l = findlabel(label)
+    if (l == 0) {
+        printf "Label unknown: %s\n", label > "/dev/stderr"
+        return 0
+    }
+    
+    # Resolve based on reference type
+    if (rune == "_" || rune == ",") {
+        rel = labels[l, "addr"] - addr - 2
+        data[addr] = rel
+    } else if (rune == "-" || rune == ".") {
+        data[addr] = labels[l, "addr"]
+    } else if (rune == "=" || rune == ";") {
+        data[addr] = int(labels[l, "addr"] / 256)
+        data[addr + 1] = labels[l, "addr"] % 256
+    } else if (rune == "?" || rune == "!") {
+        rel = labels[l, "addr"] - addr - 2
+        data[addr] = int(rel / 256)
+        data[addr + 1] = rel % 256
+    }
+    
+    labels[l, "refs"]++
+    return 1
+}
+
+function resolve_device_ref(device_field, addr,    device_name, field_name, device_addr) {
+    # Split device/field
+    split(device_field, parts, "/")
+    if (length(parts) != 2) {
+        printf "Invalid device field: %s\n", device_field > "/dev/stderr"
+        return 0
+    }
+    
+    device_name = parts[1]
+    field_name = parts[2]
+    
+    device_addr = finddevicefield(device_name, field_name)
+    if (device_addr == -1) {
+        printf "Device field unknown: %s\n", device_field > "/dev/stderr"
+        return 0
+    }
+    
+    data[addr] = device_addr
+    return 1
+}
+
+function writebyte(b) {
+    if (ptr >= 65536) {
+        printf "Writing outside memory\n" > "/dev/stderr"
+        return 0
+    }
+    
+    # Only write to data array if we're in the code section (ptr >= PAGE)
+    if (ptr >= PAGE) {
+        data[ptr] = b
+        if (b) {
+            data_length = ptr
+        }
+        printf "DEBUG: writebyte(%d) at ptr %d, data_length now %d\n", b, ptr, data_length > "/dev/stderr"
+    }
+    ptr++
+    return 1
+}
+
+function writeshort(x) {
+    return writebyte(int(x / 256)) && writebyte(x % 256)
+}
+
+function writehex(w) {
+    if (substr(w, 1, 1) == "#") {
+        # Write LIT opcode
+        if (length(substr(w, 2)) > 2) {
+            writebyte(32)  # LIT2
+        } else {
+            writebyte(128)   # LIT (BRK + 128)
+        }
+        w = substr(w, 2)
+    }
+    
+    if (ishex(w)) {
+        if (length(w) == 2) {
+            return writebyte(shex(w))
+        } else if (length(w) == 4) {
+            return writeshort(shex(w))
+        }
+    }
+    
+    printf "Hexadecimal invalid: %s\n", w > "/dev/stderr"
+    return 0
+}
+
+# Macro functions
+function findmacro(name,    i) {
+    for (i = 0; i < macros_len; i++) {
+        if (macro_names[i] == name) {
+            return i
+        }
+    }
+    return -1
+}
+
+function makemacro(name,    i) {
+    printf "DEBUG: makemacro called with name: %s\n", name > "/dev/stderr"
+    if (macros_len >= 256) {
+        printf "Macros limit exceeded\n" > "/dev/stderr"
+        return 0
+    }
+    if (findmacro(name) >= 0) {
+        printf "Macro duplicate: %s\n", name > "/dev/stderr"
+        return 0
+    }
+    if (findlabel(name) >= 0) {
+        printf "Label duplicate: %s\n", name > "/dev/stderr"
+        return 0
+    }
+    
+    # Store macro name and initialize empty body
+    macro_names[macros_len] = name
+    macro_data[macros_len] = ""
+    macros_len++
+    
+    # Note: We'll capture the macro body in pass2 when we process the file again
+    return 1
+}
+
+function capture_macro_body(name, filename,    line, in_macro, macro_body, depth) {
+    # Reset file to beginning
+    close(filename)
+    in_macro = 0
+    macro_body = ""
+    depth = 0
+    
+    while ((getline line < filename) > 0) {
+        if (in_macro) {
+            # Check if we've reached the end of the macro (next label or closing brace)
+            if (substr(line, 1, 1) == "@" && substr(line, 2, 1) != "|") {
+                # Found next label, end of macro
+                break
+            }
+            
+            # Count braces for nested macro handling
+            for (i = 1; i <= length(line); i++) {
+                c = substr(line, i, 1)
+                if (c == "{") depth++
+                else if (c == "}") {
+                    depth--
+                    if (depth < 0) break  # End of macro
+                }
+            }
+            
+            if (depth < 0) break  # End of macro
+            
+            # Add line to macro body
+            macro_body = macro_body line "\n"
+        } else if (substr(line, 1, 1) == "@" && substr(line, 2, 1) == "<") {
+            # Check if this is our macro
+            macro_name = substr(line, 3)
+            if (substr(macro_name, length(macro_name), 1) == ">") {
+                macro_name = substr(macro_name, 1, length(macro_name) - 1)
+                if (macro_name == name) {
+                    in_macro = 1
+                    # Start capturing from next line
+                }
+            }
+        }
+    }
+    
+    close(filename)
+    
+    # Store the macro body
+    for (i = 0; i < macros_len; i++) {
+        if (macro_names[i] == name) {
+            macro_data[i] = macro_body
+            return 1
+        }
+    }
+    return 0
+}
+
+function expandmacro(name,    macro_idx, macro_text, tokens, i) {
+    macro_idx = findmacro(name)
+    if (macro_idx < 0) {
+        printf "Macro not found: %s\n", name > "/dev/stderr"
+        return 0
+    }
+    
+    # If macro body is empty, try to capture it
+    if (macro_data[macro_idx] == "") {
+        capture_macro_body(name, ARGV[1])
+    }
+    
+    macro_text = macro_data[macro_idx]
+    if (macro_text == "") {
+        printf "Macro body empty: %s\n", name > "/dev/stderr"
+        return 0
+    }
+    
+    # Process macro body line by line
+    split(macro_text, lines, "\n")
+    for (i = 1; i <= length(lines); i++) {
+        if (lines[i] != "") {
+            pass2_process_line(lines[i])
+        }
+    }
+    return 1
+}
+
+# Lambda functions
+function makelambda(id) {
+    # Create a unique lambda name like "λb" where suffix is hex digit
+    return sprintf("λ%c", substr(hexad, int(id / 16) + 1, 1) substr(hexad, (id % 16) + 1, 1))
+}
+
+function ord(c) {
+    return index(" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", c) + 31
+}
+
+function writestring(w,    i, c) {
+    for (i = 1; i <= length(w); i++) {
+        c = substr(w, i, 1)
+        # Simple ASCII conversion
+        if (!writebyte(ord(c))) {
+            printf "String invalid\n" > "/dev/stderr"
+            return 0
+        }
+    }
+    return 1
+}
+
+function build_output(rompath) {
+    # Write ROM file
+    printf "DEBUG: Writing %d bytes from PAGE (%d) to data_length (%d)\n", data_length - PAGE + 1, PAGE, data_length > "/dev/stderr"
+    for (i = PAGE; i <= data_length; i++) {
+        printf "%c", data[i] > rompath
+    }
+    close(rompath)
+    
+    return 1
+}
diff --git a/awk/uxn/ref/uxnasm.c b/awk/uxn/ref/uxnasm.c
new file mode 100644
index 0000000..f25d6ce
--- /dev/null
+++ b/awk/uxn/ref/uxnasm.c
@@ -0,0 +1,481 @@
+#include <stdio.h>
+
+/*
+Copyright (c) 2021-2024 Devine Lu Linvega, Andrew Alderwick
+
+Permission to use, copy, modify, and distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE.
+*/
+
+/* clang-format off */
+
+#define PAGE 0x0100
+#define ishex(x) (shex(x) >= 0)
+#define isopc(x) (findopcode(x) || scmp(x, "BRK", 4))
+#define isinvalid(x) (!x[0] || ishex(x) || isopc(x) || find(runes, x[0]) >= 0)
+#define writeshort(x) (writebyte(x >> 8, ctx) && writebyte(x & 0xff, ctx))
+#define findlabel(x) finditem(x, labels, labels_len)
+#define findmacro(x) finditem(x, macros, macro_len)
+#define error_top(id, msg) !printf("%s: %s\n", id, msg)
+#define error_asm(id) !printf("%s: %s in @%s, %s:%d.\n", id, token, scope, ctx->path, ctx->line)
+#define error_ref(id) !printf("%s: %s, %s:%d\n", id, r->name, r->data, r->line)
+
+typedef unsigned char Uint8;
+typedef signed char Sint8;
+typedef unsigned short Uint16;
+typedef struct { int line; char *path; } Context;
+typedef struct { char *name, rune, *data; Uint16 addr, refs, line; } Item;
+
+static int ptr, length;
+static char token[0x30], scope[0x40], lambda[0x05];
+static char dict[0x8000], *dictnext = dict;
+static Uint8 data[0x10000], lambda_stack[0x100], lambda_ptr, lambda_len;
+static Uint16 labels_len, refs_len, macro_len;
+static Item labels[0x400], refs[0x1000], macros[0x100];
+
+static char *runes = "|$@&,_.-;=!?#\"%~";
+static char *hexad = "0123456789abcdef";
+static char ops[][4] = {
+	"LIT", "INC", "POP", "NIP", "SWP", "ROT", "DUP", "OVR",
+	"EQU", "NEQ", "GTH", "LTH", "JMP", "JCN", "JSR", "STH",
+	"LDZ", "STZ", "LDR", "STR", "LDA", "STA", "DEI", "DEO",
+	"ADD", "SUB", "MUL", "DIV", "AND", "ORA", "EOR", "SFT"
+};
+
+/* clang-format on */
+
+static int
+find(char *s, char t)
+{
+	int i = 0;
+	char c;
+	while((c = *s++)) {
+		if(c == t) return i;
+		i++;
+	}
+	return -1;
+}
+
+static int
+shex(char *s)
+{
+	int d, n = 0;
+	char c;
+	while((c = *s++)) {
+		d = find(hexad, c);
+		if(d < 0) return d;
+		n = n << 4, n |= d;
+	}
+	return n;
+}
+
+static int
+scmp(char *a, char *b, int len)
+{
+	int i = 0;
+	while(a[i] == b[i])
+		if(!a[i] || ++i >= len) return 1;
+	return 0;
+}
+
+static char *
+copy(char *src, char *dst, char c)
+{
+	while(*src && *src != c) *dst++ = *src++;
+	*dst++ = 0;
+	return dst;
+}
+
+static char *
+save(char *s, char c)
+{
+	char *o = dictnext;
+	while((*dictnext++ = *s++) && *s);
+	*dictnext++ = c;
+	return o;
+}
+
+static char *
+join(char *a, char j, char *b)
+{
+	char *res = dictnext;
+	save(a, j), save(b, 0);
+	return res;
+}
+
+static char *
+push(char *s, char c)
+{
+	char *d;
+	for(d = dict; d < dictnext; d++) {
+		char *ss = s, *dd = d, a, b;
+		while((a = *dd++) == (b = *ss++))
+			if(!a && !b) return d;
+	}
+	return save(s, c);
+}
+
+static Item *
+finditem(char *name, Item *list, int len)
+{
+	int i;
+	if(name[0] == '&')
+		name = join(scope, '/', name + 1);
+	for(i = 0; i < len; i++)
+		if(scmp(list[i].name, name, 0x40))
+			return &list[i];
+	return NULL;
+}
+
+static Uint8
+findopcode(char *s)
+{
+	int i;
+	for(i = 0; i < 0x20; i++) {
+		int m = 3;
+		if(!scmp(ops[i], s, 3)) continue;
+		if(!i) i |= (1 << 7);
+		while(s[m]) {
+			if(s[m] == '2')
+				i |= (1 << 5);
+			else if(s[m] == 'r')
+				i |= (1 << 6);
+			else if(s[m] == 'k')
+				i |= (1 << 7);
+			else
+				return 0;
+			m++;
+		}
+		return i;
+	}
+	return 0;
+}
+
+static int
+walkcomment(FILE *f, Context *ctx)
+{
+	char c, last = 0;
+	int depth = 1;
+	while(f && fread(&c, 1, 1, f)) {
+		if(c <= 0x20) {
+			if(c == 0xa) ctx->line++;
+			if(last == '(')
+				depth++;
+			else if(last == ')' && --depth < 1)
+				return 1;
+			last = 0;
+		} else if(last <= 0x20)
+			last = c;
+		else
+			last = '~';
+	}
+	return error_asm("Comment incomplete");
+}
+
+static int parse(char *w, FILE *f, Context *ctx);
+
+static int
+walkmacro(Item *m, Context *ctx)
+{
+	unsigned char c;
+	char *dataptr = m->data, *_token = token;
+	while((c = *dataptr++)) {
+		if(c < 0x21) {
+			*_token = 0x00;
+			if(token[0] && !parse(token, NULL, ctx)) return 0;
+			_token = token;
+		} else if(_token - token < 0x2f)
+			*_token++ = c;
+		else
+			return error_asm("Token size exceeded");
+	}
+	return 1;
+}
+
+static int
+walkfile(FILE *f, Context *ctx)
+{
+	unsigned char c;
+	char *_token = token;
+	while(f && fread(&c, 1, 1, f)) {
+		if(c < 0x21) {
+			*_token = 0x00;
+			if(token[0] && !parse(token, f, ctx)) return 0;
+			if(c == 0xa) ctx->line++;
+			_token = token;
+		} else if(_token - token < 0x2f)
+			*_token++ = c;
+		else
+			return error_asm("Token size exceeded");
+	}
+	*_token = 0;
+	return parse(token, f, ctx);
+}
+
+static char *
+makelambda(int id)
+{
+	lambda[0] = (char)0xce;
+	lambda[1] = (char)0xbb;
+	lambda[2] = hexad[id >> 0x4];
+	lambda[3] = hexad[id & 0xf];
+	return lambda;
+}
+
+static int
+makemacro(char *name, FILE *f, Context *ctx)
+{
+	int depth = 0;
+	char c;
+	Item *m;
+	if(macro_len >= 0x100) return error_asm("Macros limit exceeded");
+	if(isinvalid(name)) return error_asm("Macro invalid");
+	if(findmacro(name)) return error_asm("Macro duplicate");
+	if(findlabel(name)) return error_asm("Label duplicate");
+	m = &macros[macro_len++];
+	m->name = push(name, 0);
+	m->data = dictnext;
+	while(f && fread(&c, 1, 1, f) && c != '{')
+		if(c == 0xa) ctx->line++;
+	while(f && fread(&c, 1, 1, f)) {
+		if(c == 0xa) ctx->line++;
+		if(c == '%') return error_asm("Macro nested");
+		if(c == '{') depth++;
+		if(c == '}' && --depth) break;
+		*dictnext++ = c;
+	}
+	*dictnext++ = 0;
+	return 1;
+}
+
+static int
+makelabel(char *name, int setscope, Context *ctx)
+{
+	Item *l;
+	if(name[0] == '&')
+		name = join(scope, '/', name + 1);
+	if(labels_len >= 0x400) return error_asm("Labels limit exceeded");
+	if(isinvalid(name)) return error_asm("Label invalid");
+	if(findmacro(name)) return error_asm("Label duplicate");
+	if(findlabel(name)) return error_asm("Label duplicate");
+	l = &labels[labels_len++];
+	l->name = push(name, 0);
+	l->addr = ptr;
+	l->refs = 0;
+	if(setscope) copy(name, scope, '/');
+	return 1;
+}
+
+static int
+makeref(char *label, char rune, Uint16 addr, Context *ctx)
+{
+	Item *r;
+	if(refs_len >= 0x1000) return error_asm("References limit exceeded");
+	r = &refs[refs_len++];
+	if(label[0] == '{') {
+		lambda_stack[lambda_ptr++] = lambda_len;
+		r->name = push(makelambda(lambda_len++), 0);
+		if(label[1]) return error_asm("Label invalid");
+	} else if(label[0] == '&' || label[0] == '/') {
+		r->name = join(scope, '/', label + 1);
+	} else
+		r->name = push(label, 0);
+	r->rune = rune;
+	r->addr = addr;
+	r->line = ctx->line;
+	r->data = ctx->path;
+	return 1;
+}
+
+static int
+writepad(char *w, Context *ctx)
+{
+	Item *l;
+	int rel = w[0] == '$' ? ptr : 0;
+	if(ishex(w + 1)) {
+		ptr = shex(w + 1) + rel;
+		return 1;
+	}
+	if((l = findlabel(w + 1))) {
+		ptr = l->addr + rel;
+		return 1;
+	}
+	return error_asm("Padding invalid");
+}
+
+static int
+writebyte(Uint8 b, Context *ctx)
+{
+	if(ptr < PAGE)
+		return error_asm("Writing zero-page");
+	else if(ptr >= 0x10000)
+		return error_asm("Writing outside memory");
+	else if(ptr < length)
+		return error_asm("Writing rewind");
+	data[ptr++] = b;
+	if(b)
+		length = ptr;
+	return 1;
+}
+
+static int
+writehex(char *w, Context *ctx)
+{
+	if(*w == '#')
+		writebyte(findopcode("LIT") | !!(++w)[2] << 5, ctx);
+	if(ishex(w)) {
+		if(w[1] && !w[2])
+			return writebyte(shex(w), ctx);
+		else if(w[3] && !w[4])
+			return writeshort(shex(w));
+	}
+	return error_asm("Hexadecimal invalid");
+}
+
+static int
+writestring(char *w, Context *ctx)
+{
+	char c;
+	while((c = *(w++)))
+		if(!writebyte(c, ctx)) return error_asm("String invalid");
+	return 1;
+}
+
+static int
+assemble(char *filename)
+{
+	FILE *f;
+	int res;
+	Context ctx;
+	ctx.line = 1;
+	ctx.path = push(filename, 0);
+	if(!(f = fopen(filename, "r")))
+		return error_top("File missing", filename);
+	res = walkfile(f, &ctx);
+	fclose(f);
+	return res;
+}
+
+static int
+parse(char *w, FILE *f, Context *ctx)
+{
+	Item *m;
+	switch(w[0]) {
+	case 0x0: return 1;
+	case '(':
+		if(w[1] <= 0x20)
+			return walkcomment(f, ctx);
+		else
+			return error_asm("Invalid word");
+	case '%': return makemacro(w + 1, f, ctx);
+	case '@': return makelabel(w + 1, 1, ctx);
+	case '&': return makelabel(w, 0, ctx);
+	case '}': return makelabel(makelambda(lambda_stack[--lambda_ptr]), 0, ctx);
+	case '#': return writehex(w, ctx);
+	case '_': return makeref(w + 1, w[0], ptr, ctx) && writebyte(0xff, ctx);
+	case ',': return makeref(w + 1, w[0], ptr + 1, ctx) && writebyte(findopcode("LIT"), ctx) && writebyte(0xff, ctx);
+	case '-': return makeref(w + 1, w[0], ptr, ctx) && writebyte(0xff, ctx);
+	case '.': return makeref(w + 1, w[0], ptr + 1, ctx) && writebyte(findopcode("LIT"), ctx) && writebyte(0xff, ctx);
+	case ':': printf("Deprecated rune %s, use =%s\n", w, w + 1); /* fall-through */
+	case '=': return makeref(w + 1, w[0], ptr, ctx) && writeshort(0xffff);
+	case ';': return makeref(w + 1, w[0], ptr + 1, ctx) && writebyte(findopcode("LIT2"), ctx) && writeshort(0xffff);
+	case '?': return makeref(w + 1, w[0], ptr + 1, ctx) && writebyte(0x20, ctx) && writeshort(0xffff);
+	case '!': return makeref(w + 1, w[0], ptr + 1, ctx) && writebyte(0x40, ctx) && writeshort(0xffff);
+	case '"': return writestring(w + 1, ctx);
+	case '~': return !assemble(w + 1) ? error_asm("Include missing") : 1;
+	case '$':
+	case '|': return writepad(w, ctx);
+	case '[':
+	case ']': return 1;
+	}
+	if(ishex(w)) return writehex(w, ctx);
+	if(isopc(w)) return writebyte(findopcode(w), ctx);
+	if((m = findmacro(w))) return walkmacro(m, ctx);
+	return makeref(w, ' ', ptr + 1, ctx) && writebyte(0x60, ctx) && writeshort(0xffff);
+}
+
+static int
+resolve(char *filename)
+{
+	int i, rel;
+	if(!length) return error_top("Output empty", filename);
+	for(i = 0; i < refs_len; i++) {
+		Item *r = &refs[i], *l = findlabel(r->name);
+		Uint8 *rom = data + r->addr;
+		if(!l) return error_ref("Label unknown");
+		switch(r->rune) {
+		case '_':
+		case ',':
+			*rom = rel = l->addr - r->addr - 2;
+			if((Sint8)data[r->addr] != rel)
+				return error_ref("Reference too far");
+			break;
+		case '-':
+		case '.':
+			*rom = l->addr;
+			break;
+		case ':':
+		case '=':
+		case ';':
+			*rom++ = l->addr >> 8, *rom = l->addr;
+			break;
+		case '?':
+		case '!':
+		default:
+			rel = l->addr - r->addr - 2;
+			*rom++ = rel >> 8, *rom = rel;
+			break;
+		}
+		l->refs++;
+	}
+	return 1;
+}
+
+static int
+build(char *rompath)
+{
+	int i;
+	FILE *dst, *dstsym;
+	char *sympath = join(rompath, '.', "sym");
+	/* rom */
+	if(!(dst = fopen(rompath, "wb")))
+		return !error_top("Output file invalid", rompath);
+	for(i = 0; i < labels_len; i++)
+		if(!labels[i].refs && (unsigned char)(labels[i].name[0] - 'A') > 25)
+			printf("-- Unused label: %s\n", labels[i].name);
+	fwrite(data + PAGE, length - PAGE, 1, dst);
+	printf(
+		"Assembled %s in %d bytes(%.2f%% used), %d labels, %d macros.\n",
+		rompath,
+		length - PAGE,
+		(length - PAGE) / 652.80,
+		labels_len,
+		macro_len);
+	/* sym */
+	if(!(dstsym = fopen(sympath, "wb")))
+		return !error_top("Symbols file invalid", sympath);
+	for(i = 0; i < labels_len; i++) {
+		Uint8 hb = labels[i].addr >> 8, lb = labels[i].addr;
+		char c, d = 0, *name = labels[i].name;
+		fwrite(&hb, 1, 1, dstsym);
+		fwrite(&lb, 1, 1, dstsym);
+		while((c = *name++)) fwrite(&c, 1, 1, dstsym);
+		fwrite(&d, 1, 1, dstsym);
+	}
+	fclose(dst), fclose(dstsym);
+	return 1;
+}
+
+int
+main(int argc, char *argv[])
+{
+	ptr = PAGE;
+	copy("on-reset", scope, 0);
+	if(argc == 2 && scmp(argv[1], "-v", 2)) return !printf("Uxnasm - Uxntal Assembler, 15 Jan 2025.\n");
+	if(argc != 3) return error_top("usage", "uxnasm [-v] input.tal output.rom");
+	return !assemble(argv[1]) || !resolve(argv[2]) || !build(argv[2]);
+}
diff --git a/awk/uxn/test/runner.sh b/awk/uxn/test/runner.sh
new file mode 100755
index 0000000..711dd28
--- /dev/null
+++ b/awk/uxn/test/runner.sh
@@ -0,0 +1,105 @@
+#!/bin/bash
+
+# Test runner for Uxntal AWK assembler
+# Compares output with reference C implementation
+
+set -e
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m' # No Color
+
+# Paths
+AWK_ASM="../awk/uxnasm.awk"
+REF_ASM="../ref/uxnasm"
+TEST_DIR="tal"
+OUT_DIR="out"
+
+# Ensure output directory exists
+mkdir -p "$OUT_DIR"
+
+echo "Testing Uxntal AWK Assembler"
+echo "============================="
+
+# Check if reference assembler exists
+if [ ! -f "$REF_ASM" ]; then
+    echo -e "${RED}Error: Reference assembler not found at $REF_ASM${NC}"
+    echo "Please compile the reference implementation first:"
+    echo "  cd ../ref && make"
+    exit 1
+fi
+
+# Check if AWK assembler exists
+if [ ! -f "$AWK_ASM" ]; then
+    echo -e "${RED}Error: AWK assembler not found at $AWK_ASM${NC}"
+    exit 1
+fi
+
+# Test function
+test_file() {
+    local input_file="$1"
+    local base_name=$(basename "$input_file" .tal)
+    
+    echo -n "Testing $base_name.tal... "
+    
+    # Run reference assembler
+    if ! "$REF_ASM" "$input_file" "$OUT_DIR/${base_name}_ref.rom" >/dev/null 2>&1; then
+        echo -e "${RED}FAIL${NC} (reference assembler failed)"
+        return 1
+    fi
+    
+    # Run AWK assembler
+    if [ "$DEBUG_AWK" -eq 1 ]; then
+        awk -f "$AWK_ASM" "$input_file" "$OUT_DIR/${base_name}_awk.rom" 2> "$OUT_DIR/${base_name}_awk.debug"
+    else
+        awk -f "$AWK_ASM" "$input_file" "$OUT_DIR/${base_name}_awk.rom" >/dev/null 2>&1
+    fi
+    
+    # Compare outputs
+    if cmp -s "$OUT_DIR/${base_name}_ref.rom" "$OUT_DIR/${base_name}_awk.rom"; then
+        echo -e "${GREEN}PASS${NC}"
+        if [ "$DEBUG_AWK" -eq 1 ]; then
+            echo "  Debug output: $OUT_DIR/${base_name}_awk.debug"
+        fi
+        return 0
+    else
+        echo -e "${RED}FAIL${NC} (outputs differ)"
+        echo "  Reference size: $(wc -c < "$OUT_DIR/${base_name}_ref.rom") bytes"
+        echo "  AWK size: $(wc -c < "$OUT_DIR/${base_name}_awk.rom") bytes"
+        echo "  Diff:"
+        xxd "$OUT_DIR/${base_name}_ref.rom" > "$OUT_DIR/${base_name}_ref.hex"
+        xxd "$OUT_DIR/${base_name}_awk.rom" > "$OUT_DIR/${base_name}_awk.hex"
+        diff "$OUT_DIR/${base_name}_ref.hex" "$OUT_DIR/${base_name}_awk.hex" || true
+        if [ "$DEBUG_AWK" -eq 1 ]; then
+            echo "  Debug output: $OUT_DIR/${base_name}_awk.debug"
+        fi
+        return 1
+    fi
+}
+
+# Run tests
+failed=0
+total=0
+
+for test_file in "$TEST_DIR"/*.tal; do
+    if [ -f "$test_file" ]; then
+        total=$((total + 1))
+        if ! test_file "$test_file"; then
+            failed=$((failed + 1))
+        fi
+    fi
+done
+
+echo
+echo "============================="
+echo "Results: $((total - failed))/$total tests passed"
+
+if [ $failed -eq 0 ]; then
+    echo -e "${GREEN}All tests passed!${NC}"
+    exit 0
+else
+    echo -e "${RED}$failed tests failed${NC}"
+    exit 1
+fi
diff --git a/awk/uxn/test/tal/brk.tal b/awk/uxn/test/tal/brk.tal
new file mode 100644
index 0000000..bf83010
--- /dev/null
+++ b/awk/uxn/test/tal/brk.tal
@@ -0,0 +1 @@
+BRK
\ No newline at end of file
diff --git a/awk/uxn/test/tal/brk_test.tal b/awk/uxn/test/tal/brk_test.tal
new file mode 100644
index 0000000..487f63d
--- /dev/null
+++ b/awk/uxn/test/tal/brk_test.tal
@@ -0,0 +1 @@
+BRK 
\ No newline at end of file
diff --git a/awk/uxn/test/tal/brk_with_data.tal b/awk/uxn/test/tal/brk_with_data.tal
new file mode 100644
index 0000000..c055e74
--- /dev/null
+++ b/awk/uxn/test/tal/brk_with_data.tal
@@ -0,0 +1 @@
+#01 #02 ADD BRK 
\ No newline at end of file
diff --git a/awk/uxn/test/tal/bunnymark.tal b/awk/uxn/test/tal/bunnymark.tal
new file mode 100644
index 0000000..579d305
--- /dev/null
+++ b/awk/uxn/test/tal/bunnymark.tal
@@ -0,0 +1,221 @@
+( bunnymark.tal )
+	( November 2021, Kira Oakley )
+	( March 2022, Devine Lu Linvega )
+
+|00 @System &vector $2 &pad $6 &r $2 &g $2 &b $2 &debug $1 &halt $1
+|20 @Screen &vector $2 &width $2 &height $2 &auto $1 &pad $1 &x $2 &y $2 &addr $2 &pixel $1 &sprite $1
+|80 @Controller &vector $2 &button $1 &key $1
+|90 @Mouse &vector $2 &x $2 &y $2 &state $1 &wheel $1
+|c0 @DateTime &year $2 &month $1 &day $1 &hour $1 &minute $1 &second $1 &dotw $1 &doty $2 &isdst $1
+
+|0000
+
+	@frames $2
+	@last $1
+
+|0100
+
+@on-reset ( -> )
+	( | theme )
+	#2ce9 .System/r DEO2
+	#01c0 .System/g DEO2
+	#2ce5 .System/b DEO2
+	( | interrupts )
+	;on-frame .Screen/vector DEO2
+	( | fps label )
+	.Screen/width DEI2 #0046 SUB2 .Screen/x DEO2
+	#0008 .Screen/y DEO2
+	;text/fps #42 <draw-str>
+	( | bunnies label )
+	#0004 .Screen/x DEO2
+	;text/bunnies #42 <draw-str>
+	( | instructions label )
+	.Screen/width DEI2 #01 SFT2 #0050 SUB2 .Screen/x DEO2
+	;text/instructions #43 <draw-str>
+	#0028 #0008 #0000 <draw-dec>
+	( | seed prng )
+	prng-init BRK
+
+@on-frame ( -> )
+	.frames LDZ2k INC2 ROT STZ2
+	.DateTime/second DEI .last LDZ EQU ?{
+		.DateTime/second DEI .last STZ
+		.Screen/width DEI2 #002b SUB2 #0008 .frames LDZ2 <draw-dec>
+		#0000 .frames STZ2 }
+	( | mouse handling )
+	.Mouse/state DEI
+	( ) DUP #01 NEQ ?{ add-bunny }
+	( ) #02 LTH ?{ remove-bunny }
+	( | controller handling )
+	.Controller/button DEI
+	( ) DUP #01 NEQ ?{ add-bunny }
+	( ) #02 LTH ?{ remove-bunny }
+	( | clear )
+	#0000 DUP2 .Screen/x DEO2
+	.Screen/y DEO2
+	[ LIT2 80 -Screen/pixel ] DEO
+	;sprite/length LDA2 #0000
+	&loop ( -- )
+		EQU2k ?&bail
+		DUP2 <draw-bunny>
+		INC2 !&loop
+	&bail ( -- )
+		POP2 POP2 BRK
+
+@add-bunny ( -- )
+	;sprite/length LDA2
+	( | cap bunny count at 65535 )
+	DUP2 #ffff EQU2 ?&bail
+	( | compute the offset to the beginning of this new bunny's data )
+	DUP2 #30 SFT2 ;sprite/array ADD2
+	( | populate the new bunny's x/y/xvel/yvel with random values )
+	#00 rand OVR2 STA2
+	rand #1f AND rand OVR2 INC2 INC2 STA2
+	#00 rand #7f AND OVR2 #0004 ADD2 STA2
+	#00 rand #7f AND OVR2 #0006 ADD2 STA2
+	( | pop ptr to bunny data )
+	POP2
+	( | write new increased array length )
+	INC2 DUP2 ;sprite/length STA2
+	( | update label )
+	STH2k #0028 #0008 STH2r <draw-dec>
+	&bail ( pop sprite/length )
+		POP2 JMP2r
+
+@remove-bunny ( -- )
+	;sprite/length LDA2
+	( don't let length go below 0 ) ORAk #00 EQU ?&bail
+	#0001 SUB2 DUP2 ;sprite/length STA2
+	( update label ) STH2k #0028 #0008 STH2r <draw-dec>
+	&bail POP2 JMP2r
+
+(
+@|drawing )
+
+@<draw-bunny> ( idx -- )
+	( | compute the offset to the beginning of this bunny's data )
+	#30 SFT2 ;sprite/array ADD2
+	( | move the sprite by its velocity )
+	LDA2k OVR2 #0004 ADD2 LDA2 ADD2 OVR2 STA2
+	INC2k INC2 LDA2 OVR2 #0006 ADD2 LDA2 ADD2 OVR2 INC2 INC2 STA2
+	( | check for right wall collision + bounce x )
+	DUP2 #0004 ADD2 LDA2 #0f SFT2 #0001 EQU2 ?&skip-max-x
+	LDA2k #05 SFT2 #0008 ADD2 .Screen/width DEI2 LTH2 ?&skip-max-x
+	DUP2 #0004 ADD2 LDA2 #ffff MUL2 OVR2 #0004 ADD2 STA2
+	&skip-max-x ( check for left wall collision + bounce x )
+		LDA2k #0f SFT2 #0000 EQU2 ?&skip-min-x
+		DUP2 #0004 ADD2 LDA2 #ffff MUL2 OVR2 #0004 ADD2 STA2
+	&skip-min-x ( check for bottom wall collision + bounce y )
+		DUP2 #0006 ADD2 LDA2 #0f SFT2 #0001 EQU2 ?&skip-max-y
+		INC2k INC2 LDA2 #05 SFT2 #0010 ADD2 .Screen/height DEI2 LTH2 ?&skip-max-y
+		DUP2 #0006 ADD2 LDA2 #ffff MUL2 OVR2 #0006 ADD2 STA2
+		!&skip-gravity
+	&skip-max-y ( check for top wall collision + bounce x )
+		INC2k INC2 LDA2 #0f SFT2 #0000 EQU2 ?&skip-min-y
+		DUP2 #0006 ADD2 LDA2 #ffff MUL2 OVR2 #0006 ADD2 STA2
+		!&skip-gravity
+	&skip-min-y ( apply gravity )
+		DUP2 #0006 ADD2 LDA2 #0004 ADD2 OVR2 #0006 ADD2 STA2
+	&skip-gravity ( draw the sprite )
+		( top ) LDA2k #05 SFT2 .Screen/x DEO2
+		INC2 INC2 LDA2 #05 SFT2 .Screen/y DEO2
+		( draw ) [ LIT2 15 -Screen/auto ] DEO
+		;bunny-chr .Screen/addr DEO2
+		[ LIT2 85 -Screen/sprite ] DEO
+		[ LIT2 00 -Screen/auto ] DEO
+		JMP2r
+
+@<draw-str> ( x* y* text* color -- )
+	,&t STR
+	[ LIT2 01 -Screen/auto ] DEO
+	&loop ( -- )
+		LDAk #20 SUB #00 SWP #30 SFT2 ;font ADD2 .Screen/addr DEO2
+		[ LIT2 &t $1 -Screen/sprite ] DEO
+		INC2 LDAk ?&loop
+	POP2 JMP2r
+
+@<draw-dec> ( x* y* num* -- )
+	[ LIT2 01 -Screen/auto ] DEO
+	SWP2 .Screen/y DEO2
+	SWP2 .Screen/x DEO2
+	#2710 DIV2k DUP <draw-digit>
+	MUL2 SUB2 #03e8 DIV2k DUP <draw-digit>
+	MUL2 SUB2 #0064 DIV2k DUP <draw-digit>
+	MUL2 SUB2 NIP #0a DIVk DUP <draw-digit>
+	MUL SUB <draw-digit>
+	[ LIT2 00 -Screen/auto ] DEO
+	JMP2r
+
+@<draw-digit> ( num -- )
+	#30 SFT #00 SWP ;font/num ADD2 .Screen/addr DEO2
+	[ LIT2 41 -Screen/sprite ] DEO
+	JMP2r
+
+(
+@|random )
+
+@prng-init ( -- )
+	[ LIT2 00 -DateTime/second ] DEI [ LIT2 00 -DateTime/minute ] DEI #60 SFT2 EOR2 [ LIT2 00 -DateTime/hour ] DEI #c0 SFT2 EOR2 ,prng/x STR2
+	[ LIT2 00 -DateTime/hour ] DEI #04 SFT2 [ LIT2 00 -DateTime/day ] DEI #10 SFT2 EOR2 [ LIT2 00 -DateTime/month ] DEI #60 SFT2 EOR2 .DateTime/year DEI2 #a0 SFT2 EOR2 ,prng/y STR2
+	JMP2r
+
+@prng ( -- number* )
+	[ LIT2 &x $2 ] DUP2 #50 SFT2 EOR2 DUP2 #03 SFT2 EOR2 [ LIT2 &y $2 ] DUP2 ,&x STR2
+	DUP2 #01 SFT2 EOR2 EOR2 ,&y STR2k POP JMP2r
+
+@rand ( -- number )
+	prng ADD JMP2r
+	( static string data )
+
+(
+@|assets )
+
+@text &fps "FPS: $1
+	&bunnies "BUNS: $1
+	&instructions "CLICK 20 "TO 20 "ADD 20 "BUNNIES! $1
+
+@font ( atari8.uf1 )
+	[
+	0000 0000 0000 0000 6060 6060 6000 6000
+	6666 6600 0000 0000 006c fe6c 6cfe 6c00
+	183e 603c 067c 1800 0066 6c18 3066 4600
+	386c 3870 decc 7600 6060 6000 0000 0000
+	0e1c 1818 181c 0e00 7038 1818 1838 7000
+	0066 3cff 3c66 0000 0018 187e 1818 0000
+	0000 0000 0030 3060 0000 007e 0000 0000
+	0000 0000 0018 1800 0206 0c18 3060 4000 ] &num [
+	3c66 6e76 6666 3c00 1838 1818 1818 7e00
+	3c66 060c 1830 7e00 7e0c 180c 0666 3c00
+	0c1c 3c6c 7e0c 0c00 7e60 7c06 0666 3c00
+	3c60 607c 6666 3c00 7e06 0c18 3030 3000
+	3c66 663c 6666 3c00 3c66 663e 060c 3800
+	0060 6000 6060 0000 0030 3000 3030 6000
+	0c18 3060 3018 0c00 0000 7e00 007e 0000
+	6030 180c 1830 6000 3c66 060c 1800 1800
+	3c66 6e6a 6e60 3e00 183c 6666 7e66 6600
+	7c66 667c 6666 7c00 3c66 6060 6066 3c00
+	786c 6666 666c 7800 7e60 607c 6060 7e00
+	7e60 607c 6060 6000 3e60 606e 6666 3e00
+	6666 667e 6666 6600 7830 3030 3030 7800
+	0606 0606 0666 3c00 666c 7870 786c 6600
+	6060 6060 6060 7e00 c6ee fed6 c6c6 c600
+	6676 7e7e 6e66 6600 3c66 6666 6666 3c00
+	7c66 667c 6060 6000 3c66 6666 766c 3600
+	7c66 667c 6c66 6600 3c66 603c 0666 3c00
+	7e18 1818 1818 1800 6666 6666 6666 3e00
+	6666 6666 663c 1800 c6c6 c6d6 feee c600
+	6666 3c18 3c66 6600 6666 663c 1818 1800
+	7e06 0c18 3060 7e00 7860 6060 6060 7800 ]
+
+@fill-icn [ ffff ffff ffff ffff ]
+
+@bunny-chr [
+	2466 6600 2424 003c 4200 007e 7e7e 7e7e
+	1818 3c3c 1800 0000 ff66 4242 667e 4242 ]
+
+(
+@|memory )
+
+@sprite &length $2
+	&array &x 0600 &y 0500 &xvel 0060 &yvel 0010
+
diff --git a/awk/uxn/test/tal/life.tal b/awk/uxn/test/tal/life.tal
new file mode 100644
index 0000000..718068b
--- /dev/null
+++ b/awk/uxn/test/tal/life.tal
@@ -0,0 +1,221 @@
+( uxnemu life.rom )
+	( Any live cell with fewer than two live neighbours dies, as if by underpopulation. )
+	( Any live cell with two or three live neighbours lives on to the next generation. )
+	( Any live cell with more than three live neighbours dies, as if by overpopulation. )
+	( Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction. )
+
+|00 @System &vector $2 &expansion $2 &wst $1 &rst $1 &metadata $2 &r $2 &g $2 &b $2 &debug $1 &state $1
+|10 @Console &vector $2 &read $1 &pad $5 &write $1 &error $1
+|20 @Screen &vector $2 &width $2 &height $2 &auto $1 &pad $1 &x $2 &y $2 &addr $2 &pixel $1 &sprite $1
+|30 @Audio0 &vector $2 &position $2 &output $1 &pad $3 &adsr $2 &length $2 &addr $2 &volume $1 &pitch $1
+|80 @Controller &vector $2 &button $1 &key $1
+|90 @Mouse &vector $2 &x $2 &y $2 &state $1 &wheel $1
+|000
+
+	@world &count $2
+	@anchor &x $2 &y $2 &x2 $2 &y2 $2
+
+|100
+
+@on-reset ( -> )
+	( | theme )
+	#02cf .System/r DEO2
+	#02ff .System/g DEO2
+	#024f .System/b DEO2
+	( | resize )
+	#00c0 DUP2 .Screen/width DEO2
+	.Screen/height DEO2
+	( | vectors )
+	;on-frame .Screen/vector DEO2
+	;on-mouse .Mouse/vector DEO2
+	;on-control .Controller/vector DEO2
+	( | glider )
+	#0703 <set-cell>
+	#0704 <set-cell>
+	#0504 <set-cell>
+	#0705 <set-cell>
+	#0605 <set-cell>
+	( | center )
+	.Screen/width DEI2 #01 SFT2 #0040 SUB2 DUP2 .anchor/x STZ2
+	#007e ADD2 .anchor/x2 STZ2
+	.Screen/height DEI2 #01 SFT2 #0040 SUB2 DUP2 .anchor/y STZ2
+	#007e ADD2 .anchor/y2 STZ2
+	BRK
+
+@on-frame ( -> )
+	[ LIT2 00 -Mouse/state ] DEI EQU ?{ BRK }
+	#0000 .world/count STZ2
+	[ LIT &f $1 ] INCk ,&f STR
+	( ) #03 AND #00 EQU ?{ BRK }
+	<run>
+	BRK
+
+@on-mouse ( -> )
+	[ LIT2 00 -Mouse/state ] DEI NEQ #42 ADD ;cursor-icn <update-cursor>
+	( | on touch in rect )
+	.Mouse/state DEI ?{ BRK }
+	.Mouse/x DEI2 .Mouse/y DEI2 .anchor within-rect ?{ BRK }
+	( | paint )
+	.Mouse/x DEI2 .anchor/x LDZ2 SUB2 #01 SFT NIP
+	( ) .Mouse/y DEI2 .anchor/y LDZ2 SUB2 #01 SFT NIP <set-cell>
+	<redraw>
+	BRK
+
+@on-control ( -> )
+	.Controller/key DEI
+	( ) DUP #20 NEQ ?{
+		#0000 ;on-frame .Screen/vector DEI2 ORA ?{ SWP2 }
+		POP2 .Screen/vector DEO2 }
+	( ) #1b NEQ ?{ ;MMU/clear1 .System/expansion DEO2 }
+	BRK
+
+(
+@|core )
+
+@<run> ( -- )
+	;MMU/clear2 .System/expansion DEO2
+	#4000
+	&ver ( -- )
+		DUP ,&y STR
+		#4000
+	&hor ( -- )
+		DUP [ LIT &y $1 ] <run-cell>
+		INC GTHk ?&hor
+	POP2 INC GTHk ?&ver
+	POP2
+	( move ) ;MMU/move21 .System/expansion DEO2
+	!<redraw>
+
+@<run-cell> ( x y -- )
+	( x y ) DUP2 STH2k
+	( neighbours ) get-neighbours
+	( state ) STH2r get-index LDA #00 EQU ?&dead
+	DUP #02 LTH ?&dies
+	DUP #03 GTH ?&dies
+	POP !&save
+	&dies POP POP2 JMP2r
+	&dead ( -- )
+		DUP #03 EQU ?&birth
+		POP POP2 JMP2r
+		&birth POP !&save
+	&save ( x y -- )
+		STH2
+		#01 STH2r get-index #1000 ADD2 STA
+		.world/count LDZ2 INC2 .world/count STZ2
+		JMP2r
+
+@get-index ( x y -- index* )
+	( y ) #3f AND #00 SWP #60 SFT2 ROT
+	( x ) #3f AND #00 SWP ADD2 ;bank1 ADD2 JMP2r
+
+@<set-cell> ( x y -- )
+	get-index STH2
+	#01 STH2r STA
+	JMP2r
+
+@get-neighbours ( x y -- neighbours )
+	,&y STR
+	,&x STR
+	[ LITr 00 ] #0800
+	&l ( -- )
+		#00 OVRk ADD2 ;&mask ADD2 LDA2
+		( ) [ LIT &y $1 ] ADD SWP
+		( ) [ LIT &x $1 ] ADD SWP get-index LDA [ STH ADDr ]
+		( stop at 3 ) DUPr [ LITr 03 ] GTHr [ LITr _&end ] JCNr
+		( ) INC GTHk ?&l
+	&end POP2 STHr JMP2r
+	&mask [
+	ffff 00ff 01ff ff00 0100 ff01 0001 0101 ]
+
+@within-rect ( x* y* rect -- flag )
+	STH
+	( y < rect.y1 ) DUP2 STHkr INC INC LDZ2 LTH2 ?&skip
+	( y > rect.y2 ) DUP2 STHkr #06 ADD LDZ2 GTH2 ?&skip
+	SWP2
+	( x < rect.x1 ) DUP2 STHkr LDZ2 LTH2 ?&skip
+	( x > rect.x2 ) DUP2 STHkr #04 ADD LDZ2 GTH2 ?&skip
+	POP2 POP2 POPr #01 JMP2r
+	&skip POP2 POP2 POPr #00 JMP2r
+
+(
+@|drawing )
+
+@<redraw> ( -- )
+	( | draw count )
+	.anchor/x LDZ2 .Screen/x DEO2
+	.anchor/y2 LDZ2 #0008 ADD2 .Screen/y DEO2
+	[ LIT2 01 -Screen/auto ] DEO
+	.world/count LDZ2 <draw-short>
+	( | draw grid )
+	[ LIT2 01 -Screen/auto ] DEO
+	.anchor/y LDZ2 .Screen/y DEO2
+	;bank2 ;bank1
+	&l ( -- )
+		DUP #3f AND ?{
+			.Screen/y DEI2k INC2 INC2 ROT DEO2
+			.anchor/x LDZ2 .Screen/x DEO2 }
+		LDAk INC .Screen/pixel DEO
+		[ LIT2 00 -Screen/pixel ] DEO
+		INC2 GTH2k ?&l
+	POP2 POP2 JMP2r
+
+@<draw-short> ( short* -- )
+	SWP <draw-byte>
+	( >> )
+
+@<draw-byte> ( byte color -- )
+	DUP #04 SFT <draw-hex>
+	#0f AND
+	( >> )
+
+@<draw-hex> ( char color -- )
+	#00 SWP #30 SFT2 ;font-hex ADD2 .Screen/addr DEO2
+	[ LIT2 03 -Screen/sprite ] DEO
+	JMP2r
+
+@<update-cursor> ( color addr* -- )
+	[ LIT2 00 -Screen/auto ] DEO
+	;fill-icn .Screen/addr DEO2
+	#40 <draw-cursor>
+	.Mouse/x DEI2 ,<draw-cursor>/x STR2
+	.Mouse/y DEI2 ,<draw-cursor>/y STR2
+	.Screen/addr DEO2
+	( >> )
+
+@<draw-cursor> ( color -- )
+	[ LIT2 &x $2 ] .Screen/x DEO2
+	[ LIT2 &y $2 ] .Screen/y DEO2
+	.Screen/sprite DEO
+	JMP2r
+
+(
+@|assets )
+
+@MMU ( programs )
+	&clear1 [ 01 1000 0000 =bank3 0000 =bank1 ]
+	&clear2 [ 01 1000 0000 =bank3 0000 =bank2 ]
+	&move21 [ 01 1000 0000 =bank2 0000 =bank1 ]
+
+@cursor-icn [ 80c0 e0f0 f8e0 1000 ]
+
+@fill-icn [ ffff ffff ffff ffff ]
+
+@font-hex [
+	7c82 8282 8282 7c00 3010 1010 1010 3800
+	7c82 027c 8080 fe00 7c82 021c 0282 7c00
+	2242 82fe 0202 0200 fe80 807c 0282 7c00
+	7c82 80fc 8282 7c00 fe82 0408 0810 1000
+	7c82 827c 8282 7c00 7c82 827e 0202 0200
+	7c82 82fe 8282 8200 fc82 82fc 8282 fc00
+	7c82 8080 8082 7c00 fc82 8282 8282 fc00
+	fe80 80f0 8080 fe00 fe80 80f0 8080 8000 ]
+
+(
+@|memory )
+
+|8000 @bank1 $1000
+
+@bank2 $1000
+
+@bank3 $1000
+
diff --git a/awk/uxn/test/tal/opctest.tal b/awk/uxn/test/tal/opctest.tal
new file mode 100644
index 0000000..b803de6
--- /dev/null
+++ b/awk/uxn/test/tal/opctest.tal
@@ -0,0 +1,492 @@
+( Opcode Tester )
+
+|0013
+
+	@Zeropage &byte $1 &short $2
+	@id $1
+
+|100
+
+@on-reset ( -> )
+
+	( part 1
+		> LIT2: Puts a short on the stack
+		> LIT: Puts a byte on the stack
+		> #06 DEO: Write to metadata ports
+		> #18 DEO: Write a letter in terminal )
+
+	;meta #06 DEO2
+	[ LIT2 "kO ] #18 DEO #18 DEO
+	[ LIT2 "1 18 ] DEO #0a18 DEO
+
+	( part 2
+		> LITr: Put a byte on return stack
+		> STH: Move a byte from working stack to return stack
+		> STH2r: Move a short from return stack to working stack )
+
+	[ LITr "k ] [ LIT "O ] STH STH2r #18 DEO #18 DEO
+	[ LIT2 "2 18 ] DEO #0a18 DEO
+
+	( part 3
+		> LIT2r: Put a short on return stack
+		> DUP: Duplicate byte
+		> ADDr: Add bytes on return stack )
+
+	[ LIT2r "k 4d ] #01 DUP STH ADDr STH ADDr STH2r #18 DEO #18 DEO
+	[ LIT2 "3 18 ] DEO #0a18 DEO
+
+	( part 4
+		> JSI: Subroutine to relative short address
+		> JMP2r: Jumps to absolute address on return stack )
+
+	subroutine
+	[ LIT2 "4 18 ] DEO #0a18 DEO
+
+	( part 5
+		> POP2: Removes a short from the stack
+		> INC2: Increments short on stack
+		> DUP2: Duplicate short
+		> LDA: load byte from absolute address
+		> JCI: Conditional subroutine to relative short address )
+
+	;Dict/ok pstr
+	[ LIT2 "5 18 ] DEO #0a18 DEO
+
+	( part 6
+		> JSR2: Jump to subroutine from short pointer
+		> LDAk: Non-destructive load byte from absolute address )
+
+	{ "Ok $1 } STH2r ;pstr-jcn JSR2
+	[ LIT2 "6 18 ] DEO #0a18 DEO
+
+	( part 7
+		> Relative distance bytes )
+
+	rel-distance/entry SWP #18 DEO #18 DEO
+	[ LIT2 "7 18 ] DEO #0a18 DEO
+
+	( part xx
+		> GTH2k: Non-destructive greater-than short
+		> LDA2k: Non-destructive load short from absolute address
+		> STA2: Store short at absolute address )
+
+	[ LIT2r 0000 ]
+	;tests/end ;tests
+	&l
+		run-test [ LITr 00 ] STH ADD2r
+		INC2 INC2 GTH2k ?&l
+	POP2 POP2
+	STH2r ;tests/end ;tests SUB2 #01 SFT2
+	EQU2 ;Dict/opctests test-part
+
+	( Part xx
+		> Testing that stacks are circular and wrapping
+		> Storing 12 at -1 and 34 at 0 )
+
+	POP #12 #34 ADD #46 EQU STH
+	POP #1234 ADD #46 EQU STH
+	POP2 #1111 #2222 ADD2 #3333 EQU2
+	STHr AND STHr AND
+	;Dict/stack-wrap test-part
+
+	( restore stack ) #0000 #0000
+
+	( Part xx
+		> Testing RAM wrapping
+		> Storing 12 in 0xffff, and 34 in 0x0000 )
+
+	#1234 #ffff STA2
+	( LDA ) #0000 LDA #ffff LDA ADD #46 EQU
+	( LDA2 ) #ffff LDA2 ADD #46 EQU
+	AND ;Dict/ram-wrap test-part
+
+	( Part xx
+		> Testing that zero-page is wrapping )
+
+	#5678 #ff STZ2
+	( LDZ ) #00 LDZ #ff LDZ ADD #ce EQU
+	( LDZ2 ) #ff LDZ2 ADD #ce EQU
+	AND ;Dict/zp-wrap test-part
+
+	( Part xx
+		> Testing that device page is wrapping )
+
+	#1234 #ff DEO2
+	( DEI ) #00 DEI #ff DEI ADD #46 EQU
+	( DEI2 ) #ff DEI2 ADD #46 EQU
+	AND ;Dict/dev-wrap test-part
+	#0000 DEO #00ff DEO
+
+	( end )
+
+	[ LIT &fail 80 ]
+		DUP #80 EQU ;Dict/result test-part
+		#0f DEO
+
+	#0a18 DEO
+	#010e DEO
+
+BRK
+
+(
+@|metadata )
+
+@meta 00
+	( name ) "Opctest 0a
+	( details ) "A 20 "Testing 20 "Program 0a
+	( author ) "By 20 "Devine 20 "Lu 20 "Linvega 0a
+	( date ) "24 20 "Jun 20 "2025 $2
+
+@test-part ( f name* -- )
+	pstr ?{
+		#01 ;on-reset/fail STA
+		;Dict/failed !pstr }
+	;Dict/passed !pstr
+
+@run-test ( addr* -- addr* f )
+
+	LDA2k JSR2 DUP ?&pass
+		;Dict/missed pstr
+		[ LIT2 &name $2 ] pstr
+		[ LIT2 "# 18 ] DEO
+		[ LIT2 "a -id ] LDZ ADD #18 DEO
+		#0a18 DEO
+		#01 ;on-reset/fail STA
+		&pass
+	.id LDZ INC .id STZ
+
+JMP2r
+
+@set ( name* -- f )
+
+	;run-test/name STA2 #01
+	[ LIT2 ff -id ] STZ
+
+JMP2r
+
+@pstr ( str* -- )
+	DUP2 LDA
+		DUP ?{ POP POP2 JMP2r }
+		#18 DEO
+	INC2 !pstr
+
+@pstr-jcn ( str* -- )
+	LDAk #18 DEO
+	INC2 LDAk ,pstr-jcn JCN
+	POP2
+	JMP2r
+
+@tests
+=op-equ [
+	=op-equ/a =op-equ/b =op-equ/c =op-equ/d
+	=op-equ/e =op-equ/f =op-equ/g =op-equ/h ]
+=op-neq [
+	=op-neq/a =op-neq/b =op-neq/c =op-neq/d
+	=op-neq/e =op-neq/f =op-neq/g =op-neq/h ]
+=op-gth [
+	=op-gth/a =op-gth/b =op-gth/c =op-gth/d
+	=op-gth/e =op-gth/f =op-gth/g =op-gth/h ]
+=op-lth [
+	=op-lth/a =op-lth/b =op-lth/c =op-lth/d
+	=op-lth/e =op-lth/f =op-lth/g =op-lth/h ]
+=op-add [
+	=op-add/a =op-add/b =op-add/c =op-add/d
+	=op-add/e =op-add/f =op-add/g =op-add/h ]
+=op-sub [
+	=op-sub/a =op-sub/b =op-sub/c =op-sub/d
+	=op-sub/e =op-sub/f =op-sub/g =op-sub/h ]
+=op-mul [
+	=op-mul/a =op-mul/b =op-mul/c =op-mul/d
+	=op-mul/e =op-mul/f =op-mul/g =op-mul/h ]
+=op-div [
+	=op-div/a =op-div/b =op-div/c =op-div/d =op-div/e
+	=op-div/f =op-div/g =op-div/h =op-div/i =op-div/j ]
+=op-inc [
+	=op-inc/a =op-inc/b =op-inc/c =op-inc/d
+	=op-inc/e =op-inc/f =op-inc/g =op-inc/h ]
+=op-pop [
+	=op-pop/a =op-pop/b =op-pop/c =op-pop/d
+	=op-pop/e =op-pop/f =op-pop/g =op-pop/h ]
+=op-dup [
+	=op-dup/a =op-dup/b ]
+=op-nip [
+	=op-nip/a =op-nip/b =op-nip/c =op-nip/d ]
+=op-swp [
+	=op-swp/a =op-swp/b ]
+=op-ovr [
+	=op-ovr/a =op-ovr/b ]
+=op-rot [
+	=op-rot/a =op-rot/b ]
+=op-and [
+	=op-and/a =op-and/b =op-and/c =op-and/d
+	=op-and/e =op-and/f =op-and/g =op-and/h ]
+=op-ora [
+	=op-ora/a =op-ora/b =op-ora/c =op-ora/d
+	=op-ora/e =op-ora/f =op-ora/g =op-ora/h ]
+=op-eor [
+	=op-eor/a =op-eor/b =op-eor/c =op-eor/d
+	=op-eor/e =op-eor/f =op-eor/g =op-eor/h ]
+=op-sft [
+	=op-sft/a =op-sft/b =op-sft/c =op-sft/d
+	=op-sft/e =op-sft/f =op-sft/g =op-sft/h ]
+=op-stz [
+	=op-stz/a =op-stz/b =op-stz/c =op-stz/d ]
+=op-str [
+	=op-str/a =op-str/b =op-str/c =op-str/d ]
+=op-sta [
+	=op-sta/a =op-sta/b =op-sta/c =op-sta/d ]
+=op-jmp [
+	=op-jmp/a =op-jmp/b ]
+=op-jcn [
+	=op-jcn/a =op-jcn/b =op-jcn/c =op-jcn/d ]
+=op-jsr [
+	=op-jsr/a =op-jsr/b ]
+=op-sth [
+	=op-sth/a =op-sth/b ]
+=op-jci [
+	=op-jci/a =op-jci/b =op-jci/c ]
+=op-jmi [
+	=op-jmi/a ]
+=op-jsi [
+	=op-jsi/a =op-jsi/b =op-jsi/c =op-jsi/d ]
+	&end
+
+@op-equ ;Dict/equ !set
+	&a #f8 #f8 EQU [ #01 ] EQU JMP2r
+	&b #01 #01 EQU [ #01 ] EQU JMP2r
+	&c #f8 #01 EQU [ #00 ] EQU JMP2r
+	&d #00 #ff EQU [ #00 ] EQU JMP2r
+	&e #f801 #f801 EQU2 [ #01 ] EQU JMP2r
+	&f #01f8 #01f8 EQU2 [ #01 ] EQU JMP2r
+	&g #f801 #01f8 EQU2 [ #00 ] EQU JMP2r
+	&h #01f8 #f801 EQU2 [ #00 ] EQU JMP2r
+@op-neq ;Dict/neq !set
+	&a #f8 #f8 NEQ [ #00 ] EQU JMP2r
+	&b #01 #01 NEQ [ #00 ] EQU JMP2r
+	&c #f8 #01 NEQ [ #01 ] EQU JMP2r
+	&d #01 #f8 NEQ [ #01 ] EQU JMP2r
+	&e #f801 #f801 NEQ2 [ #00 ] EQU JMP2r
+	&f #01f8 #01f8 NEQ2 [ #00 ] EQU JMP2r
+	&g #f801 #01f8 NEQ2 [ #01 ] EQU JMP2r
+	&h #01f8 #f801 NEQ2 [ #01 ] EQU JMP2r
+@op-gth ;Dict/gth !set
+	&a #f8 #f8 GTH [ #00 ] EQU JMP2r
+	&b #01 #01 GTH [ #00 ] EQU JMP2r
+	&c #f8 #01 GTH [ #01 ] EQU JMP2r
+	&d #01 #f8 GTH [ #00 ] EQU JMP2r
+	&e #f801 #f801 GTH2 [ #00 ] EQU JMP2r
+	&f #01f8 #01f8 GTH2 [ #00 ] EQU JMP2r
+	&g #f801 #01f8 GTH2 [ #01 ] EQU JMP2r
+	&h #01f8 #f801 GTH2 [ #00 ] EQU JMP2r
+@op-lth ;Dict/lth !set
+	&a #f8 #f8 LTH [ #00 ] EQU JMP2r
+	&b #01 #01 LTH [ #00 ] EQU JMP2r
+	&c #f8 #01 LTH [ #00 ] EQU JMP2r
+	&d #01 #ff LTH [ #01 ] EQU JMP2r
+	&e #f801 #f801 LTH2 [ #00 ] EQU JMP2r
+	&f #01f8 #01f8 LTH2 [ #00 ] EQU JMP2r
+	&g #f801 #01f8 LTH2 [ #00 ] EQU JMP2r
+	&h #01f8 #f801 LTH2 [ #01 ] EQU JMP2r
+@op-add ;Dict/add !set
+	&a #ff #00 ADD [ #ff ] EQU JMP2r
+	&b #01 #ff ADD [ #00 ] EQU JMP2r
+	&c #ff #ff ADD [ #fe ] EQU JMP2r
+	&d #12 #34 ADDk ADD ADD [ #8c ] EQU JMP2r
+	&e #ffff #0000 ADD2 [ #ffff ] EQU2 JMP2r
+	&f #0001 #ffff ADD2 [ #0000 ] EQU2 JMP2r
+	&g #ffff #ffff ADD2 [ #fffe ] EQU2 JMP2r
+	&h #fffe #ffff ADD2 [ #fffd ] EQU2 JMP2r
+@op-sub ;Dict/sub !set
+	&a #ff #00 SUB [ #ff ] EQU JMP2r
+	&b #01 #ff SUB [ #02 ] EQU JMP2r
+	&c #ff #ff SUB [ #00 ] EQU JMP2r
+	&d #fe #ff SUB [ #ff ] EQU JMP2r
+	&e #ffff #0000 SUB2 [ #ffff ] EQU2 JMP2r
+	&f #0001 #ffff SUB2 [ #0002 ] EQU2 JMP2r
+	&g #ffff #ffff SUB2 [ #0000 ] EQU2 JMP2r
+	&h #fffe #ffff SUB2 [ #ffff ] EQU2 JMP2r
+@op-mul ;Dict/mul !set
+	&a #00 #01 MUL [ #00 ] EQU JMP2r
+	&b #3f #e7 MUL [ #d9 ] EQU JMP2r
+	&c #37 #3f MUL [ #89 ] EQU JMP2r
+	&d #10 #02 MUL [ #20 ] EQU JMP2r
+	&e #1000 #0003 MUL2 [ #3000 ] EQU2 JMP2r
+	&f #abcd #1234 MUL2 [ #4fa4 ] EQU2 JMP2r
+	&g #8000 #0200 MUL2 [ #0000 ] EQU2 JMP2r
+	&h #2222 #0003 MUL2 [ #6666 ] EQU2 JMP2r
+@op-div ;Dict/div !set
+	&a #10 #06 DIV [ #02 ] EQU JMP2r
+	&b #20 #20 DIV [ #01 ] EQU JMP2r
+	&c #34 #01 DIV [ #34 ] EQU JMP2r
+	&d #02 #ef DIV [ #00 ] EQU JMP2r
+	&e #02 #00 DIV [ #00 ] EQU JMP2r
+	&f #03e8 #0006 DIV2 [ #00a6 ] EQU2 JMP2r
+	&g #abcd #1234 DIV2 [ #0009 ] EQU2 JMP2r
+	&h #8000 #0200 DIV2 [ #0040 ] EQU2 JMP2r
+	&i #2222 #0003 DIV2 [ #0b60 ] EQU2 JMP2r
+	&j #0202 #0000 DIV2 [ #0000 ] EQU2 JMP2r
+@op-inc ;Dict/inc !set
+	&a #01 INC [ #02 ] EQU JMP2r
+	&b #ff INC [ #00 ] EQU JMP2r
+	&c #fe INC [ #ff ] EQU JMP2r
+	&d #00 INC [ #01 ] EQU JMP2r
+	&e #0001 INC2 [ #0002 ] EQU2 JMP2r
+	&f #ffff INC2 [ #0000 ] EQU2 JMP2r
+	&g #fffe INC2 [ #ffff ] EQU2 JMP2r
+	&h #0000 INC2 [ #0001 ] EQU2 JMP2r
+@op-pop ;Dict/pop !set
+	&a #0a #0b POP [ #0a ] EQU JMP2r
+	&b #0a #0b #0c POP POP [ #0a ] EQU JMP2r
+	&c #0a #0b #0c ADD POP [ #0a ] EQU JMP2r
+	&d #0a #0b #0c POP ADD [ #15 ] EQU JMP2r
+	&e #0a0b #0c0d POP2 [ #0a0b ] EQU2 JMP2r
+	&f #0a0b #0c0d #0e0f POP2 POP2 [ #0a0b ] EQU2 JMP2r
+	&g #0a0b #0c0d #0e0f ADD2 POP2 [ #0a0b ] EQU2 JMP2r
+	&h #0a0b #0c0d #0e0f POP2 ADD2 [ #1618 ] EQU2 JMP2r
+@op-dup ;Dict/dup !set
+	&a #0a #0b DUP ADD ADD [ #20 ] EQU JMP2r
+	&b #0a0b DUP2 ADD2 [ #1416 ] EQU2 JMP2r
+@op-nip ;Dict/nip !set
+	&a #12 #34 #56 NIP ADD [ #68 ] EQU JMP2r
+	&b #12 #34 #56 NIPk ADD2 ADD [ #f2 ] EQU JMP2r
+	&c #1234 #5678 #9abc NIP2 ADD2 [ #acf0 ] EQU2 JMP2r
+	&d #1234 #5678 #9abc NIP2k ADD2 ADD2 ADD2 [ #9e24 ] EQU2 JMP2r
+@op-swp ;Dict/swp !set
+	&a #02 #10 SWP DIV [ #08 ] EQU JMP2r
+	&b #0a0b #0c0d SWP2 NIP2 [ #0a0b ] EQU2 JMP2r
+@op-ovr ;Dict/ovr !set
+	&a #02 #10 OVR DIV ADD [ #0a ] EQU JMP2r
+	&b #0a0b #0c0d OVR2 NIP2 ADD2 [ #1416 ] EQU2 JMP2r
+@op-rot ;Dict/rot !set
+	&a #02 #04 #10 ROT DIV ADD [ #0c ] EQU JMP2r
+	&b #0a0b #0c0d #0c0f ROT2 ADD2 NIP2 [ #161a ] EQU2 JMP2r
+@op-and ;Dict/and !set
+	&a #fc #3f AND [ #3c ] EQU JMP2r
+	&b #f0 #0f AND [ #00 ] EQU JMP2r
+	&c #ff #3c AND [ #3c ] EQU JMP2r
+	&d #02 #03 AND [ #02 ] EQU JMP2r
+	&e #f0f0 #00f0 AND2 [ #00f0 ] EQU2 JMP2r
+	&f #aaaa #5555 AND2 [ #0000 ] EQU2 JMP2r
+	&g #ffff #1234 AND2 [ #1234 ] EQU2 JMP2r
+	&h #abcd #0a0c AND2 [ #0a0c ] EQU2 JMP2r
+@op-ora ;Dict/ora !set
+	&a #0f #f0 ORA [ #ff ] EQU JMP2r
+	&b #ab #cd ORA [ #ef ] EQU JMP2r
+	&c #12 #34 ORA [ #36 ] EQU JMP2r
+	&d #88 #10 ORA [ #98 ] EQU JMP2r
+	&e #0f0f #f0f0 ORA2 [ #ffff ] EQU2 JMP2r
+	&f #abab #cdcd ORA2 [ #efef ] EQU2 JMP2r
+	&g #1122 #1234 ORA2 [ #1336 ] EQU2 JMP2r
+	&h #8888 #1000 ORA2 [ #9888 ] EQU2 JMP2r
+@op-eor ;Dict/eor !set
+	&a #00 #00 EOR [ #00 ] EQU JMP2r
+	&b #ff #00 EOR [ #ff ] EQU JMP2r
+	&c #aa #55 EOR [ #ff ] EQU JMP2r
+	&d #ff #ff EOR [ #00 ] EQU JMP2r
+	&e #ffff #ff00 EOR2 [ #00ff ] EQU2 JMP2r
+	&f #aaaa #5555 EOR2 [ #ffff ] EQU2 JMP2r
+	&g #1122 #1234 EOR2 [ #0316 ] EQU2 JMP2r
+	&h #8888 #1000 EOR2 [ #9888 ] EQU2 JMP2r
+@op-sft ;Dict/sft !set
+	&a #ff #08 SFT [ #00 ] EQU JMP2r
+	&b #ff #e0 SFT [ #00 ] EQU JMP2r
+	&c #ff #11 SFT [ #fe ] EQU JMP2r
+	&d #ff #12 SFT [ #7e ] EQU JMP2r
+	&e #ffff #01 SFT2 [ #7fff ] EQU2 JMP2r
+	&f #ffff #70 SFT2 [ #ff80 ] EQU2 JMP2r
+	&g #ffff #7e SFT2 [ #0180 ] EQU2 JMP2r
+	&h #ffff #e3 SFT2 [ #c000 ] EQU2 JMP2r
+@op-stz ;Dict/stz !set
+	&a #ab .Zeropage/byte STZ .Zeropage/byte LDZ [ #ab ] EQU JMP2r
+	&b #cd .Zeropage/byte STZ .Zeropage/byte LDZ [ #cd ] EQU JMP2r
+	&c #1234 .Zeropage/short STZ2 .Zeropage/short LDZ2 [ #1234 ] EQU2 JMP2r
+	&d #5678 .Zeropage/short STZ2 .Zeropage/short LDZ2 [ #5678 ] EQU2 JMP2r
+@op-str ;Dict/str !set
+	[ LIT &before1 $1 ] [ LIT2 &before2 $2 ]
+	&a #22 ,&before1 STR ,&before1 LDR [ #22 ] EQU JMP2r
+	&b #ef ,&after1 STR ,&after1 LDR [ #ef ] EQU JMP2r
+	&c #1234 ,&before2 STR2 ,&before2 LDR2 [ #1234 ] EQU2 JMP2r
+	&d #5678 ,&after2 STR2 ,&after2 LDR2 [ #5678 ] EQU2 JMP2r
+	[ LIT &after1 $1 ] [ LIT2 &after2 $2 ]
+@op-sta ;Dict/sta !set
+	&a #34 ;Absolute/byte STA ;Absolute/byte LDA [ #34 ] EQU JMP2r
+	&b #56 ;Absolute/byte STA ;Absolute/byte LDA [ #56 ] EQU JMP2r
+	&c #1234 ;Absolute/short STA2 ;Absolute/short LDA2 [ #1234 ] EQU2 JMP2r
+	&d #5678 ;Absolute/short STA2 ;Absolute/short LDA2 [ #5678 ] EQU2 JMP2r
+@op-jmp ;Dict/jmp !set
+	&a #12 #34 ,&reljmp JMP SWP &reljmp POP [ #12 ] EQU JMP2r
+	&b #56 #78 ;&absjmp JMP2 SWP &absjmp POP [ #56 ] EQU JMP2r
+@op-jcn ;Dict/jcn !set
+	&a #23 #01 ,&reljcn-y JCN INC &reljcn-y [ #23 ] EQU JMP2r
+	&b #23 #00 ,&reljcn-n JCN INC &reljcn-n [ #24 ] EQU JMP2r
+	&c #23 #01 ;&absjcn-y JCN2 INC &absjcn-y [ #23 ] EQU JMP2r
+	&d #23 #00 ;&absjcn-n JCN2 INC &absjcn-n [ #24 ] EQU JMP2r
+@op-jsr ;Dict/jsr !set
+	&a #1234 #5678 ,&routine JSR [ #68ac ] EQU2 JMP2r
+	&b #12 #34 ;routine JSR2 [ #46 ] EQU JMP2r
+	&routine ADD2 JMP2r
+@op-sth ;Dict/sth !set
+	&a #0a STH #0b STH ADDr STHr [ #15 ] EQU JMP2r
+	&b #000a STH2 #000b STH2 ADD2r STH2r [ #0015 ] EQU2 JMP2r
+@op-jci ;Dict/jci !set
+	&before #01 JMP2r
+	&a #01 ?&skip-a #00 JMP2r &skip-a #01 JMP2r
+	&b #00 ?&skip-b #01 JMP2r &skip-b #00 JMP2r
+	&c #01 ?&before #00 JMP2r
+@op-jmi ;Dict/jmi !set
+	&a !&skip-a #00 JMP2r &skip-a #01 JMP2r
+@op-jsi ;Dict/jsi !set
+	&a #02 #04 routine #06 EQU JMP2r
+	&b ;&return special &return JMP2r
+	&c ,&skip-c JMP &routine-c ADD JMP2r &skip-c #02 #04 op-jsi/routine-c #06 EQU JMP2r
+	&d ,&skip-d JMP &routine-d ADD JMP2r &skip-d #02 #04 op-jsi-far-routine-d #06 EQU JMP2r
+
+@special ( routine* -- f )
+
+	( test that the stack order is LIFO )
+	DUP2 STH2kr EQU2
+	ROT ROT DUP2r STHr STHr SWP EQU2 AND
+
+JMP2r
+
+@routine ( a b -- c ) ADD JMP2r
+@subroutine ( -- ) [ LIT2 "kO ] #18 DEO #18 DEO JMP2r
+@Absolute &byte $1 &short $2
+
+@Dict [
+	&ok "Ok $1
+	&done "Tests 20 "Complete. 0a $1
+	&opctests "Opcodes $1
+	&stack-wrap "Stack-wrap $1
+	&ram-wrap "RAM-wrap $1
+	&zp-wrap "Zeropage-wrap $1
+	&dev-wrap "Devices-wrap $1
+	&result "Result: $1
+	&passed 20 "passed! 0a $1
+	&missed "Opcode 20 "Failed 20 "-- 20 $1
+	&failed 20 "failed. 0a $1
+	&equ "EQU $1 &neq "NEQ $1 &gth "GTH $1 &lth "LTH $1
+	&add "ADD $1 &sub "SUB $1 &mul "MUL $1 &div "DIV $1
+	&inc "INC $1 &pop "POP $1 &dup "DUP $1 &nip "NIP $1
+	&swp "SWP $1 &ovr "OVR $1 &rot "ROT $1
+	&and "AND $1 &ora "ORA $1 &eor "EOR $1 &sft "SFT $1
+	&stz "STZ $1 &str "STR $1 &sta "STA $1
+	&jmp "JMP $1 &jcn "JCN $1 &jsr "JSR $1 &sth "STH $1
+	&jmi "JMI $1 &jci "JCI $1 &jsi "JSI $1
+]
+
+(
+@|Relative Distance Bytes )
+
+@rel-distance
+&back "O $7c
+&entry
+	,&back LDR
+	,&forw LDR
+	JMP2r
+$7e
+&forw "k
+
+@op-jsi-far-routine-d
+	op-jsi/routine-d JMP2r
+
diff --git a/awk/uxn/test/tal/proper.tal b/awk/uxn/test/tal/proper.tal
new file mode 100644
index 0000000..be8e04b
--- /dev/null
+++ b/awk/uxn/test/tal/proper.tal
@@ -0,0 +1 @@
+@on-reset #01 #02 ADD BRK
\ No newline at end of file
diff --git a/awk/uxn/test/tal/simple.tal b/awk/uxn/test/tal/simple.tal
new file mode 100644
index 0000000..c055e74
--- /dev/null
+++ b/awk/uxn/test/tal/simple.tal
@@ -0,0 +1 @@
+#01 #02 ADD BRK 
\ No newline at end of file
diff --git a/awk/uxn/test/tal/simple2.tal b/awk/uxn/test/tal/simple2.tal
new file mode 100644
index 0000000..6a37b65
--- /dev/null
+++ b/awk/uxn/test/tal/simple2.tal
@@ -0,0 +1 @@
+#01 #02 ADD BRK
\ No newline at end of file
diff --git a/awk/uxn/test/tal/simple3.tal b/awk/uxn/test/tal/simple3.tal
new file mode 100644
index 0000000..09086b7
--- /dev/null
+++ b/awk/uxn/test/tal/simple3.tal
@@ -0,0 +1 @@
+#01 #02 ADD
\ No newline at end of file