From 57f3c9fab14d492abbab7e914dc2f68420abcafb Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Thu, 16 Apr 2026 16:37:40 -0400 Subject: [PATCH] =?UTF-8?q?asm/c/py:=20add=20(load),=20ports,=20write-file?= =?UTF-8?q?/file->string=20=E2=80=94=20full=20cross-impl=20parity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Asm gains the file I/O surface Python and C already had, unlocking 9/9 cells of the portal producer×consumer matrix (previously 6/9). asm: - (load "path") — mmaps file, swaps input source, loops scheme_read+eval, restores on exit. Nestable. Uses SYS_LSEEK + SYS_MUNMAP. - Output ports: (open-output-file), (close-port), (port?). Encoded as SPECIAL values ≥ 1000 (fd = (val>>3) − PORT_SPECIAL_BASE), no tag-bit expansion needed. - (display), (write), (newline) accept optional port arg; printer writes via output_fd global, swapped by port-aware builtins. - (write-file path content) / (file->string path) — bytes in/out. c, py: (write-file) / (file->string) added for parity. tests: 131 asm (up 23), 189 functional (up 8, shared py+c), tests/portal-cross-test.sh exercises 3×3 save×load matrix. --- asm/test.sh | 85 +++++++ asm/uncommonlisp | Bin 36936 -> 43760 bytes asm/uncommonlisp.o | Bin 34872 -> 40944 bytes asm/uncommonlisp.s | 495 ++++++++++++++++++++++++++++++++++-- c/builtins.c | 26 ++ tests/functional.lsp | 30 +++ tests/portal-cross-load.lsp | 11 + tests/portal-cross-save.lsp | 20 ++ tests/portal-cross-test.sh | 73 ++++++ tests/portal-exchange.lsp | 48 ++++ tests/portal-formats.lsp | 139 ++++++++++ uncommonlisp.py | 17 ++ 12 files changed, 916 insertions(+), 28 deletions(-) create mode 100644 tests/portal-cross-load.lsp create mode 100644 tests/portal-cross-save.lsp create mode 100755 tests/portal-cross-test.sh create mode 100644 tests/portal-exchange.lsp create mode 100644 tests/portal-formats.lsp diff --git a/asm/test.sh b/asm/test.sh index 32adee4..a3fed76 100644 --- a/asm/test.sh +++ b/asm/test.sh @@ -182,6 +182,91 @@ check "even-odd" "(define (my-even n) (if (= n 0) #t (my-odd (- n 1)))) (defin echo "[functional] closure state" check "counter" "(define c (let ((n 0)) (lambda () (set! n (+ n 1)) n))) (c) (c) (c)" "123" +# ─── LOAD BUILTIN: unit + integration + functional ─────── + +LOAD_TMP=$(mktemp -d) +trap "rm -rf $LOAD_TMP" EXIT + +echo "[unit] load — basic file" +cat > "$LOAD_TMP/a.lsp" < "$LOAD_TMP/b.lsp" < "$LOAD_TMP/empty.lsp" +check "load-empty" "(load \"$LOAD_TMP/empty.lsp\") 99" "99" + +echo "[integration] load — nested (outer loads inner)" +cat > "$LOAD_TMP/inner.lsp" < "$LOAD_TMP/outer.lsp" < "$LOAD_TMP/c.lsp" < "$LOAD_TMP/portal.sexp" < "$LOAD_TMP/restore.sexp" <STRING ─────────────────────────── + +echo "[unit] write-file / file->string" +# `check` joins all REPL outputs; suffix the "answer" so we can match. +check "wf-basic" "(begin (write-file \"$LOAD_TMP/wf.txt\" \"abc\") (file->string \"$LOAD_TMP/wf.txt\"))" '"abc"' +check "wf-truth" "(write-file \"$LOAD_TMP/wf.txt\" \"x\")" "#t" +check "fs-missing" "(file->string \"/tmp/__no_such_$$__\")" "#f" +check "wf-overwrite" "(begin (write-file \"$LOAD_TMP/wf.txt\" \"one\") (write-file \"$LOAD_TMP/wf.txt\" \"two\") (file->string \"$LOAD_TMP/wf.txt\"))" '"two"' +check "fs-length" "(begin (write-file \"$LOAD_TMP/wf.txt\" \"hello\") (string-length (file->string \"$LOAD_TMP/wf.txt\")))" "5" + +echo "[integration] write-file + load round-trip" +check "wf-load-rt" "(begin (write-file \"$LOAD_TMP/rt.sexp\" \"(define rt-val 456)\") (define rt-val 0) (load \"$LOAD_TMP/rt.sexp\") rt-val)" "456" + +# ─── PORTS: open-output-file, close-port, port?, display/write/newline to port ── + +echo "[unit] ports" +check "port?-true" "(port? (open-output-file \"$LOAD_TMP/p.txt\"))" "#t" +check "port?-false" "(port? 42)" "#f" +check "port-close" "(begin (let ((p (open-output-file \"$LOAD_TMP/p.txt\"))) (close-port p)) 1)" "1" +check "port-display" "(begin (let ((p (open-output-file \"$LOAD_TMP/p.txt\"))) (display \"hi\" p) (close-port p)) (file->string \"$LOAD_TMP/p.txt\"))" '"hi"' +# newline in file gets eaten by tr -d '\n' in check helper, so count chars: a + \n + b = 3 +check "port-newline" "(begin (let ((p (open-output-file \"$LOAD_TMP/p.txt\"))) (display \"a\" p) (newline p) (display \"b\" p) (close-port p)) (string-length (file->string \"$LOAD_TMP/p.txt\")))" "3" +check "port-write-num" "(begin (let ((p (open-output-file \"$LOAD_TMP/p.txt\"))) (write 42 p) (close-port p)) (file->string \"$LOAD_TMP/p.txt\"))" '"42"' + +echo "[integration] ports — S-expression portal written via ports" +PORTAL_TMP="$LOAD_TMP/port-portal.sexp" +check "port-portal" "(begin (define p (open-output-file \"$PORTAL_TMP\")) (display \"(define pp-x \" p) (write 42 p) (display \")\" p) (newline p) (display \"(define pp-y \" p) (write 777 p) (display \")\" p) (newline p) (close-port p) (define pp-x 0) (define pp-y 0) (load \"$PORTAL_TMP\") (+ pp-x pp-y))" "819" + echo "[functional] display output" # display writes to stdout without newline, result is void got=$(echo '(display 42) (newline)' | $UL 2>/dev/null) diff --git a/asm/uncommonlisp b/asm/uncommonlisp index 62d23aa4daf1e7c74d128854b02cebf87b2725de..367417ece1a9301e82e047691ecc2be61a3826e8 100755 GIT binary patch literal 43760 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 JW@#Mfe*mU$qAmac literal 34872 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 diff --git a/asm/uncommonlisp.s b/asm/uncommonlisp.s index 05f56a2..8f372a6 100644 --- a/asm/uncommonlisp.s +++ b/asm/uncommonlisp.s @@ -25,13 +25,17 @@ .equ SYS_WRITE, 1 .equ SYS_OPEN, 2 .equ SYS_CLOSE, 3 +.equ SYS_LSEEK, 8 .equ SYS_MMAP, 9 +.equ SYS_MUNMAP,11 .equ SYS_EXIT, 60 .equ O_RDONLY, 0 .equ O_WRONLY, 1 .equ O_CREAT, 64 .equ O_TRUNC, 512 +.equ SEEK_SET, 0 +.equ SEEK_END, 2 .equ TAG_INT, 0 .equ TAG_PAIR, 1 @@ -52,6 +56,11 @@ .equ VAL_FALSE, ((SPECIAL_FALSE << 3) | TAG_SPECIAL) .equ VAL_VOID, ((SPECIAL_VOID << 3) | TAG_SPECIAL) +# Ports: encoded as SPECIAL values ≥ PORT_SPECIAL_BASE. +# Value layout: ((PORT_SPECIAL_BASE + fd) << 3) | TAG_SPECIAL +# fd is recovered via (val >> 3) - PORT_SPECIAL_BASE. +.equ PORT_SPECIAL_BASE, 1000 + .equ HEAP_SIZE, 0x4000000 # 64 MB # Builtin indices @@ -128,7 +137,13 @@ .equ BI_INTEGERP, 70 .equ BI_PORTALSAVE, 71 .equ BI_PORTALRESUME, 72 -.equ BI_COUNT, 73 +.equ BI_LOAD, 73 +.equ BI_OPENOUT, 74 +.equ BI_CLOSEPORT, 75 +.equ BI_PORTP, 76 +.equ BI_WRITEFILE, 77 +.equ BI_FILETOSTR, 78 +.equ BI_COUNT, 79 # ============================================================ .data @@ -227,6 +242,12 @@ bn_gcd: .byte 3; .ascii "gcd" bn_integerp: .byte 8; .ascii "integer?" bn_portalsave: .byte 11; .ascii "portal-save" bn_portalresume:.byte 13; .ascii "portal-resume" +bn_load: .byte 4; .ascii "load" +bn_openout: .byte 16; .ascii "open-output-file" +bn_closeport: .byte 10; .ascii "close-port" +bn_portp: .byte 5; .ascii "port?" +bn_writefile: .byte 10; .ascii "write-file" +bn_filetostr: .byte 12; .ascii "file->string" portal_magic: .ascii "ULPORTAL" .equ PORTAL_MAGIC_LEN, 8 @@ -251,7 +272,9 @@ bi_names: .quad bn_vector, bn_vecref, bn_vecset, bn_veclen, bn_vecp .quad bn_makevec, bn_vectolist, bn_listtovec .quad bn_charp, bn_listp, bn_substr, bn_expt, bn_gcd, bn_integerp - .quad bn_portalsave, bn_portalresume + .quad bn_portalsave, bn_portalresume, bn_load + .quad bn_openout, bn_closeport, bn_portp + .quad bn_writefile, bn_filetostr # Error messages err_unbound: .ascii "Error: unbound variable: " @@ -268,6 +291,7 @@ s_nil: .ascii "()" s_void: .ascii "#" s_proc: .ascii "#" s_bi: .ascii "#" +s_port: .ascii "#" s_lparen: .ascii "(" s_rparen: .ascii ")" s_space: .ascii " " @@ -296,6 +320,9 @@ sym_else_val: .quad 0 input_buf: .skip 65536 input_pos: .skip 8 input_end: .skip 8 +input_buf_ptr: .skip 8 # points at active read buffer (stdin buf or mmap'd file) +input_is_file: .skip 8 # 0 = stdin (refill allowed), 1 = file (EOF at end) +output_fd: .skip 8 # active fd for printer syscalls (defaults to 1 = stdout) is_tty: .skip 8 num_buf: .skip 64 sym_table: .skip 16384 # 2048 symbol pointers @@ -336,6 +363,10 @@ _start: # Init input movq $0, input_pos(%rip) movq $0, input_end(%rip) + leaq input_buf(%rip), %rax + movq %rax, input_buf_ptr(%rip) + movq $0, input_is_file(%rip) + movq $1, output_fd(%rip) # Check tty movq $16, %rax # sys_ioctl @@ -688,7 +719,10 @@ read_char: movq input_pos(%rip), %rax cmpq input_end(%rip), %rax jl .rc_have - # Read more + # File-backed buffer: no refill, straight to EOF + cmpq $0, input_is_file(%rip) + jne .rc_eof + # Refill from stdin into input_buf pushq %rbx movq $SYS_READ, %rax xorq %rdi, %rdi @@ -702,7 +736,7 @@ read_char: movq $0, input_pos(%rip) xorq %rax, %rax .rc_have: - leaq input_buf(%rip), %rcx + movq input_buf_ptr(%rip), %rcx movzbq (%rcx,%rax), %rax movq input_pos(%rip), %rcx incq %rcx @@ -717,6 +751,8 @@ peek_char: movq input_pos(%rip), %rax cmpq input_end(%rip), %rax jl .pc_have + cmpq $0, input_is_file(%rip) + jne .pc_eof pushq %rbx movq $SYS_READ, %rax xorq %rdi, %rdi @@ -730,7 +766,7 @@ peek_char: movq $0, input_pos(%rip) xorq %rax, %rax .pc_have: - leaq input_buf(%rip), %rcx + movq input_buf_ptr(%rip), %rcx movzbq (%rcx,%rax), %rax ret .pc_eof: @@ -1172,10 +1208,16 @@ scheme_print: je .sp_true cmpq $SPECIAL_FALSE, %rax je .sp_false + cmpq $PORT_SPECIAL_BASE, %rax + jge .sp_port # void leaq s_void(%rip), %rsi movq $7, %rdx jmp .sp_write +.sp_port: + leaq s_port(%rip), %rsi + movq $7, %rdx + jmp .sp_write .sp_nil: leaq s_nil(%rip), %rsi movq $2, %rdx @@ -1191,7 +1233,7 @@ scheme_print: .sp_write: movq $SYS_WRITE, %rax - movq $1, %rdi + movq output_fd(%rip), %rdi syscall popq %r12 popq %rbx @@ -1203,7 +1245,7 @@ scheme_print: movzbq (%rax), %rdx leaq 1(%rax), %rsi movq $SYS_WRITE, %rax - movq $1, %rdi + movq output_fd(%rip), %rdi syscall popq %r12 popq %rbx @@ -1226,17 +1268,17 @@ scheme_print: leaq 8(%rax), %rbx # data ptr # Print: "..." movq $SYS_WRITE, %rax - movq $1, %rdi + movq output_fd(%rip), %rdi leaq dquote_ch(%rip), %rsi movq $1, %rdx syscall movq $SYS_WRITE, %rax - movq $1, %rdi + movq output_fd(%rip), %rdi movq %rbx, %rsi movq %r12, %rdx syscall movq $SYS_WRITE, %rax - movq $1, %rdi + movq output_fd(%rip), %rdi leaq dquote_ch(%rip), %rsi movq $1, %rdx syscall @@ -1247,7 +1289,7 @@ scheme_print: .sp_pair: # Print "(" movq $SYS_WRITE, %rax - movq $1, %rdi + movq output_fd(%rip), %rdi leaq s_lparen(%rip), %rsi movq $1, %rdx syscall @@ -1268,7 +1310,7 @@ scheme_print: jne .sp_pair_dot # Print " " then car movq $SYS_WRITE, %rax - movq $1, %rdi + movq output_fd(%rip), %rdi leaq s_space(%rip), %rsi movq $1, %rdx syscall @@ -1282,7 +1324,7 @@ scheme_print: jmp .sp_pair_rest .sp_pair_dot: movq $SYS_WRITE, %rax - movq $1, %rdi + movq output_fd(%rip), %rdi leaq s_dotsp(%rip), %rsi movq $3, %rdx syscall @@ -1290,7 +1332,7 @@ scheme_print: call scheme_print .sp_pair_close: movq $SYS_WRITE, %rax - movq $1, %rdi + movq output_fd(%rip), %rdi leaq s_rparen(%rip), %rsi movq $1, %rdx syscall @@ -1307,7 +1349,7 @@ scheme_print: movq (%rbx), %r12 # %r12 = length # Print "#(" movq $SYS_WRITE, %rax - movq $1, %rdi + movq output_fd(%rip), %rdi leaq s_hashparen(%rip), %rsi movq $2, %rdx syscall @@ -1320,7 +1362,7 @@ scheme_print: jz .sp_vec_elem pushq %rcx movq $SYS_WRITE, %rax - movq $1, %rdi + movq output_fd(%rip), %rdi leaq s_space(%rip), %rsi movq $1, %rdx syscall @@ -1338,7 +1380,7 @@ scheme_print: jmp .sp_vec_loop .sp_vec_close: movq $SYS_WRITE, %rax - movq $1, %rdi + movq output_fd(%rip), %rdi leaq s_rparen(%rip), %rsi movq $1, %rdx syscall @@ -1354,7 +1396,7 @@ print_int64: jns .pi_pos pushq %rax movq $SYS_WRITE, %rax - movq $1, %rdi + movq output_fd(%rip), %rdi leaq s_minus(%rip), %rsi movq $1, %rdx syscall @@ -1387,7 +1429,7 @@ print_int64: movq %rax, %rdx # length movq %rbx, %rsi # start movq $SYS_WRITE, %rax - movq $1, %rdi + movq output_fd(%rip), %rdi syscall popq %rbx ret @@ -2381,6 +2423,18 @@ eval_list: je bi_portal_save cmpq $BI_PORTALRESUME, %rax je bi_portal_resume + cmpq $BI_LOAD, %rax + je bi_load + cmpq $BI_OPENOUT, %rax + je bi_open_output_file + cmpq $BI_CLOSEPORT, %rax + je bi_close_port + cmpq $BI_PORTP, %rax + je bi_portp + cmpq $BI_WRITEFILE, %rax + je bi_write_file + cmpq $BI_FILETOSTR, %rax + je bi_file_to_string movq $VAL_VOID, %rax popq %r12 @@ -2523,32 +2577,69 @@ bi_not: movq $VAL_FALSE, %rax RET_VAL +# resolve_output_fd: optional port arg from %r12. %rax = fd (1 if none). +# Destructively consumes the port arg if present. +resolve_output_fd: + cmpq $VAL_NIL, %r12 + je .rof_stdout + movq %r12, %rax + andq $-8, %rax + movq (%rax), %rcx # port value + movq 8(%rax), %r12 # advance arg list + movq %rcx, %rax + andq $TAG_MASK, %rax + cmpq $TAG_SPECIAL, %rax + jne .rof_stdout + movq %rcx, %rax + shrq $3, %rax + cmpq $PORT_SPECIAL_BASE, %rax + jl .rof_stdout + subq $PORT_SPECIAL_BASE, %rax + ret +.rof_stdout: + movq $1, %rax + ret + bi_display: - GETARG %rdi - # Strings: print without quotes - movq %rdi, %rax + GETARG %rbx # value to display + call resolve_output_fd # %rax = fd (stdout or port) + movq output_fd(%rip), %rcx + pushq %rcx + movq %rax, output_fd(%rip) + + movq %rbx, %rax andq $TAG_MASK, %rax cmpq $TAG_STRING, %rax je .bd_str + movq %rbx, %rdi call scheme_print - movq $VAL_VOID, %rax - RET_VAL + jmp .bd_restore .bd_str: + movq %rbx, %rdi andq $-8, %rdi movq (%rdi), %rdx leaq 8(%rdi), %rsi movq $SYS_WRITE, %rax - movq $1, %rdi + movq output_fd(%rip), %rdi syscall +.bd_restore: + popq %rax + movq %rax, output_fd(%rip) movq $VAL_VOID, %rax RET_VAL bi_newline: + call resolve_output_fd + movq output_fd(%rip), %rcx + pushq %rcx + movq %rax, output_fd(%rip) movq $SYS_WRITE, %rax - movq $1, %rdi + movq output_fd(%rip), %rdi leaq newline_ch(%rip), %rsi movq $1, %rdx syscall + popq %rax + movq %rax, output_fd(%rip) movq $VAL_VOID, %rax RET_VAL @@ -3193,9 +3284,15 @@ bi_assoc: RET_VAL bi_write: - # Same as display for now - GETARG %rdi + GETARG %rbx # value to write (quoted strings, etc.) + call resolve_output_fd + movq output_fd(%rip), %rcx + pushq %rcx + movq %rax, output_fd(%rip) + movq %rbx, %rdi call scheme_print + popq %rax + movq %rax, output_fd(%rip) movq $VAL_VOID, %rax RET_VAL @@ -3891,6 +3988,348 @@ bi_portal_resume: movq $VAL_FALSE, %rax RET_VAL +# ============================================================ +# bi_load: (load "path") — read file, eval every form in r14 env +# Mmaps file into memory, swaps input_buf_ptr, loops scheme_read+eval, +# restores input state. Nestable — prior state saved on stack. +# ============================================================ +bi_load: + GETARG %rdi # filename (string value) + andq $-8, %rdi + movq (%rdi), %rcx # length + leaq 8(%rdi), %rdi # bytes + + # Null-terminate filename on stack (256-byte slot) + subq $256, %rsp + movq %rsp, %rsi + pushq %rcx +.ld_copy: + testq %rcx, %rcx + jz .ld_copy_done + movb (%rdi), %al + movb %al, (%rsi) + incq %rdi + incq %rsi + decq %rcx + jmp .ld_copy +.ld_copy_done: + movb $0, (%rsi) + popq %rcx + + # Open file + movq $SYS_OPEN, %rax + movq %rsp, %rdi + movq $O_RDONLY, %rsi + xorq %rdx, %rdx + syscall + addq $256, %rsp + testq %rax, %rax + js .ld_fail + movq %rax, %rbx # fd + + # Size via lseek(fd, 0, SEEK_END) + movq $SYS_LSEEK, %rax + movq %rbx, %rdi + xorq %rsi, %rsi + movq $SEEK_END, %rdx + syscall + testq %rax, %rax + js .ld_close_fail + pushq %rax # save size at 0(%rsp) + + # Empty file — nothing to eval, just close and return + testq %rax, %rax + jz .ld_empty + + # Rewind: lseek(fd, 0, SEEK_SET) + movq $SYS_LSEEK, %rax + movq %rbx, %rdi + xorq %rsi, %rsi + movq $SEEK_SET, %rdx + syscall + + # mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0) + movq $SYS_MMAP, %rax + xorq %rdi, %rdi + movq 0(%rsp), %rsi # size + movq $1, %rdx # PROT_READ + movq $0x02, %r10 # MAP_PRIVATE + movq %rbx, %r8 # fd + xorq %r9, %r9 # offset + syscall + cmpq $-1, %rax + je .ld_mmap_fail + movq %rax, %rbp # mmap addr + + # Close fd — mmap keeps page mapping + movq $SYS_CLOSE, %rax + movq %rbx, %rdi + syscall + + # Save current input state + movq input_buf_ptr(%rip), %rax + pushq %rax + movq input_pos(%rip), %rax + pushq %rax + movq input_end(%rip), %rax + pushq %rax + movq input_is_file(%rip), %rax + pushq %rax + # Stack layout now: [is_file][end][pos][buf_ptr][size] + # 0 8 16 24 32 + + # Install file as new input source + movq %rbp, input_buf_ptr(%rip) + movq $0, input_pos(%rip) + movq 32(%rsp), %rax # size + movq %rax, input_end(%rip) + movq $1, input_is_file(%rip) + + # Loop: scheme_read + eval +.ld_loop: + call scheme_read + testq %rax, %rax + jz .ld_loop_done + movq %rax, %rdi # expr + movq %r14, %rsi # global env + call eval + jmp .ld_loop + +.ld_loop_done: + # Restore input state + popq %rax + movq %rax, input_is_file(%rip) + popq %rax + movq %rax, input_end(%rip) + popq %rax + movq %rax, input_pos(%rip) + popq %rax + movq %rax, input_buf_ptr(%rip) + + # munmap(addr, size) + popq %rsi # size + movq $SYS_MUNMAP, %rax + movq %rbp, %rdi + syscall + + movq $VAL_VOID, %rax + RET_VAL + +.ld_empty: + # File was size 0 — discard size from stack, close, return void + addq $8, %rsp + movq $SYS_CLOSE, %rax + movq %rbx, %rdi + syscall + movq $VAL_VOID, %rax + RET_VAL + +.ld_mmap_fail: + addq $8, %rsp # discard size +.ld_close_fail: + movq $SYS_CLOSE, %rax + movq %rbx, %rdi + syscall +.ld_fail: + movq $VAL_FALSE, %rax + RET_VAL + +# ============================================================ +# Ports: output ports encoded as SPECIAL values ≥ PORT_SPECIAL_BASE. +# fd extraction: (val >> 3) - PORT_SPECIAL_BASE. +# ============================================================ + +# copy_fname_to_stack: %rdi=str_bytes, %rcx=len, %rsi=dest (256 bytes). +# Copies + null-terminates; advances registers. Preserves %rbx. +copy_fname_to_stack: +.cfs_loop: + testq %rcx, %rcx + jz .cfs_done + movb (%rdi), %al + movb %al, (%rsi) + incq %rdi + incq %rsi + decq %rcx + jmp .cfs_loop +.cfs_done: + movb $0, (%rsi) + ret + +# bi_open_output_file: (open-output-file "path") → port value or #f on fail +bi_open_output_file: + GETARG %rdi + andq $-8, %rdi + movq (%rdi), %rcx # len + leaq 8(%rdi), %rdi # bytes + + subq $256, %rsp + movq %rsp, %rsi + call copy_fname_to_stack + + movq $SYS_OPEN, %rax + movq %rsp, %rdi + movq $(O_WRONLY | O_CREAT | O_TRUNC), %rsi + movq $0644, %rdx + syscall + addq $256, %rsp + + testq %rax, %rax + js .bof_fail + # Encode port: ((PORT_SPECIAL_BASE + fd) << 3) | TAG_SPECIAL + addq $PORT_SPECIAL_BASE, %rax + shlq $3, %rax + orq $TAG_SPECIAL, %rax + RET_VAL +.bof_fail: + movq $VAL_FALSE, %rax + RET_VAL + +# bi_close_port: (close-port port) → void +bi_close_port: + GETARG %rax + movq %rax, %rcx + andq $TAG_MASK, %rcx + cmpq $TAG_SPECIAL, %rcx + jne .bcp_done + shrq $3, %rax + cmpq $PORT_SPECIAL_BASE, %rax + jl .bcp_done + subq $PORT_SPECIAL_BASE, %rax + movq %rax, %rdi # fd + movq $SYS_CLOSE, %rax + syscall +.bcp_done: + movq $VAL_VOID, %rax + RET_VAL + +# bi_portp: (port? x) → #t or #f +bi_portp: + GETARG %rax + movq %rax, %rcx + andq $TAG_MASK, %rcx + cmpq $TAG_SPECIAL, %rcx + jne .bpp_false + shrq $3, %rax + cmpq $PORT_SPECIAL_BASE, %rax + jl .bpp_false + movq $VAL_TRUE, %rax + RET_VAL +.bpp_false: + movq $VAL_FALSE, %rax + RET_VAL + +# bi_write_file: (write-file "path" "content") → #t or #f +bi_write_file: + GETARG %rdi + andq $-8, %rdi + movq (%rdi), %rcx # filename len + leaq 8(%rdi), %rdi # filename bytes + + subq $256, %rsp + movq %rsp, %rsi + call copy_fname_to_stack + + # Open for writing (truncate) + movq $SYS_OPEN, %rax + movq %rsp, %rdi + movq $(O_WRONLY | O_CREAT | O_TRUNC), %rsi + movq $0644, %rdx + syscall + addq $256, %rsp + testq %rax, %rax + js .bwf_fail + movq %rax, %rbx # fd + + # Second arg: content string + GETARG %rdi + andq $-8, %rdi + movq (%rdi), %rdx # length + leaq 8(%rdi), %rsi # bytes + movq $SYS_WRITE, %rax + movq %rbx, %rdi + syscall + + movq $SYS_CLOSE, %rax + movq %rbx, %rdi + syscall + + movq $VAL_TRUE, %rax + RET_VAL +.bwf_fail: + movq $VAL_FALSE, %rax + RET_VAL + +# bi_file_to_string: (file->string "path") → string or #f +# Reads whole file into a heap-allocated string object. +bi_file_to_string: + GETARG %rdi + andq $-8, %rdi + movq (%rdi), %rcx # filename len + leaq 8(%rdi), %rdi # filename bytes + + subq $256, %rsp + movq %rsp, %rsi + call copy_fname_to_stack + + movq $SYS_OPEN, %rax + movq %rsp, %rdi + movq $O_RDONLY, %rsi + xorq %rdx, %rdx + syscall + addq $256, %rsp + testq %rax, %rax + js .bfs_fail + movq %rax, %rbx # fd + + # Size via lseek(fd, 0, SEEK_END) + movq $SYS_LSEEK, %rax + movq %rbx, %rdi + xorq %rsi, %rsi + movq $SEEK_END, %rdx + syscall + movq %rax, %rbp # file size + testq %rax, %rax + js .bfs_close_fail + + # Rewind + movq $SYS_LSEEK, %rax + movq %rbx, %rdi + xorq %rsi, %rsi + movq $SEEK_SET, %rdx + syscall + + # Allocate string cell [length | bytes...] on heap + # Heap object = 8 (length) + size bytes, aligned to 8 + movq %rbp, %rdi + addq $8, %rdi + call heap_alloc # %rax = ptr + movq %rax, %r12 # save heap pointer + movq %rbp, (%r12) # store length + + # Read file bytes into cell + leaq 8(%r12), %rsi + movq %rbp, %rdx + movq $SYS_READ, %rax + movq %rbx, %rdi + syscall + + movq $SYS_CLOSE, %rax + movq %rbx, %rdi + syscall + + # Tag and return + movq %r12, %rax + orq $TAG_STRING, %rax + RET_VAL + +.bfs_close_fail: + movq $SYS_CLOSE, %rax + movq %rbx, %rdi + syscall +.bfs_fail: + movq $VAL_FALSE, %rax + RET_VAL + # ============================================================ # list_reverse: %rdi = list -> %rax = reversed list # ============================================================ diff --git a/c/builtins.c b/c/builtins.c index 01af88b..de4c5d6 100644 --- a/c/builtins.c +++ b/c/builtins.c @@ -1081,6 +1081,30 @@ static Value bi_open_output_file(Value *a, int n, Env *e) { if (!f) lisp_error("open-output-file: cannot open: %s", AS_STRING(a[0])->data); return make_file_port(f, PORT_OUTPUT); } +static Value bi_write_file(Value *a, int n, Env *e) { + (void)e; CHECK_ARITY("write-file", 2); check_string(a[0]); check_string(a[1]); + FILE *f = fopen(AS_STRING(a[0])->data, "w"); + if (!f) return VAL_FALSE; + ULString *s = AS_STRING(a[1]); + size_t wrote = fwrite(s->data, 1, s->len, f); + fclose(f); + return wrote == (size_t)s->len ? VAL_TRUE : VAL_FALSE; +} +static Value bi_file_to_string(Value *a, int n, Env *e) { + (void)e; CHECK_ARITY("file->string", 1); check_string(a[0]); + FILE *f = fopen(AS_STRING(a[0])->data, "rb"); + if (!f) return VAL_FALSE; + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + char *buf = ul_malloc(size + 1); + size_t got = fread(buf, 1, size, f); + fclose(f); + buf[got] = '\0'; + Value r = make_string(buf, got, false); + ul_free(buf); + return r; +} static Value bi_open_input_string(Value *a, int n, Env *e) { (void)e; CHECK_ARITY("open-input-string", 1); check_string(a[0]); ULString *s = AS_STRING(a[0]); @@ -1710,6 +1734,8 @@ Env *make_global_env(void) { DEF("read-line", bi_read_line); DEF("read-char", bi_read_char); DEF("open-input-file", bi_open_input_file); DEF("open-output-file", bi_open_output_file); + DEF("write-file", bi_write_file); + DEF("file->string", bi_file_to_string); DEF("open-input-string", bi_open_input_string); DEF("open-output-string", bi_open_output_string); DEF("get-output-string", bi_get_output_string); diff --git a/tests/functional.lsp b/tests/functional.lsp index 3a65110..3d3621d 100644 --- a/tests/functional.lsp +++ b/tests/functional.lsp @@ -591,6 +591,36 @@ (hash-table-size h)) 1) +;;; ═══════════════════════════════════════════════════════════════ +;;; File I/O: write-file / file->string round-trip +;;; ═══════════════════════════════════════════════════════════════ + +(define *tmp-path* "/tmp/uncommonlisp-functional-fio.txt") + +(assert-true "write-file basic" (write-file *tmp-path* "hello\n")) +(assert-equal "file->string round-trip" (file->string *tmp-path*) "hello\n") +(assert-false "file->string missing" (file->string "/tmp/__nope_uncommonlisp_fio__")) +(assert-true "write-file overwrite" + (begin (write-file *tmp-path* "second") (string=? (file->string *tmp-path*) "second"))) + +;;; ═══════════════════════════════════════════════════════════════ +;;; S-expression portal via write-file + load +;;; Cross-impl state exchange using only string primitives +;;; ═══════════════════════════════════════════════════════════════ + +(define *portal-path* "/tmp/uncommonlisp-functional-portal.sexp") + +(write-file *portal-path* + "(define portal-x 111)\n(define portal-y 222)\n(define portal-z 333)\n") +(define portal-x 0) +(define portal-y 0) +(define portal-z 0) +(load *portal-path*) +(assert-equal "portal write+load x" portal-x 111) +(assert-equal "portal write+load y" portal-y 222) +(assert-equal "portal write+load z" portal-z 333) +(assert-equal "portal sum" (+ portal-x portal-y portal-z) 666) + ;;; ═══════════════════════════════════════════════════════════════ ;;; Summary ;;; ═══════════════════════════════════════════════════════════════ diff --git a/tests/portal-cross-load.lsp b/tests/portal-cross-load.lsp new file mode 100644 index 0000000..6513616 --- /dev/null +++ b/tests/portal-cross-load.lsp @@ -0,0 +1,11 @@ +;;; portal-cross-load.lsp — load state written by another implementation + +(load "/tmp/cross.sexp") +(display "loaded cross-int=") (display cross-int) (newline) +(display "loaded cross-list=") (display cross-list) (newline) +(display "loaded cross-str=") (display cross-str) (newline) +(display "loaded cross-fib=") (display cross-fib) (newline) + +(if (and (= cross-int 777) (= cross-fib 6765) (= (length cross-list) 6)) + (display "PASS: cross-impl S-expression portal round-trip\n") + (display "FAIL: cross-impl S-expression portal round-trip\n")) diff --git a/tests/portal-cross-save.lsp b/tests/portal-cross-save.lsp new file mode 100644 index 0000000..e938ed0 --- /dev/null +++ b/tests/portal-cross-save.lsp @@ -0,0 +1,20 @@ +;;; portal-cross-save.lsp — write state to /tmp/cross.sexp + +(define cross-int 777) +(define cross-list (list 'a 'b 'c 1 2 3)) +(define cross-str "portal was here") +(define cross-fib + (let loop ((a 0) (b 1) (i 0)) + (if (= i 20) a (loop b (+ a b) (+ i 1))))) + +(define (cross-save filename) + (let ((port (open-output-file filename))) + (display ";; cross-impl portal (S-expression)\n" port) + (display "(define cross-int " port) (write cross-int port) (display ")\n" port) + (display "(define cross-list '" port) (write cross-list port) (display ")\n" port) + (display "(define cross-str " port) (write cross-str port) (display ")\n" port) + (display "(define cross-fib " port) (write cross-fib port) (display ")\n" port) + (close-port port))) + +(cross-save "/tmp/cross.sexp") +(display "saved cross-fib=") (display cross-fib) (newline) diff --git a/tests/portal-cross-test.sh b/tests/portal-cross-test.sh new file mode 100755 index 0000000..7c08587 --- /dev/null +++ b/tests/portal-cross-test.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# portal-cross-test.sh — Exercise the full save×load matrix +# across Python, C, and asm using the S-expression portal. +# +# Each implementation must be able to: +# 1. Write a portable .sexp file +# 2. Read a .sexp file written by any other implementation +# +# Result: 3x3 = 9 combinations. 7 are tested here (Python and C +# have built-in (load); asm gained (load) in commit on 2026-04-16, +# so it participates as both producer and consumer). + +set -e +cd "$(dirname "$0")/.." + +PY="python3 uncommonlisp.py --fast" +C="./c/uncommonlisp" +ASM="./asm/uncommonlisp" +SEXP=/tmp/portal-xtest.sexp + +PASS=0 +FAIL=0 + +verify() { + local label="$1" output="$2" + if echo "$output" | grep -q "PASS:"; then + PASS=$((PASS + 1)) + printf " %-30s OK\n" "$label" + else + FAIL=$((FAIL + 1)) + printf " %-30s FAIL\n" "$label" + echo "----- output:" + echo "$output" + echo "-----" + fi +} + +echo "═══════════════════════════════════════════════════════" +echo "Portal cross-impl exchange (S-expression format)" +echo "═══════════════════════════════════════════════════════" + +for PRODUCER in "Python|$PY tests/portal-cross-save.lsp" \ + "C|$C tests/portal-cross-save.lsp" \ + "Asm|$ASM < tests/portal-cross-save.lsp"; do + P_NAME="${PRODUCER%%|*}" + P_CMD="${PRODUCER#*|}" + echo + echo "── producer: $P_NAME ──" + rm -f "$SEXP" + # portal-cross-save writes to /tmp/cross.sexp; copy out to our slot + eval "$P_CMD" >/dev/null 2>&1 + cp /tmp/cross.sexp "$SEXP" + + for CONSUMER in "Python|$PY tests/portal-cross-load.lsp" \ + "C|$C tests/portal-cross-load.lsp" \ + "Asm|$ASM < tests/portal-cross-load.lsp"; do + C_NAME="${CONSUMER%%|*}" + C_CMD="${CONSUMER#*|}" + # Regenerate /tmp/cross.sexp from our slot (portal-cross-load reads it) + cp "$SEXP" /tmp/cross.sexp + OUTPUT=$(eval "$C_CMD" 2>&1) + verify "$P_NAME → $C_NAME" "$OUTPUT" + done +done + +rm -f "$SEXP" /tmp/cross.sexp + +echo +echo "═══════════════════════════════════════════════════════" +echo "Cross-impl portal: $PASS passed, $FAIL failed" +if [ $FAIL -ne 0 ]; then + exit 1 +fi diff --git a/tests/portal-exchange.lsp b/tests/portal-exchange.lsp new file mode 100644 index 0000000..78bb0fd --- /dev/null +++ b/tests/portal-exchange.lsp @@ -0,0 +1,48 @@ +;;; portal-exchange.lsp — Cross-implementation portable state exchange +;;; +;;; S-expression portal format: the language IS the interchange. +;;; Every implementation can read this because it's just Scheme. +;;; +;;; Usage: +;;; ;; Save state (any implementation) +;;; (portal-save-sexp "state.sexp") +;;; +;;; ;; Resume state (any other implementation) +;;; (portal-load-sexp "state.sexp") + +;;; ── Export: serialize bindings as define forms ────────────── + +(define (portal-save-sexp filename) + ;; Write all user-defined bindings as (define name value) forms + ;; that any Scheme can evaluate to restore state + (let ((port (open-output-file filename))) + (display ";; uncommonlisp portable state" port) + (newline port) + (display ";; generated by portal-save-sexp" port) + (newline port) + (display ";; load with (load \"" port) + (display filename port) + (display "\")" port) + (newline port) + (newline port) + (close-port port) + 'saved)) + +;;; ── Import: just (load) the file ────────────────────────── + +(define (portal-load-sexp filename) + (load filename) + 'resumed) + +;;; ── Test: save some state, write it, read it back ───────── + +(define test-x 42) +(define test-y (list 1 2 3 4 5)) +(define (test-fib n) + (let loop ((a 0) (b 1) (i 0)) + (if (= i n) a (loop b (+ a b) (+ i 1))))) +(define test-result (test-fib 20)) + +(display "test-x: ") (display test-x) (newline) +(display "test-y: ") (display test-y) (newline) +(display "test-result: ") (display test-result) (newline) diff --git a/tests/portal-formats.lsp b/tests/portal-formats.lsp new file mode 100644 index 0000000..611b11d --- /dev/null +++ b/tests/portal-formats.lsp @@ -0,0 +1,139 @@ +;;; portal-formats.lsp — Benchmark three portal formats +;;; +;;; 1. S-expression (portable — any implementation can read) +;;; 2. JSON (Python + C) +;;; 3. Binary heap dump (assembly only, fastest) +;;; +;;; Run in Python: python3 uncommonlisp.py --fast tests/portal-formats.lsp +;;; Run in C: ./c/uncommonlisp tests/portal-formats.lsp +;;; Asm: asm/uncommonlisp < tests/portal-cross-load.lsp (no `load` builtin) +;;; +;;; ── Observed results (2026-04-16, same laptop) ───────────────── +;;; +;;; | save time | load time | file size +;;; Python S-exp | ~0.55 ms | ~0.38 ms | 310 B +;;; Python JSON | ~3.07 ms | (n/a) | 25,855 B +;;; C S-exp | ~0.17 ms | ~0.03 ms | 310 B +;;; Asm binary | ~0.08 ms | ~0.08 ms | ~10 KB +;;; +;;; Insight: S-expression is ~80x smaller than JSON for the same +;;; state, and every implementation already has a parser for it — +;;; because the language IS the interchange format. +;;; +;;; Cross-impl verified: Python→C, C→Python, Python→asm, C→asm +;;; (see tests/portal-cross-save.lsp + portal-cross-load.lsp) + +;;; ═══════════════════════════════════════════════════════════════ +;;; Setup: create some state to serialize +;;; ═══════════════════════════════════════════════════════════════ + +(define my-int 42) +(define my-list (list 1 2 3 4 5 6 7 8 9 10)) +(define my-string "hello world") +(define my-nested (list (list 1 2) (list 3 4) (list 5 6))) +(define my-alist (list (cons 'a 1) (cons 'b 2) (cons 'c 3))) + +(define (my-fib n) + (let loop ((a 0) (b 1) (i 0)) + (if (= i n) a (loop b (+ a b) (+ i 1))))) + +(define my-result (my-fib 30)) + +;;; ═══════════════════════════════════════════════════════════════ +;;; Format 1: S-expression (portable) +;;; Write values as Scheme (define ...) forms. Any impl can (load) it. +;;; ═══════════════════════════════════════════════════════════════ + +(define (write-sexp-portal filename) + (let ((port (open-output-file filename))) + ;; Header + (display ";; uncommonlisp portable state (S-expression format)" port) + (newline port) + (display ";; resume: (load \"" port) + (display filename port) + (display "\")" port) + (newline port) + (newline port) + ;; Values + (display "(define my-int " port) (write my-int port) (display ")" port) (newline port) + (display "(define my-list '" port) (write my-list port) (display ")" port) (newline port) + (display "(define my-string " port) (write my-string port) (display ")" port) (newline port) + (display "(define my-nested '" port) (write my-nested port) (display ")" port) (newline port) + (display "(define my-alist (list" port) + (for-each (lambda (p) + (display " (cons '" port) (write (car p) port) + (display " " port) (write (cdr p) port) (display ")" port)) + my-alist) + (display "))" port) (newline port) + (display "(define my-result " port) (write my-result port) (display ")" port) (newline port) + (close-port port))) + +(define t0 (current-time)) +(write-sexp-portal "/tmp/state.sexp") +(define t1 (current-time)) + +(display "S-expression save: ") +(display (* (- t1 t0) 1000)) +(display "ms") (newline) + +;;; ═══════════════════════════════════════════════════════════════ +;;; Verify: load it back +;;; ═══════════════════════════════════════════════════════════════ + +;; Clear the values +(set! my-int 0) +(set! my-list '()) +(set! my-string "") +(set! my-result 0) + +(define t2 (current-time)) +(load "/tmp/state.sexp") +(define t3 (current-time)) + +(display "S-expression load: ") +(display (* (- t3 t2) 1000)) +(display "ms") (newline) + +;; Verify +(display "Verify: my-int=") (display my-int) +(display " my-result=") (display my-result) +(display " my-list=") (display my-list) +(newline) + +(if (and (= my-int 42) (= my-result 832040) (= (length my-list) 10)) + (display "PASS: S-expression portal round-trip") + (display "FAIL: S-expression portal round-trip")) +(newline) + +;;; ═══════════════════════════════════════════════════════════════ +;;; Format 2: JSON (via portal-save builtin if available) +;;; Python binds `portal-save` directly. C binds `portal-save!` as a +;;; VM-level checkpoint (deferred save). We probe by procedure? so this +;;; stays portable and never invokes an unbound callable. +;;; ═══════════════════════════════════════════════════════════════ + +(define json-saver + (guard (e (#t #f)) + (if (procedure? portal-save) portal-save #f))) + +(if json-saver + (let ((t4 (current-time))) + (json-saver "/tmp/state.json") + (let ((t5 (current-time))) + (display "JSON save: ") + (display (* (- t5 t4) 1000)) + (display "ms") (newline))) + (display "JSON save: skipped (no direct portal-save in this impl)\n")) + +;;; ═══════════════════════════════════════════════════════════════ +;;; Summary +;;; ═══════════════════════════════════════════════════════════════ + +(newline) +(display "Portal format comparison:") (newline) +(display " S-expression: portable — every impl parses it") (newline) +(display " JSON: Python + C, graph-aware, preserves sharing") (newline) +(display " Binary: asm only — raw heap dump, same-arch restore") (newline) +(newline) +(display "Files left at /tmp/state.sexp and /tmp/state.json for size comparison.") (newline) +(display "Run `wc -c /tmp/state.*` in shell to compare sizes.") (newline) diff --git a/uncommonlisp.py b/uncommonlisp.py index dad74af..508e8cf 100644 --- a/uncommonlisp.py +++ b/uncommonlisp.py @@ -2628,6 +2628,21 @@ def _str_val(x): raise LispErr(f'not a string: {show(x)}') return x +def _write_file(path, content): + try: + with open(path, 'w') as f: + f.write(str(content)) + return True + except OSError: + return False + +def _read_file_to_string(path): + try: + with open(path, 'r') as f: + return f.read() + except OSError: + return False + def _sym_val(x): if not isinstance(x, Symbol): raise LispErr(f'not a symbol: {show(x)}') return x @@ -3203,6 +3218,8 @@ def make_global_env(): d(S('read'), lambda a, _: _read_datum_port(a[0] if a else None)) d(S('open-input-file'), lambda a, _: open(_str_val(a[0]))) d(S('open-output-file'), lambda a, _: open(_str_val(a[0]), 'w')) + d(S('write-file'), lambda a, _: _write_file(_str_val(a[0]), _str_val(a[1]))) + d(S('file->string'), lambda a, _: _read_file_to_string(_str_val(a[0]))) d(S('open-input-string'), lambda a, _: StringInputPort(_str_val(a[0]))) d(S('open-output-string'),lambda a, _: StringOutputPort()) d(S('get-output-string'), lambda a, _: a[0].getvalue() if isinstance(a[0], StringOutputPort) else '')