From d758578ef35edd28f7bbf8a8bec60330deadd096 Mon Sep 17 00:00:00 2001 From: d_m Date: Tue, 27 Feb 2024 14:47:56 -0500 Subject: [PATCH] zeno network programs --- zenochat.tal | 315 +++++++++++++++++++++++++++++++++++++++++++++++++++ zenosrv.tal | 214 ++++++++++++++++++++++++++++++++++ zenoutil.tal | 49 ++++++++ 3 files changed, 578 insertions(+) create mode 100644 zenochat.tal create mode 100644 zenosrv.tal create mode 100644 zenoutil.tal diff --git a/zenochat.tal b/zenochat.tal new file mode 100644 index 0000000..bfde7cc --- /dev/null +++ b/zenochat.tal @@ -0,0 +1,315 @@ +( zenochat.tal ) +( ) +( USAGE: uxnemu zenochat.rom ADDR PORT NICK ) +( ) +( COMMANDS DESCRIPTION ) +( /help show help message ) +( /names list the current people in the chat ) +( /rename NICK change nickname to NICK ) +( /quit exit the program ) +( ) +( DETAILS ) +( - max nickname is 8 characters long, not including null ) +( - no history, no scrolling ) +( - no direct messages, yet ) +( ) +( MESSAGES ) +( - 27 visible lines, 80 chars per line ) +( - 5 chars: time "12:34" ) +( - 1 char: space ) +( - 8 chars: username "theodore" ) +( - 1 char: space ) +( - 65 chars: message line ) +( ) +( HISTORY LINE ) +( - each history line is 80 bytes ) +( - 1 byte: flags, 0x80 is-system, 0x01 is-continued ) +( - 2 bytes: padding ) +( - 2 bytes: hour [0-23] and minute [0-59] ) +( - 9 bytes: nickname + null terminator; empty means system ) +( - 66 bytes: text + null terminator ) +( - window dimensions: 30 rows by 80 columns ) +( - 27 visible lines; 65 text columns ) +( ) +( USER LIST ) +( - 9 bytes, nickname + null terminator ) + +|00 @System [ &vect $2 &expansion $2 &title $2 &metadata $2 &r $2 &g $2 &b $2 &dbg $1 &st $1 ] +|10 @Console [ &vect $2 &r $1 &pad $4 &type $1 &w $1 &e $1 ] +|20 @Screen [ &vect $2 &w $2 &h $2 &auto $1 &pad $1 &x $2 &y $2 &addr $2 &px $1 &sprite $1 ] +|80 @Controller [ &vect $2 &button $1 &key $1 &fn $1 ] +|90 @Mouse [ &vect $2 &x $2 &y $2 &state $1 &pad $3 &scrollx $2 &scrolly $2 ] +|a0 @File1 [ &vect $2 &ok $2 &stat $2 &del $1 &append $1 &name $2 &len $2 &r $2 &w $2 ] +|b0 @File2 [ &vect $2 &ok $2 &stat $2 &del $1 &append $1 &name $2 &len $2 &r $2 &w $2 ] +|c0 @DateTime [ &year $2 &month $1 &day $1 &hr $1 &min $1 &sec $1 &dotw $1 &doty $2 &dst $1 ] + +|0000 + @pos $2 + @dirty $1 + @last-sec $1 + +|0100 + ( system settings ) + #0280 .Screen/w DEO2 + #0168 .Screen/h DEO2 + #27ff .System/r DEO2 + #039b .System/g DEO2 + #1107 .System/b DEO2 + + ( zero page ) + #01 .dirty STZ + ;input .pos STZ2 + + ( vectors ) + ;on-refresh .Screen/vect DEO2 + ;on-key .Controller/vect DEO2 + ;read-args .Console/vect DEO2 + BRK + +@usage "usage: 20 "zenochat.rom 20 "chat.server.net 20 "9999 20 "nick 0a 00 + +@usage-and-exit ( -> BRK ) + ;usage emit #01 .System/st DEO BRK + +@read-arg ( -> BRK ) + .Console/r DEI .pos LDZ2 STA + .pos LDZ2k INC2 ROT STZ2 BRK + +@save-arg ( dst* maxlen* -> ) + #00 .pos LDZ2 STA + .pos LDZ2 ;input SUB2 + LTH2 ?usage-and-exit + ;input emit #0a18 DEO + ;input SWP2 copy + ;input .pos STZ2 JMP2r + +@read-args ( -> BRK ) + .Console/type DEI #00 EQU ?usage-and-exit + .Console/type DEI #01 EQU ?usage-and-exit + .Console/type DEI #02 EQU ?read-arg + ( state is 3 or 4 ) + ;addr LDA #00 EQU ?&addr + ;port LDA #00 EQU ?&port + ;nick LDA #00 EQU ?&nick + !usage-and-exit + &addr ;addr #00ff save-arg BRK + &port ;port #0005 save-arg BRK + &nick ;nick #000f save-arg + .Console/type DEI #04 NEQ ?usage-and-exit + ;read-stdin .Console/vect DEO2 + BRK + +@read-stdin ( -> BRK ) BRK + +@start-client ( -> BRK ) BRK + +@update-clock ( -> ) + LITr -last-sec STHkr LDZ ( ls^ [zp^] ) + .DateTime/sec DEI DUP STHr STZ ( ls^ s^ ; zp<-s ) + NEQ .dirty STZ JMP2r ( ; dirty<-s!=ls ) + +@on-refresh ( -> BRK ) + update-clock + .dirty LDZ ?&refresh BRK + &refresh + redraw + #00 .dirty STZ BRK + +@on-key ( -> BRK ) + .Controller/key DEI + DUP #0d EQU ?&enter + DUP #08 EQU ?&backspace + DUP #20 LTH ?&skip + DUP #7f GTH ?&skip + .pos LDZ2 STA + .pos LDZ2 INC2 .pos STZ2 + #00 .pos LDZ2 STA redraw BRK + &enter POP send-msg redraw BRK + &skip POP BRK + &backspace + ;input .pos LDZ2 EQU2 ?&skip + POP + .pos LDZ2 #0001 SUB2 .pos STZ2 + #00 .pos LDZ2 STA #0028 #015c #4b clear-line redraw BRK + +@clear-line ( x* y* len^ -> ) + #00 SWP SUB STH ( x* y* [-len^] ) + .Screen/y DEO2 .Screen/x DEO2 ( [-len^] ) + &loop ( [-i^] ) + #03 #20 draw-char ( [-i^] ) + .Screen/x DEI2k ( dev^ x* [-i^] ) + #0008 ADD2 ROT DEO2 ( [-i^] ; dev<-x+8 ) + INCr STHkr ?&loop ( [-i+1^] ) + POPr JMP2r ( ) + +@send-msg + ( ;input .pos LDZ2 #010e DEO POP2 POP2 ) + !clear-input + +@clear-input ( -> ) + #0028 #015c #4b clear-line + #0000 ;input STA2 + ;input .pos STZ2 JMP2r + +( TODO: write to intermediary buffers, then write to final topbar buffer ) +( this will allow us to handle "long" strings better, e.g. hostnames ) +( also consider reorder fields, put people before address ) +( write clock last unconditionally with leading space + glyph ) +@gui + &topbar + 20 15 20 "z "e "n "o "c "h "a "t 20 + 20 02 20 + &username + "c "a "r "o "l "i "n "e 20 + 20 03 20 + &users + "1 "2 "9 20 "p "e "o "p "l "e 20 + 20 1d 20 + &server + ( 2001:0db8:85a3:0000:0000:8a2e:0370:7334 - 39 characters long ) + ( fe80::3210:b3ff:fe77:c5c6/64 ) + ( 123.156.189.123 - 15 characters long ) + "1 "2 "3 ". "1 "5 "6 ". "1 "8 "9 ". "1 "2 "3 20 "1 "9 "9 "9 "9 20 20 20 20 20 20 + 20 0f 20 + &hour + "2 "2 ": + &minute + "0 "3 ": + &second + "0 "9 + 20 00 + + &separator + cd cd cd cd cd cd cd cd cd cd cd cd cd cd cd cd + cd cd cd cd cd cd cd cd cd cd cd cd cd cd cd cd + cd cd cd cd cd cd cd cd cd cd cd cd cd cd cd cd + cd cd cd cd cd cd cd cd cd cd cd cd cd cd cd cd + cd cd cd cd cd cd cd cd cd cd cd cd cd cd cd cd + 00 + + &time1 "21:58 20 00 + &u1 "alice 20 00 + &msg1 "Isn't 20 "this 20 "whole 20 "thing 20 "kind 20 "of 20 "weird? 00 + + &time2 "21:59 20 00 + &u2 "bob 20 00 + &msg2 "Nah. 00 + + &info1 20 c4 c4 20 "013 20 "has 20 "connected 20 c4 c4 00 + + &time3 "22:02 20 00 + &u3 "caroline 20 00 + &msg3 "Well, 20 "I 20 "do 20 "think 20 "it's 20 "pretty 20 "wild... 00 + + &info2 20 c4 c4 20 "013 20 "is 20 "now 20 "called 20 "danny 20 c4 c4 00 + + &say "Say: 20 00 + &edit "This 20 "is 20 "a 20 "test. 00 + +@draw-str-at ( tint^ s* x* y* -> ) + .Screen/y DEO2 .Screen/x DEO2 !draw-str + +@two-digit-copy ( n^ s* -> ) + STH2 DUP #0a DIVk MUL SUB ( n^ n%10^ [s*] ) + LIT "0 ADD STH2kr INC2 STA ( n^ [s*] ; s+1<-asc[n%10] ) + #0a DIV LIT "0 ADD STH2r STA ( ; s<-asc[n/10] ) + JMP2r ( ) + +@redraw ( -> ) + .DateTime/hr DEI ;gui/hour two-digit-copy + .DateTime/min DEI ;gui/minute two-digit-copy + .DateTime/sec DEI ;gui/second two-digit-copy + + #06 ;gui/topbar #0000 #0000 draw-str-at + + #03 ;gui/time1 #0000 #000c draw-str-at + #02 ;gui/u1 #0030 #000c draw-str-at + #03 ;gui/msg1 #0078 #000c draw-str-at + + #03 ;gui/time2 #0000 #0018 draw-str-at + #02 ;gui/u2 #0030 #0018 draw-str-at + #03 ;gui/msg2 #0078 #0018 draw-str-at + + #01 ;gui/info1 #0000 #0024 draw-str-at + + #03 ;gui/time3 #0000 #0030 draw-str-at + #02 ;gui/u3 #0030 #0030 draw-str-at + #03 ;gui/msg3 #0078 #0030 draw-str-at + + #01 ;gui/info2 #0000 #003c draw-str-at + + #03 ;gui/edit #0078 #0048 draw-str-at + #03 ;gui/edit #0078 #0054 draw-str-at + #03 ;gui/edit #0078 #0060 draw-str-at + #03 ;gui/edit #0078 #006c draw-str-at + #03 ;gui/edit #0078 #0078 draw-str-at + #03 ;gui/edit #0078 #0084 draw-str-at + #03 ;gui/edit #0078 #0090 draw-str-at + #03 ;gui/edit #0078 #009c draw-str-at + #03 ;gui/edit #0078 #00a8 draw-str-at + #03 ;gui/edit #0078 #00b4 draw-str-at + #03 ;gui/edit #0078 #00c0 draw-str-at + #03 ;gui/edit #0078 #00cc draw-str-at + #03 ;gui/edit #0078 #00d8 draw-str-at + #03 ;gui/edit #0078 #00e4 draw-str-at + #03 ;gui/edit #0078 #00f0 draw-str-at + #03 ;gui/edit #0078 #00fc draw-str-at + #03 ;gui/edit #0078 #0108 draw-str-at + #03 ;gui/edit #0078 #0114 draw-str-at + #03 ;gui/edit #0078 #0120 draw-str-at + #03 ;gui/edit #0078 #012c draw-str-at + #03 ;gui/edit #0078 #0138 draw-str-at + #03 ;gui/edit #0078 #0144 draw-str-at + + #01 ;gui/separator #0000 #0150 draw-str-at + #01 ;gui/say #0000 #015c draw-str-at + #03 ;input #0028 #015c draw-str-at + #08 #20 draw-char ( cursor ) + JMP2r + +@draw-lit #0000 DIV + +@draw-str ( tint^ s* -> ) + STH2 ( tint^ [s*] ) + &loop DUP STH2kr LDA DUP ?&ok ( tint^ tint^ c^ [pos*] ) + POP POP2 POP2r JMP2r ( ) + &ok draw-char INC2r ( tint^ [pos+1*] ) + .Screen/x DEI2k #0008 ADD2 ( tint^ s/x^ x+8* [pos+1*] ) + ROT DEO2 !&loop ( tint^ [pos+1*] ) + +@draw-char ( tint^ c^ -> ) + SWP STH ( c^ [tint^] ) + #00 SWP #40 SFT2 ;cp437 ADD2 ( addr* [tint^] ) + .Screen/addr DEO2k ( addr* s/a^ [tint^] ) + STHkr .Screen/sprite DEO ( addr* s/a^ [tint^] ) + .Screen/y DEI2k #0004 ADD2 ROT DEO2 ( addr* s/a^ [tint^] ) + STH #0004 ADD2 STHr DEO2 ( [tint^] ) + STHr .Screen/sprite DEO ( ) + .Screen/y DEI2k #0004 SUB2 ROT DEO2 ( ) + JMP2r ( ) + +@history-append-msg ( user* msg* -> ) + JMP2r + +@history-append-sys ( msg* -> ) + JMP2r + +~zenoutil.tal + +( 256 chars x 2 tiles/char x 8 bytes/tile = 4096 bytes ) +( second tile only uses top 50% ) +@cp437 + ~cp437.tal + +( input and args ) +@addr $100 +@port $10 +@nick $20 +@input $1000 + +( compose buffer ) +@compose $51 &limit + +( 640x480 resolution means 80 x 40 = 3200 characters ) +@history $c80 &start =history &limit =history + diff --git a/zenosrv.tal b/zenosrv.tal new file mode 100644 index 0000000..aa8bdec --- /dev/null +++ b/zenosrv.tal @@ -0,0 +1,214 @@ +( zenochat.tal ) +( ) +( uses uxnet protocol, see uxnet.txt for details ) +( ) +( the server assumes its assigned idxs are exactly 3 bytes long. ) +( this is an implementation detail of uxnet.py not currently ) +( required by the spec. ) +( ) +( clients send messages to the server using the > command. ) +( each type of messages is identified by its first ASCII ) +( character, which is otherwise ignored: ) +( ) +( CHAR MEANING ) +( N sets username to $text ) +( B broadcasts a chat message of $text ) +( C notifies that user $text has connected ) +( D notifies that user $text has disconnected ) +( R expects $prev/$curr, user $prev is renamed to $curr ) +( ) +( nicknames are not allowed to have slashes, i.e. /, and must ) +( be 16 bytes or fewer long. messages are not allowed to contain ) +( the NULL byte, and are not expected to contain newlines. ) + +|10 @Console [ &vect $2 &r $1 &pad $4 &type $1 &w $1 &e $1 ] + +|0000 + @interpret $2 + @pos $2 + @next-client $2 + +|0100 + ;before-start .interpret STZ2 + ;addr .pos STZ2 + ;clients .next-client STZ2 + ;read-args .Console/vect DEO2 + BRK + +( we don't expect anything ) +@before-start ( -> ) + #0000 DIV JMP2r + +( we expect to read $/ ) +@when-starting ( -> ) + ;input LDAk LIT "$ NEQ ?&error + INC2 LDAk LIT "/ NEQ ?&error + POP2 + ( TODO: validate input ) + ;when-listening .interpret STZ2 JMP2r + &error #010f DEO BRK + +( we expect to read $ # < ) +@when-listening ( -> ) + ;input LDAk LIT "$ EQU ?connect + LDAk LIT "# EQU ?disconnect + LDAk LIT "< EQU ?receive + POP2 #0000 DIV + +@connect ( input* -> ) + ( TODO: validate input ) + INC2 !add-client + +@disconnect ( input* -> ) + ( TODO: validate input ) + INC2 !rm-client + +( expect: <999/N...\0 ) +( or: <999/B...\0 ) +@receive ( input* -> ) + ( TODO: validate input ) + INC2 DUP2 find-client ( msg=input+1* maybe* ) + ORAk ?&found POP2 emit JMP2r ( ) + &found ( msg* client* ) + SWP2 #0004 ADD2 ( client* body=msg+4 ) + LDAk LIT "N EQU ?rename ( client* body* ) + LDAk LIT "B EQU ?broadcast ( client* body* ) + POP2 POP2 JMP2r ( ) + +@broadcast ( client* body* -> ) + INC2 SWP2 #0003 ADD2 ( data=body+1* name=client+3* ) + start-output copy-dst0 ( data* dst+n* ; copy name into dst+5 ) + #3a20 OVR2 STA2 ( data* dst+n-1* ; dst+n-1<-": " ) + INC2 INC2 copy ( ; copy data into dst+n+1 ) + !send-to-all ( ) + +( sends ;output to all clients ) +( fn should have shape: client* -> ) +@send-to-all ( -> ) + .next-client LDZ2 ;clients ( limit* start* ) + &loop GTH2k ?&ok POP2 POP2 JMP2r ( ) + &ok DUP2 send-to #0014 ADD2 !&loop ( limit* pos+20* ) + POP2 POP2 JMP2r ( ) + +( sends ;output to a client ) +@send-to ( client* -> ) + ;output STH2k INC2 ( client* output+1* [output*] ) + idx-copy ( [output*] ; copy idx into output+1 ) + STH2r emit ( ; emit output ) +( #0a18 DEO JMP2r ( need newline for interactive testing ) ) + #0018 DEO JMP2r + +@rename-msg 20 "is 20 "now 20 "called 20 00 + +@start-output ( -> dst* ) + LIT2r :output + LIT "> STH2kr STA INC2r + LIT2 "00 STH2kr STA2 INC2r INC2r + LIT2 "0/ STH2kr STA2 INC2r INC2r + STH2r JMP2r + +@rename ( client* body* -> ) + OVR2 #0003 ADD2 ( client* body* name=client+3* ) + start-output copy-dst0 ( client* body* dst* ; copy old name to output ) + ;rename-msg SWP2 copy-dst0 ( client* body* dst2* ) + STH2 INC2 STH2k #00 STH2kr ( client* data=body+1* 0^ data* [dst2* data*] ) + #0010 ADD2 STA ( client* data* ; data+16<-0 [dst2* data*] ) + SWP2 #0003 ADD2 copy ( [dst2* data*] ; copy data into client+3 ) + STH2r STH2r ( data* dst2* ) + copy !send-to-all ( ; copy data into dst2 and send ) + +( we expect to read addr then port ) +@read-args ( -> BRK ) + .Console/type DEI #04 EQU ?start-server + .Console/type DEI #03 EQU ?&end-addr + .Console/r DEI .pos LDZ2 STA ( ; write c into buf ) + .pos LDZ2k INC2 ROT STZ2 BRK ( BRK ; increment buf pos ) + &end-addr ;port .pos STZ2 BRK ( BRK ; start raeding port port ) + +@read-stdin +( .Console/r DEI #0a NEQ ?&continue ( ; did we read null? ) ) + .Console/r DEI ?&continue ( ; did we read null? ) + #00 .pos LDZ2 STA ( ; write null ) + .interpret LDZ2 JSR2 ( ; interpret message ) + ;input .pos STZ2 BRK ( ; reset input buffer ) + &continue ( ) + .Console/r DEI .pos LDZ2 STA ( ; write c into input buffer ) + .pos LDZ2k INC2 ROT STZ2 BRK ( ; increment input buffer ) + +@start-server ( -> BRK ) + LIT "@ .Console/w DEO ;addr emit + LIT "/ .Console/w DEO ;port emit +( #0a .Console/w DEO ) + #00 .Console/w DEO + ;input .pos STZ2 + ;when-starting .interpret STZ2 + ;read-stdin .Console/vect DEO2 + BRK + +( assumes idxs are 3 bytes long ) +@idx-eq ( idx1* idx2* -> bool^ ) + STH2 LDAk LDAkr STHr NEQ ?&nope + INC2 INC2r LDAk LDAkr STHr NEQ ?&nope + INC2 INC2r LDA LDAr STHr EQU JMP2r + &nope #00 JMP2r + +( assumes idxs are 3 bytes long ) +@idx-copy ( idx* dst* -> ) + STH2 LDAk STH2kr STA INC2 INC2r ( idx+1* [dst+1*] ; dst<-idx ) + LDAk STH2kr STA INC2 INC2r ( idx+2* [dst+2*] ; dst+1<-idx+1 ) + LDA STH2r STA JMP2r ( ; dst+2<-idx+2 ) + +@connect-msg 20 "has 20 "connected 00 + +@add-client ( idx* -> ) + LITr -next-client LDZ2r ( idx* [next*] ) + DUP2 STH2kr copy-slash ( idx* [next*] ; copy idx to client-idx ) + STH2kr #0003 ADD2 copy-slash ( [next*] ; copy idx to client-name ) + #00 STH2kr #0006 ADD2 STA ( [next*] ; null terminate ) + STH2kr #0003 ADD2 ( next+3* [next*] ) + start-output copy-dst0 ( dst* [next*] ; start message ) + ;connect-msg SWP2 copy ( [next*] ; finish message ) + STH2r #0014 ADD2 .next-client STZ2 ( ; next-client<-next+20 ) + !send-to-all ( ) + +@disconnect-msg 20 "has 20 "disconnected 00 + +@rm-client ( idx* -> ) + find-client ORAk ?&found ( addr* ) + POP2 JMP2r ( ) + &found ( addr* ) + DUP2 #0003 ADD2 start-output ( addr* addr+3* dst* ) + copy-dst0 ( addr* dst2* ) + ;disconnect-msg SWP2 copy ( addr* ) + .next-client LDZ2 #0014 SUB2 SWP2 ( limit* addr* ) + &loop GTH2k ?&ok !&done ( limit* pos* ) + &ok DUP2 #0014 ADD2 LDA2 ( limit* pos* cc* ) + OVR2 STA2 INC2 INC2 !&loop ( limit* pos+2* ) + &done .next-client STZ2 POP2 ( ) + !send-to-all ( ) + +( returns client addr or 0000 on failure ) +( finds client based on 3 byte idx ) +@find-client ( idx* -> addr* ) + STH2 .next-client LDZ2 ;clients ( limit* clients* [idx*] ) + &loop ( limit* c* [idx*] ) + DUP2 STH2kr idx-eq ?&found ( limit* c* [idx*] ) + #0014 ADD2 GTH2k ?&loop ( limit* c+20* [idx*] ) + POP2r POP2 DUP2 EOR2 JMP2r ( 0* ) + &found NIP2 POP2r JMP2r ( c* ) + +~zenoutil.tal + +@addr $100 +@port $6 +@input $1000 +@output $1000 + +( client layout: ) +( - bytes 1-3: idx ) +( - bytes 4-20: nickname\0 ) +( every idx is 3 bytes ) +( max username is 16 chars ) +( username is null-terminated; idx is not ) +( max # of clients is 64 ) +@clients $500 &limit diff --git a/zenoutil.tal b/zenoutil.tal new file mode 100644 index 0000000..8d9eaf7 --- /dev/null +++ b/zenoutil.tal @@ -0,0 +1,49 @@ +( copy null-terminated string from src into dst ) +( includes the null terminator. ) +( returns first unwritten address. ) +@copy-dst ( src* dst* -> dst2* ) + STH2 &loop ( in* [out*] ) + LDAk DUP STH2kr STA ( in* c^ [out*] ) + INC2r STH INC2 ( in+1* [out+1* c^] ) + STHr ?&loop ( in+1 [out+1*] ) + POP2 STH2r JMP2r ( dst2=out+1* ) + +( copy-dst but without null termination ) +@copy-dst0 ( src* dst* -> dst2* ) + copy-dst #0001 SUB2 JMP2r + +( copy null-terminated string from src into dst ) +( includes the null terminator. ) +@copy ( src* dst* -> ) + copy-dst POP2 JMP2r + +( copy slash-terminated string from src into dst. ) +( writes a null terminator. ) +@copy-slash ( src/* dst* -> ) + STH2 &loop LDAk LIT "/ EQU ?&done ( in* [out*] ) + LDAk STH2kr STA INC2 INC2r !&loop ( in+1* [out+1*] ) + &done #00 STH2r STA POP2 JMP2r ( ) + +( compare two null-terminated strings ) +@eq ( s* t* -> eq^ ) + STH2 ( s1* [s2*] ) + &l LDAk LDAkr STHr NEQk ?&d ( s1* c1^ c2^ [s2*] ) + DUP EOR EQUk ?&d ( s1* c1^ 0^ [s2*] ) + POP2 INC2 INC2r !&l ( s1+1* [s2+1*] ) + &d NIP2 POP2r EQU JMP2r ( eq^ ) + +( compare slash-terminated string against null-terminated ) +( slash terminated string cannot contain nulls ) +@eq-slash ( s/* str* -> ) + STH2 ( s/* [str*] ) + &loop LDAk LIT "/ EQU ?&end ( s/* [str*] ) + LDAk LDAkr STHr NEQk ?&done ( s/* c1^ c2^ [str*] ) + POP2 INC2 INC2r !&loop ( s/+1* [str+1*] ) + &end LDAkr STHr DUPk EOR ( s/* c2^ 0^ [str*] ) + &done NIP2 POP2r EQU JMP2r ( 0^ ) + +( write null-terminated string to stdout ) +@emit ( buf* -> ) + LITr -Console/w ( buf* [dev^] ) + &loop LDAk ?&ok POP2 POPr JMP2r ( ) + &ok LDAk STHkr DEO INC2 !&loop ( buf+1* [dev^] )