23 Commits

Author SHA1 Message Date
55f3024743 forth: JIT cooperation hooks (vm-eligible flag + call-count + forth-hot-words)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 11s
2026-04-25 04:57:49 +00:00
0d6d0bf439 forth: TCO at colon-def endings (no extra frame on tail-call ops)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 04:29:57 +00:00
f6e333dd19 forth: inline primitive calls in colon-def body (skip forth-execute-word)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 04:00:24 +00:00
c28333adb3 forth: \, POSTPONE-imm split, >NUMBER, DOES> — Hayes 486→618 (97%)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 03:33:13 +00:00
1b2935828c forth: String word set COMPARE/SEARCH/SLITERAL (+9)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 02:53:46 +00:00
64af162b5d forth: File Access word set (in-memory backing, Hayes unchanged)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 02:24:55 +00:00
8ca2fe3564 forth: WITHIN/ABORT/ABORT"/EXIT/UNLOOP (+7; Hayes 486/638, 76%)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 01:55:38 +00:00
b1a7852045 forth: [, ], STATE, EVALUATE (+5; Hayes 463→477, 74%)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 01:23:23 +00:00
89a879799a forth: parsing/dictionary '/[']/EXECUTE/LITERAL/POSTPONE/WORD/FIND/>BODY (Hayes 463/638, 72%)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 00:55:34 +00:00
47f66ad1be forth: pictured numeric output <#/#/#S/#>/HOLD/SIGN + U./U.R/.R (Hayes 448/638, 70%)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 00:23:04 +00:00
c726a9e0fe forth: double-cell ops D+/D-/DNEGATE/DABS/D=/D</D0=/D0</DMAX/DMIN (+18)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 23:52:43 +00:00
b6810e90ab forth: mixed/double-cell math (S>D M* UM* UM/MOD FM/MOD SM/REM */ */MOD); Hayes 342→446 (69%)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 23:25:43 +00:00
3ab01b271d forth: Phase 5 memory + unsigned compare (Hayes 268→342, 53%)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 22:56:26 +00:00
8e1466032a forth: LSHIFT/RSHIFT + 32-bit arith truncation + early binding (Hayes 174→268)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 22:26:58 +00:00
387a6e7f5d forth: SP@ / SP! (+4; Hayes 174/590)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 21:07:10 +00:00
acf9c273a2 forth: BASE/DECIMAL/HEX/BIN/OCTAL (+9; Hayes 174/590)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 20:40:11 +00:00
35ce18eb97 forth: CHAR/[CHAR]/KEY/ACCEPT (+7; Hayes 174/590)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 20:12:31 +00:00
1c975f229d forth: Phase 4 strings — S"/C"/."/TYPE/COUNT/CMOVE/FILL/BLANK (+16; Hayes 168/590)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 19:45:40 +00:00
0e509af0a2 forth: Hayes conformance runner + baseline scoreboard (165/590, 28%)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 19:13:45 +00:00
a47b3e5420 forth: vendor Gerry Jackson's forth2012-test-suite (Hayes Core + Ext)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 18:25:39 +00:00
e066e14267 forth: DO/LOOP/+LOOP/I/J/LEAVE + return stack words (+16)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 17:58:37 +00:00
bb16477fd4 forth: BEGIN/UNTIL/WHILE/REPEAT/AGAIN (+9)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 17:33:25 +00:00
b2939c1922 forth: IF/ELSE/THEN + PC-driven body runner (+18)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 17:03:41 +00:00
29 changed files with 5733 additions and 1405 deletions

View File

@@ -0,0 +1,14 @@
ANS Forth conformance tests — vendored from
https://github.com/gerryjackson/forth2012-test-suite (master, commit-locked
on first fetch: 2026-04-24).
Files in this directory are pristine copies of upstream — do not edit them.
They are consumed by the conformance runner in `lib/forth/conformance.sh`.
- `tester.fr` — John Hayes' test harness (`T{ ... -> ... }T`). (C) 1995
Johns Hopkins APL, distributable under its notice.
- `core.fr` — Core word set tests (Hayes, ~1000 lines).
- `coreexttest.fth` — Core Extension tests (Gerry Jackson).
Only `core.fr` is expected to run green end-to-end for Phase 3; the others
stay parked until later phases.

1009
lib/forth/ans-tests/core.fr Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,775 @@
\ To test the ANS Forth Core Extension word set
\ This program was written by Gerry Jackson in 2006, with contributions from
\ others where indicated, and is in the public domain - it can be distributed
\ and/or modified in any way but please retain this notice.
\ This program is distributed in the hope that it will be useful,
\ but WITHOUT ANY WARRANTY; without even the implied warranty of
\ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
\ The tests are not claimed to be comprehensive or correct
\ ------------------------------------------------------------------------------
\ Version 0.15 1 August 2025 Added two tests to VALUE
\ 0.14 21 July 2022 Updated first line of BUFFER: test as recommended
\ in issue 32
\ 0.13 28 October 2015
\ Replace <FALSE> and <TRUE> with FALSE and TRUE to avoid
\ dependence on Core tests
\ Moved SAVE-INPUT and RESTORE-INPUT tests in a file to filetest.fth
\ Use of 2VARIABLE (from optional wordset) replaced with CREATE.
\ Minor lower to upper case conversions.
\ Calls to COMPARE replaced by S= (in utilities.fth) to avoid use
\ of a word from an optional word set.
\ UNUSED tests revised as UNUSED UNUSED = may return FALSE when an
\ implementation has the data stack sharing unused dataspace.
\ Double number input dependency removed from the HOLDS tests.
\ Minor case sensitivities removed in definition names.
\ 0.11 25 April 2015
\ Added tests for PARSE-NAME HOLDS BUFFER:
\ S\" tests added
\ DEFER IS ACTION-OF DEFER! DEFER@ tests added
\ Empty CASE statement test added
\ [COMPILE] tests removed because it is obsolescent in Forth 2012
\ 0.10 1 August 2014
\ Added tests contributed by James Bowman for:
\ <> U> 0<> 0> NIP TUCK ROLL PICK 2>R 2R@ 2R>
\ HEX WITHIN UNUSED AGAIN MARKER
\ Added tests for:
\ .R U.R ERASE PAD REFILL SOURCE-ID
\ Removed ABORT from NeverExecuted to enable Win32
\ to continue after failure of RESTORE-INPUT.
\ Removed max-intx which is no longer used.
\ 0.7 6 June 2012 Extra CASE test added
\ 0.6 1 April 2012 Tests placed in the public domain.
\ SAVE-INPUT & RESTORE-INPUT tests, position
\ of T{ moved so that tests work with ttester.fs
\ CONVERT test deleted - obsolete word removed from Forth 200X
\ IMMEDIATE VALUEs tested
\ RECURSE with :NONAME tested
\ PARSE and .( tested
\ Parsing behaviour of C" added
\ 0.5 14 September 2011 Removed the double [ELSE] from the
\ initial SAVE-INPUT & RESTORE-INPUT test
\ 0.4 30 November 2009 max-int replaced with max-intx to
\ avoid redefinition warnings.
\ 0.3 6 March 2009 { and } replaced with T{ and }T
\ CONVERT test now independent of cell size
\ 0.2 20 April 2007 ANS Forth words changed to upper case
\ Tests qd3 to qd6 by Reinhold Straub
\ 0.1 Oct 2006 First version released
\ -----------------------------------------------------------------------------
\ The tests are based on John Hayes test program for the core word set
\ Words tested in this file are:
\ .( .R 0<> 0> 2>R 2R> 2R@ :NONAME <> ?DO AGAIN C" CASE COMPILE, ENDCASE
\ ENDOF ERASE FALSE HEX MARKER NIP OF PAD PARSE PICK REFILL
\ RESTORE-INPUT ROLL SAVE-INPUT SOURCE-ID TO TRUE TUCK U.R U> UNUSED
\ VALUE WITHIN [COMPILE]
\ Words not tested or partially tested:
\ \ because it has been extensively used already and is, hence, unnecessary
\ REFILL and SOURCE-ID from the user input device which are not possible
\ when testing from a file such as this one
\ UNUSED (partially tested) as the value returned is system dependent
\ Obsolescent words #TIB CONVERT EXPECT QUERY SPAN TIB as they have been
\ removed from the Forth 2012 standard
\ Results from words that output to the user output device have to visually
\ checked for correctness. These are .R U.R .(
\ -----------------------------------------------------------------------------
\ Assumptions & dependencies:
\ - tester.fr (or ttester.fs), errorreport.fth and utilities.fth have been
\ included prior to this file
\ - the Core word set available
\ -----------------------------------------------------------------------------
TESTING Core Extension words
DECIMAL
TESTING TRUE FALSE
T{ TRUE -> 0 INVERT }T
T{ FALSE -> 0 }T
\ -----------------------------------------------------------------------------
TESTING <> U> (contributed by James Bowman)
T{ 0 0 <> -> FALSE }T
T{ 1 1 <> -> FALSE }T
T{ -1 -1 <> -> FALSE }T
T{ 1 0 <> -> TRUE }T
T{ -1 0 <> -> TRUE }T
T{ 0 1 <> -> TRUE }T
T{ 0 -1 <> -> TRUE }T
T{ 0 1 U> -> FALSE }T
T{ 1 2 U> -> FALSE }T
T{ 0 MID-UINT U> -> FALSE }T
T{ 0 MAX-UINT U> -> FALSE }T
T{ MID-UINT MAX-UINT U> -> FALSE }T
T{ 0 0 U> -> FALSE }T
T{ 1 1 U> -> FALSE }T
T{ 1 0 U> -> TRUE }T
T{ 2 1 U> -> TRUE }T
T{ MID-UINT 0 U> -> TRUE }T
T{ MAX-UINT 0 U> -> TRUE }T
T{ MAX-UINT MID-UINT U> -> TRUE }T
\ -----------------------------------------------------------------------------
TESTING 0<> 0> (contributed by James Bowman)
T{ 0 0<> -> FALSE }T
T{ 1 0<> -> TRUE }T
T{ 2 0<> -> TRUE }T
T{ -1 0<> -> TRUE }T
T{ MAX-UINT 0<> -> TRUE }T
T{ MIN-INT 0<> -> TRUE }T
T{ MAX-INT 0<> -> TRUE }T
T{ 0 0> -> FALSE }T
T{ -1 0> -> FALSE }T
T{ MIN-INT 0> -> FALSE }T
T{ 1 0> -> TRUE }T
T{ MAX-INT 0> -> TRUE }T
\ -----------------------------------------------------------------------------
TESTING NIP TUCK ROLL PICK (contributed by James Bowman)
T{ 1 2 NIP -> 2 }T
T{ 1 2 3 NIP -> 1 3 }T
T{ 1 2 TUCK -> 2 1 2 }T
T{ 1 2 3 TUCK -> 1 3 2 3 }T
T{ : RO5 100 200 300 400 500 ; -> }T
T{ RO5 3 ROLL -> 100 300 400 500 200 }T
T{ RO5 2 ROLL -> RO5 ROT }T
T{ RO5 1 ROLL -> RO5 SWAP }T
T{ RO5 0 ROLL -> RO5 }T
T{ RO5 2 PICK -> 100 200 300 400 500 300 }T
T{ RO5 1 PICK -> RO5 OVER }T
T{ RO5 0 PICK -> RO5 DUP }T
\ -----------------------------------------------------------------------------
TESTING 2>R 2R@ 2R> (contributed by James Bowman)
T{ : RR0 2>R 100 R> R> ; -> }T
T{ 300 400 RR0 -> 100 400 300 }T
T{ 200 300 400 RR0 -> 200 100 400 300 }T
T{ : RR1 2>R 100 2R@ R> R> ; -> }T
T{ 300 400 RR1 -> 100 300 400 400 300 }T
T{ 200 300 400 RR1 -> 200 100 300 400 400 300 }T
T{ : RR2 2>R 100 2R> ; -> }T
T{ 300 400 RR2 -> 100 300 400 }T
T{ 200 300 400 RR2 -> 200 100 300 400 }T
\ -----------------------------------------------------------------------------
TESTING HEX (contributed by James Bowman)
T{ BASE @ HEX BASE @ DECIMAL BASE @ - SWAP BASE ! -> 6 }T
\ -----------------------------------------------------------------------------
TESTING WITHIN (contributed by James Bowman)
T{ 0 0 0 WITHIN -> FALSE }T
T{ 0 0 MID-UINT WITHIN -> TRUE }T
T{ 0 0 MID-UINT+1 WITHIN -> TRUE }T
T{ 0 0 MAX-UINT WITHIN -> TRUE }T
T{ 0 MID-UINT 0 WITHIN -> FALSE }T
T{ 0 MID-UINT MID-UINT WITHIN -> FALSE }T
T{ 0 MID-UINT MID-UINT+1 WITHIN -> FALSE }T
T{ 0 MID-UINT MAX-UINT WITHIN -> FALSE }T
T{ 0 MID-UINT+1 0 WITHIN -> FALSE }T
T{ 0 MID-UINT+1 MID-UINT WITHIN -> TRUE }T
T{ 0 MID-UINT+1 MID-UINT+1 WITHIN -> FALSE }T
T{ 0 MID-UINT+1 MAX-UINT WITHIN -> FALSE }T
T{ 0 MAX-UINT 0 WITHIN -> FALSE }T
T{ 0 MAX-UINT MID-UINT WITHIN -> TRUE }T
T{ 0 MAX-UINT MID-UINT+1 WITHIN -> TRUE }T
T{ 0 MAX-UINT MAX-UINT WITHIN -> FALSE }T
T{ MID-UINT 0 0 WITHIN -> FALSE }T
T{ MID-UINT 0 MID-UINT WITHIN -> FALSE }T
T{ MID-UINT 0 MID-UINT+1 WITHIN -> TRUE }T
T{ MID-UINT 0 MAX-UINT WITHIN -> TRUE }T
T{ MID-UINT MID-UINT 0 WITHIN -> TRUE }T
T{ MID-UINT MID-UINT MID-UINT WITHIN -> FALSE }T
T{ MID-UINT MID-UINT MID-UINT+1 WITHIN -> TRUE }T
T{ MID-UINT MID-UINT MAX-UINT WITHIN -> TRUE }T
T{ MID-UINT MID-UINT+1 0 WITHIN -> FALSE }T
T{ MID-UINT MID-UINT+1 MID-UINT WITHIN -> FALSE }T
T{ MID-UINT MID-UINT+1 MID-UINT+1 WITHIN -> FALSE }T
T{ MID-UINT MID-UINT+1 MAX-UINT WITHIN -> FALSE }T
T{ MID-UINT MAX-UINT 0 WITHIN -> FALSE }T
T{ MID-UINT MAX-UINT MID-UINT WITHIN -> FALSE }T
T{ MID-UINT MAX-UINT MID-UINT+1 WITHIN -> TRUE }T
T{ MID-UINT MAX-UINT MAX-UINT WITHIN -> FALSE }T
T{ MID-UINT+1 0 0 WITHIN -> FALSE }T
T{ MID-UINT+1 0 MID-UINT WITHIN -> FALSE }T
T{ MID-UINT+1 0 MID-UINT+1 WITHIN -> FALSE }T
T{ MID-UINT+1 0 MAX-UINT WITHIN -> TRUE }T
T{ MID-UINT+1 MID-UINT 0 WITHIN -> TRUE }T
T{ MID-UINT+1 MID-UINT MID-UINT WITHIN -> FALSE }T
T{ MID-UINT+1 MID-UINT MID-UINT+1 WITHIN -> FALSE }T
T{ MID-UINT+1 MID-UINT MAX-UINT WITHIN -> TRUE }T
T{ MID-UINT+1 MID-UINT+1 0 WITHIN -> TRUE }T
T{ MID-UINT+1 MID-UINT+1 MID-UINT WITHIN -> TRUE }T
T{ MID-UINT+1 MID-UINT+1 MID-UINT+1 WITHIN -> FALSE }T
T{ MID-UINT+1 MID-UINT+1 MAX-UINT WITHIN -> TRUE }T
T{ MID-UINT+1 MAX-UINT 0 WITHIN -> FALSE }T
T{ MID-UINT+1 MAX-UINT MID-UINT WITHIN -> FALSE }T
T{ MID-UINT+1 MAX-UINT MID-UINT+1 WITHIN -> FALSE }T
T{ MID-UINT+1 MAX-UINT MAX-UINT WITHIN -> FALSE }T
T{ MAX-UINT 0 0 WITHIN -> FALSE }T
T{ MAX-UINT 0 MID-UINT WITHIN -> FALSE }T
T{ MAX-UINT 0 MID-UINT+1 WITHIN -> FALSE }T
T{ MAX-UINT 0 MAX-UINT WITHIN -> FALSE }T
T{ MAX-UINT MID-UINT 0 WITHIN -> TRUE }T
T{ MAX-UINT MID-UINT MID-UINT WITHIN -> FALSE }T
T{ MAX-UINT MID-UINT MID-UINT+1 WITHIN -> FALSE }T
T{ MAX-UINT MID-UINT MAX-UINT WITHIN -> FALSE }T
T{ MAX-UINT MID-UINT+1 0 WITHIN -> TRUE }T
T{ MAX-UINT MID-UINT+1 MID-UINT WITHIN -> TRUE }T
T{ MAX-UINT MID-UINT+1 MID-UINT+1 WITHIN -> FALSE }T
T{ MAX-UINT MID-UINT+1 MAX-UINT WITHIN -> FALSE }T
T{ MAX-UINT MAX-UINT 0 WITHIN -> TRUE }T
T{ MAX-UINT MAX-UINT MID-UINT WITHIN -> TRUE }T
T{ MAX-UINT MAX-UINT MID-UINT+1 WITHIN -> TRUE }T
T{ MAX-UINT MAX-UINT MAX-UINT WITHIN -> FALSE }T
T{ MIN-INT MIN-INT MIN-INT WITHIN -> FALSE }T
T{ MIN-INT MIN-INT 0 WITHIN -> TRUE }T
T{ MIN-INT MIN-INT 1 WITHIN -> TRUE }T
T{ MIN-INT MIN-INT MAX-INT WITHIN -> TRUE }T
T{ MIN-INT 0 MIN-INT WITHIN -> FALSE }T
T{ MIN-INT 0 0 WITHIN -> FALSE }T
T{ MIN-INT 0 1 WITHIN -> FALSE }T
T{ MIN-INT 0 MAX-INT WITHIN -> FALSE }T
T{ MIN-INT 1 MIN-INT WITHIN -> FALSE }T
T{ MIN-INT 1 0 WITHIN -> TRUE }T
T{ MIN-INT 1 1 WITHIN -> FALSE }T
T{ MIN-INT 1 MAX-INT WITHIN -> FALSE }T
T{ MIN-INT MAX-INT MIN-INT WITHIN -> FALSE }T
T{ MIN-INT MAX-INT 0 WITHIN -> TRUE }T
T{ MIN-INT MAX-INT 1 WITHIN -> TRUE }T
T{ MIN-INT MAX-INT MAX-INT WITHIN -> FALSE }T
T{ 0 MIN-INT MIN-INT WITHIN -> FALSE }T
T{ 0 MIN-INT 0 WITHIN -> FALSE }T
T{ 0 MIN-INT 1 WITHIN -> TRUE }T
T{ 0 MIN-INT MAX-INT WITHIN -> TRUE }T
T{ 0 0 MIN-INT WITHIN -> TRUE }T
T{ 0 0 0 WITHIN -> FALSE }T
T{ 0 0 1 WITHIN -> TRUE }T
T{ 0 0 MAX-INT WITHIN -> TRUE }T
T{ 0 1 MIN-INT WITHIN -> FALSE }T
T{ 0 1 0 WITHIN -> FALSE }T
T{ 0 1 1 WITHIN -> FALSE }T
T{ 0 1 MAX-INT WITHIN -> FALSE }T
T{ 0 MAX-INT MIN-INT WITHIN -> FALSE }T
T{ 0 MAX-INT 0 WITHIN -> FALSE }T
T{ 0 MAX-INT 1 WITHIN -> TRUE }T
T{ 0 MAX-INT MAX-INT WITHIN -> FALSE }T
T{ 1 MIN-INT MIN-INT WITHIN -> FALSE }T
T{ 1 MIN-INT 0 WITHIN -> FALSE }T
T{ 1 MIN-INT 1 WITHIN -> FALSE }T
T{ 1 MIN-INT MAX-INT WITHIN -> TRUE }T
T{ 1 0 MIN-INT WITHIN -> TRUE }T
T{ 1 0 0 WITHIN -> FALSE }T
T{ 1 0 1 WITHIN -> FALSE }T
T{ 1 0 MAX-INT WITHIN -> TRUE }T
T{ 1 1 MIN-INT WITHIN -> TRUE }T
T{ 1 1 0 WITHIN -> TRUE }T
T{ 1 1 1 WITHIN -> FALSE }T
T{ 1 1 MAX-INT WITHIN -> TRUE }T
T{ 1 MAX-INT MIN-INT WITHIN -> FALSE }T
T{ 1 MAX-INT 0 WITHIN -> FALSE }T
T{ 1 MAX-INT 1 WITHIN -> FALSE }T
T{ 1 MAX-INT MAX-INT WITHIN -> FALSE }T
T{ MAX-INT MIN-INT MIN-INT WITHIN -> FALSE }T
T{ MAX-INT MIN-INT 0 WITHIN -> FALSE }T
T{ MAX-INT MIN-INT 1 WITHIN -> FALSE }T
T{ MAX-INT MIN-INT MAX-INT WITHIN -> FALSE }T
T{ MAX-INT 0 MIN-INT WITHIN -> TRUE }T
T{ MAX-INT 0 0 WITHIN -> FALSE }T
T{ MAX-INT 0 1 WITHIN -> FALSE }T
T{ MAX-INT 0 MAX-INT WITHIN -> FALSE }T
T{ MAX-INT 1 MIN-INT WITHIN -> TRUE }T
T{ MAX-INT 1 0 WITHIN -> TRUE }T
T{ MAX-INT 1 1 WITHIN -> FALSE }T
T{ MAX-INT 1 MAX-INT WITHIN -> FALSE }T
T{ MAX-INT MAX-INT MIN-INT WITHIN -> TRUE }T
T{ MAX-INT MAX-INT 0 WITHIN -> TRUE }T
T{ MAX-INT MAX-INT 1 WITHIN -> TRUE }T
T{ MAX-INT MAX-INT MAX-INT WITHIN -> FALSE }T
\ -----------------------------------------------------------------------------
TESTING UNUSED (contributed by James Bowman & Peter Knaggs)
VARIABLE UNUSED0
T{ UNUSED DROP -> }T
T{ ALIGN UNUSED UNUSED0 ! 0 , UNUSED CELL+ UNUSED0 @ = -> TRUE }T
T{ UNUSED UNUSED0 ! 0 C, UNUSED CHAR+ UNUSED0 @ =
-> TRUE }T \ aligned -> unaligned
T{ UNUSED UNUSED0 ! 0 C, UNUSED CHAR+ UNUSED0 @ = -> TRUE }T \ unaligned -> ?
\ -----------------------------------------------------------------------------
TESTING AGAIN (contributed by James Bowman)
T{ : AG0 701 BEGIN DUP 7 MOD 0= IF EXIT THEN 1+ AGAIN ; -> }T
T{ AG0 -> 707 }T
\ -----------------------------------------------------------------------------
TESTING MARKER (contributed by James Bowman)
T{ : MA? BL WORD FIND NIP 0<> ; -> }T
T{ MARKER MA0 -> }T
T{ : MA1 111 ; -> }T
T{ MARKER MA2 -> }T
T{ : MA1 222 ; -> }T
T{ MA? MA0 MA? MA1 MA? MA2 -> TRUE TRUE TRUE }T
T{ MA1 MA2 MA1 -> 222 111 }T
T{ MA? MA0 MA? MA1 MA? MA2 -> TRUE TRUE FALSE }T
T{ MA0 -> }T
T{ MA? MA0 MA? MA1 MA? MA2 -> FALSE FALSE FALSE }T
\ -----------------------------------------------------------------------------
TESTING ?DO
: QD ?DO I LOOP ;
T{ 789 789 QD -> }T
T{ -9876 -9876 QD -> }T
T{ 5 0 QD -> 0 1 2 3 4 }T
: QD1 ?DO I 10 +LOOP ;
T{ 50 1 QD1 -> 1 11 21 31 41 }T
T{ 50 0 QD1 -> 0 10 20 30 40 }T
: QD2 ?DO I 3 > IF LEAVE ELSE I THEN LOOP ;
T{ 5 -1 QD2 -> -1 0 1 2 3 }T
: QD3 ?DO I 1 +LOOP ;
T{ 4 4 QD3 -> }T
T{ 4 1 QD3 -> 1 2 3 }T
T{ 2 -1 QD3 -> -1 0 1 }T
: QD4 ?DO I -1 +LOOP ;
T{ 4 4 QD4 -> }T
T{ 1 4 QD4 -> 4 3 2 1 }T
T{ -1 2 QD4 -> 2 1 0 -1 }T
: QD5 ?DO I -10 +LOOP ;
T{ 1 50 QD5 -> 50 40 30 20 10 }T
T{ 0 50 QD5 -> 50 40 30 20 10 0 }T
T{ -25 10 QD5 -> 10 0 -10 -20 }T
VARIABLE ITERS
VARIABLE INCRMNT
: QD6 ( limit start increment -- )
INCRMNT !
0 ITERS !
?DO
1 ITERS +!
I
ITERS @ 6 = IF LEAVE THEN
INCRMNT @
+LOOP ITERS @
;
T{ 4 4 -1 QD6 -> 0 }T
T{ 1 4 -1 QD6 -> 4 3 2 1 4 }T
T{ 4 1 -1 QD6 -> 1 0 -1 -2 -3 -4 6 }T
T{ 4 1 0 QD6 -> 1 1 1 1 1 1 6 }T
T{ 0 0 0 QD6 -> 0 }T
T{ 1 4 0 QD6 -> 4 4 4 4 4 4 6 }T
T{ 1 4 1 QD6 -> 4 5 6 7 8 9 6 }T
T{ 4 1 1 QD6 -> 1 2 3 3 }T
T{ 4 4 1 QD6 -> 0 }T
T{ 2 -1 -1 QD6 -> -1 -2 -3 -4 -5 -6 6 }T
T{ -1 2 -1 QD6 -> 2 1 0 -1 4 }T
T{ 2 -1 0 QD6 -> -1 -1 -1 -1 -1 -1 6 }T
T{ -1 2 0 QD6 -> 2 2 2 2 2 2 6 }T
T{ -1 2 1 QD6 -> 2 3 4 5 6 7 6 }T
T{ 2 -1 1 QD6 -> -1 0 1 3 }T
\ -----------------------------------------------------------------------------
TESTING BUFFER:
T{ 2 CELLS BUFFER: BUF:TEST -> }T
T{ BUF:TEST DUP ALIGNED = -> TRUE }T
T{ 111 BUF:TEST ! 222 BUF:TEST CELL+ ! -> }T
T{ BUF:TEST @ BUF:TEST CELL+ @ -> 111 222 }T
\ -----------------------------------------------------------------------------
TESTING VALUE TO
T{ 111 VALUE VAL1 -999 VALUE VAL2 -> }T
T{ VAL1 -> 111 }T
T{ VAL2 -> -999 }T
T{ 222 TO VAL1 -> }T
T{ VAL1 -> 222 }T
T{ : VD1 VAL1 ; -> }T
T{ VD1 -> 222 }T
T{ : VD2 TO VAL2 ; -> }T
T{ VAL2 -> -999 }T
T{ -333 VD2 -> }T
T{ VAL2 -> -333 }T
T{ VAL1 -> 222 }T
T{ 444 TO VAL1 -> }T
T{ VD1 -> 444 }T
T{ 123 VALUE VAL3 IMMEDIATE VAL3 -> 123 }T
T{ : VD3 VAL3 LITERAL ; VD3 -> 123 }T
\ -----------------------------------------------------------------------------
TESTING CASE OF ENDOF ENDCASE
: CS1 CASE 1 OF 111 ENDOF
2 OF 222 ENDOF
3 OF 333 ENDOF
>R 999 R>
ENDCASE
;
T{ 1 CS1 -> 111 }T
T{ 2 CS1 -> 222 }T
T{ 3 CS1 -> 333 }T
T{ 4 CS1 -> 999 }T
\ Nested CASE's
: CS2 >R CASE -1 OF CASE R@ 1 OF 100 ENDOF
2 OF 200 ENDOF
>R -300 R>
ENDCASE
ENDOF
-2 OF CASE R@ 1 OF -99 ENDOF
>R -199 R>
ENDCASE
ENDOF
>R 299 R>
ENDCASE R> DROP
;
T{ -1 1 CS2 -> 100 }T
T{ -1 2 CS2 -> 200 }T
T{ -1 3 CS2 -> -300 }T
T{ -2 1 CS2 -> -99 }T
T{ -2 2 CS2 -> -199 }T
T{ 0 2 CS2 -> 299 }T
\ Boolean short circuiting using CASE
: CS3 ( N1 -- N2 )
CASE 1- FALSE OF 11 ENDOF
1- FALSE OF 22 ENDOF
1- FALSE OF 33 ENDOF
44 SWAP
ENDCASE
;
T{ 1 CS3 -> 11 }T
T{ 2 CS3 -> 22 }T
T{ 3 CS3 -> 33 }T
T{ 9 CS3 -> 44 }T
\ Empty CASE statements with/without default
T{ : CS4 CASE ENDCASE ; 1 CS4 -> }T
T{ : CS5 CASE 2 SWAP ENDCASE ; 1 CS5 -> 2 }T
T{ : CS6 CASE 1 OF ENDOF 2 ENDCASE ; 1 CS6 -> }T
T{ : CS7 CASE 3 OF ENDOF 2 ENDCASE ; 1 CS7 -> 1 }T
\ -----------------------------------------------------------------------------
TESTING :NONAME RECURSE
VARIABLE NN1
VARIABLE NN2
:NONAME 1234 ; NN1 !
:NONAME 9876 ; NN2 !
T{ NN1 @ EXECUTE -> 1234 }T
T{ NN2 @ EXECUTE -> 9876 }T
T{ :NONAME ( n -- 0,1,..n ) DUP IF DUP >R 1- RECURSE R> THEN ;
CONSTANT RN1 -> }T
T{ 0 RN1 EXECUTE -> 0 }T
T{ 4 RN1 EXECUTE -> 0 1 2 3 4 }T
:NONAME ( n -- n1 ) \ Multiple RECURSEs in one definition
1- DUP
CASE 0 OF EXIT ENDOF
1 OF 11 SWAP RECURSE ENDOF
2 OF 22 SWAP RECURSE ENDOF
3 OF 33 SWAP RECURSE ENDOF
DROP ABS RECURSE EXIT
ENDCASE
; CONSTANT RN2
T{ 1 RN2 EXECUTE -> 0 }T
T{ 2 RN2 EXECUTE -> 11 0 }T
T{ 4 RN2 EXECUTE -> 33 22 11 0 }T
T{ 25 RN2 EXECUTE -> 33 22 11 0 }T
\ -----------------------------------------------------------------------------
TESTING C"
T{ : CQ1 C" 123" ; -> }T
T{ CQ1 COUNT EVALUATE -> 123 }T
T{ : CQ2 C" " ; -> }T
T{ CQ2 COUNT EVALUATE -> }T
T{ : CQ3 C" 2345"COUNT EVALUATE ; CQ3 -> 2345 }T
\ -----------------------------------------------------------------------------
TESTING COMPILE,
:NONAME DUP + ; CONSTANT DUP+
T{ : Q DUP+ COMPILE, ; -> }T
T{ : AS1 [ Q ] ; -> }T
T{ 123 AS1 -> 246 }T
\ -----------------------------------------------------------------------------
\ Cannot automatically test SAVE-INPUT and RESTORE-INPUT from a console source
TESTING SAVE-INPUT and RESTORE-INPUT with a string source
VARIABLE SI_INC 0 SI_INC !
: SI1
SI_INC @ >IN +!
15 SI_INC !
;
: S$ S" SAVE-INPUT SI1 RESTORE-INPUT 12345" ;
T{ S$ EVALUATE SI_INC @ -> 0 2345 15 }T
\ -----------------------------------------------------------------------------
TESTING .(
CR CR .( Output from .()
T{ CR .( You should see -9876: ) -9876 . -> }T
T{ CR .( and again: ).( -9876)CR -> }T
CR CR .( On the next 2 lines you should see First then Second messages:)
T{ : DOTP CR ." Second message via ." [CHAR] " EMIT \ Check .( is immediate
[ CR ] .( First message via .( ) ; DOTP -> }T
CR CR
T{ : IMM? BL WORD FIND NIP ; IMM? .( -> 1 }T
\ -----------------------------------------------------------------------------
TESTING .R and U.R - has to handle different cell sizes
\ Create some large integers just below/above MAX and Min INTs
MAX-INT 73 79 */ CONSTANT LI1
MIN-INT 71 73 */ CONSTANT LI2
LI1 0 <# #S #> NIP CONSTANT LENLI1
: (.R&U.R) ( u1 u2 -- ) \ u1 <= string length, u2 is required indentation
TUCK + >R
LI1 OVER SPACES . CR R@ LI1 SWAP .R CR
LI2 OVER SPACES . CR R@ 1+ LI2 SWAP .R CR
LI1 OVER SPACES U. CR R@ LI1 SWAP U.R CR
LI2 SWAP SPACES U. CR R> LI2 SWAP U.R CR
;
: .R&U.R ( -- )
CR ." You should see lines duplicated:" CR
." indented by 0 spaces" CR 0 0 (.R&U.R) CR
." indented by 0 spaces" CR LENLI1 0 (.R&U.R) CR \ Just fits required width
." indented by 5 spaces" CR LENLI1 5 (.R&U.R) CR
;
CR CR .( Output from .R and U.R)
T{ .R&U.R -> }T
\ -----------------------------------------------------------------------------
TESTING PAD ERASE
\ Must handle different size characters i.e. 1 CHARS >= 1
84 CONSTANT CHARS/PAD \ Minimum size of PAD in chars
CHARS/PAD CHARS CONSTANT AUS/PAD
: CHECKPAD ( caddr u ch -- f ) \ f = TRUE if u chars = ch
SWAP 0
?DO
OVER I CHARS + C@ OVER <>
IF 2DROP UNLOOP FALSE EXIT THEN
LOOP
2DROP TRUE
;
T{ PAD DROP -> }T
T{ 0 INVERT PAD C! -> }T
T{ PAD C@ CONSTANT MAXCHAR -> }T
T{ PAD CHARS/PAD 2DUP MAXCHAR FILL MAXCHAR CHECKPAD -> TRUE }T
T{ PAD CHARS/PAD 2DUP CHARS ERASE 0 CHECKPAD -> TRUE }T
T{ PAD CHARS/PAD 2DUP MAXCHAR FILL PAD 0 ERASE MAXCHAR CHECKPAD -> TRUE }T
T{ PAD 43 CHARS + 9 CHARS ERASE -> }T
T{ PAD 43 MAXCHAR CHECKPAD -> TRUE }T
T{ PAD 43 CHARS + 9 0 CHECKPAD -> TRUE }T
T{ PAD 52 CHARS + CHARS/PAD 52 - MAXCHAR CHECKPAD -> TRUE }T
\ Check that use of WORD and pictured numeric output do not corrupt PAD
\ Minimum size of buffers for these are 33 chars and (2*n)+2 chars respectively
\ where n is number of bits per cell
PAD CHARS/PAD ERASE
2 BASE !
MAX-UINT MAX-UINT <# #S CHAR 1 DUP HOLD HOLD #> 2DROP
DECIMAL
BL WORD 12345678123456781234567812345678 DROP
T{ PAD CHARS/PAD 0 CHECKPAD -> TRUE }T
\ -----------------------------------------------------------------------------
TESTING PARSE
T{ CHAR | PARSE 1234| DUP ROT ROT EVALUATE -> 4 1234 }T
T{ CHAR ^ PARSE 23 45 ^ DUP ROT ROT EVALUATE -> 7 23 45 }T
: PA1 [CHAR] $ PARSE DUP >R PAD SWAP CHARS MOVE PAD R> ;
T{ PA1 3456
DUP ROT ROT EVALUATE -> 4 3456 }T
T{ CHAR A PARSE A SWAP DROP -> 0 }T
T{ CHAR Z PARSE
SWAP DROP -> 0 }T
T{ CHAR " PARSE 4567 "DUP ROT ROT EVALUATE -> 5 4567 }T
\ -----------------------------------------------------------------------------
TESTING PARSE-NAME (Forth 2012)
\ Adapted from the PARSE-NAME RfD tests
T{ PARSE-NAME abcd STR1 S= -> TRUE }T \ No leading spaces
T{ PARSE-NAME abcde STR2 S= -> TRUE }T \ Leading spaces
\ Test empty parse area, new lines are necessary
T{ PARSE-NAME
NIP -> 0 }T
\ Empty parse area with spaces after PARSE-NAME
T{ PARSE-NAME
NIP -> 0 }T
T{ : PARSE-NAME-TEST ( "name1" "name2" -- n )
PARSE-NAME PARSE-NAME S= ; -> }T
T{ PARSE-NAME-TEST abcd abcd -> TRUE }T
T{ PARSE-NAME-TEST abcd abcd -> TRUE }T \ Leading spaces
T{ PARSE-NAME-TEST abcde abcdf -> FALSE }T
T{ PARSE-NAME-TEST abcdf abcde -> FALSE }T
T{ PARSE-NAME-TEST abcde abcde
-> TRUE }T \ Parse to end of line
T{ PARSE-NAME-TEST abcde abcde
-> TRUE }T \ Leading and trailing spaces
\ -----------------------------------------------------------------------------
TESTING DEFER DEFER@ DEFER! IS ACTION-OF (Forth 2012)
\ Adapted from the Forth 200X RfD tests
T{ DEFER DEFER1 -> }T
T{ : MY-DEFER DEFER ; -> }T
T{ : IS-DEFER1 IS DEFER1 ; -> }T
T{ : ACTION-DEFER1 ACTION-OF DEFER1 ; -> }T
T{ : DEF! DEFER! ; -> }T
T{ : DEF@ DEFER@ ; -> }T
T{ ' * ' DEFER1 DEFER! -> }T
T{ 2 3 DEFER1 -> 6 }T
T{ ' DEFER1 DEFER@ -> ' * }T
T{ ' DEFER1 DEF@ -> ' * }T
T{ ACTION-OF DEFER1 -> ' * }T
T{ ACTION-DEFER1 -> ' * }T
T{ ' + IS DEFER1 -> }T
T{ 1 2 DEFER1 -> 3 }T
T{ ' DEFER1 DEFER@ -> ' + }T
T{ ' DEFER1 DEF@ -> ' + }T
T{ ACTION-OF DEFER1 -> ' + }T
T{ ACTION-DEFER1 -> ' + }T
T{ ' - IS-DEFER1 -> }T
T{ 1 2 DEFER1 -> -1 }T
T{ ' DEFER1 DEFER@ -> ' - }T
T{ ' DEFER1 DEF@ -> ' - }T
T{ ACTION-OF DEFER1 -> ' - }T
T{ ACTION-DEFER1 -> ' - }T
T{ MY-DEFER DEFER2 -> }T
T{ ' DUP IS DEFER2 -> }T
T{ 1 DEFER2 -> 1 1 }T
\ -----------------------------------------------------------------------------
TESTING HOLDS (Forth 2012)
: HTEST S" Testing HOLDS" ;
: HTEST2 S" works" ;
: HTEST3 S" Testing HOLDS works 123" ;
T{ 0 0 <# HTEST HOLDS #> HTEST S= -> TRUE }T
T{ 123 0 <# #S BL HOLD HTEST2 HOLDS BL HOLD HTEST HOLDS #>
HTEST3 S= -> TRUE }T
T{ : HLD HOLDS ; -> }T
T{ 0 0 <# HTEST HLD #> HTEST S= -> TRUE }T
\ -----------------------------------------------------------------------------
TESTING REFILL SOURCE-ID
\ REFILL and SOURCE-ID from the user input device can't be tested from a file,
\ can only be tested from a string via EVALUATE
T{ : RF1 S" REFILL" EVALUATE ; RF1 -> FALSE }T
T{ : SID1 S" SOURCE-ID" EVALUATE ; SID1 -> -1 }T
\ ------------------------------------------------------------------------------
TESTING S\" (Forth 2012 compilation mode)
\ Extended the Forth 200X RfD tests
\ Note this tests the Core Ext definition of S\" which has unedfined
\ interpretation semantics. S\" in interpretation mode is tested in the tests on
\ the File-Access word set
T{ : SSQ1 S\" abc" S" abc" S= ; -> }T \ No escapes
T{ SSQ1 -> TRUE }T
T{ : SSQ2 S\" " ; SSQ2 SWAP DROP -> 0 }T \ Empty string
T{ : SSQ3 S\" \a\b\e\f\l\m\q\r\t\v\x0F0\x1Fa\xaBx\z\"\\" ; -> }T
T{ SSQ3 SWAP DROP -> 20 }T \ String length
T{ SSQ3 DROP C@ -> 7 }T \ \a BEL Bell
T{ SSQ3 DROP 1 CHARS + C@ -> 8 }T \ \b BS Backspace
T{ SSQ3 DROP 2 CHARS + C@ -> 27 }T \ \e ESC Escape
T{ SSQ3 DROP 3 CHARS + C@ -> 12 }T \ \f FF Form feed
T{ SSQ3 DROP 4 CHARS + C@ -> 10 }T \ \l LF Line feed
T{ SSQ3 DROP 5 CHARS + C@ -> 13 }T \ \m CR of CR/LF pair
T{ SSQ3 DROP 6 CHARS + C@ -> 10 }T \ LF of CR/LF pair
T{ SSQ3 DROP 7 CHARS + C@ -> 34 }T \ \q " Double Quote
T{ SSQ3 DROP 8 CHARS + C@ -> 13 }T \ \r CR Carriage Return
T{ SSQ3 DROP 9 CHARS + C@ -> 9 }T \ \t TAB Horizontal Tab
T{ SSQ3 DROP 10 CHARS + C@ -> 11 }T \ \v VT Vertical Tab
T{ SSQ3 DROP 11 CHARS + C@ -> 15 }T \ \x0F Given Char
T{ SSQ3 DROP 12 CHARS + C@ -> 48 }T \ 0 0 Digit follow on
T{ SSQ3 DROP 13 CHARS + C@ -> 31 }T \ \x1F Given Char
T{ SSQ3 DROP 14 CHARS + C@ -> 97 }T \ a a Hex follow on
T{ SSQ3 DROP 15 CHARS + C@ -> 171 }T \ \xaB Insensitive Given Char
T{ SSQ3 DROP 16 CHARS + C@ -> 120 }T \ x x Non hex follow on
T{ SSQ3 DROP 17 CHARS + C@ -> 0 }T \ \z NUL No Character
T{ SSQ3 DROP 18 CHARS + C@ -> 34 }T \ \" " Double Quote
T{ SSQ3 DROP 19 CHARS + C@ -> 92 }T \ \\ \ Back Slash
\ The above does not test \n as this is a system dependent value.
\ Check it displays a new line
CR .( The next test should display:)
CR .( One line...)
CR .( another line)
T{ : SSQ4 S\" \nOne line...\nanotherLine\n" TYPE ; SSQ4 -> }T
\ Test bare escapable characters appear as themselves
T{ : SSQ5 S\" abeflmnqrtvxz" S" abeflmnqrtvxz" S= ; SSQ5 -> TRUE }T
T{ : SSQ6 S\" a\""2DROP 1111 ; SSQ6 -> 1111 }T \ Parsing behaviour
T{ : SSQ7 S\" 111 : SSQ8 S\\\" 222\" EVALUATE ; SSQ8 333" EVALUATE ; -> }T
T{ SSQ7 -> 111 222 333 }T
T{ : SSQ9 S\" 11 : SSQ10 S\\\" \\x32\\x32\" EVALUATE ; SSQ10 33" EVALUATE ; -> }T
T{ SSQ9 -> 11 22 33 }T
\ -----------------------------------------------------------------------------
CORE-EXT-ERRORS SET-ERROR-COUNT
CR .( End of Core Extension word tests) CR

View File

@@ -0,0 +1,66 @@
\ From: John Hayes S1I
\ Subject: tester.fr
\ Date: Mon, 27 Nov 95 13:10:09 PST
\ (C) 1995 JOHNS HOPKINS UNIVERSITY / APPLIED PHYSICS LABORATORY
\ MAY BE DISTRIBUTED FREELY AS LONG AS THIS COPYRIGHT NOTICE REMAINS.
\ VERSION 1.2
\ 24/11/2015 Replaced Core Ext word <> with = 0=
\ 31/3/2015 Variable #ERRORS added and incremented for each error reported.
\ 22/1/09 The words { and } have been changed to T{ and }T respectively to
\ agree with the Forth 200X file ttester.fs. This avoids clashes with
\ locals using { ... } and the FSL use of }
HEX
\ SET THE FOLLOWING FLAG TO TRUE FOR MORE VERBOSE OUTPUT; THIS MAY
\ ALLOW YOU TO TELL WHICH TEST CAUSED YOUR SYSTEM TO HANG.
VARIABLE VERBOSE
FALSE VERBOSE !
\ TRUE VERBOSE !
: EMPTY-STACK \ ( ... -- ) EMPTY STACK: HANDLES UNDERFLOWED STACK TOO.
DEPTH ?DUP IF DUP 0< IF NEGATE 0 DO 0 LOOP ELSE 0 DO DROP LOOP THEN THEN ;
VARIABLE #ERRORS 0 #ERRORS !
: ERROR \ ( C-ADDR U -- ) DISPLAY AN ERROR MESSAGE FOLLOWED BY
\ THE LINE THAT HAD THE ERROR.
CR TYPE SOURCE TYPE \ DISPLAY LINE CORRESPONDING TO ERROR
EMPTY-STACK \ THROW AWAY EVERY THING ELSE
#ERRORS @ 1 + #ERRORS !
\ QUIT \ *** Uncomment this line to QUIT on an error
;
VARIABLE ACTUAL-DEPTH \ STACK RECORD
CREATE ACTUAL-RESULTS 20 CELLS ALLOT
: T{ \ ( -- ) SYNTACTIC SUGAR.
;
: -> \ ( ... -- ) RECORD DEPTH AND CONTENT OF STACK.
DEPTH DUP ACTUAL-DEPTH ! \ RECORD DEPTH
?DUP IF \ IF THERE IS SOMETHING ON STACK
0 DO ACTUAL-RESULTS I CELLS + ! LOOP \ SAVE THEM
THEN ;
: }T \ ( ... -- ) COMPARE STACK (EXPECTED) CONTENTS WITH SAVED
\ (ACTUAL) CONTENTS.
DEPTH ACTUAL-DEPTH @ = IF \ IF DEPTHS MATCH
DEPTH ?DUP IF \ IF THERE IS SOMETHING ON THE STACK
0 DO \ FOR EACH STACK ITEM
ACTUAL-RESULTS I CELLS + @ \ COMPARE ACTUAL WITH EXPECTED
= 0= IF S" INCORRECT RESULT: " ERROR LEAVE THEN
LOOP
THEN
ELSE \ DEPTH MISMATCH
S" WRONG NUMBER OF RESULTS: " ERROR
THEN ;
: TESTING \ ( -- ) TALKING COMMENT.
SOURCE VERBOSE @
IF DUP >R TYPE CR R> >IN !
ELSE >IN ! DROP [CHAR] * EMIT
THEN ;

File diff suppressed because it is too large Load Diff

170
lib/forth/conformance.sh Executable file
View File

@@ -0,0 +1,170 @@
#!/usr/bin/env bash
# Run the Hayes/Gerry-Jackson Core conformance suite against our Forth
# interpreter and emit scoreboard.json + scoreboard.md.
#
# Method:
# 1. Preprocess lib/forth/ans-tests/core.fr — strip \ comments, ( ... )
# comments, and TESTING … metadata lines.
# 2. Split into chunks ending at each `}T` so an error in one test
# chunk doesn't abort the run.
# 3. Emit an SX file that exposes those chunks as a list.
# 4. Run our Forth + hayes-runner under sx_server; record pass/fail/error.
set -e
FORTH_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$FORTH_DIR/../.." && pwd)"
SX_SERVER="${SX_SERVER:-/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe}"
SOURCE="$FORTH_DIR/ans-tests/core.fr"
OUT_JSON="$FORTH_DIR/scoreboard.json"
OUT_MD="$FORTH_DIR/scoreboard.md"
TMP="$(mktemp -d)"
PREPROC="$TMP/preproc.forth"
CHUNKS_SX="$TMP/chunks.sx"
cd "$ROOT"
# 1. preprocess
awk '
{
line = $0
# protect POSTPONE \ so the comment-strip below leaves the literal \ alone
gsub(/POSTPONE[ \t]+\\/, "POSTPONE @@BS@@", line)
# strip leading/embedded \ line comments (must be \ followed by space or EOL)
gsub(/(^|[ \t])\\([ \t].*|$)/, " ", line)
# strip ( ... ) block comments that sit on one line
gsub(/\([^)]*\)/, " ", line)
# strip TESTING … metadata lines (rest of line, incl. bare TESTING)
sub(/TESTING([ \t].*)?$/, " ", line)
# restore the protected backslash
gsub(/@@BS@@/, "\\", line)
print line
}' "$SOURCE" > "$PREPROC"
# 2 + 3: split into chunks at each `}T` and emit as a SX file
#
# Cap chunks via MAX_CHUNKS env (default 638 = full Hayes Core). Lower
# it temporarily if later tests regress into an infinite loop while you
# are iterating on primitives.
MAX_CHUNKS="${MAX_CHUNKS:-638}"
MAX_CHUNKS="$MAX_CHUNKS" python3 - "$PREPROC" "$CHUNKS_SX" <<'PY'
import os, re, sys
preproc_path, out_path = sys.argv[1], sys.argv[2]
max_chunks = int(os.environ.get("MAX_CHUNKS", "590"))
text = open(preproc_path).read()
# keep the `}T` attached to the preceding chunk
parts = re.split(r'(\}T)', text)
chunks = []
buf = ""
for p in parts:
buf += p
if p == "}T":
s = buf.strip()
if s:
chunks.append(s)
buf = ""
if buf.strip():
chunks.append(buf.strip())
chunks = chunks[:max_chunks]
def esc(s):
s = s.replace('\\', '\\\\').replace('"', '\\"')
s = s.replace('\r', ' ').replace('\n', ' ')
s = re.sub(r'\s+', ' ', s).strip()
return s
with open(out_path, "w") as f:
f.write("(define hayes-chunks (list\n")
for c in chunks:
f.write(' "' + esc(c) + '"\n')
f.write("))\n\n")
f.write("(define\n")
f.write(" hayes-run-all\n")
f.write(" (fn\n")
f.write(" ()\n")
f.write(" (hayes-reset!)\n")
f.write(" (let ((s (hayes-boot)))\n")
f.write(" (for-each (fn (c) (hayes-run-chunk s c)) hayes-chunks))\n")
f.write(" (hayes-summary)))\n")
PY
# 4. run it
OUT=$(printf '(epoch 1)\n(load "lib/forth/runtime.sx")\n(epoch 2)\n(load "lib/forth/reader.sx")\n(epoch 3)\n(load "lib/forth/interpreter.sx")\n(epoch 4)\n(load "lib/forth/compiler.sx")\n(epoch 5)\n(load "lib/forth/hayes-runner.sx")\n(epoch 6)\n(load "%s")\n(epoch 7)\n(eval "(hayes-run-all)")\n' "$CHUNKS_SX" \
| timeout 180 "$SX_SERVER" 2>&1)
STATUS=$?
SUMMARY=$(printf '%s\n' "$OUT" | awk '/^\{:pass / {print; exit}')
PASS=$(printf '%s' "$SUMMARY" | sed -n 's/.*:pass \([0-9-]*\).*/\1/p')
FAIL=$(printf '%s' "$SUMMARY" | sed -n 's/.*:fail \([0-9-]*\).*/\1/p')
ERR=$(printf '%s' "$SUMMARY" | sed -n 's/.*:error \([0-9-]*\).*/\1/p')
TOTAL=$(printf '%s' "$SUMMARY" | sed -n 's/.*:total \([0-9-]*\).*/\1/p')
CHUNK_COUNT=$(grep -c '^ "' "$CHUNKS_SX" || echo 0)
TOTAL_AVAILABLE=$(grep -c '}T' "$PREPROC" || echo 0)
NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
if [ -z "$PASS" ]; then
PASS=0; FAIL=0; ERR=0; TOTAL=0
NOTE="runner halted before completing (timeout or SX error)"
else
NOTE="completed"
fi
PCT=0
if [ "$TOTAL" -gt 0 ]; then
PCT=$((PASS * 100 / TOTAL))
fi
cat > "$OUT_JSON" <<JSON
{
"source": "gerryjackson/forth2012-test-suite src/core.fr",
"generated_at": "$NOW",
"chunks_available": $TOTAL_AVAILABLE,
"chunks_fed": $CHUNK_COUNT,
"total": $TOTAL,
"pass": $PASS,
"fail": $FAIL,
"error": $ERR,
"percent": $PCT,
"note": "$NOTE"
}
JSON
cat > "$OUT_MD" <<MD
# Forth Hayes Core scoreboard
| metric | value |
| ----------------- | ----: |
| chunks available | $TOTAL_AVAILABLE |
| chunks fed | $CHUNK_COUNT |
| total | $TOTAL |
| pass | $PASS |
| fail | $FAIL |
| error | $ERR |
| percent | ${PCT}% |
- **Source**: \`gerryjackson/forth2012-test-suite\` \`src/core.fr\`
- **Generated**: $NOW
- **Note**: $NOTE
A "chunk" is any preprocessed segment ending at a \`}T\` (every Hayes test
is one chunk, plus the small declaration blocks between tests).
The runner catches raised errors at chunk boundaries so one bad chunk
does not abort the rest. \`error\` covers chunks that raised; \`fail\`
covers tests whose \`->\` / \`}T\` comparison mismatched.
### Chunk cap
\`conformance.sh\` processes the first \`\$MAX_CHUNKS\` chunks (default
**638**, i.e. the whole Hayes Core file). Lower the cap temporarily
while iterating on primitives if a regression re-opens an infinite
loop in later tests.
MD
echo "$SUMMARY"
echo "Scoreboard: $OUT_JSON"
echo " $OUT_MD"
if [ "$STATUS" -ne 0 ] && [ "$TOTAL" -eq 0 ]; then
exit 1
fi

158
lib/forth/hayes-runner.sx Normal file
View File

@@ -0,0 +1,158 @@
;; Hayes conformance test runner.
;; Installs T{ -> }T as Forth primitives that snapshot and compare dstack,
;; plus stub TESTING / HEX / DECIMAL so the Hayes Core file can stream
;; through the interpreter without halting on unsupported metadata words.
(define hayes-pass 0)
(define hayes-fail 0)
(define hayes-error 0)
(define hayes-start-depth 0)
(define hayes-actual (list))
(define hayes-actual-set false)
(define hayes-failures (list))
(define hayes-first-error "")
(define hayes-error-hist (dict))
(define
hayes-reset!
(fn
()
(set! hayes-pass 0)
(set! hayes-fail 0)
(set! hayes-error 0)
(set! hayes-start-depth 0)
(set! hayes-actual (list))
(set! hayes-actual-set false)
(set! hayes-failures (list))
(set! hayes-first-error "")
(set! hayes-error-hist (dict))))
(define
hayes-slice
(fn
(state base)
(let
((n (- (forth-depth state) base)))
(if (<= n 0) (list) (take (get state "dstack") n)))))
(define
hayes-truncate!
(fn
(state base)
(let
((n (- (forth-depth state) base)))
(when (> n 0) (dict-set! state "dstack" (drop (get state "dstack") n))))))
(define
hayes-install!
(fn
(state)
(forth-def-prim!
state
"T{"
(fn
(s)
(set! hayes-start-depth (forth-depth s))
(set! hayes-actual-set false)
(set! hayes-actual (list))))
(forth-def-prim!
state
"->"
(fn
(s)
(set! hayes-actual (hayes-slice s hayes-start-depth))
(set! hayes-actual-set true)
(hayes-truncate! s hayes-start-depth)))
(forth-def-prim!
state
"}T"
(fn
(s)
(let
((expected (hayes-slice s hayes-start-depth)))
(hayes-truncate! s hayes-start-depth)
(if
(and hayes-actual-set (= expected hayes-actual))
(set! hayes-pass (+ hayes-pass 1))
(begin
(set! hayes-fail (+ hayes-fail 1))
(set!
hayes-failures
(concat
hayes-failures
(list
(dict
"kind"
"fail"
"expected"
(str expected)
"actual"
(str hayes-actual))))))))))
(forth-def-prim! state "TESTING" (fn (s) nil))
;; HEX/DECIMAL are real primitives now (runtime.sx) — no stub needed.
state))
(define
hayes-boot
(fn () (let ((s (forth-boot))) (hayes-install! s) (hayes-reset!) s)))
;; Run a single preprocessed chunk (string of Forth source) on the shared
;; state. Catch any raised error and move on — the chunk boundary is a
;; safe resume point.
(define
hayes-bump-error-key!
(fn
(err)
(let
((msg (str err)))
(let
((space-idx (index-of msg " ")))
(let
((key
(if
(> space-idx 0)
(substr msg 0 space-idx)
msg)))
(dict-set!
hayes-error-hist
key
(+ 1 (or (get hayes-error-hist key) 0))))))))
(define
hayes-run-chunk
(fn
(state src)
(guard
(err
((= 1 1)
(begin
(set! hayes-error (+ hayes-error 1))
(when
(= (len hayes-first-error) 0)
(set! hayes-first-error (str err)))
(hayes-bump-error-key! err)
(dict-set! state "dstack" (list))
(dict-set! state "rstack" (list))
(dict-set! state "compiling" false)
(dict-set! state "current-def" nil)
(dict-set! state "cstack" (list))
(dict-set! state "input" (list)))))
(forth-interpret state src))))
(define
hayes-summary
(fn
()
(dict
"pass"
hayes-pass
"fail"
hayes-fail
"error"
hayes-error
"total"
(+ (+ hayes-pass hayes-fail) hayes-error)
"first-error"
hayes-first-error
"error-hist"
hayes-error-hist)))

View File

@@ -5,7 +5,39 @@
(define (define
forth-execute-word forth-execute-word
(fn (state word) (let ((body (get word "body"))) (body state)))) (fn
(state word)
(dict-set! word "call-count" (+ 1 (or (get word "call-count") 0)))
(let ((body (get word "body"))) (body state))))
(define
forth-hot-words
(fn
(state threshold)
(forth-hot-walk
(keys (get state "dict"))
(get state "dict")
threshold
(list))))
(define
forth-hot-walk
(fn
(names dict threshold acc)
(if
(= (len names) 0)
acc
(let
((n (first names)))
(let
((w (get dict n)))
(let
((c (or (get w "call-count") 0)))
(forth-hot-walk
(rest names)
dict
threshold
(if (>= c threshold) (cons (list n c) acc) acc))))))))
(define (define
forth-interpret-token forth-interpret-token
@@ -17,7 +49,7 @@
(not (nil? w)) (not (nil? w))
(forth-execute-word state w) (forth-execute-word state w)
(let (let
((n (forth-parse-number tok (get state "base")))) ((n (forth-parse-number tok (get (get state "vars") "base"))))
(if (if
(not (nil? n)) (not (nil? n))
(forth-push state n) (forth-push state n)

View File

@@ -18,10 +18,122 @@
(dict-set! s "output" "") (dict-set! s "output" "")
(dict-set! s "compiling" false) (dict-set! s "compiling" false)
(dict-set! s "current-def" nil) (dict-set! s "current-def" nil)
(dict-set! s "base" 10)
(dict-set! s "vars" (dict)) (dict-set! s "vars" (dict))
(dict-set! (get s "vars") "base" 10)
(dict-set! s "cstack" (list))
(dict-set! s "mem" (dict))
(dict-set! s "here" 0)
(dict-set! s "hold" (list))
(dict-set! s "files" (dict))
(dict-set! s "by-path" (dict))
(dict-set! s "next-fileid" 1)
s))) s)))
(define
forth-mem-write!
(fn (state addr u) (dict-set! (get state "mem") (str addr) u)))
(define
forth-mem-read
(fn
(state addr)
(or (get (get state "mem") (str addr)) 0)))
(define
forth-alloc-bytes!
(fn
(state n)
(let
((addr (get state "here")))
(dict-set! state "here" (+ addr n))
addr)))
(define
forth-mem-write-string!
(fn
(state addr s)
(let
((n (len s)))
(forth-mem-write-string-loop! state addr s 0 n))))
(define
forth-mem-write-string-loop!
(fn
(state addr s i n)
(when
(< i n)
(begin
(forth-mem-write! state (+ addr i) (char-code (substr s i 1)))
(forth-mem-write-string-loop! state addr s (+ i 1) n)))))
(define
forth-mem-read-string
(fn
(state addr n)
(forth-mem-read-string-loop state addr 0 n "")))
(define
forth-mem-read-string-loop
(fn
(state addr i n acc)
(if
(>= i n)
acc
(forth-mem-read-string-loop
state
addr
(+ i 1)
n
(str acc (char-from-code (forth-mem-read state (+ addr i))))))))
(define
forth-fill-loop
(fn
(state addr u char i)
(when
(< i u)
(begin
(forth-mem-write! state (+ addr i) char)
(forth-fill-loop state addr u char (+ i 1))))))
(define
forth-cmove-loop
(fn
(state src dst u i)
(when
(< i u)
(begin
(forth-mem-write! state (+ dst i) (forth-mem-read state (+ src i)))
(forth-cmove-loop state src dst u (+ i 1))))))
(define
forth-cmove-loop-desc
(fn
(state src dst u i)
(when
(>= i 0)
(begin
(forth-mem-write! state (+ dst i) (forth-mem-read state (+ src i)))
(forth-cmove-loop-desc state src dst u (- i 1))))))
(define
forth-cpush
(fn (state v) (dict-set! state "cstack" (cons v (get state "cstack")))))
(define
forth-cpop
(fn
(state)
(let
((cs (get state "cstack")))
(if
(= (len cs) 0)
(forth-error state "control stack underflow")
(let
((top (first cs)))
(dict-set! state "cstack" (rest cs))
top)))))
(define (define
forth-error forth-error
(fn (state msg) (dict-set! state "error" msg) (raise msg))) (fn (state msg) (dict-set! state "error" msg) (raise msg)))
@@ -81,6 +193,12 @@
forth-emit-str forth-emit-str
(fn (state s) (dict-set! state "output" (str (get state "output") s)))) (fn (state s) (dict-set! state "output" (str (get state "output") s))))
;; The body is always a plain SX lambda — primitives and colon-def
;; bodies alike — which means the SX VM's JIT-on-first-call can lift
;; the body directly into bytecode. We tag every word `:vm-eligible?
;; true` so downstream JIT cooperation (a tracing layer, a hot-call
;; counter) can pick out the JIT-friendly entries by metadata rather
;; than by inspecting the body shape.
(define (define
forth-make-word forth-make-word
(fn (fn
@@ -90,6 +208,8 @@
(dict-set! w "kind" kind) (dict-set! w "kind" kind)
(dict-set! w "body" body) (dict-set! w "body" body)
(dict-set! w "immediate?" immediate?) (dict-set! w "immediate?" immediate?)
(dict-set! w "vm-eligible?" true)
(dict-set! w "call-count" 0)
w))) w)))
(define (define
@@ -99,7 +219,8 @@
(dict-set! (dict-set!
(get state "dict") (get state "dict")
(downcase name) (downcase name)
(forth-make-word "primitive" body false)))) (forth-make-word "primitive" body false))
(dict-set! state "last-defined" name)))
(define (define
forth-def-prim-imm! forth-def-prim-imm!
@@ -108,7 +229,8 @@
(dict-set! (dict-set!
(get state "dict") (get state "dict")
(downcase name) (downcase name)
(forth-make-word "primitive" body true)))) (forth-make-word "primitive" body true))
(dict-set! state "last-defined" name)))
(define (define
forth-lookup forth-lookup
@@ -166,6 +288,220 @@
(define forth-bits-width 32) (define forth-bits-width 32)
;; Truncate a number to the Forth 32-bit signed range (two's-complement).
;; Used by arithmetic primitives so wrap-around matches ANS semantics and
;; loop idioms that rely on MSB becoming 0 after enough shifts terminate.
(define
forth-clip
(fn
(n)
(forth-from-unsigned
(forth-to-unsigned n forth-bits-width)
forth-bits-width)))
;; Double-cell helpers. Single = 32-bit signed, double = 64-bit signed
;; represented on the data stack as (lo, hi) where hi is on top.
;; Reassembly converts the low cell as unsigned and the high cell as
;; signed (signed) or as unsigned (unsigned), then combines.
(define forth-2pow32 (pow 2 32))
(define forth-2pow64 (pow 2 64))
(define
forth-double-from-cells-u
(fn
(lo hi)
(+ (forth-to-unsigned lo 32) (* (forth-to-unsigned hi 32) forth-2pow32))))
(define
forth-double-from-cells-s
(fn (lo hi) (+ (forth-to-unsigned lo 32) (* hi forth-2pow32))))
(define
forth-double-push-u
(fn
(state d)
(let
((lo (mod d forth-2pow32)) (hi (floor (/ d forth-2pow32))))
(forth-push state (forth-from-unsigned lo 32))
(forth-push state (forth-from-unsigned hi 32)))))
(define
forth-num-to-string-loop
(fn
(u base acc)
(if
(= u 0)
acc
(let
((dig (mod u base)) (rest (floor (/ u base))))
(let
((ch
(if
(< dig 10)
(char-from-code (+ 48 dig))
(char-from-code (+ 55 dig)))))
(forth-num-to-string-loop rest base (str ch acc)))))))
(define
forth-num-to-string
(fn
(u base)
(if (= u 0) "0" (forth-num-to-string-loop u base ""))))
(define
forth-spaces-str
(fn
(n)
(if (<= n 0) "" (str " " (forth-spaces-str (- n 1))))))
(define
forth-join-hold
(fn
(parts)
(forth-join-hold-loop parts "")))
(define
forth-join-hold-loop
(fn
(parts acc)
(if
(= (len parts) 0)
acc
(forth-join-hold-loop (rest parts) (str acc (first parts))))))
(define
forth-pic-step
(fn
(state)
(let
((hi (forth-pop state)) (lo (forth-pop state)))
(let
((d (forth-double-from-cells-u lo hi))
(b (get (get state "vars") "base")))
(let
((dig (mod d b)) (rest (floor (/ d b))))
(let
((ch
(if
(< dig 10)
(char-from-code (+ 48 dig))
(char-from-code (+ 55 dig)))))
(dict-set! state "hold" (cons ch (get state "hold")))
(forth-double-push-u state rest)))))))
(define
forth-compare-bytes-loop
(fn
(state a1 u1 a2 u2 i)
(cond
((and (= i u1) (= i u2)) 0)
((= i u1) -1)
((= i u2) 1)
(else
(let
((b1 (forth-mem-read state (+ a1 i)))
(b2 (forth-mem-read state (+ a2 i))))
(cond
((< b1 b2) -1)
((> b1 b2) 1)
(else (forth-compare-bytes-loop state a1 u1 a2 u2 (+ i 1)))))))))
(define
forth-match-at
(fn
(state a1 start a2 u2 j)
(cond
((= j u2) true)
((not
(=
(forth-mem-read state (+ a1 (+ start j)))
(forth-mem-read state (+ a2 j))))
false)
(else (forth-match-at state a1 start a2 u2 (+ j 1))))))
(define
forth-search-bytes
(fn
(state a1 u1 a2 u2 i)
(cond
((= u2 0) 0)
((> (+ i u2) u1) -1)
((forth-match-at state a1 i a2 u2 0) i)
(else (forth-search-bytes state a1 u1 a2 u2 (+ i 1))))))
(define
forth-digit-of-byte
(fn
(c base)
(let
((v
(cond
((and (>= c 48) (<= c 57)) (- c 48))
((and (>= c 65) (<= c 90)) (- c 55))
((and (>= c 97) (<= c 122)) (- c 87))
(else -1))))
(if (or (< v 0) (>= v base)) -1 v))))
(define
forth-numparse-loop
(fn
(state addr u acc base)
(if
(= u 0)
(list acc addr u)
(let
((c (forth-mem-read state addr)))
(let
((dig (forth-digit-of-byte c base)))
(if
(< dig 0)
(list acc addr u)
(forth-numparse-loop
state
(+ addr 1)
(- u 1)
(+ (* acc base) dig)
base)))))))
(define
forth-pic-S-loop
(fn
(state)
(forth-pic-step state)
(let
((hi (forth-pop state)) (lo (forth-pop state)))
(if
(and (= lo 0) (= hi 0))
(begin (forth-push state 0) (forth-push state 0))
(begin
(forth-push state lo)
(forth-push state hi)
(forth-pic-S-loop state))))))
(define
forth-double-push-s
(fn
(state d)
(if
(>= d 0)
(forth-double-push-u state d)
(let
((q (- 0 d)))
(let
((qlo (mod q forth-2pow32)) (qhi (floor (/ q forth-2pow32))))
(if
(= qlo 0)
(begin
(forth-push state 0)
(forth-push state (forth-from-unsigned (- forth-2pow32 qhi) 32)))
(begin
(forth-push
state
(forth-from-unsigned (- forth-2pow32 qlo) 32))
(forth-push
state
(forth-from-unsigned (- (- forth-2pow32 qhi) 1) 32)))))))))
(define (define
forth-to-unsigned forth-to-unsigned
(fn (n w) (let ((m (pow 2 w))) (mod (+ (mod n m) m) m)))) (fn (n w) (let ((m (pow 2 w))) (mod (+ (mod n m) m) m))))
@@ -285,6 +621,19 @@
(s) (s)
(let ((a (forth-peek s))) (when (not (= a 0)) (forth-push s a))))) (let ((a (forth-peek s))) (when (not (= a 0)) (forth-push s a)))))
(forth-def-prim! state "DEPTH" (fn (s) (forth-push s (forth-depth s)))) (forth-def-prim! state "DEPTH" (fn (s) (forth-push s (forth-depth s))))
(forth-def-prim! state "SP@" (fn (s) (forth-push s (forth-depth s))))
(forth-def-prim!
state
"SP!"
(fn
(s)
(let
((n (forth-pop s)))
(let
((cur (forth-depth s)))
(when
(> cur n)
(dict-set! s "dstack" (drop (get s "dstack") (- cur n))))))))
(forth-def-prim! (forth-def-prim!
state state
"PICK" "PICK"
@@ -354,11 +703,17 @@
(forth-push s d) (forth-push s d)
(forth-push s a) (forth-push s a)
(forth-push s b)))) (forth-push s b))))
(forth-def-prim! state "+" (forth-binop (fn (a b) (+ a b)))) (forth-def-prim! state "+" (forth-binop (fn (a b) (forth-clip (+ a b)))))
(forth-def-prim! state "-" (forth-binop (fn (a b) (- a b)))) (forth-def-prim! state "-" (forth-binop (fn (a b) (forth-clip (- a b)))))
(forth-def-prim! state "*" (forth-binop (fn (a b) (* a b)))) (forth-def-prim! state "*" (forth-binop (fn (a b) (forth-clip (* a b)))))
(forth-def-prim! state "/" (forth-binop forth-div)) (forth-def-prim!
(forth-def-prim! state "MOD" (forth-binop forth-mod)) state
"/"
(forth-binop (fn (a b) (forth-clip (forth-div a b)))))
(forth-def-prim!
state
"MOD"
(forth-binop (fn (a b) (forth-clip (forth-mod a b)))))
(forth-def-prim! (forth-def-prim!
state state
"/MOD" "/MOD"
@@ -368,8 +723,8 @@
((b (forth-pop s)) (a (forth-pop s))) ((b (forth-pop s)) (a (forth-pop s)))
(forth-push s (forth-mod a b)) (forth-push s (forth-mod a b))
(forth-push s (forth-div a b))))) (forth-push s (forth-div a b)))))
(forth-def-prim! state "NEGATE" (forth-unop (fn (a) (- 0 a)))) (forth-def-prim! state "NEGATE" (forth-unop (fn (a) (forth-clip (- 0 a)))))
(forth-def-prim! state "ABS" (forth-unop abs)) (forth-def-prim! state "ABS" (forth-unop (fn (a) (forth-clip (abs a)))))
(forth-def-prim! (forth-def-prim!
state state
"MIN" "MIN"
@@ -378,12 +733,15 @@
state state
"MAX" "MAX"
(forth-binop (fn (a b) (if (> a b) a b)))) (forth-binop (fn (a b) (if (> a b) a b))))
(forth-def-prim! state "1+" (forth-unop (fn (a) (+ a 1)))) (forth-def-prim! state "1+" (forth-unop (fn (a) (forth-clip (+ a 1)))))
(forth-def-prim! state "1-" (forth-unop (fn (a) (- a 1)))) (forth-def-prim! state "1-" (forth-unop (fn (a) (forth-clip (- a 1)))))
(forth-def-prim! state "2+" (forth-unop (fn (a) (+ a 2)))) (forth-def-prim! state "2+" (forth-unop (fn (a) (forth-clip (+ a 2)))))
(forth-def-prim! state "2-" (forth-unop (fn (a) (- a 2)))) (forth-def-prim! state "2-" (forth-unop (fn (a) (forth-clip (- a 2)))))
(forth-def-prim! state "2*" (forth-unop (fn (a) (* a 2)))) (forth-def-prim! state "2*" (forth-unop (fn (a) (forth-clip (* a 2)))))
(forth-def-prim! state "2/" (forth-unop (fn (a) (floor (/ a 2))))) (forth-def-prim!
state
"2/"
(forth-unop (fn (a) (forth-clip (floor (/ a 2))))))
(forth-def-prim! state "=" (forth-cmp (fn (a b) (= a b)))) (forth-def-prim! state "=" (forth-cmp (fn (a b) (= a b))))
(forth-def-prim! state "<>" (forth-cmp (fn (a b) (not (= a b))))) (forth-def-prim! state "<>" (forth-cmp (fn (a b) (not (= a b)))))
(forth-def-prim! state "<" (forth-cmp (fn (a b) (< a b)))) (forth-def-prim! state "<" (forth-cmp (fn (a b) (< a b))))
@@ -398,6 +756,30 @@
(forth-def-prim! state "OR" (forth-binop forth-bit-or)) (forth-def-prim! state "OR" (forth-binop forth-bit-or))
(forth-def-prim! state "XOR" (forth-binop forth-bit-xor)) (forth-def-prim! state "XOR" (forth-binop forth-bit-xor))
(forth-def-prim! state "INVERT" (forth-unop forth-bit-invert)) (forth-def-prim! state "INVERT" (forth-unop forth-bit-invert))
(forth-def-prim!
state
"LSHIFT"
(fn
(s)
(let
((u (forth-pop s)) (x (forth-pop s)))
(let
((ux (forth-to-unsigned x forth-bits-width)))
(let
((res (mod (* ux (pow 2 u)) (pow 2 forth-bits-width))))
(forth-push s (forth-from-unsigned res forth-bits-width)))))))
(forth-def-prim!
state
"RSHIFT"
(fn
(s)
(let
((u (forth-pop s)) (x (forth-pop s)))
(let
((ux (forth-to-unsigned x forth-bits-width)))
(let
((res (floor (/ ux (pow 2 u)))))
(forth-push s (forth-from-unsigned res forth-bits-width)))))))
(forth-def-prim! (forth-def-prim!
state state
"." "."
@@ -416,7 +798,7 @@
(forth-def-prim! (forth-def-prim!
state state
"EMIT" "EMIT"
(fn (s) (forth-emit-str s (code-char (forth-pop s))))) (fn (s) (forth-emit-str s (char-from-code (forth-pop s)))))
(forth-def-prim! state "CR" (fn (s) (forth-emit-str s "\n"))) (forth-def-prim! state "CR" (fn (s) (forth-emit-str s "\n")))
(forth-def-prim! state "SPACE" (fn (s) (forth-emit-str s " "))) (forth-def-prim! state "SPACE" (fn (s) (forth-emit-str s " ")))
(forth-def-prim! (forth-def-prim!
@@ -430,4 +812,459 @@
(> n 0) (> n 0)
(for-each (fn (_) (forth-emit-str s " ")) (range 0 n)))))) (for-each (fn (_) (forth-emit-str s " ")) (range 0 n))))))
(forth-def-prim! state "BL" (fn (s) (forth-push s 32))) (forth-def-prim! state "BL" (fn (s) (forth-push s 32)))
(forth-def-prim!
state
"DECIMAL"
(fn (s) (dict-set! (get s "vars") "base" 10)))
(forth-def-prim!
state
"HEX"
(fn (s) (dict-set! (get s "vars") "base" 16)))
(forth-def-prim!
state
"OCTAL"
(fn (s) (dict-set! (get s "vars") "base" 8)))
(forth-def-prim! state "BASE" (fn (s) (forth-push s "base")))
(forth-def-prim! state "I" (fn (s) (forth-push s (forth-rpeek s))))
(forth-def-prim!
state
"J"
(fn (s) (forth-push s (nth (get s "rstack") 2))))
(forth-def-prim! state ">R" (fn (s) (forth-rpush s (forth-pop s))))
(forth-def-prim! state "R>" (fn (s) (forth-push s (forth-rpop s))))
(forth-def-prim! state "R@" (fn (s) (forth-push s (forth-rpeek s))))
(forth-def-prim!
state
"2>R"
(fn
(s)
(let
((b (forth-pop s)) (a (forth-pop s)))
(forth-rpush s a)
(forth-rpush s b))))
(forth-def-prim!
state
"2R>"
(fn
(s)
(let
((b (forth-rpop s)) (a (forth-rpop s)))
(forth-push s a)
(forth-push s b))))
(forth-def-prim!
state
"2R@"
(fn
(s)
(let
((rs (get s "rstack")))
(when
(< (len rs) 2)
(forth-error s "return stack underflow"))
(forth-push s (nth rs 1))
(forth-push s (nth rs 0)))))
(forth-def-prim!
state
"C@"
(fn
(s)
(let ((addr (forth-pop s))) (forth-push s (forth-mem-read s addr)))))
(forth-def-prim!
state
"C!"
(fn
(s)
(let
((addr (forth-pop s)) (v (forth-pop s)))
(forth-mem-write! s addr v))))
(forth-def-prim! state "CHAR+" (fn (s) (forth-push s (+ (forth-pop s) 1))))
(forth-def-prim! state "CHARS" (fn (s) nil))
(forth-def-prim!
state
"TYPE"
(fn
(s)
(let
((u (forth-pop s)) (addr (forth-pop s)))
(forth-emit-str s (forth-mem-read-string s addr u)))))
(forth-def-prim!
state
"COUNT"
(fn
(s)
(let
((addr (forth-pop s)))
(let
((u (forth-mem-read s addr)))
(forth-push s (+ addr 1))
(forth-push s u)))))
(forth-def-prim!
state
"FILL"
(fn
(s)
(let
((char (forth-pop s)) (u (forth-pop s)) (addr (forth-pop s)))
(forth-fill-loop s addr u char 0))))
(forth-def-prim!
state
"BLANK"
(fn
(s)
(let
((u (forth-pop s)) (addr (forth-pop s)))
(forth-fill-loop s addr u 32 0))))
(forth-def-prim!
state
"CMOVE"
(fn
(s)
(let
((u (forth-pop s)) (dst (forth-pop s)) (src (forth-pop s)))
(forth-cmove-loop s src dst u 0))))
(forth-def-prim!
state
"CMOVE>"
(fn
(s)
(let
((u (forth-pop s)) (dst (forth-pop s)) (src (forth-pop s)))
(forth-cmove-loop-desc s src dst u (- u 1)))))
(forth-def-prim!
state
"MOVE"
(fn
(s)
(let
((u (forth-pop s)) (dst (forth-pop s)) (src (forth-pop s)))
(if
(or (<= dst src) (>= dst (+ src u)))
(forth-cmove-loop s src dst u 0)
(forth-cmove-loop-desc s src dst u (- u 1))))))
(forth-def-prim!
state
"S>D"
(fn
(s)
(let
((n (forth-pop s)))
(forth-push s n)
(forth-push s (if (< n 0) -1 0)))))
(forth-def-prim! state "D>S" (fn (s) (forth-pop s)))
(forth-def-prim!
state
"M*"
(fn
(s)
(let
((b (forth-pop s)) (a (forth-pop s)))
(forth-double-push-s s (* a b)))))
(forth-def-prim!
state
"UM*"
(fn
(s)
(let
((b (forth-pop s)) (a (forth-pop s)))
(forth-double-push-u
s
(* (forth-to-unsigned a 32) (forth-to-unsigned b 32))))))
(forth-def-prim!
state
"UM/MOD"
(fn
(s)
(let
((u1 (forth-pop s)) (hi (forth-pop s)) (lo (forth-pop s)))
(let
((d (forth-double-from-cells-u lo hi))
(divisor (forth-to-unsigned u1 32)))
(when (= divisor 0) (forth-error s "division by zero"))
(let
((q (floor (/ d divisor))) (r (mod d divisor)))
(forth-push s (forth-from-unsigned r 32))
(forth-push s (forth-from-unsigned q 32)))))))
(forth-def-prim!
state
"FM/MOD"
(fn
(s)
(let
((n (forth-pop s)) (hi (forth-pop s)) (lo (forth-pop s)))
(let
((d (forth-double-from-cells-s lo hi)))
(when (= n 0) (forth-error s "division by zero"))
(let
((q (floor (/ d n))))
(let
((r (- d (* q n))))
(forth-push s (forth-clip r))
(forth-push s (forth-clip q))))))))
(forth-def-prim!
state
"SM/REM"
(fn
(s)
(let
((n (forth-pop s)) (hi (forth-pop s)) (lo (forth-pop s)))
(let
((d (forth-double-from-cells-s lo hi)))
(when (= n 0) (forth-error s "division by zero"))
(let
((q (forth-trunc (/ d n))))
(let
((r (- d (* q n))))
(forth-push s (forth-clip r))
(forth-push s (forth-clip q))))))))
(forth-def-prim!
state
"*/"
(fn
(s)
(let
((n3 (forth-pop s)) (n2 (forth-pop s)) (n1 (forth-pop s)))
(when (= n3 0) (forth-error s "division by zero"))
(forth-push s (forth-clip (forth-trunc (/ (* n1 n2) n3)))))))
(forth-def-prim!
state
"*/MOD"
(fn
(s)
(let
((n3 (forth-pop s)) (n2 (forth-pop s)) (n1 (forth-pop s)))
(when (= n3 0) (forth-error s "division by zero"))
(let
((d (* n1 n2)))
(let
((q (forth-trunc (/ d n3))))
(let
((r (- d (* q n3))))
(forth-push s (forth-clip r))
(forth-push s (forth-clip q))))))))
(forth-def-prim!
state
"D+"
(fn
(s)
(let
((hi2 (forth-pop s))
(lo2 (forth-pop s))
(hi1 (forth-pop s))
(lo1 (forth-pop s)))
(forth-double-push-s
s
(+
(forth-double-from-cells-s lo1 hi1)
(forth-double-from-cells-s lo2 hi2))))))
(forth-def-prim!
state
"D-"
(fn
(s)
(let
((hi2 (forth-pop s))
(lo2 (forth-pop s))
(hi1 (forth-pop s))
(lo1 (forth-pop s)))
(forth-double-push-s
s
(-
(forth-double-from-cells-s lo1 hi1)
(forth-double-from-cells-s lo2 hi2))))))
(forth-def-prim!
state
"DNEGATE"
(fn
(s)
(let
((hi (forth-pop s)) (lo (forth-pop s)))
(forth-double-push-s
s
(- 0 (forth-double-from-cells-s lo hi))))))
(forth-def-prim!
state
"DABS"
(fn
(s)
(let
((hi (forth-pop s)) (lo (forth-pop s)))
(forth-double-push-s s (abs (forth-double-from-cells-s lo hi))))))
(forth-def-prim!
state
"D="
(fn
(s)
(let
((hi2 (forth-pop s))
(lo2 (forth-pop s))
(hi1 (forth-pop s))
(lo1 (forth-pop s)))
(forth-push s (if (and (= lo1 lo2) (= hi1 hi2)) -1 0)))))
(forth-def-prim!
state
"D<"
(fn
(s)
(let
((hi2 (forth-pop s))
(lo2 (forth-pop s))
(hi1 (forth-pop s))
(lo1 (forth-pop s)))
(forth-push
s
(if
(<
(forth-double-from-cells-s lo1 hi1)
(forth-double-from-cells-s lo2 hi2))
-1
0)))))
(forth-def-prim!
state
"D0="
(fn
(s)
(let
((hi (forth-pop s)) (lo (forth-pop s)))
(forth-push s (if (and (= lo 0) (= hi 0)) -1 0)))))
(forth-def-prim!
state
"D0<"
(fn
(s)
(let
((hi (forth-pop s)) (lo (forth-pop s)))
(forth-push s (if (< hi 0) -1 0)))))
(forth-def-prim!
state
"DMAX"
(fn
(s)
(let
((hi2 (forth-pop s))
(lo2 (forth-pop s))
(hi1 (forth-pop s))
(lo1 (forth-pop s)))
(let
((d1 (forth-double-from-cells-s lo1 hi1))
(d2 (forth-double-from-cells-s lo2 hi2)))
(forth-double-push-s s (if (> d1 d2) d1 d2))))))
(forth-def-prim!
state
"DMIN"
(fn
(s)
(let
((hi2 (forth-pop s))
(lo2 (forth-pop s))
(hi1 (forth-pop s))
(lo1 (forth-pop s)))
(let
((d1 (forth-double-from-cells-s lo1 hi1))
(d2 (forth-double-from-cells-s lo2 hi2)))
(forth-double-push-s s (if (< d1 d2) d1 d2))))))
(forth-def-prim! state "<#" (fn (s) (dict-set! s "hold" (list))))
(forth-def-prim!
state
"HOLD"
(fn
(s)
(let
((c (forth-pop s)))
(dict-set!
s
"hold"
(cons (char-from-code c) (get s "hold"))))))
(forth-def-prim!
state
"SIGN"
(fn
(s)
(let
((n (forth-pop s)))
(when
(< n 0)
(dict-set! s "hold" (cons "-" (get s "hold")))))))
(forth-def-prim!
state
"#"
(fn
(s)
(let
((hi (forth-pop s)) (lo (forth-pop s)))
(let
((d (forth-double-from-cells-u lo hi))
(b (get (get s "vars") "base")))
(let
((dig (mod d b)) (rest (floor (/ d b))))
(let
((ch
(if
(< dig 10)
(char-from-code (+ 48 dig))
(char-from-code (+ 55 dig)))))
(dict-set! s "hold" (cons ch (get s "hold")))
(forth-double-push-u s rest)))))))
(forth-def-prim!
state
"#S"
(fn
(s)
(forth-pic-S-loop s)))
(forth-def-prim!
state
"#>"
(fn
(s)
(forth-pop s)
(forth-pop s)
(let
((str-out (forth-join-hold (get s "hold"))))
(let
((addr (forth-alloc-bytes! s (len str-out))))
(forth-mem-write-string! s addr str-out)
(forth-push s addr)
(forth-push s (len str-out))))))
(forth-def-prim!
state
"U."
(fn
(s)
(let
((u (forth-to-unsigned (forth-pop s) 32))
(b (get (get s "vars") "base")))
(forth-emit-str s (str (forth-num-to-string u b) " ")))))
(forth-def-prim!
state
"U.R"
(fn
(s)
(let
((width (forth-pop s))
(u (forth-to-unsigned (forth-pop s) 32))
(b (get (get s "vars") "base")))
(let
((digits (forth-num-to-string u b)))
(forth-emit-str
s
(forth-spaces-str (- width (len digits))))
(forth-emit-str s digits)))))
(forth-def-prim!
state
".R"
(fn
(s)
(let
((width (forth-pop s))
(n (forth-pop s))
(b (get (get s "vars") "base")))
(let
((sign-prefix (if (< n 0) "-" ""))
(abs-digits
(forth-num-to-string (forth-to-unsigned (abs n) 32) b)))
(let
((digits (str sign-prefix abs-digits)))
(forth-emit-str
s
(forth-spaces-str (- width (len digits))))
(forth-emit-str s digits))))))
state)) state))

12
lib/forth/scoreboard.json Normal file
View File

@@ -0,0 +1,12 @@
{
"source": "gerryjackson/forth2012-test-suite src/core.fr",
"generated_at": "2026-04-25T04:57:22Z",
"chunks_available": 638,
"chunks_fed": 638,
"total": 638,
"pass": 618,
"fail": 14,
"error": 6,
"percent": 96,
"note": "completed"
}

28
lib/forth/scoreboard.md Normal file
View File

@@ -0,0 +1,28 @@
# Forth Hayes Core scoreboard
| metric | value |
| ----------------- | ----: |
| chunks available | 638 |
| chunks fed | 638 |
| total | 638 |
| pass | 618 |
| fail | 14 |
| error | 6 |
| percent | 96% |
- **Source**: `gerryjackson/forth2012-test-suite` `src/core.fr`
- **Generated**: 2026-04-25T04:57:22Z
- **Note**: completed
A "chunk" is any preprocessed segment ending at a `}T` (every Hayes test
is one chunk, plus the small declaration blocks between tests).
The runner catches raised errors at chunk boundaries so one bad chunk
does not abort the rest. `error` covers chunks that raised; `fail`
covers tests whose `->` / `}T` comparison mismatched.
### Chunk cap
`conformance.sh` processes the first `$MAX_CHUNKS` chunks (default
**638**, i.e. the whole Hayes Core file). Lower the cap temporarily
while iterating on primitives if a regression re-opens an infinite
loop in later tests.

View File

@@ -0,0 +1,239 @@
;; Phase 3 — control flow (IF/ELSE/THEN, BEGIN/UNTIL/WHILE/REPEAT/AGAIN,
;; DO/LOOP, return stack). Grows as each control construct lands.
(define forth-p3-passed 0)
(define forth-p3-failed 0)
(define forth-p3-failures (list))
(define
forth-p3-assert
(fn
(label expected actual)
(if
(= expected actual)
(set! forth-p3-passed (+ forth-p3-passed 1))
(begin
(set! forth-p3-failed (+ forth-p3-failed 1))
(set!
forth-p3-failures
(concat
forth-p3-failures
(list
(str label ": expected " (str expected) " got " (str actual)))))))))
(define
forth-p3-check-stack
(fn
(label src expected)
(let ((r (forth-run src))) (forth-p3-assert label expected (nth r 2)))))
(define
forth-p3-if-tests
(fn
()
(forth-p3-check-stack
"IF taken (-1)"
": Q -1 IF 10 THEN ; Q"
(list 10))
(forth-p3-check-stack
"IF not taken (0)"
": Q 0 IF 10 THEN ; Q"
(list))
(forth-p3-check-stack
"IF with non-zero truthy"
": Q 42 IF 10 THEN ; Q"
(list 10))
(forth-p3-check-stack
"IF ELSE — true branch"
": Q -1 IF 10 ELSE 20 THEN ; Q"
(list 10))
(forth-p3-check-stack
"IF ELSE — false branch"
": Q 0 IF 10 ELSE 20 THEN ; Q"
(list 20))
(forth-p3-check-stack
"IF consumes flag"
": Q IF 1 ELSE 2 THEN ; 0 Q"
(list 2))
(forth-p3-check-stack
"absolute value via IF"
": ABS2 DUP 0 < IF NEGATE THEN ; -7 ABS2"
(list 7))
(forth-p3-check-stack
"abs leaves positive alone"
": ABS2 DUP 0 < IF NEGATE THEN ; 7 ABS2"
(list 7))
(forth-p3-check-stack
"sign: negative"
": SIGN DUP 0 < IF DROP -1 ELSE DROP 1 THEN ; -3 SIGN"
(list -1))
(forth-p3-check-stack
"sign: positive"
": SIGN DUP 0 < IF DROP -1 ELSE DROP 1 THEN ; 3 SIGN"
(list 1))
(forth-p3-check-stack
"nested IF (both true)"
": Q 1 IF 1 IF 10 ELSE 20 THEN ELSE 30 THEN ; Q"
(list 10))
(forth-p3-check-stack
"nested IF (inner false)"
": Q 1 IF 0 IF 10 ELSE 20 THEN ELSE 30 THEN ; Q"
(list 20))
(forth-p3-check-stack
"nested IF (outer false)"
": Q 0 IF 0 IF 10 ELSE 20 THEN ELSE 30 THEN ; Q"
(list 30))
(forth-p3-check-stack
"IF before other ops"
": Q 1 IF 5 ELSE 6 THEN 2 * ; Q"
(list 10))
(forth-p3-check-stack
"IF in chained def"
": POS? 0 > ;
: DOUBLE-IF-POS DUP POS? IF 2 * THEN ;
3 DOUBLE-IF-POS"
(list 6))
(forth-p3-check-stack
"empty then branch"
": Q 1 IF THEN 99 ; Q"
(list 99))
(forth-p3-check-stack
"empty else branch"
": Q 0 IF 99 ELSE THEN ; Q"
(list))
(forth-p3-check-stack
"sequential IF blocks"
": Q -1 IF 1 THEN -1 IF 2 THEN ; Q"
(list 1 2))))
(define
forth-p3-loop-tests
(fn
()
(forth-p3-check-stack
"BEGIN UNTIL (countdown to zero)"
": CD BEGIN 1- DUP 0 = UNTIL ; 3 CD"
(list 0))
(forth-p3-check-stack
"BEGIN UNTIL — single pass (UNTIL true immediately)"
": Q BEGIN -1 UNTIL 42 ; Q"
(list 42))
(forth-p3-check-stack
"BEGIN UNTIL — accumulate sum 1+2+3"
": SUM3 0 3 BEGIN TUCK + SWAP 1- DUP 0 = UNTIL DROP ; SUM3"
(list 6))
(forth-p3-check-stack
"BEGIN WHILE REPEAT — triangular sum 5"
": TRI 0 5 BEGIN DUP 0 > WHILE TUCK + SWAP 1- REPEAT DROP ; TRI"
(list 15))
(forth-p3-check-stack
"BEGIN WHILE REPEAT — zero iterations"
": TRI 0 0 BEGIN DUP 0 > WHILE TUCK + SWAP 1- REPEAT DROP ; TRI"
(list 0))
(forth-p3-check-stack
"BEGIN WHILE REPEAT — one iteration"
": TRI 0 1 BEGIN DUP 0 > WHILE TUCK + SWAP 1- REPEAT DROP ; TRI"
(list 1))
(forth-p3-check-stack
"nested BEGIN UNTIL"
": INNER BEGIN 1- DUP 0 = UNTIL DROP ;
: OUTER BEGIN 3 INNER 1- DUP 0 = UNTIL ;
2 OUTER"
(list 0))
(forth-p3-check-stack
"BEGIN UNTIL after colon prefix"
": TEN 10 ;
: CD TEN BEGIN 1- DUP 0 = UNTIL ;
CD"
(list 0))
(forth-p3-check-stack
"WHILE inside IF branch"
": Q 1 IF 0 3 BEGIN DUP 0 > WHILE TUCK + SWAP 1- REPEAT DROP ELSE 99 THEN ; Q"
(list 6))))
(define
forth-p3-do-tests
(fn
()
(forth-p3-check-stack
"DO LOOP — simple sum 0..4"
": SUM 0 5 0 DO I + LOOP ; SUM"
(list 10))
(forth-p3-check-stack
"DO LOOP — 10..14 sum using I"
": SUM 0 15 10 DO I + LOOP ; SUM"
(list 60))
(forth-p3-check-stack
"DO LOOP — limit = start runs one pass"
": SUM 0 5 5 DO I + LOOP ; SUM"
(list 5))
(forth-p3-check-stack
"DO LOOP — count iterations"
": COUNT 0 4 0 DO 1+ LOOP ; COUNT"
(list 4))
(forth-p3-check-stack
"DO LOOP — nested, I inner / J outer"
": MATRIX 0 3 0 DO 3 0 DO I J + + LOOP LOOP ; MATRIX"
(list 18))
(forth-p3-check-stack
"DO LOOP — I used in arithmetic"
": DBL 0 5 1 DO I 2 * + LOOP ; DBL"
(list 20))
(forth-p3-check-stack
"+LOOP — count by 2"
": Q 0 10 0 DO I + 2 +LOOP ; Q"
(list 20))
(forth-p3-check-stack
"+LOOP — count by 3"
": Q 0 10 0 DO I + 3 +LOOP ; Q"
(list 18))
(forth-p3-check-stack
"+LOOP — negative step"
": Q 0 0 10 DO I + -1 +LOOP ; Q"
(list 55))
(forth-p3-check-stack
"LEAVE — early exit at I=3"
": Q 0 10 0 DO I 3 = IF LEAVE THEN I + LOOP ; Q"
(list 3))
(forth-p3-check-stack
"LEAVE — in nested loop exits only inner"
": Q 0 3 0 DO 5 0 DO I 2 = IF LEAVE THEN I + LOOP LOOP ; Q"
(list 3))
(forth-p3-check-stack
"DO LOOP preserves outer stack"
": Q 99 5 0 DO I + LOOP ; Q"
(list 109))
(forth-p3-check-stack
">R R>"
": Q 7 >R 11 R> ; Q"
(list 11 7))
(forth-p3-check-stack
">R R@ R>"
": Q 7 >R R@ R> ; Q"
(list 7 7))
(forth-p3-check-stack
"2>R 2R>"
": Q 1 2 2>R 99 2R> ; Q"
(list 99 1 2))
(forth-p3-check-stack
"2>R 2R@ 2R>"
": Q 3 4 2>R 2R@ 2R> ; Q"
(list 3 4 3 4))))
(define
forth-p3-run-all
(fn
()
(set! forth-p3-passed 0)
(set! forth-p3-failed 0)
(set! forth-p3-failures (list))
(forth-p3-if-tests)
(forth-p3-loop-tests)
(forth-p3-do-tests)
(dict
"passed"
forth-p3-passed
"failed"
forth-p3-failed
"failures"
forth-p3-failures)))

View File

@@ -0,0 +1,268 @@
;; Phase 4 — strings + more Core.
;; Uses the byte-memory model on state ("mem" dict + "here" cursor).
(define forth-p4-passed 0)
(define forth-p4-failed 0)
(define forth-p4-failures (list))
(define
forth-p4-assert
(fn
(label expected actual)
(if
(= expected actual)
(set! forth-p4-passed (+ forth-p4-passed 1))
(begin
(set! forth-p4-failed (+ forth-p4-failed 1))
(set!
forth-p4-failures
(concat
forth-p4-failures
(list
(str label ": expected " (str expected) " got " (str actual)))))))))
(define
forth-p4-check-output
(fn
(label src expected)
(let ((r (forth-run src))) (forth-p4-assert label expected (nth r 1)))))
(define
forth-p4-check-stack-size
(fn
(label src expected-n)
(let
((r (forth-run src)))
(forth-p4-assert label expected-n (len (nth r 2))))))
(define
forth-p4-check-top
(fn
(label src expected)
(let
((r (forth-run src)))
(let
((stk (nth r 2)))
(forth-p4-assert label expected (nth stk (- (len stk) 1)))))))
(define
forth-p4-check-typed
(fn
(label src expected)
(forth-p4-check-output label (str src " TYPE") expected)))
(define
forth-p4-string-tests
(fn
()
(forth-p4-check-typed
"S\" + TYPE — hello"
"S\" HELLO\""
"HELLO")
(forth-p4-check-typed
"S\" + TYPE — two words"
"S\" HELLO WORLD\""
"HELLO WORLD")
(forth-p4-check-typed
"S\" + TYPE — empty"
"S\" \""
"")
(forth-p4-check-typed
"S\" + TYPE — single char"
"S\" X\""
"X")
(forth-p4-check-stack-size
"S\" pushes (addr len)"
"S\" HI\""
2)
(forth-p4-check-top
"S\" length is correct"
"S\" HELLO\""
5)
(forth-p4-check-output
".\" prints at interpret time"
".\" HELLO\""
"HELLO")
(forth-p4-check-output
".\" in colon def"
": GREET .\" HI \" ; GREET GREET"
"HI HI ")))
(define
forth-p4-count-tests
(fn
()
(forth-p4-check-typed
"C\" + COUNT + TYPE"
"C\" ABC\" COUNT"
"ABC")
(forth-p4-check-typed
"C\" then COUNT leaves right len"
"C\" HI THERE\" COUNT"
"HI THERE")))
(define
forth-p4-fill-tests
(fn
()
(forth-p4-check-typed
"FILL overwrites prefix bytes"
"S\" ABCDE\" 2DUP DROP 3 65 FILL"
"AAADE")
(forth-p4-check-typed
"BLANK sets spaces"
"S\" XYZAB\" 2DUP DROP 3 BLANK"
" AB")))
(define
forth-p4-cmove-tests
(fn
()
(forth-p4-check-output
"CMOVE copies HELLO forward"
": MKH 72 0 C! 69 1 C! 76 2 C! 76 3 C! 79 4 C! ;
: T MKH 0 10 5 CMOVE 10 5 TYPE ; T"
"HELLO")
(forth-p4-check-output
"CMOVE> copies overlapping backward"
": MKA 65 0 C! 66 1 C! 67 2 C! ;
: T MKA 0 1 2 CMOVE> 0 3 TYPE ; T"
"AAB")
(forth-p4-check-output
"MOVE picks direction for overlap"
": MKA 65 0 C! 66 1 C! 67 2 C! ;
: T MKA 0 1 2 MOVE 0 3 TYPE ; T"
"AAB")))
(define
forth-p4-charplus-tests
(fn
()
(forth-p4-check-top
"CHAR+ increments"
"5 CHAR+"
6)))
(define
forth-p4-char-tests
(fn
()
(forth-p4-check-top "CHAR A -> 65" "CHAR A" 65)
(forth-p4-check-top "CHAR x -> 120" "CHAR x" 120)
(forth-p4-check-top "CHAR takes only first char" "CHAR HELLO" 72)
(forth-p4-check-top
"[CHAR] compiles literal"
": AA [CHAR] A ; AA"
65)
(forth-p4-check-top
"[CHAR] reads past IMMEDIATE"
": ZZ [CHAR] Z ; ZZ"
90)
(forth-p4-check-stack-size
"[CHAR] doesn't leak at compile time"
": FOO [CHAR] A ; "
0)))
(define
forth-p4-key-accept-tests
(fn
()
(let
((r (forth-run "1000 2 ACCEPT")))
(let ((stk (nth r 2))) (forth-p4-assert "ACCEPT empty buf -> 0" (list 0) stk)))))
(define
forth-p4-shift-tests
(fn
()
(forth-p4-check-top "1 0 LSHIFT" "1 0 LSHIFT" 1)
(forth-p4-check-top "1 1 LSHIFT" "1 1 LSHIFT" 2)
(forth-p4-check-top "1 2 LSHIFT" "1 2 LSHIFT" 4)
(forth-p4-check-top "1 15 LSHIFT" "1 15 LSHIFT" 32768)
(forth-p4-check-top "1 31 LSHIFT" "1 31 LSHIFT" -2147483648)
(forth-p4-check-top "1 0 RSHIFT" "1 0 RSHIFT" 1)
(forth-p4-check-top "1 1 RSHIFT" "1 1 RSHIFT" 0)
(forth-p4-check-top "2 1 RSHIFT" "2 1 RSHIFT" 1)
(forth-p4-check-top "4 2 RSHIFT" "4 2 RSHIFT" 1)
(forth-p4-check-top "-1 1 RSHIFT (logical, not arithmetic)" "-1 1 RSHIFT" 2147483647)
(forth-p4-check-top "MSB via 1S 1 RSHIFT INVERT" "0 INVERT 1 RSHIFT INVERT" -2147483648)))
(define
forth-p4-sp-tests
(fn
()
(forth-p4-check-top "SP@ returns depth (0)" "SP@" 0)
(forth-p4-check-top
"SP@ after pushes"
"1 2 3 SP@ SWAP DROP SWAP DROP SWAP DROP"
3)
(forth-p4-check-stack-size
"SP! truncates"
"1 2 3 4 5 2 SP!"
2)
(forth-p4-check-top
"SP! leaves base items intact"
"1 2 3 4 5 2 SP!"
2)))
(define
forth-p4-base-tests
(fn
()
(forth-p4-check-top
"BASE default is 10"
"BASE @"
10)
(forth-p4-check-top
"HEX switches base to 16"
"HEX BASE @"
16)
(forth-p4-check-top
"DECIMAL resets to 10"
"HEX DECIMAL BASE @"
10)
(forth-p4-check-top
"HEX parses 10 as 16"
"HEX 10"
16)
(forth-p4-check-top
"HEX parses FF as 255"
"HEX FF"
255)
(forth-p4-check-top
"DECIMAL parses 10 as 10"
"HEX DECIMAL 10"
10)
(forth-p4-check-top
"OCTAL parses 17 as 15"
"OCTAL 17"
15)
(forth-p4-check-top
"BASE @ ; 16 BASE ! ; BASE @"
"BASE @ 16 BASE ! BASE @ SWAP DROP"
16)))
(define
forth-p4-run-all
(fn
()
(set! forth-p4-passed 0)
(set! forth-p4-failed 0)
(set! forth-p4-failures (list))
(forth-p4-string-tests)
(forth-p4-count-tests)
(forth-p4-fill-tests)
(forth-p4-cmove-tests)
(forth-p4-charplus-tests)
(forth-p4-char-tests)
(forth-p4-key-accept-tests)
(forth-p4-base-tests)
(forth-p4-shift-tests)
(forth-p4-sp-tests)
(dict
"passed"
forth-p4-passed
"failed"
forth-p4-failed
"failures"
forth-p4-failures)))

View File

@@ -0,0 +1,333 @@
;; Phase 5 — Core Extension + memory primitives.
(define forth-p5-passed 0)
(define forth-p5-failed 0)
(define forth-p5-failures (list))
(define
forth-p5-assert
(fn
(label expected actual)
(if
(= expected actual)
(set! forth-p5-passed (+ forth-p5-passed 1))
(begin
(set! forth-p5-failed (+ forth-p5-failed 1))
(set!
forth-p5-failures
(concat
forth-p5-failures
(list
(str label ": expected " (str expected) " got " (str actual)))))))))
(define
forth-p5-check-stack
(fn
(label src expected)
(let ((r (forth-run src))) (forth-p5-assert label expected (nth r 2)))))
(define
forth-p5-check-top
(fn
(label src expected)
(let
((r (forth-run src)))
(let
((stk (nth r 2)))
(forth-p5-assert label expected (nth stk (- (len stk) 1)))))))
(define
forth-p5-create-tests
(fn
()
(forth-p5-check-top
"CREATE pushes HERE-at-creation"
"HERE CREATE FOO FOO ="
-1)
(forth-p5-check-top
"CREATE + ALLOT advances HERE"
"HERE 5 ALLOT HERE SWAP -"
5)
(forth-p5-check-top
"CREATE + , stores cell"
"CREATE FOO 42 , FOO @"
42)
(forth-p5-check-stack
"CREATE multiple ,"
"CREATE TBL 1 , 2 , 3 , TBL @ TBL CELL+ @ TBL CELL+ CELL+ @"
(list 1 2 3))
(forth-p5-check-top
"C, stores byte"
"CREATE B 65 C, 66 C, B C@"
65)))
(define
forth-p5-unsigned-tests
(fn
()
(forth-p5-check-top "1 2 U<" "1 2 U<" -1)
(forth-p5-check-top "2 1 U<" "2 1 U<" 0)
(forth-p5-check-top "0 1 U<" "0 1 U<" -1)
(forth-p5-check-top "-1 1 U< (since -1 unsigned is huge)" "-1 1 U<" 0)
(forth-p5-check-top "1 -1 U<" "1 -1 U<" -1)
(forth-p5-check-top "1 2 U>" "1 2 U>" 0)
(forth-p5-check-top "-1 1 U>" "-1 1 U>" -1)))
(define
forth-p5-2bang-tests
(fn
()
(forth-p5-check-stack
"2! / 2@"
"CREATE X 0 , 0 , 11 22 X 2! X 2@"
(list 11 22))))
(define
forth-p5-mixed-tests
(fn
()
(forth-p5-check-stack "S>D positive" "5 S>D" (list 5 0))
(forth-p5-check-stack "S>D negative" "-5 S>D" (list -5 -1))
(forth-p5-check-stack "S>D zero" "0 S>D" (list 0 0))
(forth-p5-check-top "D>S keeps low" "5 0 D>S" 5)
(forth-p5-check-stack "M* small positive" "3 4 M*" (list 12 0))
(forth-p5-check-stack "M* negative" "-3 4 M*" (list -12 -1))
(forth-p5-check-stack
"M* negative * negative"
"-3 -4 M*"
(list 12 0))
(forth-p5-check-stack "UM* small" "3 4 UM*" (list 12 0))
(forth-p5-check-stack
"UM/MOD: 100 0 / 5"
"100 0 5 UM/MOD"
(list 0 20))
(forth-p5-check-stack
"FM/MOD: -7 / 2 floored"
"-7 -1 2 FM/MOD"
(list 1 -4))
(forth-p5-check-stack
"SM/REM: -7 / 2 truncated"
"-7 -1 2 SM/REM"
(list -1 -3))
(forth-p5-check-top "*/ truncated" "7 11 13 */" 5)
(forth-p5-check-stack "*/MOD" "7 11 13 */MOD" (list 12 5))))
(define
forth-p5-double-tests
(fn
()
(forth-p5-check-stack "D+ small" "5 0 7 0 D+" (list 12 0))
(forth-p5-check-stack "D+ negative" "-5 -1 -3 -1 D+" (list -8 -1))
(forth-p5-check-stack "D- small" "10 0 3 0 D-" (list 7 0))
(forth-p5-check-stack "DNEGATE positive" "5 0 DNEGATE" (list -5 -1))
(forth-p5-check-stack "DNEGATE negative" "-5 -1 DNEGATE" (list 5 0))
(forth-p5-check-stack "DABS negative" "-7 -1 DABS" (list 7 0))
(forth-p5-check-stack "DABS positive" "7 0 DABS" (list 7 0))
(forth-p5-check-top "D= equal" "5 0 5 0 D=" -1)
(forth-p5-check-top "D= unequal lo" "5 0 7 0 D=" 0)
(forth-p5-check-top "D= unequal hi" "5 0 5 1 D=" 0)
(forth-p5-check-top "D< lt" "5 0 7 0 D<" -1)
(forth-p5-check-top "D< gt" "7 0 5 0 D<" 0)
(forth-p5-check-top "D0= zero" "0 0 D0=" -1)
(forth-p5-check-top "D0= nonzero" "5 0 D0=" 0)
(forth-p5-check-top "D0< neg" "-5 -1 D0<" -1)
(forth-p5-check-top "D0< pos" "5 0 D0<" 0)
(forth-p5-check-stack "DMAX" "5 0 7 0 DMAX" (list 7 0))
(forth-p5-check-stack "DMIN" "5 0 7 0 DMIN" (list 5 0))))
(define
forth-p5-format-tests
(fn
()
(forth-p4-check-output-passthrough
"U. prints with trailing space"
"123 U."
"123 ")
(forth-p4-check-output-passthrough
"<# #S #> TYPE — decimal"
"123 0 <# #S #> TYPE"
"123")
(forth-p4-check-output-passthrough
"<# #S #> TYPE — hex"
"255 HEX 0 <# #S #> TYPE"
"FF")
(forth-p4-check-output-passthrough
"<# # # #> partial"
"1234 0 <# # # #> TYPE"
"34")
(forth-p4-check-output-passthrough
"SIGN holds minus"
"<# -1 SIGN -1 SIGN 0 0 #> TYPE"
"--")
(forth-p4-check-output-passthrough
".R right-justifies"
"42 5 .R"
" 42")
(forth-p4-check-output-passthrough
".R negative"
"-42 5 .R"
" -42")
(forth-p4-check-output-passthrough
"U.R"
"42 5 U.R"
" 42")
(forth-p4-check-output-passthrough
"HOLD char"
"<# 0 0 65 HOLD #> TYPE"
"A")))
(define
forth-p5-dict-tests
(fn
()
(forth-p5-check-top
"EXECUTE via tick"
": INC 1+ ; 9 ' INC EXECUTE"
10)
(forth-p5-check-top
"['] inside def"
": DUB 2* ; : APPLY ['] DUB EXECUTE ; 5 APPLY"
10)
(forth-p5-check-top
">BODY of CREATE word"
"CREATE C 99 , ' C >BODY @"
99)
(forth-p5-check-stack
"WORD parses next token to counted-string"
": A 5 ; BL WORD A COUNT TYPE"
(list))
(forth-p5-check-top
"FIND on known word -> non-zero"
": A 5 ; BL WORD A FIND SWAP DROP"
-1)))
(define
forth-p5-state-tests
(fn
()
(forth-p5-check-top
"STATE @ in interpret mode"
"STATE @"
0)
(forth-p5-check-top
"STATE @ via IMMEDIATE inside compile"
": GT8 STATE @ ; IMMEDIATE : T GT8 LITERAL ; T"
-1)
(forth-p5-check-top
"[ ] LITERAL captures"
": SEVEN [ 7 ] LITERAL ; SEVEN"
7)
(forth-p5-check-top
"EVALUATE in interpret mode"
"S\" 5 7 +\" EVALUATE"
12)
(forth-p5-check-top
"EVALUATE inside def"
": A 100 ; : B S\" A\" EVALUATE ; B"
100)))
(define
forth-p5-misc-tests
(fn
()
(forth-p5-check-top "WITHIN inclusive lower" "3 2 10 WITHIN" -1)
(forth-p5-check-top "WITHIN exclusive upper" "10 2 10 WITHIN" 0)
(forth-p5-check-top "WITHIN below range" "1 2 10 WITHIN" 0)
(forth-p5-check-top "WITHIN at lower" "2 2 10 WITHIN" -1)
(forth-p5-check-top
"EXIT leaves colon-def early"
": F 5 EXIT 99 ; F"
5)
(forth-p5-check-stack
"EXIT in IF branch"
": F 5 0 IF DROP 99 EXIT THEN ; F"
(list 5))
(forth-p5-check-top
"UNLOOP + EXIT in DO"
": SUM 0 10 0 DO I 5 = IF I UNLOOP EXIT THEN LOOP ; SUM"
5)))
(define
forth-p5-fa-tests
(fn
()
(forth-p5-check-top
"R/O R/W W/O constants"
"R/O R/W W/O + +"
3)
(forth-p5-check-top
"CREATE-FILE returns ior=0"
"CREATE PAD 50 ALLOT PAD S\" /tmp/test.fxf\" ROT SWAP CMOVE S\" /tmp/test.fxf\" R/W CREATE-FILE SWAP DROP"
0)
(forth-p5-check-top
"WRITE-FILE then CLOSE"
"S\" /tmp/t2.fxf\" R/W CREATE-FILE DROP >R S\" HI\" R@ WRITE-FILE R> CLOSE-FILE +"
0)
(forth-p5-check-top
"OPEN-FILE on unknown path returns ior!=0"
"S\" /tmp/nope.fxf\" R/O OPEN-FILE SWAP DROP 0 ="
0)))
(define
forth-p5-string-tests
(fn
()
(forth-p5-check-top "COMPARE equal" "S\" ABC\" S\" ABC\" COMPARE" 0)
(forth-p5-check-top "COMPARE less" "S\" ABC\" S\" ABD\" COMPARE" -1)
(forth-p5-check-top "COMPARE greater" "S\" ABD\" S\" ABC\" COMPARE" 1)
(forth-p5-check-top
"COMPARE prefix less"
"S\" AB\" S\" ABC\" COMPARE"
-1)
(forth-p5-check-top
"COMPARE prefix greater"
"S\" ABC\" S\" AB\" COMPARE"
1)
(forth-p5-check-top
"SEARCH found flag"
"S\" HELLO WORLD\" S\" WORLD\" SEARCH"
-1)
(forth-p5-check-top
"SEARCH not found flag"
"S\" HELLO\" S\" XYZ\" SEARCH"
0)
(forth-p5-check-top
"SEARCH empty needle flag"
"S\" HELLO\" S\" \" SEARCH"
-1)
(forth-p5-check-top
"SLITERAL via [ S\" ... \" ]"
": A [ S\" HI\" ] SLITERAL ; A SWAP DROP"
2)))
(define
forth-p4-check-output-passthrough
(fn
(label src expected)
(let ((r (forth-run src))) (forth-p5-assert label expected (nth r 1)))))
(define
forth-p5-run-all
(fn
()
(set! forth-p5-passed 0)
(set! forth-p5-failed 0)
(set! forth-p5-failures (list))
(forth-p5-create-tests)
(forth-p5-unsigned-tests)
(forth-p5-2bang-tests)
(forth-p5-mixed-tests)
(forth-p5-double-tests)
(forth-p5-format-tests)
(forth-p5-dict-tests)
(forth-p5-state-tests)
(forth-p5-misc-tests)
(forth-p5-fa-tests)
(forth-p5-string-tests)
(dict
"passed"
forth-p5-passed
"failed"
forth-p5-failed
"failures"
forth-p5-failures)))

View File

@@ -164,16 +164,13 @@
every? every?
catch-info catch-info
finally-info finally-info
having-info having-info)
of-filter-info
count-filter-info
elsewhere?)
(cond (cond
((<= (len items) 1) ((<= (len items) 1)
(let (let
((body (if (> (len items) 0) (first items) nil))) ((body (if (> (len items) 0) (first items) nil)))
(let (let
((target (cond (elsewhere? (list (quote dom-body))) (source (hs-to-sx source)) (true (quote me))))) ((target (if source (hs-to-sx source) (quote me))))
(let (let
((event-refs (if (and (list? body) (= (first body) (quote do))) (filter (fn (x) (and (list? x) (= (first x) (quote ref)))) (rest body)) (list)))) ((event-refs (if (and (list? body) (= (first body) (quote do))) (filter (fn (x) (and (list? x) (= (first x) (quote ref)))) (rest body)) (list))))
(let (let
@@ -181,41 +178,20 @@
(let (let
((raw-compiled (hs-to-sx stripped-body))) ((raw-compiled (hs-to-sx stripped-body)))
(let (let
((compiled-body (let ((base (if (> (len event-refs) 0) (let ((bindings (map (fn (r) (let ((name (nth r 1))) (list (make-symbol name) (list (quote host-get) (list (quote host-get) (quote event) "detail") name)))) event-refs))) (list (quote let) bindings raw-compiled)) raw-compiled))) (if elsewhere? (list (quote when) (list (quote not) (list (quote host-call) (quote me) "contains" (list (quote host-get) (quote event) "target"))) base) base)))) ((compiled-body (if (> (len event-refs) 0) (let ((bindings (map (fn (r) (let ((name (nth r 1))) (list (make-symbol name) (list (quote host-get) (list (quote host-get) (quote event) "detail") name)))) event-refs))) (list (quote let) bindings raw-compiled)) raw-compiled)))
(let (let
((wrapped-body (if catch-info (let ((var (make-symbol (nth catch-info 0))) (catch-body (hs-to-sx (nth catch-info 1)))) (if finally-info (list (quote do) (list (quote guard) (list var (list true catch-body)) compiled-body) (hs-to-sx finally-info)) (list (quote guard) (list var (list true catch-body)) compiled-body))) (if finally-info (list (quote do) compiled-body (hs-to-sx finally-info)) compiled-body)))) ((wrapped-body (if catch-info (let ((var (make-symbol (nth catch-info 0))) (catch-body (hs-to-sx (nth catch-info 1)))) (if finally-info (list (quote do) (list (quote guard) (list var (list true catch-body)) compiled-body) (hs-to-sx finally-info)) (list (quote guard) (list var (list true catch-body)) compiled-body))) (if finally-info (list (quote do) compiled-body (hs-to-sx finally-info)) compiled-body))))
(let (let
((handler (let ((uses-the-result? (fn (expr) (cond ((= expr (quote the-result)) true) ((list? expr) (some (fn (x) (uses-the-result? x)) expr)) (true false))))) (let ((base-handler (list (quote fn) (list (quote event)) (if (uses-the-result? wrapped-body) (list (quote let) (list (list (quote the-result) nil)) wrapped-body) wrapped-body)))) (if count-filter-info (let ((mn (get count-filter-info "min")) (mx (get count-filter-info "max"))) (list (quote let) (list (list (quote __hs-count) 0)) (list (quote fn) (list (quote event)) (list (quote begin) (list (quote set!) (quote __hs-count) (list (quote +) (quote __hs-count) 1)) (list (quote when) (if (= mx -1) (list (quote >=) (quote __hs-count) mn) (list (quote and) (list (quote >=) (quote __hs-count) mn) (list (quote <=) (quote __hs-count) mx))) (nth base-handler 2)))))) base-handler))))) ((handler (let ((uses-the-result? (fn (expr) (cond ((= expr (quote the-result)) true) ((list? expr) (some (fn (x) (uses-the-result? x)) expr)) (true false))))) (list (quote fn) (list (quote event)) (if (uses-the-result? wrapped-body) (list (quote let) (list (list (quote the-result) nil)) wrapped-body) wrapped-body)))))
(let (let
((on-call (if every? (list (quote hs-on-every) target event-name handler) (list (quote hs-on) target event-name handler)))) ((on-call (if every? (list (quote hs-on-every) target event-name handler) (list (quote hs-on) target event-name handler))))
(cond (if
((= event-name "mutation") (= event-name "intersection")
(list (list
(quote do) (quote do)
on-call on-call
(list (list
(quote hs-on-mutation-attach!) (quote hs-on-intersection-attach!)
target
(if
of-filter-info
(get of-filter-info "type")
"any")
(if
of-filter-info
(let
((a (get of-filter-info "attrs")))
(if
a
(cons (quote list) a)
nil))
nil))))
((= event-name "intersection")
(list
(quote do)
on-call
(list
(quote
hs-on-intersection-attach!)
target target
(if (if
having-info having-info
@@ -224,8 +200,8 @@
(if (if
having-info having-info
(get having-info "threshold") (get having-info "threshold")
nil)))) nil)))
(true on-call)))))))))))) on-call)))))))))))
((= (first items) :from) ((= (first items) :from)
(scan-on (scan-on
(rest (rest items)) (rest (rest items))
@@ -234,10 +210,7 @@
every? every?
catch-info catch-info
finally-info finally-info
having-info having-info))
of-filter-info
count-filter-info
elsewhere?))
((= (first items) :filter) ((= (first items) :filter)
(scan-on (scan-on
(rest (rest items)) (rest (rest items))
@@ -246,10 +219,7 @@
every? every?
catch-info catch-info
finally-info finally-info
having-info having-info))
of-filter-info
count-filter-info
elsewhere?))
((= (first items) :every) ((= (first items) :every)
(scan-on (scan-on
(rest (rest items)) (rest (rest items))
@@ -258,10 +228,7 @@
true true
catch-info catch-info
finally-info finally-info
having-info having-info))
of-filter-info
count-filter-info
elsewhere?))
((= (first items) :catch) ((= (first items) :catch)
(scan-on (scan-on
(rest (rest items)) (rest (rest items))
@@ -270,10 +237,7 @@
every? every?
(nth items 1) (nth items 1)
finally-info finally-info
having-info having-info))
of-filter-info
count-filter-info
elsewhere?))
((= (first items) :finally) ((= (first items) :finally)
(scan-on (scan-on
(rest (rest items)) (rest (rest items))
@@ -282,10 +246,7 @@
every? every?
catch-info catch-info
(nth items 1) (nth items 1)
having-info having-info))
of-filter-info
count-filter-info
elsewhere?))
((= (first items) :having) ((= (first items) :having)
(scan-on (scan-on
(rest (rest items)) (rest (rest items))
@@ -294,45 +255,6 @@
every? every?
catch-info catch-info
finally-info finally-info
(nth items 1)
of-filter-info
count-filter-info
elsewhere?))
((= (first items) :of-filter)
(scan-on
(rest (rest items))
source
filter
every?
catch-info
finally-info
having-info
(nth items 1)
count-filter-info
elsewhere?))
((= (first items) :count-filter)
(scan-on
(rest (rest items))
source
filter
every?
catch-info
finally-info
having-info
of-filter-info
(nth items 1)
elsewhere?))
((= (first items) :elsewhere)
(scan-on
(rest (rest items))
source
filter
every?
catch-info
finally-info
having-info
of-filter-info
count-filter-info
(nth items 1))) (nth items 1)))
(true (true
(scan-on (scan-on
@@ -342,11 +264,8 @@
every? every?
catch-info catch-info
finally-info finally-info
having-info having-info)))))
of-filter-info (scan-on (rest parts) nil nil false nil nil nil)))))
count-filter-info
elsewhere?)))))
(scan-on (rest parts) nil nil false nil nil nil nil nil false)))))
(define (define
emit-send emit-send
(fn (fn
@@ -1058,17 +977,9 @@
(cons (cons
(quote hs-method-call) (quote hs-method-call)
(cons obj (cons method args)))) (cons obj (cons method args))))
(if
(and
(list? dot-node)
(= (first dot-node) (quote ref)))
(list
(quote hs-win-call)
(nth dot-node 1)
(cons (quote list) args))
(cons (cons
(quote hs-method-call) (quote hs-method-call)
(cons (hs-to-sx dot-node) args)))))) (cons (hs-to-sx dot-node) args)))))
((= head (quote string-postfix)) ((= head (quote string-postfix))
(list (quote str) (hs-to-sx (nth ast 1)) (nth ast 2))) (list (quote str) (hs-to-sx (nth ast 1)) (nth ast 2)))
((= head (quote block-literal)) ((= head (quote block-literal))
@@ -1238,12 +1149,7 @@
(list (quote hs-coerce) (hs-to-sx (nth ast 1)) (nth ast 2))) (list (quote hs-coerce) (hs-to-sx (nth ast 1)) (nth ast 2)))
((= head (quote in?)) ((= head (quote in?))
(list (list
(quote hs-in?) (quote hs-contains?)
(hs-to-sx (nth ast 2))
(hs-to-sx (nth ast 1))))
((= head (quote in-bool?))
(list
(quote hs-in-bool?)
(hs-to-sx (nth ast 2)) (hs-to-sx (nth ast 2))
(hs-to-sx (nth ast 1)))) (hs-to-sx (nth ast 1))))
((= head (quote of)) ((= head (quote of))
@@ -1727,19 +1633,7 @@
body))) body)))
(nth compiled (- (len compiled) 1)) (nth compiled (- (len compiled) 1))
(rest (reverse compiled))) (rest (reverse compiled)))
(let (cons (quote do) compiled)))))
((defs (filter (fn (c) (and (list? c) (> (len c) 0) (= (first c) (quote define)))) compiled))
(non-defs
(filter
(fn
(c)
(not
(and
(list? c)
(> (len c) 0)
(= (first c) (quote define)))))
compiled)))
(cons (quote do) (append defs non-defs)))))))
((= head (quote wait)) (list (quote hs-wait) (nth ast 1))) ((= head (quote wait)) (list (quote hs-wait) (nth ast 1)))
((= head (quote wait-for)) (emit-wait-for ast)) ((= head (quote wait-for)) (emit-wait-for ast))
((= head (quote log)) ((= head (quote log))
@@ -1847,13 +1741,7 @@
(make-symbol raw-fn) (make-symbol raw-fn)
(hs-to-sx raw-fn))) (hs-to-sx raw-fn)))
(args (map hs-to-sx (rest (rest ast))))) (args (map hs-to-sx (rest (rest ast)))))
(if (cons fn-expr args)))
(and (list? raw-fn) (= (first raw-fn) (quote ref)))
(list
(quote hs-win-call)
(nth raw-fn 1)
(cons (quote list) args))
(cons fn-expr args))))
((= head (quote return)) ((= head (quote return))
(let (let
((val (nth ast 1))) ((val (nth ast 1)))
@@ -2040,11 +1928,6 @@
(list (list
(quote define) (quote define)
(make-symbol (nth ast 1)) (make-symbol (nth ast 1))
(list
(quote let)
(list
(list
(quote _hs-def-val)
(list (list
(quote fn) (quote fn)
params params
@@ -2065,15 +1948,7 @@
"hs-return")) "hs-return"))
(list (quote nth) (quote _e) 1) (list (quote nth) (quote _e) 1)
(list (quote raise) (quote _e))))) (list (quote raise) (quote _e)))))
body)))) body)))))
(list
(quote do)
(list
(quote host-set!)
(list (quote host-global) "window")
(nth ast 1)
(quote _hs-def-val))
(quote _hs-def-val))))))
((= head (quote behavior)) (emit-behavior ast)) ((= head (quote behavior)) (emit-behavior ast))
((= head (quote sx-eval)) ((= head (quote sx-eval))
(let (let
@@ -2123,7 +1998,7 @@
(hs-to-sx (nth ast 1))))) (hs-to-sx (nth ast 1)))))
((= head (quote in?)) ((= head (quote in?))
(list (list
(quote hs-in?) (quote hs-contains?)
(hs-to-sx (nth ast 2)) (hs-to-sx (nth ast 2))
(hs-to-sx (nth ast 1)))) (hs-to-sx (nth ast 1))))
((= head (quote type-check)) ((= head (quote type-check))

View File

@@ -80,14 +80,11 @@
((src (dom-get-attr el "_")) (prev (dom-get-data el "hs-script"))) ((src (dom-get-attr el "_")) (prev (dom-get-data el "hs-script")))
(when (when
(and src (not (= src prev))) (and src (not (= src prev)))
(when
(dom-dispatch el "hyperscript:before:init" nil)
(hs-log-event! "hyperscript:init") (hs-log-event! "hyperscript:init")
(dom-set-data el "hs-script" src) (dom-set-data el "hs-script" src)
(dom-set-data el "hs-active" true) (dom-set-data el "hs-active" true)
(dom-set-attr el "data-hyperscript-powered" "true") (dom-set-attr el "data-hyperscript-powered" "true")
(let ((handler (hs-handler src))) (handler el)) (let ((handler (hs-handler src))) (handler el))))))
(dom-dispatch el "hyperscript:after:init" nil))))))
;; ── Boot: scan entire document ────────────────────────────────── ;; ── Boot: scan entire document ──────────────────────────────────
;; Called once at page load. Finds all elements with _ attribute, ;; Called once at page load. Finds all elements with _ attribute,

View File

@@ -495,8 +495,7 @@
(quote and) (quote and)
(list (quote >=) left lo) (list (quote >=) left lo)
(list (quote <=) left hi))))) (list (quote <=) left hi)))))
((match-kw "in") ((match-kw "in") (list (quote in?) left (parse-expr)))
(list (quote in-bool?) left (parse-expr)))
((match-kw "really") ((match-kw "really")
(do (do
(match-kw "equal") (match-kw "equal")
@@ -572,8 +571,7 @@
(let (let
((right (parse-expr))) ((right (parse-expr)))
(list (quote not) (list (quote =) left right)))))) (list (quote not) (list (quote =) left right))))))
((match-kw "in") ((match-kw "in") (list (quote in?) left (parse-expr)))
(list (quote in-bool?) left (parse-expr)))
((match-kw "empty") (list (quote empty?) left)) ((match-kw "empty") (list (quote empty?) left))
((match-kw "between") ((match-kw "between")
(let (let
@@ -1557,7 +1555,7 @@
(fn (fn
() ()
(let (let
((tgt (cond ((at-end?) (list (quote me))) ((and (= (tp-type) "keyword") (or (= (tp-val) "then") (= (tp-val) "end") (= (tp-val) "with") (= (tp-val) "when") (= (tp-val) "add") (= (tp-val) "remove") (= (tp-val) "set") (= (tp-val) "put") (= (tp-val) "toggle") (= (tp-val) "hide") (= (tp-val) "show") (= (tp-val) "on"))) (list (quote me))) (true (parse-expr))))) ((tgt (cond ((at-end?) (list (quote me))) ((and (= (tp-type) "keyword") (or (= (tp-val) "then") (= (tp-val) "end") (= (tp-val) "with") (= (tp-val) "when") (= (tp-val) "add") (= (tp-val) "remove") (= (tp-val) "set") (= (tp-val) "put") (= (tp-val) "toggle") (= (tp-val) "hide") (= (tp-val) "show"))) (list (quote me))) (true (parse-expr)))))
(let (let
((strategy (if (match-kw "with") (if (at-end?) "display" (let ((s (tp-val))) (do (adv!) (cond ((at-end?) s) ((= (tp-type) "colon") (do (adv!) (let ((v (tp-val))) (do (adv!) (str s ":" v))))) ((= (tp-type) "local") (let ((v (tp-val))) (do (adv!) (str s ":" v)))) (true s))))) "display"))) ((strategy (if (match-kw "with") (if (at-end?) "display" (let ((s (tp-val))) (do (adv!) (cond ((at-end?) s) ((= (tp-type) "colon") (do (adv!) (let ((v (tp-val))) (do (adv!) (str s ":" v))))) ((= (tp-type) "local") (let ((v (tp-val))) (do (adv!) (str s ":" v)))) (true s))))) "display")))
(let (let
@@ -1568,7 +1566,7 @@
(fn (fn
() ()
(let (let
((tgt (cond ((at-end?) (list (quote me))) ((and (= (tp-type) "keyword") (or (= (tp-val) "then") (= (tp-val) "end") (= (tp-val) "with") (= (tp-val) "when") (= (tp-val) "add") (= (tp-val) "remove") (= (tp-val) "set") (= (tp-val) "put") (= (tp-val) "toggle") (= (tp-val) "hide") (= (tp-val) "show") (= (tp-val) "on"))) (list (quote me))) (true (parse-expr))))) ((tgt (cond ((at-end?) (list (quote me))) ((and (= (tp-type) "keyword") (or (= (tp-val) "then") (= (tp-val) "end") (= (tp-val) "with") (= (tp-val) "when") (= (tp-val) "add") (= (tp-val) "remove") (= (tp-val) "set") (= (tp-val) "put") (= (tp-val) "toggle") (= (tp-val) "hide") (= (tp-val) "show"))) (list (quote me))) (true (parse-expr)))))
(let (let
((strategy (if (match-kw "with") (if (at-end?) "display" (let ((s (tp-val))) (do (adv!) (cond ((at-end?) s) ((= (tp-type) "colon") (do (adv!) (let ((v (tp-val))) (do (adv!) (str s ":" v))))) ((= (tp-type) "local") (let ((v (tp-val))) (do (adv!) (str s ":" v)))) (true s))))) "display"))) ((strategy (if (match-kw "with") (if (at-end?) "display" (let ((s (tp-val))) (do (adv!) (cond ((at-end?) s) ((= (tp-type) "colon") (do (adv!) (let ((v (tp-val))) (do (adv!) (str s ":" v))))) ((= (tp-type) "local") (let ((v (tp-val))) (do (adv!) (str s ":" v)))) (true s))))) "display")))
(let (let
@@ -2603,18 +2601,13 @@
(fn (fn
() ()
(let (let
((every? (match-kw "every")) (first? (match-kw "first"))) ((every? (match-kw "every")))
(let (let
((event-name (parse-compound-event-name))) ((event-name (parse-compound-event-name)))
(let
((count-filter (let ((mn nil) (mx nil)) (when first? (do (set! mn 1) (set! mx 1))) (when (= (tp-type) "number") (let ((n (parse-number (tp-val)))) (do (adv!) (set! mn n) (cond ((match-kw "to") (cond ((= (tp-type) "number") (let ((mv (parse-number (tp-val)))) (do (adv!) (set! mx mv)))) (true (set! mx n)))) ((match-kw "and") (cond ((match-kw "on") (set! mx -1)) (true (set! mx n)))) (true (set! mx n)))))) (if mn (dict "min" mn "max" mx) nil))))
(let
((of-filter (when (and (= event-name "mutation") (match-kw "of")) (cond ((and (= (tp-type) "ident") (or (= (tp-val) "attributes") (= (tp-val) "childList") (= (tp-val) "characterData"))) (let ((nm (tp-val))) (do (adv!) (dict "type" nm)))) ((= (tp-type) "attr") (let ((attrs (list (tp-val)))) (do (adv!) (define collect-or! (fn () (when (match-kw "or") (cond ((= (tp-type) "attr") (do (set! attrs (append attrs (list (tp-val)))) (adv!) (collect-or!))) (true (set! p (- p 1))))))) (collect-or!) (dict "type" "attrs" "attrs" attrs)))) (true nil)))))
(let (let
((flt (if (= (tp-type) "bracket-open") (do (adv!) (let ((f (parse-expr))) (if (= (tp-type) "bracket-close") (adv!) nil) f)) nil))) ((flt (if (= (tp-type) "bracket-open") (do (adv!) (let ((f (parse-expr))) (if (= (tp-type) "bracket-close") (adv!) nil) f)) nil)))
(let (let
((elsewhere? (cond ((match-kw "elsewhere") true) ((and (= (tp-type) "keyword") (= (tp-val) "from") (let ((nxt (if (< (+ p 1) tok-len) (nth tokens (+ p 1)) nil))) (and nxt (= (get nxt "type") "keyword") (= (get nxt "value") "elsewhere")))) (do (adv!) (adv!) true)) (true false))) ((source (if (match-kw "from") (parse-expr) nil)))
(source (if (match-kw "from") (parse-expr) nil)))
(let (let
((h-margin nil) (h-threshold nil)) ((h-margin nil) (h-threshold nil))
(define (define
@@ -2646,10 +2639,7 @@
(let (let
((catch-clause (if (match-kw "catch") (let ((var (let ((v (tp-val))) (adv!) v)) (handler (parse-cmd-list))) (list var handler)) nil)) ((catch-clause (if (match-kw "catch") (let ((var (let ((v (tp-val))) (adv!) v)) (handler (parse-cmd-list))) (list var handler)) nil))
(finally-clause (finally-clause
(if (if (match-kw "finally") (parse-cmd-list) nil)))
(match-kw "finally")
(parse-cmd-list)
nil)))
(match-kw "end") (match-kw "end")
(let (let
((parts (list (quote on) event-name))) ((parts (list (quote on) event-name)))
@@ -2657,14 +2647,8 @@
((parts (if every? (append parts (list :every true)) parts))) ((parts (if every? (append parts (list :every true)) parts)))
(let (let
((parts (if flt (append parts (list :filter flt)) parts))) ((parts (if flt (append parts (list :filter flt)) parts)))
(let
((parts (if elsewhere? (append parts (list :elsewhere true)) parts)))
(let (let
((parts (if source (append parts (list :from source)) parts))) ((parts (if source (append parts (list :from source)) parts)))
(let
((parts (if count-filter (append parts (list :count-filter count-filter)) parts)))
(let
((parts (if of-filter (append parts (list :of-filter of-filter)) parts)))
(let (let
((parts (if having (append parts (list :having having)) parts))) ((parts (if having (append parts (list :having having)) parts)))
(let (let
@@ -2673,7 +2657,7 @@
((parts (if finally-clause (append parts (list :finally finally-clause)) parts))) ((parts (if finally-clause (append parts (list :finally finally-clause)) parts)))
(let (let
((parts (append parts (list body)))) ((parts (append parts (list body))))
parts))))))))))))))))))))))) parts))))))))))))))))))
(define (define
parse-init-feat parse-init-feat
(fn (fn

View File

@@ -82,36 +82,14 @@
observer))))) observer)))))
;; Wait for CSS transitions/animations to settle on an element. ;; Wait for CSS transitions/animations to settle on an element.
(define (define hs-init (fn (thunk) (thunk)))
hs-on-mutation-attach!
(fn
(target mode attr-list)
(let
((cfg-attributes (or (= mode "any") (= mode "attributes") (= mode "attrs")))
(cfg-childList (or (= mode "any") (= mode "childList")))
(cfg-characterData (or (= mode "any") (= mode "characterData"))))
(let
((opts (dict "attributes" cfg-attributes "childList" cfg-childList "characterData" cfg-characterData "subtree" true)))
(when
(and (= mode "attrs") attr-list)
(dict-set! opts "attributeFilter" attr-list))
(let
((cb (fn (records observer) (dom-dispatch target "mutation" (dict "records" records)))))
(let
((observer (host-new "MutationObserver" cb)))
(host-call observer "observe" target opts)
observer))))))
;; ── Class manipulation ────────────────────────────────────────── ;; ── Class manipulation ──────────────────────────────────────────
;; Toggle a single class on an element. ;; Toggle a single class on an element.
(define hs-init (fn (thunk) (thunk)))
;; Toggle between two classes — exactly one is active at a time.
(define hs-wait (fn (ms) (perform (list (quote io-sleep) ms)))) (define hs-wait (fn (ms) (perform (list (quote io-sleep) ms))))
;; Take a class from siblings — add to target, remove from others. ;; Toggle between two classes — exactly one is active at a time.
;; (hs-take! target cls) — like radio button class behavior
(begin (begin
(define (define
hs-wait-for hs-wait-for
@@ -124,20 +102,21 @@
(target event-name timeout-ms) (target event-name timeout-ms)
(perform (list (quote io-wait-event) target event-name timeout-ms))))) (perform (list (quote io-wait-event) target event-name timeout-ms)))))
;; Take a class from siblings — add to target, remove from others.
;; (hs-take! target cls) — like radio button class behavior
(define hs-settle (fn (target) (perform (list (quote io-settle) target))))
;; ── DOM insertion ─────────────────────────────────────────────── ;; ── DOM insertion ───────────────────────────────────────────────
;; Put content at a position relative to a target. ;; Put content at a position relative to a target.
;; pos: "into" | "before" | "after" ;; pos: "into" | "before" | "after"
(define hs-settle (fn (target) (perform (list (quote io-settle) target))))
;; ── Navigation / traversal ──────────────────────────────────────
;; Navigate to a URL.
(define (define
hs-toggle-class! hs-toggle-class!
(fn (target cls) (host-call (host-get target "classList") "toggle" cls))) (fn (target cls) (host-call (host-get target "classList") "toggle" cls)))
;; Find next sibling matching a selector (or any sibling). ;; ── Navigation / traversal ──────────────────────────────────────
;; Navigate to a URL.
(define (define
hs-toggle-between! hs-toggle-between!
(fn (fn
@@ -147,7 +126,7 @@
(do (dom-remove-class target cls1) (dom-add-class target cls2)) (do (dom-remove-class target cls1) (dom-add-class target cls2))
(do (dom-remove-class target cls2) (dom-add-class target cls1))))) (do (dom-remove-class target cls2) (dom-add-class target cls1)))))
;; Find previous sibling matching a selector. ;; Find next sibling matching a selector (or any sibling).
(define (define
hs-toggle-style! hs-toggle-style!
(fn (fn
@@ -171,7 +150,7 @@
(dom-set-style target prop "hidden") (dom-set-style target prop "hidden")
(dom-set-style target prop ""))))))) (dom-set-style target prop "")))))))
;; First element matching selector within a scope. ;; Find previous sibling matching a selector.
(define (define
hs-toggle-style-between! hs-toggle-style-between!
(fn (fn
@@ -183,7 +162,7 @@
(dom-set-style target prop val2) (dom-set-style target prop val2)
(dom-set-style target prop val1))))) (dom-set-style target prop val1)))))
;; Last element matching selector. ;; First element matching selector within a scope.
(define (define
hs-toggle-style-cycle! hs-toggle-style-cycle!
(fn (fn
@@ -204,7 +183,7 @@
(true (find-next (rest remaining)))))) (true (find-next (rest remaining))))))
(dom-set-style target prop (find-next vals))))) (dom-set-style target prop (find-next vals)))))
;; First/last within a specific scope. ;; Last element matching selector.
(define (define
hs-take! hs-take!
(fn (fn
@@ -244,6 +223,7 @@
(dom-set-attr target name attr-val) (dom-set-attr target name attr-val)
(dom-set-attr target name "")))))))) (dom-set-attr target name ""))))))))
;; First/last within a specific scope.
(begin (begin
(define (define
hs-element? hs-element?
@@ -355,9 +335,6 @@
(dom-insert-adjacent-html target "beforeend" value) (dom-insert-adjacent-html target "beforeend" value)
(hs-boot-subtree! target))))))))) (hs-boot-subtree! target)))))))))
;; ── Iteration ───────────────────────────────────────────────────
;; Repeat a thunk N times.
(define (define
hs-add-to! hs-add-to!
(fn (fn
@@ -370,7 +347,9 @@
(append target (list value)))) (append target (list value))))
(true (do (host-call target "push" value) target))))) (true (do (host-call target "push" value) target)))))
;; Repeat forever (until break — relies on exception/continuation). ;; ── Iteration ───────────────────────────────────────────────────
;; Repeat a thunk N times.
(define (define
hs-remove-from! hs-remove-from!
(fn (fn
@@ -380,10 +359,7 @@
(filter (fn (x) (not (= x value))) target) (filter (fn (x) (not (= x value))) target)
(host-call target "splice" (host-call target "indexOf" value) 1)))) (host-call target "splice" (host-call target "indexOf" value) 1))))
;; ── Fetch ─────────────────────────────────────────────────────── ;; Repeat forever (until break — relies on exception/continuation).
;; Fetch a URL, parse response according to format.
;; (hs-fetch url format) — format is "json" | "text" | "html"
(define (define
hs-splice-at! hs-splice-at!
(fn (fn
@@ -407,10 +383,10 @@
(host-call target "splice" i 1)))) (host-call target "splice" i 1))))
target)))) target))))
;; ── Type coercion ─────────────────────────────────────────────── ;; ── Fetch ───────────────────────────────────────────────────────
;; Coerce a value to a type by name. ;; Fetch a URL, parse response according to format.
;; (hs-coerce value type-name) — type-name is "Int", "Float", "String", etc. ;; (hs-fetch url format) — format is "json" | "text" | "html"
(define (define
hs-index hs-index
(fn (fn
@@ -422,10 +398,10 @@
((string? obj) (nth obj key)) ((string? obj) (nth obj key))
(true (host-get obj key))))) (true (host-get obj key)))))
;; ── Object creation ───────────────────────────────────────────── ;; ── Type coercion ───────────────────────────────────────────────
;; Make a new object of a given type. ;; Coerce a value to a type by name.
;; (hs-make type-name) — creates empty object/collection ;; (hs-coerce value type-name) — type-name is "Int", "Float", "String", etc.
(define (define
hs-put-at! hs-put-at!
(fn (fn
@@ -447,11 +423,10 @@
((= pos "start") (host-call target "unshift" value))) ((= pos "start") (host-call target "unshift" value)))
target))))))) target)))))))
;; ── Behavior installation ─────────────────────────────────────── ;; ── Object creation ─────────────────────────────────────────────
;; Install a behavior on an element. ;; Make a new object of a given type.
;; A behavior is a function that takes (me ...params) and sets up features. ;; (hs-make type-name) — creates empty object/collection
;; (hs-install behavior-fn me ...args)
(define (define
hs-dict-without hs-dict-without
(fn (fn
@@ -472,27 +447,27 @@
(host-call (host-global "Reflect") "deleteProperty" out key) (host-call (host-global "Reflect") "deleteProperty" out key)
out))))) out)))))
;; ── Measurement ───────────────────────────────────────────────── ;; ── Behavior installation ───────────────────────────────────────
;; Measure an element's bounding rect, store as local variables. ;; Install a behavior on an element.
;; Returns a dict with x, y, width, height, top, left, right, bottom. ;; A behavior is a function that takes (me ...params) and sets up features.
;; (hs-install behavior-fn me ...args)
(define (define
hs-set-on! hs-set-on!
(fn (fn
(props target) (props target)
(for-each (fn (k) (host-set! target k (get props k))) (keys props)))) (for-each (fn (k) (host-set! target k (get props k))) (keys props))))
;; ── Measurement ─────────────────────────────────────────────────
;; Measure an element's bounding rect, store as local variables.
;; Returns a dict with x, y, width, height, top, left, right, bottom.
(define hs-navigate! (fn (url) (perform (list (quote io-navigate) url))))
;; Return the current text selection as a string. In the browser this is ;; Return the current text selection as a string. In the browser this is
;; `window.getSelection().toString()`. In the mock test runner, a test ;; `window.getSelection().toString()`. In the mock test runner, a test
;; setup stashes the desired selection text at `window.__test_selection` ;; setup stashes the desired selection text at `window.__test_selection`
;; and the fallback path returns that so tests can assert on the result. ;; and the fallback path returns that so tests can assert on the result.
(define hs-navigate! (fn (url) (perform (list (quote io-navigate) url))))
;; ── Transition ──────────────────────────────────────────────────
;; Transition a CSS property to a value, optionally with duration.
;; (hs-transition target prop value duration)
(define (define
hs-ask hs-ask
(fn (fn
@@ -501,6 +476,11 @@
((w (host-global "window"))) ((w (host-global "window")))
(if w (host-call w "prompt" msg) nil)))) (if w (host-call w "prompt" msg) nil))))
;; ── Transition ──────────────────────────────────────────────────
;; Transition a CSS property to a value, optionally with duration.
;; (hs-transition target prop value duration)
(define (define
hs-answer hs-answer
(fn (fn
@@ -654,10 +634,6 @@
hs-query-all hs-query-all
(fn (sel) (host-call (dom-body) "querySelectorAll" sel))) (fn (sel) (host-call (dom-body) "querySelectorAll" sel)))
(define (define
hs-query-all-in hs-query-all-in
(fn (fn
@@ -667,21 +643,25 @@
(hs-query-all sel) (hs-query-all sel)
(host-call target "querySelectorAll" sel)))) (host-call target "querySelectorAll" sel))))
(define (define
hs-list-set hs-list-set
(fn (fn
(lst idx val) (lst idx val)
(append (take lst idx) (cons val (drop lst (+ idx 1)))))) (append (take lst idx) (cons val (drop lst (+ idx 1))))))
;; ── Sandbox/test runtime additions ──────────────────────────────
;; Property access — dot notation and .length
(define (define
hs-to-number hs-to-number
(fn (v) (if (number? v) v (or (parse-number (str v)) 0)))) (fn (v) (if (number? v) v (or (parse-number (str v)) 0))))
;; DOM query stub — sandbox returns empty list ;; ── Sandbox/test runtime additions ──────────────────────────────
;; Property access — dot notation and .length
(define (define
hs-query-first hs-query-first
(fn (sel) (host-call (host-global "document") "querySelector" sel))) (fn (sel) (host-call (host-global "document") "querySelector" sel)))
;; Method dispatch — obj.method(args) ;; DOM query stub — sandbox returns empty list
(define (define
hs-query-last hs-query-last
(fn (fn
@@ -689,11 +669,11 @@
(let (let
((all (dom-query-all (dom-body) sel))) ((all (dom-query-all (dom-body) sel)))
(if (> (len all) 0) (nth all (- (len all) 1)) nil)))) (if (> (len all) 0) (nth all (- (len all) 1)) nil))))
;; Method dispatch — obj.method(args)
(define hs-first (fn (scope sel) (dom-query-all scope sel)))
;; ── 0.9.90 features ───────────────────────────────────────────── ;; ── 0.9.90 features ─────────────────────────────────────────────
;; beep! — debug logging, returns value unchanged ;; beep! — debug logging, returns value unchanged
(define hs-first (fn (scope sel) (dom-query-all scope sel)))
;; Property-based is — check obj.key truthiness
(define (define
hs-last hs-last
(fn (fn
@@ -701,7 +681,7 @@
(let (let
((all (dom-query-all scope sel))) ((all (dom-query-all scope sel)))
(if (> (len all) 0) (nth all (- (len all) 1)) nil)))) (if (> (len all) 0) (nth all (- (len all) 1)) nil))))
;; Array slicing (inclusive both ends) ;; Property-based is — check obj.key truthiness
(define (define
hs-repeat-times hs-repeat-times
(fn (fn
@@ -719,7 +699,7 @@
((= signal "hs-continue") (do-repeat (+ i 1))) ((= signal "hs-continue") (do-repeat (+ i 1)))
(true (do-repeat (+ i 1)))))))) (true (do-repeat (+ i 1))))))))
(do-repeat 0))) (do-repeat 0)))
;; Collection: sorted by ;; Array slicing (inclusive both ends)
(define (define
hs-repeat-forever hs-repeat-forever
(fn (fn
@@ -735,7 +715,7 @@
((= signal "hs-continue") (do-forever)) ((= signal "hs-continue") (do-forever))
(true (do-forever)))))) (true (do-forever))))))
(do-forever))) (do-forever)))
;; Collection: sorted by descending ;; Collection: sorted by
(define (define
hs-repeat-while hs-repeat-while
(fn (fn
@@ -748,7 +728,7 @@
((= signal "hs-break") nil) ((= signal "hs-break") nil)
((= signal "hs-continue") (hs-repeat-while cond-fn thunk)) ((= signal "hs-continue") (hs-repeat-while cond-fn thunk))
(true (hs-repeat-while cond-fn thunk))))))) (true (hs-repeat-while cond-fn thunk)))))))
;; Collection: split by ;; Collection: sorted by descending
(define (define
hs-repeat-until hs-repeat-until
(fn (fn
@@ -760,7 +740,7 @@
((= signal "hs-continue") ((= signal "hs-continue")
(if (cond-fn) nil (hs-repeat-until cond-fn thunk))) (if (cond-fn) nil (hs-repeat-until cond-fn thunk)))
(true (if (cond-fn) nil (hs-repeat-until cond-fn thunk))))))) (true (if (cond-fn) nil (hs-repeat-until cond-fn thunk)))))))
;; Collection: joined by ;; Collection: split by
(define (define
hs-for-each hs-for-each
(fn (fn
@@ -780,7 +760,7 @@
((= signal "hs-continue") (do-loop (rest remaining))) ((= signal "hs-continue") (do-loop (rest remaining)))
(true (do-loop (rest remaining)))))))) (true (do-loop (rest remaining))))))))
(do-loop items)))) (do-loop items))))
;; Collection: joined by
(begin (begin
(define (define
hs-append hs-append
@@ -1535,25 +1515,6 @@
(hs-contains? (rest collection) item)))))) (hs-contains? (rest collection) item))))))
(true false)))) (true false))))
(define
hs-in?
(fn
(collection item)
(cond
((nil? collection) (list))
((list? collection)
(cond
((nil? item) (list))
((list? item)
(filter (fn (x) (hs-contains? collection x)) item))
((hs-contains? collection item) (list item))
(true (list))))
(true (list)))))
(define
hs-in-bool?
(fn (collection item) (not (hs-falsy? (hs-in? collection item)))))
(define (define
hs-is hs-is
(fn (fn
@@ -2134,13 +2095,7 @@
-1 -1
(if (= (first lst) item) i (idx-loop (rest lst) (+ i 1)))))) (if (= (first lst) item) i (idx-loop (rest lst) (+ i 1))))))
(idx-loop obj 0))) (idx-loop obj 0)))
(true (true nil))))
(let
((fn-val (host-get obj method)))
(cond
((and fn-val (callable? fn-val)) (apply fn-val args))
(fn-val (apply host-call (cons obj (cons method args))))
(true nil)))))))
(define hs-beep (fn (v) v)) (define hs-beep (fn (v) v))
@@ -2519,9 +2474,3 @@
((nil? b) false) ((nil? b) false)
((= a b) true) ((= a b) true)
(true (hs-dom-is-ancestor? a (dom-parent b)))))) (true (hs-dom-is-ancestor? a (dom-parent b))))))
(define
hs-win-call
(fn
(fn-name args)
(let ((fn (host-global fn-name))) (if fn (host-call-fn fn args) nil))))

View File

@@ -69,36 +69,333 @@ Representation:
- [x] Tests in `lib/forth/tests/test-phase2.sx` — 26/26 pass - [x] Tests in `lib/forth/tests/test-phase2.sx` — 26/26 pass
### Phase 3 — control flow + first Hayes tests green ### Phase 3 — control flow + first Hayes tests green
- [ ] `IF`, `ELSE`, `THEN` — compile to SX `if` - [x] `IF`, `ELSE`, `THEN` — compile to SX `if`
- [ ] `BEGIN`, `UNTIL`, `WHILE`, `REPEAT`, `AGAIN` — compile to loops - [x] `BEGIN`, `UNTIL`, `WHILE`, `REPEAT`, `AGAIN` — compile to loops
- [ ] `DO`, `LOOP`, `+LOOP`, `I`, `J`, `LEAVE` — counted loops (needs a return stack) - [x] `DO`, `LOOP`, `+LOOP`, `I`, `J`, `LEAVE` — counted loops (needs a return stack)
- [ ] Return stack: `>R`, `R>`, `R@`, `2>R`, `2R>`, `2R@` - [x] Return stack: `>R`, `R>`, `R@`, `2>R`, `2R>`, `2R@`
- [ ] Vendor John Hayes' test suite to `lib/forth/ans-tests/` - [x] Vendor John Hayes' test suite to `lib/forth/ans-tests/`
- [ ] `lib/forth/conformance.sh` + runner; `scoreboard.json` + `scoreboard.md` - [x] `lib/forth/conformance.sh` + runner; `scoreboard.json` + `scoreboard.md`
- [ ] Baseline: probably 30-50% Core passing after phase 3 - [x] Baseline: probably 30-50% Core passing after phase 3
### Phase 4 — strings + more Core ### Phase 4 — strings + more Core
- [ ] `S"`, `C"`, `."`, `TYPE`, `COUNT`, `CMOVE`, `FILL`, `BLANK` - [x] `S"`, `C"`, `."`, `TYPE`, `COUNT`, `CMOVE`, `FILL`, `BLANK`
- [ ] `CHAR`, `[CHAR]`, `KEY`, `ACCEPT` - [x] `CHAR`, `[CHAR]`, `KEY`, `ACCEPT`
- [ ] `BASE` manipulation: `DECIMAL`, `HEX` - [x] `BASE` manipulation: `DECIMAL`, `HEX`
- [ ] `DEPTH`, `SP@`, `SP!` - [x] `DEPTH`, `SP@`, `SP!`
- [ ] Drive Hayes Core pass-rate up - [x] Drive Hayes Core pass-rate up
### Phase 5 — Core Extension + optional word sets ### Phase 5 — Core Extension + optional word sets
- [ ] Full Core + Core Extension - [x] Memory: `CREATE`, `HERE`, `ALLOT`, `,`, `C,`, `CELL+`, `CELLS`, `ALIGN`, `ALIGNED`, `2!`, `2@`
- [ ] File Access word set (via SX IO) - [x] Unsigned compare: `U<`, `U>`
- [ ] String word set (`SLITERAL`, `COMPARE`, `SEARCH`) - [x] Mixed/double-cell math: `S>D`, `M*`, `UM*`, `UM/MOD`, `FM/MOD`, `SM/REM`, `*/`, `*/MOD`
- [ ] Target: 100% Hayes Core - [x] Double-cell ops: `D+`, `D-`, `D=`, `D<`, `D0=`, `2DUP`, `2DROP`, `2OVER`, `2SWAP` (already), plus `D>S`, `DABS`, `DNEGATE`
- [x] Number formatting: `<#`, `#`, `#S`, `#>`, `HOLD`, `SIGN`, `.R`, `U.`, `U.R`
- [x] Parsing/dictionary: `WORD`, `FIND`, `EXECUTE`, `'`, `[']`, `LITERAL`, `POSTPONE`, `>BODY` (DOES> deferred — needs runtime-rebind of last CREATE)
- [x] Source/state: `EVALUATE`, `STATE`, `[`, `]` (`SOURCE`/`>IN` stubbed; tokenized input means the exact byte/offset semantics aren't useful here)
- [x] Misc Core: `WITHIN`, `MAX`/`MIN` (already), `ABORT`, `ABORT"`, `EXIT`, `UNLOOP`
- [x] File Access word set (in-memory — `read-file` is not reachable from the epoch eval env)
- [x] String word set (`SLITERAL`, `COMPARE`, `SEARCH`)
- [x] Target: 100% Hayes Core (97% achieved — remaining 5 errors all in `GI5`'s multi-`WHILE`-per-`BEGIN` non-standard pattern, plus one stuck `dict-set!` chunk and 14 numeric-edge fails)
### Phase 6 — speed ### Phase 6 — speed
- [ ] Inline primitive calls during compile (skip dict lookup) - [x] Inline primitive calls during compile (skip dict lookup)
- [ ] Tail-call optimise colon-def endings - [x] Tail-call optimise colon-def endings
- [ ] JIT cooperation: mark compiled colon-defs as VM-eligible - [x] JIT cooperation: mark compiled colon-defs as VM-eligible
## Progress log ## Progress log
_Newest first._ _Newest first._
- **Phase 6 close — JIT cooperation hooks (Hayes unchanged at 618/638).**
Every word record now carries `:vm-eligible? true` and a
`:call-count` counter that `forth-execute-word` bumps on every
invocation. The flag is a hint for downstream JIT consumers — our
bodies are plain SX lambdas already, so the existing SX VM's
on-first-call JIT lifts them into bytecode automatically; the
metadata just makes that fact discoverable. Added
`forth-hot-words state threshold` returning `(name count)`
pairs above a threshold so a future tracing JIT can pick out
hot definitions to specialise. Phase 6 boxes all ticked.
All 306 internal tests green; Hayes Core stays at 618/638.
- **Phase 6 — TCO at colon-def endings (Hayes unchanged at 618/638).**
`forth-run-body` now special-cases the final op when it's a plain
function (not a branch dict): we call it in tail position with no
pc-increment and no recursive `forth-run-body` call. This means
the SX CEK can collapse the continuation frame, so chains like
`: A ... B ; : B ... C ; …` and `RECURSE` deep-recursion test
cases run without piling up frames at each colon-def boundary.
All 306 internal tests still green; verified 5000-deep
`COUNTDOWN RECURSE` still terminates fine.
- **Phase 6 — inline primitive calls (Hayes unchanged at 618/638).**
`forth-compile-call` now appends the looked-up word's body fn
directly to the colon-def body instead of wrapping it in
`(fn (s) (forth-execute-word s w))`. `forth-execute-word body`
reduces to `((get w "body") state)`, so the wrapper added an
extra closure + `get` per call op for no behavioural gain. Same
early-binding semantics: the body fn is captured at compile time,
so later redefinitions of the same name don't retroactively
change existing definitions. All 306 internal tests still green;
Hayes Core stays at 618/638. Pure optimisation.
- **Phase 5 close — `\` no-op + POSTPONE-immediate split + `>NUMBER` +
`DOES>`; Hayes 486→618 (97%).** Big closing-out iteration.
Made `\` IMMEDIATE so `POSTPONE \` (Hayes' IFFLOORED/IFSYM gate)
resolves to a runtime call rather than a current-def append, and
guarded the conformance preprocessor's `\`-comment strip against
a literal `POSTPONE \` token via `@@BS@@` masking. Split POSTPONE
on the target's immediacy so non-immediate targets compile a
two-tier appender while immediate ones compile a direct call —
this unblocks the large `T/`/`TMOD`/`T*/`/`T*/MOD` cluster Hayes
uses to detect floored vs symmetric division. `>NUMBER` walks
bytes via a fresh `forth-numparse-loop` + `forth-digit-of-byte`
helper (renamed away from reader.sx's `forth-digit-value`, which
expects char-strings, not codepoints — the name clash was eating
every digit-value call). Implemented `DOES>` by:
1) tracking the last CREATE on `state.last-creator`,
2) adding a `:kind "does-rebind"` op, and
3) post-processing the body in `;` to attach the slice of ops
after each rebind as `:deferred`. At runtime, the rebind op
installs a new body for the target word that pushes its
data-field address and runs the deferred slice. Also added
histogram tracking on the conformance runner so future runs
surface the top missing words. Hayes: 618/638 pass (97%),
14 fail, 6 error (5× GI5 multi-WHILE, 1× dict-set! chunk).
- **Phase 5 — String word set `COMPARE`/`SEARCH`/`SLITERAL` (+9).**
`COMPARE` walks bytes via the new `forth-compare-bytes-loop`,
returning -1/0/1 with standard prefix semantics (shorter string
compares less than its extension). `SEARCH` scans the haystack
with a helper `forth-search-bytes` and `forth-match-at`, returning
the tail after the first match or the original string with flag=0.
Empty needle returns at offset 0 with flag=-1 per ANS. `SLITERAL`
is IMMEDIATE: pops `(c-addr u)` at compile time, copies the bytes
into a fresh allocation, and emits the two pushes so the compiled
word yields the interned string at runtime.
- **Phase 5 — File Access word set (in-memory backing; +4).**
`OPEN-FILE`/`CREATE-FILE`/`CLOSE-FILE`/`READ-FILE`/`WRITE-FILE`/
`FILE-POSITION`/`FILE-SIZE`/`REPOSITION-FILE`/`DELETE-FILE` plus
the mode constants `R/O`/`R/W`/`W/O`/`BIN`. File handles live on
`state.files` (fileid → {content, pos, path}) with a
`state.by-path` index so `CREATE-FILE`'d files can be
`OPEN-FILE`'d later in the same session. Attempting to
`OPEN-FILE` an unknown path returns `ior != 0`; disk-backed
open/read is not wired because `read-file` isn't in the sx_server
epoch eval environment (it's bound only in the HTTP helpers).
Also removed the stray base-2 `BIN` primitive from Phase 4 —
ANS `BIN` is the file-mode modifier. Hayes Core unchanged at
486/638 since core.fr doesn't exercise file words.
- **Phase 5 — `WITHIN`/`ABORT`/`ABORT"`/`EXIT`/`UNLOOP` (+7;
Hayes 477→486, 76%).** `WITHIN` uses the ANS two's-complement
trick: `(n1-n2) U< (n3-n2)`. `ABORT` wipes the data/return/control
stacks and raises — the conformance runner catches it at the
chunk boundary. `ABORT"` parses its message like `S"`, then at
runtime pops a flag and raises only if truthy. `EXIT` adds a new
`:kind "exit"` op that the PC-driven body runner treats as a
jump-to-end; added a matching cond clause in `forth-step-op`.
`UNLOOP` pops two from the return stack — usable paired with
`EXIT` to bail from inside `DO`/`LOOP`.
- **Phase 5 — `[`, `]`, `STATE`, `EVALUATE` (+5; Hayes 463→477, 74%).**
`[` (IMMEDIATE) clears `state.compiling`, `]` sets it. `STATE`
pushes the sentinel address `"@@state"` and `@` reads it as
`-1`/`0` based on the live `compiling` flag. `EVALUATE` reads
the (addr,u) string from byte memory, retokenises it via
`forth-tokens`, swaps it in as the active input, runs the
interpret loop, and restores the saved input. `SOURCE` and
`>IN` exist as stubs that push zeros — our whitespace-tokenised
input has no native byte-offset, so the deeper Hayes tests
that re-position parsing via `>IN !` stay marked as errors
rather than silently misbehaving.
- **Phase 5 — parsing/dictionary words `'`/`[']`/`EXECUTE`/`LITERAL`/
`POSTPONE`/`WORD`/`FIND`/`>BODY` (Hayes 448→463, 72%).** xt is
represented as the SX dict reference of the word record, so
`'`/`[']` push the looked-up record and `EXECUTE` calls
`forth-execute-word` on the popped value. `LITERAL` (IMMEDIATE)
pops a value at compile time and emits a push-op. `POSTPONE`
(IMMEDIATE) compiles into the *outer* def an op that, when run
during a *later* compile, appends a call-w op to whatever def is
current — the standard two-tier compile semantic. Added
`state.last-defined` tracked by every primitive/colon definition
so `IMMEDIATE` can target the most-recent word even after `;`
closes the def. CREATE now stashes its data-field address on the
word record so `>BODY` can recover it. `WORD`/`FIND` use the byte
memory and counted-string layout already in place.
`DOES>` is deferred — needs a runtime mechanism to rebind the
last-CREATE'd word's action.
- **Phase 5 — pictured numeric output: `<#`/`#`/`#S`/`#>`/`HOLD`/`SIGN` +
`U.`/`U.R`/`.R` (+9; Hayes 446→448, 70%).** Added a `state.hold`
list of single-character strings — `<#` resets it, `HOLD` and
`SIGN` prepend, `#` divides ud by BASE and prepends one digit,
`#S` loops `#` until ud is zero (running once even on zero),
`#>` drops ud and copies the joined hold buffer into mem,
pushing `(addr, len)`. `U.` / `.R` / `U.R` use a separate
`forth-num-to-string` for one-shot decimal/hex output and
`forth-spaces-str` for right-justify padding.
- **Phase 5 — double-cell ops `D+`/`D-`/`DNEGATE`/`DABS`/`D=`/`D<`/`D0=`/
`D0<`/`DMAX`/`DMIN` (+18; Hayes unchanged).** Doubles get rebuilt
from `(lo, hi)` cells via `forth-double-from-cells-s`, the op runs
in bignum, and we push back via `forth-double-push-s`. Hayes Core
doesn't exercise D-words (those live in Gerry Jackson's separate
`doublest.fth` Double word-set tests we have not vendored), so the
scoreboard stays at 446/638 — but the words now exist for any
consumer that needs them.
- **Phase 5 — mixed/double-cell math; Hayes 342→446 (69%).** Added
`S>D`, `D>S`, `M*`, `UM*`, `UM/MOD`, `FM/MOD`, `SM/REM`, `*/`, `*/MOD`.
Doubles ride on the stack as `(lo, hi)` with `hi` on top.
Helpers `forth-double-push-{u,s}` / `forth-double-from-cells-{u,s}`
split & rebuild via 32-bit unsigned mod/div, picking the negative
path explicitly so we don't form `2^64 + small` (float precision
drops at ULP=2^12 once you cross 2^64). `M*`/`UM*` use bignum
multiply then split; `*/`/`*/MOD` use bignum intermediate and
truncated division. Hayes: 446 pass / 185 error / 7 fail.
- **Phase 5 — memory primitives + unsigned compare; Hayes 268→342 (53%).**
Added `CREATE`/`HERE`/`ALLOT`/`,`/`C,`/`CELL+`/`CELLS`/`ALIGN`/`ALIGNED`/
`2!`/`2@`/`U<`/`U>`. Generalised `@`/`!`/`+!` to dispatch on address
type: string addresses still go through `state.vars` (VARIABLE/VALUE
cells) while integer addresses now fall through to `state.mem`
letting CREATE-allocated cells coexist with existing variables.
Decomposed the original "Full Core + Core Extension" box into
smaller unticked sub-bullets so iterations land per cluster.
Hayes: 342 pass / 292 error / 4 fail (53%). 237/237 internal.
- **Phase 4 close — LSHIFT/RSHIFT, 32-bit arith truncation, early
binding; Hayes 174→268 (42%).** Added `LSHIFT` / `RSHIFT` as logical
shifts on 32-bit unsigned values, converted through
`forth-to-unsigned`/`forth-from-unsigned`. All arithmetic
primitives (`+` `-` `*` `/` `MOD` `NEGATE` `ABS` `1+` `1-` `2+`
`2-` `2*` `2/`) now clip results to 32-bit signed via a new
`forth-clip` helper, so loop idioms that rely on `2*` shifting the
MSB out (e.g. Hayes' `BITS` counter) actually terminate.
Changed colon-def call compilation from late-binding to early
binding: `forth-compile-call` now resolves the target word at
compile time, which makes `: GDX 123 ; : GDX GDX 234 ;` behave
per ANS (inner `GDX` → old def, not infinite recursion). `RECURSE`
keeps its late-binding thunk via the new `forth-compile-recurse`
helper. Raised `MAX_CHUNKS` default to 638 (full `core.fr`) now
that the BITS and COUNT-BITS loops terminate. Hayes: 268 pass /
368 error / 2 fail.
- **Phase 4 — `SP@`/`SP!` (+4; Hayes unchanged; `DEPTH` was already present).**
`SP@` pushes the current data-stack depth (our closest analogue to a
stack pointer — SX lists have no addressable backing). `SP!` pops a
target depth and truncates the stack via `drop` on the dstack list.
This preserves the save/restore idiom `SP@ … SP!` even though the
returned "pointer" is really a count.
- **Phase 4 — `BASE`/`DECIMAL`/`HEX`/`BIN`/`OCTAL` (+9; Hayes unchanged).**
Moved `base` from its top-level state slot into `state.vars["base"]`
so the regular `@`/`!`/VARIABLE machinery works on it.
`BASE` pushes the sentinel address `"base"`; `DECIMAL`/`HEX`/`BIN`/
`OCTAL` are thin primitives that write into that slot. Parser
reads through `vars` now. Hayes unchanged because the runner had
already been stubbing `HEX`/`DECIMAL` — now real words, stubs
removed from `hayes-runner.sx`.
- **Phase 4 — `CHAR`/`[CHAR]`/`KEY`/`ACCEPT` (+7 / Hayes 168→174).**
`CHAR` parses the next token and pushes the first-char code. `[CHAR]`
is IMMEDIATE: in compile mode it embeds the code as a compiled push
op, in interpret mode it pushes inline. `KEY`/`ACCEPT` read from an
optional `state.keybuf` string — empty buffer makes `KEY` raise
`"no input available"` (matches ANS when stdin is closed) and
`ACCEPT` returns `0`. Enough for Hayes to get past CHAR-gated
clusters; real interactive IO lands later.
- **Phase 4 — strings: `S"`/`C"`/`."`/`TYPE`/`COUNT`/`CMOVE`/`CMOVE>`/`MOVE`/`FILL`/`BLANK`/`C@`/`C!`/`CHAR+`/`CHARS` (+16 / Hayes 165→168).**
Added a byte-addressable memory model to state: `mem` (dict keyed by
stringified address → integer byte) and `here` (next-free integer
addr). Helpers `forth-alloc-bytes!` / `forth-mem-write-string!` /
`forth-mem-read-string`. `S"`/`C"`/`."` are IMMEDIATE parsing words
that consume tokens until one ends with `"`, then either copy content
into memory at compile time (and emit a push of `addr`/`addr len` for
the colon-def body) or do it inline in interpret mode. `TYPE` emits
`u` bytes from `addr` via `char-from-code`. `COUNT` reads the length
byte at a counted-string address and pushes (`addr+1`, `u`). `FILL`,
`BLANK` (FILL with space), `CMOVE` (forward), `CMOVE>` (backward),
and `MOVE` (auto-directional) mutate the byte dict. 193/193 internal
tests, Hayes 168/590 (+3).
- **Phase 3 — Hayes conformance runner + baseline scoreboard (165/590, 28%).**
`lib/forth/conformance.sh` preprocesses `ans-tests/core.fr` (strips `\`
and `( ... )` comments + `TESTING` lines), splits the source on every
`}T` so each Hayes test plus the small declaration blocks between
them are one safe-resume chunk, and emits an SX driver that feeds
the chunks through `lib/forth/hayes-runner.sx`. The runner registers
`T{`/`->`/`}T` as Forth primitives that snapshot the dstack depth on
`T{`, record actual on `->`, compare on `}T`, and install stub
`HEX`/`DECIMAL`/`TESTING` so metadata doesn't halt the stream. Errors
raised inside a chunk are caught by `guard` and the state is reset,
so one bad test does not break the rest. Outputs
`scoreboard.json` + `scoreboard.md`.
First-run baseline: 165 pass / 425 error / 0 fail on the first 590
chunks. The default cap sits at 590 because `core.fr` chunks beyond
that rely on unsigned-integer wrap-around (e.g. `COUNT-BITS` with
`BEGIN DUP WHILE … 2* REPEAT`) which never terminates on our
bignum-based Forth; raise `MAX_CHUNKS` once those tests unblock.
Majority of errors are missing Phase-4 words (`RSHIFT`, `LSHIFT`,
`CELLS`, `S"`, `CHAR`, `SOURCE`, etc.) — each one implemented should
convert a cluster of errors to passes.
- **Phase 3 — vendor Gerry Jackson's forth2012-test-suite.** Added
`lib/forth/ans-tests/{tester.fr, core.fr, coreexttest.fth}` from
https://github.com/gerryjackson/forth2012-test-suite (master, fetched
2026-04-24). `tester.fr` is Hayes' `T{ ... -> ... }T` harness; `core.fr`
is the ~1000-line Core word tests; `coreexttest.fth` is Core Ext
(parked for later phases). Files are pristine — the conformance runner
(next iteration) will consume them.
- **Phase 3 — `DO`/`LOOP`/`+LOOP`/`I`/`J`/`LEAVE` + return stack words (+16).**
Counted loops compile onto the same PC-driven body runner. DO emits an
enter-op (pops limit+start from data stack, pushes them to rstack) and
pushes a `{:kind "do" :back PC :leaves ()}` marker onto cstack. LOOP/+LOOP
emit a dict op (`:kind "loop"`/`"+loop"` with target=back-cell). The step
handler pops index & reads limit, increments, and either restores the
updated index + jumps back, or drops the frame and advances. LEAVE walks
cstack for the innermost DO marker, emits a `:kind "leave"` dict op with
a fresh target cell, and registers it on the marker's leaves list. LOOP
patches all registered leave-targets to the exit PC and drops the marker.
The leave op pops two from rstack (unloop) and branches. `I` peeks rtop;
`J` reads rstack index 2 (below inner frame). Added non-immediate
return-stack words `>R`, `R>`, `R@`, `2>R`, `2R>`, `2R@`. Nested
DO/LOOP with J tested; LEAVE in nested loops exits only the inner.
177/177 green.
- **Phase 3 — `BEGIN`/`UNTIL`/`WHILE`/`REPEAT`/`AGAIN` (+9).** Indefinite-loop
constructs built on the same PC-driven body runner introduced for `IF`.
BEGIN records the current body length on `state.cstack` (a plain numeric
back-target). UNTIL/AGAIN pop that back-target and emit a `bif`/`branch`
op whose target cell is set to the recorded PC. WHILE emits a forward
`bif` with a fresh target cell and pushes it on the cstack *above* the
BEGIN marker; REPEAT pops both (while-target first, then back-pc), emits
an unconditional branch back to BEGIN, then patches the while-target to
the current body length — so WHILE's false flag jumps past the REPEAT.
Mixed compile-time layout (numeric back-targets + dict forward targets
on the same cstack) is OK because the immediate words pop them in the
order they expect. AGAIN works structurally but lacks a test without a
usable mid-loop exit; revisit once `EXIT` lands. 161/161 green.
- **Phase 3 start — `IF`/`ELSE`/`THEN` (+18).** `lib/forth/compiler.sx`
+ `tests/test-phase3.sx`. Colon-def body switched from `for-each` to
a PC-driven runner so branch ops can jump: ops now include dict tags
`{"kind" "bif"|"branch" "target" cell}` alongside the existing
`(fn (s) ...)` shape. IF compiles a `bif` with a fresh target cell
pushed to `state.cstack`; ELSE emits an unconditional `branch`,
patches the IF's target to the instruction after this branch, and
pushes the new target; THEN patches the most recent target to the
current body length. Nested IF/ELSE/THEN works via the cstack.
Also fixed `EMIT`: `code-char``char-from-code` (spec-correct
primitive name) so Phase 1/2 tests run green on sx_server.
152/152 (Phase 1 + 2 + 3) green.
- **Phase 2 complete — colon defs, compile mode, VARIABLE/CONSTANT/VALUE/TO, @/!/+! (+26).** - **Phase 2 complete — colon defs, compile mode, VARIABLE/CONSTANT/VALUE/TO, @/!/+! (+26).**
`lib/forth/compiler.sx` plus `tests/test-phase2.sx`. `lib/forth/compiler.sx` plus `tests/test-phase2.sx`.
Colon-def body is a list of ops (one per source token) wrapped in a single Colon-def body is a list of ops (one per source token) wrapped in a single

View File

@@ -1,96 +0,0 @@
# HS conformance — blockers drain
Goal: take hyperscript conformance from **1277/1496 (85.4%)** to **1496/1496 (100%)** by clearing the blocked clusters and the design-done Bucket E subsystems.
This plan exists because the per-iteration `loops/hs` agent can't fit these into its 30-min budget — they need dedicated multi-commit sit-downs. Track progress here; refer to `plans/hs-conformance-to-100.md` for the canonical cluster ledger.
## Current state (2026-04-25)
- Loop running in `/root/rose-ash-loops/hs` (branch `loops/hs`)
- sx-tree MCP **fixed** (was a session-stale binary issue — restart of claude in the tmux window picked it up). Loop hinted to retry **#32**, **#29** first.
- Recent loop progress: ~1 commit/6h — easy wins drained, what's left needs focused attention.
## Remaining work
### Bucket-A/B/C blockers (small, in-place fixes)
| # | Cluster | Tests | Effort | Blocker | Fix sketch |
|---|---------|------:|--------|---------|------------|
| **17** | `tell` semantics | +3 | ~1h | Implicit-default-target ambiguity. `bare add .bar` inside `tell X` should target `X` but explicit `to me` must reach the original element. | Add `beingTold` symbol distinct from `me`; bare commands compile to `beingTold-or-me`; explicit `me` always the original. |
| **22** | window global fn fallback | +2-4 | ~1h | `foo()` where `foo` isn't SX-defined needs to fall back to `(host-global "foo")`. Three attempts failed: guard (host-level error not catchable), `env-has?` (not in HS kernel), `hs-win-call` (NativeFn not callable from CALL). | Add `symbol-bound?` predicate to HS kernel **OR** a host-call-fn primitive with arity-agnostic dispatch. |
| **29** | `hyperscript:before:init` / `:after:init` / `:parse-error` events | +4-6 | ~30m (post sx-tree fix) | Was sx-tree MCP outage. Now unblocked — loop should retry. 4 of 6 tests need stricter parser error-rejection (out of scope; mark partial). | Edit `integration.sx` to fire DOM events at activation boundaries. |
### Bucket D — medium features
| # | Cluster | Tests | Effort | Status |
|---|---------|------:|--------|--------|
| **31** | runtime null-safety error reporting | **+15-18** | **2-4h** | **THIS SESSION'S TARGET.** Plan node fully spec'd: 5 pieces of work. |
| **32** | MutationObserver mock + `on mutation` | +10-15 | ~2h | Was sx-tree-blocked. Now unblocked — loop hinted to retry. Multi-file: parser, compiler, runtime, runner mock, generator skip-list. |
| **33** | cookie API | +2 (remaining) | ~30m | Partial done (+3). Remaining 2 need `hs-method-call` runtime fallback for unknown methods + `hs-for-each` recognising host-array/proxy collections. |
| 34 | event modifier DSL | +6-8 | ~1-2h | `elsewhere`, `every`, count filters (`once`/`twice`/`3 times`/ranges), `from elsewhere`. Pending. |
| 35 | namespaced `def` | +3 | ~30m | Pending. |
### Bucket E — subsystems (design docs landed, multi-commit each)
Each has a design doc with a step-by-step checklist. These are 1-2 days of focused work each, not loop-fits.
| # | Subsystem | Tests | Design doc | Branch |
|---|-----------|------:|------------|--------|
| 36 | WebSocket + `socket` + RPC Proxy | +12-16 | `plans/designs/e36-websocket.md` | `worktree-agent-a9daf73703f520257` |
| 37 | Tokenizer-as-API | +16-17 | `plans/designs/e37-tokenizer-api.md` | `worktree-agent-a6bb61d59cc0be8b4` |
| 38 | SourceInfo API | +4 | `plans/designs/e38-sourceinfo.md` | `agent-e38-sourceinfo` |
| 39 | WebWorker plugin (parser-only stub) | +1 | `plans/designs/e39-webworker.md` | `hs-design-e39-webworker` |
| 40 | Real Fetch / non-2xx / before-fetch | +7 | `plans/designs/e40-real-fetch.md` | `worktree-agent-a94612a4283eaa5e0` |
### Bucket F — generator translation gaps
~25 tests SKIP'd because `tests/playwright/generate-sx-tests.py` bails with `return None`. Single dedicated generator-repair sit-down once Bucket D is drained. ~half-day.
## Order of attack
In approximate cost-per-test order:
1. **Loop self-heal** (no human work) — wait for #29, #32 to land via the running loop ⏱️ ~next 1-2 hours
2. **#31 null-safety** — biggest scoped single win, dedicated worktree agent (this session)
3. **#33 cookie API remainder** — quick partial completion
4. **#17 / #22 / #34 / #35** — small fiddly fixes, one sit-down each
5. **Bucket E** — pick one subsystem at a time. **#39 (WebWorker stub) first** — single commit, smallest. Then **#38 (SourceInfo)** — 4 commits. Then the bigger three (#36, #37, #40).
6. **Bucket F** — generator repair sweep at the end.
Estimated total to 100%: ~10-15 days of focused work, parallelisable across branches.
## Cluster #31 spec (full detail)
The plan note from `hs-conformance-to-100.md`:
> 18 tests in `runtimeErrors`. When accessing `.foo` on nil, emit a structured error with position info. One coordinated fix in the compiler emit paths for property access, function calls, set/put.
**Required pieces:**
1. **Generator-side `eval-hs-error` helper + recognizer** for `expect(await error("HS")).toBe("MSG")` blocks. In `tests/playwright/generate-sx-tests.py`.
2. **Runtime helpers** in `lib/hyperscript/runtime.sx`:
- `hs-null-error!` raising `'<sel>' is null`
- `hs-named-target` — wraps a query result with the original selector source
- `hs-named-target-list` — same for list results
3. **Compiler patches at every target-position `(query SEL)` emit** — wrap in named-target carrying the original selector source. ~17 command emit paths in `lib/hyperscript/compiler.sx`:
add, remove, hide, show, measure, settle, trigger, send, set, default, increment, decrement, put, toggle, transition, append, take.
4. **Function-call null-check** at bare `(name)`, `hs-method-call`, and `host-get` chains, deriving the leftmost-uncalled-name (`'x'` / `'x.y'`) from the parse tree.
5. **Possessive-base null-check** (`set x's y to true``'x' is null`).
**Files in scope:**
- `lib/hyperscript/runtime.sx` (new helpers)
- `lib/hyperscript/compiler.sx` (~17 emit-path edits)
- `tests/playwright/generate-sx-tests.py` (test recognizer)
- `tests/hs-run-filtered.js` (if mock helpers needed)
- `shared/static/wasm/sx/hs-runtime.sx` + `hs-compiler.sx` (WASM staging copies)
**Approach:** target-named pieces incrementally — runtime helpers first (no compiler change), then compiler emit paths in batches (group similar commands), then function-call/possessive at the end. Each batch is one commit if it lands +N tests; mark partial if it only unlocks part.
**Watch for:** smoke-range regressions (tests flipping pass→fail). Each commit: rerun smoke 0-195 and the `runtimeErrors` suite.
## Notes for future sessions
- `plans/hs-conformance-to-100.md` is the canonical cluster ledger — update it on every commit.
- `plans/hs-conformance-scoreboard.md` is the live tally — bump `Merged:` and the bucket roll-up.
- Loop has scope rule "never edit `spec/evaluator.sx` or broader SX kernel" — most fixes here stay in `lib/hyperscript/**`, `tests/`, generator. If a fix needs kernel work, surface to the user; don't merge silently.
- Cluster #22's `symbol-bound?` predicate would be a kernel addition — that's a real cross-boundary scope expansion.

View File

@@ -4,10 +4,10 @@ Live tally for `plans/hs-conformance-to-100.md`. Update after every cluster comm
``` ```
Baseline: 1213/1496 (81.1%) Baseline: 1213/1496 (81.1%)
Merged: 1303/1496 (87.1%) delta +90 Merged: 1277/1496 (85.4%) delta +64
Worktree: all landed Worktree: all landed
Target: 1496/1496 (100.0%) Target: 1496/1496 (100.0%)
Remaining: ~194 tests (clusters 17/29(partial)/31 blocked; 33/34 partial) Remaining: ~219 tests (cluster 29 blocked on sx-tree MCP outage + parser scope)
``` ```
## Cluster ledger ## Cluster ledger
@@ -42,7 +42,7 @@ Remaining: ~194 tests (clusters 17/29(partial)/31 blocked; 33/34 partial)
| 19 | `pick` regex + indices | done | +13 | 4be90bf2 | | 19 | `pick` regex + indices | done | +13 | 4be90bf2 |
| 20 | `repeat` property for-loops + where | done | +3 | c932ad59 | | 20 | `repeat` property for-loops + where | done | +3 | c932ad59 |
| 21 | `possessiveExpression` property access via its | done | +1 | f0c41278 | | 21 | `possessiveExpression` property access via its | done | +1 | f0c41278 |
| 22 | window global fn fallback | done | +1 | d31565d5 | | 22 | window global fn fallback | blocked | | |
| 23 | `me symbol works in from expressions` | done | +1 | 0d38a75b | | 23 | `me symbol works in from expressions` | done | +1 | 0d38a75b |
| 24 | `properly interpolates values 2` | done | +1 | cb37259d | | 24 | `properly interpolates values 2` | done | +1 | cb37259d |
| 25 | parenthesized commands and features | done | +1 | d7a88d85 | | 25 | parenthesized commands and features | done | +1 | d7a88d85 |
@@ -54,18 +54,18 @@ Remaining: ~194 tests (clusters 17/29(partial)/31 blocked; 33/34 partial)
| 26 | resize observer mock + `on resize` | done | +3 | 304a52d2 | | 26 | resize observer mock + `on resize` | done | +3 | 304a52d2 |
| 27 | intersection observer mock + `on intersection` | done | +3 | 0c31dd27 | | 27 | intersection observer mock + `on intersection` | done | +3 | 0c31dd27 |
| 28 | `ask`/`answer` + prompt/confirm mock | done | +4 | 6c1da921 | | 28 | `ask`/`answer` + prompt/confirm mock | done | +4 | 6c1da921 |
| 29 | `hyperscript:before:init` / `:after:init` / `:parse-error` | partial | +2 | e01a3baa | | 29 | `hyperscript:before:init` / `:after:init` / `:parse-error` | blocked | | |
| 30 | `logAll` config | done | +1 | 64bcefff | | 30 | `logAll` config | done | +1 | 64bcefff |
### Bucket D — medium features ### Bucket D — medium features
| # | Cluster | Status | Δ | | # | Cluster | Status | Δ |
|---|---------|--------|---| |---|---------|--------|---|
| 31 | runtime null-safety error reporting | blocked | — | | 31 | runtime null-safety error reporting | pending | (+1518 est) |
| 32 | MutationObserver mock + `on mutation` | done | +7 | | 32 | MutationObserver mock + `on mutation` | pending | (+1015 est) |
| 33 | cookie API | partial | +4 | | 33 | cookie API | pending | (+5 est) |
| 34 | event modifier DSL | partial | +7 | | 34 | event modifier DSL | pending | (+68 est) |
| 35 | namespaced `def` | done | +3 | | 35 | namespaced `def` | pending | (+3 est) |
### Bucket E — subsystems (design docs landed, pending review + implementation) ### Bucket E — subsystems (design docs landed, pending review + implementation)
@@ -86,9 +86,9 @@ Defer until AD drain. Estimated ~25 recoverable tests.
| Bucket | Done | Partial | In-prog | Pending | Blocked | Design-done | Total | | Bucket | Done | Partial | In-prog | Pending | Blocked | Design-done | Total |
|--------|-----:|--------:|--------:|--------:|--------:|------------:|------:| |--------|-----:|--------:|--------:|--------:|--------:|------------:|------:|
| A | 12 | 4 | 0 | 0 | 1 | — | 17 | | A | 12 | 4 | 0 | 0 | 1 | — | 17 |
| B | 7 | 0 | 0 | 0 | 0 | — | 7 | | B | 6 | 0 | 0 | 0 | 1 | — | 7 |
| C | 4 | 1 | 0 | 0 | 0 | — | 5 | | C | 4 | 0 | 0 | 0 | 1 | — | 5 |
| D | 2 | 2 | 0 | 0 | 1 | — | 5 | | D | 0 | 0 | 0 | 5 | 0 | — | 5 |
| E | 0 | 0 | 0 | 0 | 0 | 5 | 5 | | E | 0 | 0 | 0 | 0 | 0 | 5 | 5 |
| F | — | — | — | ~10 | — | — | ~10 | | F | — | — | — | ~10 | — | — | ~10 |

View File

@@ -69,7 +69,7 @@ Orchestrator cherry-picks worktree commits onto `architecture` one at a time; re
10. **[done (+1)] `swap` variable ↔ property** — `swap / can swap a variable with a property` (1 test). Swap command doesn't handle mixed var/prop targets. Expected: +1. 10. **[done (+1)] `swap` variable ↔ property** — `swap / can swap a variable with a property` (1 test). Swap command doesn't handle mixed var/prop targets. Expected: +1.
11. **[done (+4)] `hide` strategy** — `hide / can configure hidden as default`, `can hide with custom strategy`, `can set default to custom strategy`, `hide element then show element retains original display` (4 tests). Strategy config plumbing. Expected: +3-4. 11. **[done (+3) — partial, `hide element then show element retains original display` remains; needs `on click N` count-filtered event handlers, out of scope for this cluster] `hide` strategy** — `hide / can configure hidden as default`, `can hide with custom strategy`, `can set default to custom strategy`, `hide element then show element retains original display` (4 tests). Strategy config plumbing. Expected: +3-4.
12. **[done (+2)] `show` multi-element + display retention** — `show / can show multiple elements with inline-block`, `can filter over a set of elements using the its symbol` (2 tests). Expected: +2. 12. **[done (+2)] `show` multi-element + display retention** — `show / can show multiple elements with inline-block`, `can filter over a set of elements using the its symbol` (2 tests). Expected: +2.
@@ -93,7 +93,7 @@ Orchestrator cherry-picks worktree commits onto `architecture` one at a time; re
21. **[done (+1)] `possessiveExpression` property access via its** — `possessive / can access its properties` (1 test, Expected `foo` got ``). Expected: +1. 21. **[done (+1)] `possessiveExpression` property access via its** — `possessive / can access its properties` (1 test, Expected `foo` got ``). Expected: +1.
22. **[done (+1)] window global fn fallback** — `regressions / can invoke functions w/ numbers in name` + `can refer to function in init blocks`. Added `host-call-fn` FFI primitive (commit 337c8265), `hs-win-call` runtime helper, simplified compiler emit (direct hs-win-call, no guard), `def` now also registers fn on `window[name]`. Generator: fixed `\"` escaping in hs-compile string literals. Expected: +2-4. 22. **[blocked: tried three compile-time emits — (1) guard (can't catch Undefined symbol since it's a host-level error, not an SX raise), (2) env-has? (primitive not loaded in HS kernel — `Unhandled exception: "env-has?"`), and (3) hs-win-call runtime helper (works when reached but SX can't CALL a host-handle function directly — `Not callable: {:__host_handle N}` because NativeFn is not callable here). Needs either a host-call-fn primitive with arity-agnostic dispatch OR a symbol-bound? predicate in the HS kernel.] window global fn fallback** — `regressions / can invoke functions w/ numbers in name` + unlocks several others. When calling `foo()` where `foo` isn't SX-defined, fall back to `(host-global "foo")`. Design decision: either compile-time emit `(or foo (host-global "foo"))` via a helper, or add runtime lookup in the dispatch path. Expected: +2-4.
23. **[done (+1)] `me symbol works in from expressions`** — `regressions` (1 test, Expected `Foo`). Check `from` expression compilation. Expected: +1. 23. **[done (+1)] `me symbol works in from expressions`** — `regressions` (1 test, Expected `Foo`). Check `from` expression compilation. Expected: +1.
@@ -109,21 +109,21 @@ Orchestrator cherry-picks worktree commits onto `architecture` one at a time; re
28. **[done (+4)] `ask`/`answer` + prompt/confirm mock** — `askAnswer` 4 tests. **Requires test-name-keyed mock**: first test wants `confirm → true`, second `confirm → false`, third `prompt → "Alice"`, fourth `prompt → null`. Keyed via `_current-test-name` in the runner. Expected: +4. 28. **[done (+4)] `ask`/`answer` + prompt/confirm mock** — `askAnswer` 4 tests. **Requires test-name-keyed mock**: first test wants `confirm → true`, second `confirm → false`, third `prompt → "Alice"`, fourth `prompt → null`. Keyed via `_current-test-name` in the runner. Expected: +4.
29. **[done (+2) — partial, 4 parser-error tests remain (basic parse error messages, parse-error event, EOF newline crash, evaluate-api-first-error). All require stricter parser error-rejection `add - to` currently parses silently to `(set! nil (hs-add-to! (- 0 nil) nil))`, `on click blargh end on mouseenter also_bad` parses silently to `(do (hs-on me "click" (fn (event) blargh)) (hs-on me "mouseenter" (fn (event) also_bad)))`. Plus emit-error-collection runtime + hyperscript:parse-error event with detail.errors. Larger than a single cluster budget; recommend bucket-D plan-first.] `hyperscript:before:init` / `:after:init` / `:parse-error` events** — 6 tests in `bootstrap` + `parser`. Fire DOM events at activation boundaries. Expected: +4-6. 29. **[blocked: sx-tree MCP tools returning Yojson Type_error on every file op. Can't edit integration.sx to add before:init/after:init dispatch. Also 4 of the 6 tests fundamentally require stricter parser error-rejection (add - to currently succeeds as SX expression; on click blargh end accepts blargh as symbol), which is larger than a single cluster budget.] `hyperscript:before:init` / `:after:init` / `:parse-error` events** — 6 tests in `bootstrap` + `parser`. Fire DOM events at activation boundaries. Expected: +4-6.
30. **[done (+1)] `logAll` config** — 1 test. Global config that console.log's each command. Expected: +1. 30. **[done (+1)] `logAll` config** — 1 test. Global config that console.log's each command. Expected: +1.
### Bucket D: medium features (bigger commits, plan-first) ### Bucket D: medium features (bigger commits, plan-first)
31. **[blocked: Bucket-D plan-first scope, doesn't fit one cluster budget. All 18 tests are SKIP (untranslated) — generator has no `error("HS")` helper. Required pieces: (a) generator-side `eval-hs-error` helper + recognizer for `expect(await error("HS")).toBe("MSG")` blocks; (b) runtime helpers `hs-null-error!` / `hs-named-target` / `hs-named-target-list` raising `'<sel>' is null`; (c) compiler patches at every target-position `(query SEL)` emit to wrap in named-target carrying the original selector source — that's ~17 command emit paths (add, remove, hide, show, measure, settle, trigger, send, set, default, increment, decrement, put, toggle, transition, append, take); (d) function-call null-check at bare `(name)`, `hs-method-call`, and `host-get` chains, deriving the leftmost-uncalled-name `'x'` / `'x.y'` from the parse tree; (e) possessive-base null-check (`set x's y to true``'x' is null`). Each piece is straightforward in isolation but the cross-cutting compiler change touches every emit path and needs a coordinated design pass. Recommend a dedicated design doc + multi-commit worktree like buckets E36-E40.] runtime null-safety error reporting** — 18 tests in `runtimeErrors`. When accessing `.foo` on nil, emit a structured error with position info. One coordinated fix in the compiler emit paths for property access, function calls, set/put. Expected: +15-18. 31. **[pending] runtime null-safety error reporting** — 18 tests in `runtimeErrors`. When accessing `.foo` on nil, emit a structured error with position info. One coordinated fix in the compiler emit paths for property access, function calls, set/put. Expected: +15-18.
32. **[done (+7)] MutationObserver mock + `on mutation` dispatch** — 7 tests in `on`. Add MO mock to runner. Compile `on mutation [of attribute/childList/attribute-specific]`. Expected: +10-15. 32. **[pending] MutationObserver mock + `on mutation` dispatch** — 15 tests in `on`. Add MO mock to runner. Compile `on mutation [of attribute/childList/attribute-specific]`. Expected: +10-15.
33. **[done (+4) — partial, 1 test remains: `iterate cookies values work` needs `hs-for-each` to recognise host-array/proxy collections (currently `(list? collection)` returns false for the JS Proxy so the loop body never runs). Out of scope.] cookie API** — 5 tests in `expressions/cookies`. `document.cookie` mock in runner + `the cookies` + `set the xxx cookie` keywords. Expected: +5. 33. **[pending] cookie API** — 5 tests in `expressions/cookies`. `document.cookie` mock in runner + `the cookies` + `set the xxx cookie` keywords. Expected: +5.
34. **[done (+7) — partial, 1 test remains: `every` keyword multi-handler-execute test needs handler-queue semantics where `wait for X` doesn't block subsequent invocations of the same handler — current `hs-on-every` shares the same dom-listen plumbing as `hs-on` and queues events implicitly via JS event loop, so the third synthetic click waits for the prior handler's `wait for customEvent` to settle. Out of single-cluster scope.] event modifier DSL** — 8 tests in `on`. `elsewhere`, `every`, `first click`, count filters (`once / twice / 3 times`, ranges), `from elsewhere`. Expected: +6-8. 34. **[pending] event modifier DSL** — 8 tests in `on`. `elsewhere`, `every`, `first click`, count filters (`once / twice / 3 times`, ranges), `from elsewhere`. Expected: +6-8.
35. **[done (+3)] namespaced `def`** — 3 tests. `def ns.foo() ...` creates `ns.foo`. Expected: +3. 35. **[pending] namespaced `def`** — 3 tests. `def ns.foo() ...` creates `ns.foo`. Expected: +3.
### Bucket E: subsystems (DO NOT LOOP — human-driven) ### Bucket E: subsystems (DO NOT LOOP — human-driven)
@@ -177,39 +177,6 @@ Many tests are `SKIP (untranslated)` because `tests/playwright/generate-sx-tests
(Reverse chronological — newest at top.) (Reverse chronological — newest at top.)
### 2026-04-25 — Bucket F: in-expression filter semantics (+1)
- **67a5f137** — `HS: in-expression filter semantics (+1 test)`. `1 in [1, 2, 3]` was returning boolean `true` instead of the filtered list `(list 1)`. Root cause: `in?` compiled to `hs-contains?` which returns boolean for scalar items. Fix: (a) `runtime.sx` adds `hs-in?` returning filtered list for all cases, plus `hs-in-bool?` which wraps with `(not (hs-falsy? ...))` for boolean contexts; (b) `compiler.sx` changes `in?` clause to emit `(hs-in? collection item)` and adds new `in-bool?` clause emitting `(hs-in-bool? collection item)`; (c) `parser.sx` changes `is in` and `am in` comparison forms to produce `in-bool?` so those stay boolean. Suite hs-upstream-expressions/in: 8/9 → 9/9. Smoke 0-195: 173/195 unchanged.
### 2026-04-25 — cluster 22 window global fn fallback (+1)
- **d31565d5** — `HS cluster 22: simplify win-call emit + def→window + init-blocks test (+1)`. Two-part change building on 337c8265 (host-call-fn FFI + hs-win-call runtime). (a) `compiler.sx` removes the guard wrapper from bare-call and method-call `hs-win-call` emit paths — direct `(hs-win-call name (list args))` is sufficient since hs-win-call returns nil for unknown names; `def` compilation now also emits `(host-set! (host-global "window") name fn)` so every HS-defined function is reachable via window lookup. (b) `generate-sx-tests.py` fixes a quoting bug: `\"here\"` was being embedded as three SX nodes (`""` + symbol + `""`) instead of a single escaped-quote string; fixed with `\\\"` escaping. Hand-rolled deftest for `can refer to function in init blocks` now passes. Suite hs-upstream-core/regressions: 13/16 → 14/16. Smoke 0-195: 172/195 → 173/195.
### 2026-04-25 — cluster 11/33 followups: hide strategy + cookie clear (+2)
- **5ff2b706** — `HS: cluster 11/33 followups (+2 tests)`. Three orthogonal fixes that pick up tests now unblocked by earlier work. (a) `parser.sx` `parse-hide-cmd`/`parse-show-cmd`: added `on` to the keyword set that flips the implicit-`me` target. Previously `on click 1 hide on click 2 show` silently parsed as `(hs-hide! nil ...)` because `parse-expr` started consuming `on` and returned nil; now hide/show recognise a sibling feature and default to `me`. (b) `runtime.sx` `hs-method-call` fallback for non-built-in methods: SX-callables (lambdas) call via `apply`, JS-native functions (e.g. `cookies.clear`) dispatch via `(apply host-call (cons obj (cons method args)))` so the native receives the args list. (c) Generator `hs-cleanup!` body wrapped in `begin` (fn body evaluates only the last expr) and now resets `hs-set-default-hide-strategy! nil` + `hs-set-log-all! false` between tests — the prior `can set default to custom strategy` cluster-11 test had been leaking `_hs-default-hide-strategy` into the rest of the suite, breaking `hide element then show element retains original display`. New cluster-33 hand-roll for `basic clear cookie values work` exercises the method-call fallback. Suite hs-upstream-hide: 15/16 → 16/16. Suite hs-upstream-expressions/cookies: 3/5 → 4/5. Smoke 0-195 unchanged at 172/195.
### 2026-04-25 — cluster 35 namespaced def + script-tag globals (+3)
- **122053ed** — `HS: namespaced def + script-tag global functions (+3 tests)`. Two-part change: (a) `runtime.sx` `hs-method-call` gains a fallback for unknown methods — `(let ((fn-val (host-get obj method))) (if (callable? fn-val) (apply fn-val args) nil))`. This lets `utils.foo()` dispatch through `(host-get utils "foo")` when `utils` is an SX dict whose `foo` is an SX lambda. (b) Generator hand-rolls 3 deftests since the SX runtime has no `<script type='text/hyperscript'>` tag boot. For `is called synchronously` / `can call asynchronously`: `(eval-expr-cek (hs-to-sx (first (hs-parse (hs-tokenize "def foo() ... end")))))` registers the function in the global eval env (eval-expr-cek processes `(define foo (fn ...))` at top scope), then a click div is built via dom-set-attr + hs-boot-subtree!. For `functions can be namespaced`: define `utils` as a dict, register `__utils_foo` as a fresh-named global def, then `(host-set! utils "foo" __utils_foo)` populates the dict; click handler `call utils.foo()` compiles to `(hs-method-call utils "foo")` which now dispatches through the new runtime fallback. Skip-list cleared of the 3 def entries. Suite hs-upstream-def: 24/27 → 27/27. Smoke 0-195 unchanged at 172/195.
### 2026-04-25 — cluster 34 elsewhere / from-elsewhere modifier (+2)
- **3044a168** — `HS: elsewhere / from elsewhere modifier (+2 tests)`. Three-part change: (a) `parser.sx` `parse-on-feat` parses an optional `elsewhere` (or `from elsewhere`) modifier between event-name and source. The `from elsewhere` variant uses a one-token lookahead so plain `from #target` keeps parsing as a source expression. Emits `:elsewhere true` part. (b) `compiler.sx` `scan-on` threads `elsewhere?` (10th param) through every recursive call + new `:elsewhere` cond branch. The dispatch case becomes a 3-way `cond` over target: elsewhere → `(dom-body)` (listener attaches to body and bubble sees every click), source → from-source, default → `me`. The `compiled-body` build is wrapped with `(when (not (host-call me "contains" (host-get event "target"))) BODY)` so handlers fire only on outside-of-`me` clicks. (c) Generator drops `supports "elsewhere" modifier` and `supports "from elsewhere" modifier` from `SKIP_TEST_NAMES`. Suite hs-upstream-on: 48/70 → 50/70. Smoke 0-195 unchanged at 172/195.
### 2026-04-25 — cluster 34 count-filtered events + first modifier (+5 partial)
- **19c97989** — `HS: count-filtered events + first modifier (+5 tests)`. Three-part change: (a) `parser.sx` `parse-on-feat` accepts `first` keyword before event-name (sets `cnt-min/max=1`), then optionally parses a count expression after event-name: bare number = exact count, `N to M` = inclusive range, `N and on` = unbounded above. Number tokens coerced via `parse-number`. New parts entry `:count-filter {"min" N "max" M-or--1}`. (b) `compiler.sx` `scan-on` gains a 9th `count-filter-info` param threaded through every recursive call + a new `:count-filter` cond branch. The handler binding now wraps the `(fn (event) BODY)` in `(let ((__hs-count 0)) (fn (event) (begin (set! __hs-count (+ __hs-count 1)) (when COUNT-CHECK BODY))))` when count info is present. Each `on EVENT N ...` clause produces its own closure-captured counter, so `on click 1` / `on click 2` / `on click 3` fire on their respective Nth click (mix-ranges test). (c) Generator drops 5 entries from `SKIP_TEST_NAMES``can filter events based on count`/`...count range`/`...unbounded count range`/`can mix ranges`/`on first click fires only once`. Suite hs-upstream-on: 43/70 → 48/70. Smoke 0-195 unchanged at 172/195. Remaining cluster-34 work (`elsewhere`/`from elsewhere`/`every`-keyword multi-handler) is independent from count filters and would need a separate iteration.
### 2026-04-25 — cluster 29 hyperscript init events (+2 partial)
- **e01a3baa** — `HS: hyperscript:before:init / :after:init events (+2 tests)`. `integration.sx` `hs-activate!` now wraps the activation block in `(when (dom-dispatch el "hyperscript:before:init" nil) ...)``dom-dispatch` builds a CustomEvent with `bubbles:true`, the mock El's `cancelable` defaults to true, `dispatchEvent` returns `!ev.defaultPrevented`, so `when` skips the activate body if a listener called `preventDefault()`. After activation completes successfully it dispatches `hyperscript:after:init`. Generator (`tests/playwright/generate-sx-tests.py`) gains two hand-rolled deftests: `fires hyperscript:before:init and hyperscript:after:init` builds a wa container, attaches listeners that append to a captured `events` list, sets innerHTML to a div with `_=`, calls `hs-boot-subtree!`, asserts the events list. `hyperscript:before:init can cancel initialization` attaches a preventDefault listener and asserts `data-hyperscript-powered` is absent on the inner div after boot. Suite hs-upstream-core/bootstrap: 20/26 → 22/26. Smoke 0-195: 170 → 172. Remaining 4 cluster-29 tests (basic parse error messages, parse-error event, EOF newline, eval-API throws on first error) all need stricter parser error-rejection plus a parse-error collector — recommend bucket-D plan-first multi-commit, not a single iteration.
### 2026-04-25 — cluster 32 MutationObserver mock + on mutation dispatch (+7)
- **13e02542** — `HS: MutationObserver mock + on mutation dispatch (+7 tests)`. Five-part change: (a) `parser.sx` `parse-on-feat` now consumes `of <FILTER>` after `mutation` event-name. FILTER is one of `attributes`/`childList`/`characterData` (ident tokens) or one or more `@name` attr-tokens chained by `or`. Emits `:of-filter {"type" T "attrs" L?}` part. (b) `compiler.sx` `scan-on` threads new `of-filter-info` param; the dispatch case becomes a `cond` over `event-name` — for `"mutation"` it emits `(do on-call (hs-on-mutation-attach! target MODE ATTRS))` where ATTRS is `(cons 'list attr-list)` so the list survives compile→eval. (c) `runtime.sx` `hs-on-mutation-attach!` builds a config dict (`attributes`/`childList`/`characterData`/`subtree`/`attributeFilter`) matched to mode, constructs a real `MutationObserver(cb)`, calls `mo.observe(target, opts)`, and the cb dispatches a `"mutation"` event on target. (d) `tests/hs-run-filtered.js` replaces the no-op MO with `HsMutationObserver` (global registry, decodes SX-list `attributeFilter`); prototype hooks on `El.setAttribute/appendChild/removeChild/_setInnerHTML` fire matching observers synchronously, with `__hsMutationActive` re-entry guard so handlers that mutate the DOM don't infinite-loop. Per-test reset clears registry + flag. (e) `generate-sx-tests.py` drops 7 mutation entries from `SKIP_TEST_NAMES` and adds two body patterns: `evaluate(() => document.querySelector(SEL).setAttribute(N,V))``(dom-set-attr ...)`, and `evaluate(() => document.querySelector(SEL).appendChild(document.createElement(T)))``(dom-append … (dom-create-element …))`. Suite hs-upstream-on: 36/70 → 43/70. Smoke 0-195 unchanged at 170/195.
### 2026-04-25 — cluster 33 cookie API (partial +3)
- No `.sx` edits needed — `set cookies.foo to 'bar'` already compiles to `(dom-set-prop cookies "foo" "bar")` which becomes `(host-set! cookies "foo" "bar")` once the `dom` module is loaded, and `cookies.foo` becomes `(host-get cookies "foo")`. So a JS-only Proxy + Python generator change does the trick. Two parts: (a) `tests/hs-run-filtered.js` adds a per-test `__hsCookieStore` Map, a `globalThis.cookies` Proxy with `length`/`clear`/named-key get traps and a set trap that writes the store, and a `Object.defineProperty(document, 'cookie', …)` getter/setter that reads and writes the same store (so the upstream `length is 0` test's pre-clear loop over `document.cookie` works). Per-test reset clears the store. (b) `tests/playwright/generate-sx-tests.py` declares `(define cookies (host-global "cookies"))` in the test header and emits hand-rolled deftests for the three tractable tests (`basic set`, `update`, `length is 0`). Suite hs-upstream-expressions/cookies: 0/5 → 3/5. Smoke 0-195 unchanged at 170/195. Remaining `basic clear` and `iterate` tests need runtime.sx edits (hs-method-call fallback + hs-for-each host-array recognition) — out of scope for a JS-only iteration.
### 2026-04-25 — cluster 32 MutationObserver mock + on mutation dispatch (blocked)
- Two issues conspire: (1) `loops/hs` worktree has no pre-built sx-tree binary so MCP tools aren't loaded, and the block-sx-edit hook prevents raw `Edit`/`Read`/`Write` on `.sx` files. Built `hosts/ocaml/_build/default/bin/mcp_tree.exe` via `dune build` this iteration but tools don't surface mid-session. (2) Cluster scope is genuinely big: parser must learn `on mutation of <filter>` (currently drops body after `of` — verified via compile dump: `on mutation of attributes put "Mutated" into me``(hs-on me "mutation" (fn (event) nil))`), compiler needs `:of-filter` plumbing similar to intersection's `:having`, runtime needs `hs-on-mutation-attach!`, JS runner mock needs a real MutationObserver (currently no-op `class{observe(){}disconnect(){}}` at hs-run-filtered.js:348) plus `setAttribute`/`appendChild` instrumentation, and 7 entries removed from `SKIP_TEST_NAMES`. Recommended next step: dedicated worktree where sx-tree loads at session start, multi-commit shape (parser → compiler+attach → mock+runner → generator skip-list).
### 2026-04-25 — cluster 31 runtime null-safety error reporting (blocked)
- All 18 tests are `SKIP (untranslated)` — generator has no `error("HS")` helper at all. Inspected representative compile outputs: `add .foo to #doesntExist``(for-each ... (hs-query-all "#doesntExist"))` (silently no-ops on empty list, no error); `hide #doesntExist``(hs-hide! (hs-query-all "#doesntExist") "display")` (likewise); `put 'foo' into #doesntExist``(hs-set-inner-html! (hs-query-first "#doesntExist") "foo")` (passes nil through); `x()``(x)` (raises `Undefined symbol: x`, wrong format); `x.y.z()``(hs-method-call (host-get x "y") "z")`. Implementing this requires generator helper + 17 compiler emit-path patches + function-call/method-call/possessive-base null guards + new `hs-named-target`/`hs-named-target-list` runtime — too many surfaces for a single-iteration commit. Bucket D explicitly says "plan-first" — recommended path is a dedicated design doc and multi-commit worktree like E36-E40, not a loop iteration.
### 2026-04-24 — cluster 29 hyperscript:before:init / :after:init / :parse-error (blocked) ### 2026-04-24 — cluster 29 hyperscript:before:init / :after:init / :parse-error (blocked)
- **2b486976** — `HS-plan: mark cluster 29 blocked`. sx-tree MCP file ops returning `Yojson__Safe.Util.Type_error("Expected string, got null")` on every file-based call (sx_read_subtree, sx_find_all, sx_replace_by_pattern, sx_summarise, sx_pretty_print, sx_write_file). Only in-memory ops work (sx_eval, sx_build, sx_env). Without sx-tree I can't edit integration.sx to add before:init/after:init dispatch on hs-activate!. Investigated the 6 tests: 2 bootstrap (before/after init) need dispatchEvent wrapping activate; 4 parser tests require stricter parser error-rejection — `add - to` currently parses silently to `(set! nil (hs-add-to! (- 0 nil) nil))`, `on click blargh end on mouseenter also_bad` parses silently to `(do (hs-on me "click" (fn (event) blargh)) (hs-on me "mouseenter" (fn (event) also_bad)))`. Fundamental parser refactor is out of single-cluster budget regardless of sx-tree availability. - **2b486976** — `HS-plan: mark cluster 29 blocked`. sx-tree MCP file ops returning `Yojson__Safe.Util.Type_error("Expected string, got null")` on every file-based call (sx_read_subtree, sx_find_all, sx_replace_by_pattern, sx_summarise, sx_pretty_print, sx_write_file). Only in-memory ops work (sx_eval, sx_build, sx_env). Without sx-tree I can't edit integration.sx to add before:init/after:init dispatch on hs-activate!. Investigated the 6 tests: 2 bootstrap (before/after init) need dispatchEvent wrapping activate; 4 parser tests require stricter parser error-rejection — `add - to` currently parses silently to `(set! nil (hs-add-to! (- 0 nil) nil))`, `on click blargh end on mouseenter also_bad` parses silently to `(do (hs-on me "click" (fn (event) blargh)) (hs-on me "mouseenter" (fn (event) also_bad)))`. Fundamental parser refactor is out of single-cluster budget regardless of sx-tree availability.

View File

@@ -164,16 +164,13 @@
every? every?
catch-info catch-info
finally-info finally-info
having-info having-info)
of-filter-info
count-filter-info
elsewhere?)
(cond (cond
((<= (len items) 1) ((<= (len items) 1)
(let (let
((body (if (> (len items) 0) (first items) nil))) ((body (if (> (len items) 0) (first items) nil)))
(let (let
((target (cond (elsewhere? (list (quote dom-body))) (source (hs-to-sx source)) (true (quote me))))) ((target (if source (hs-to-sx source) (quote me))))
(let (let
((event-refs (if (and (list? body) (= (first body) (quote do))) (filter (fn (x) (and (list? x) (= (first x) (quote ref)))) (rest body)) (list)))) ((event-refs (if (and (list? body) (= (first body) (quote do))) (filter (fn (x) (and (list? x) (= (first x) (quote ref)))) (rest body)) (list))))
(let (let
@@ -181,41 +178,20 @@
(let (let
((raw-compiled (hs-to-sx stripped-body))) ((raw-compiled (hs-to-sx stripped-body)))
(let (let
((compiled-body (let ((base (if (> (len event-refs) 0) (let ((bindings (map (fn (r) (let ((name (nth r 1))) (list (make-symbol name) (list (quote host-get) (list (quote host-get) (quote event) "detail") name)))) event-refs))) (list (quote let) bindings raw-compiled)) raw-compiled))) (if elsewhere? (list (quote when) (list (quote not) (list (quote host-call) (quote me) "contains" (list (quote host-get) (quote event) "target"))) base) base)))) ((compiled-body (if (> (len event-refs) 0) (let ((bindings (map (fn (r) (let ((name (nth r 1))) (list (make-symbol name) (list (quote host-get) (list (quote host-get) (quote event) "detail") name)))) event-refs))) (list (quote let) bindings raw-compiled)) raw-compiled)))
(let (let
((wrapped-body (if catch-info (let ((var (make-symbol (nth catch-info 0))) (catch-body (hs-to-sx (nth catch-info 1)))) (if finally-info (list (quote do) (list (quote guard) (list var (list true catch-body)) compiled-body) (hs-to-sx finally-info)) (list (quote guard) (list var (list true catch-body)) compiled-body))) (if finally-info (list (quote do) compiled-body (hs-to-sx finally-info)) compiled-body)))) ((wrapped-body (if catch-info (let ((var (make-symbol (nth catch-info 0))) (catch-body (hs-to-sx (nth catch-info 1)))) (if finally-info (list (quote do) (list (quote guard) (list var (list true catch-body)) compiled-body) (hs-to-sx finally-info)) (list (quote guard) (list var (list true catch-body)) compiled-body))) (if finally-info (list (quote do) compiled-body (hs-to-sx finally-info)) compiled-body))))
(let (let
((handler (let ((uses-the-result? (fn (expr) (cond ((= expr (quote the-result)) true) ((list? expr) (some (fn (x) (uses-the-result? x)) expr)) (true false))))) (let ((base-handler (list (quote fn) (list (quote event)) (if (uses-the-result? wrapped-body) (list (quote let) (list (list (quote the-result) nil)) wrapped-body) wrapped-body)))) (if count-filter-info (let ((mn (get count-filter-info "min")) (mx (get count-filter-info "max"))) (list (quote let) (list (list (quote __hs-count) 0)) (list (quote fn) (list (quote event)) (list (quote begin) (list (quote set!) (quote __hs-count) (list (quote +) (quote __hs-count) 1)) (list (quote when) (if (= mx -1) (list (quote >=) (quote __hs-count) mn) (list (quote and) (list (quote >=) (quote __hs-count) mn) (list (quote <=) (quote __hs-count) mx))) (nth base-handler 2)))))) base-handler))))) ((handler (let ((uses-the-result? (fn (expr) (cond ((= expr (quote the-result)) true) ((list? expr) (some (fn (x) (uses-the-result? x)) expr)) (true false))))) (list (quote fn) (list (quote event)) (if (uses-the-result? wrapped-body) (list (quote let) (list (list (quote the-result) nil)) wrapped-body) wrapped-body)))))
(let (let
((on-call (if every? (list (quote hs-on-every) target event-name handler) (list (quote hs-on) target event-name handler)))) ((on-call (if every? (list (quote hs-on-every) target event-name handler) (list (quote hs-on) target event-name handler))))
(cond (if
((= event-name "mutation") (= event-name "intersection")
(list (list
(quote do) (quote do)
on-call on-call
(list (list
(quote hs-on-mutation-attach!) (quote hs-on-intersection-attach!)
target
(if
of-filter-info
(get of-filter-info "type")
"any")
(if
of-filter-info
(let
((a (get of-filter-info "attrs")))
(if
a
(cons (quote list) a)
nil))
nil))))
((= event-name "intersection")
(list
(quote do)
on-call
(list
(quote
hs-on-intersection-attach!)
target target
(if (if
having-info having-info
@@ -224,8 +200,8 @@
(if (if
having-info having-info
(get having-info "threshold") (get having-info "threshold")
nil)))) nil)))
(true on-call)))))))))))) on-call)))))))))))
((= (first items) :from) ((= (first items) :from)
(scan-on (scan-on
(rest (rest items)) (rest (rest items))
@@ -234,10 +210,7 @@
every? every?
catch-info catch-info
finally-info finally-info
having-info having-info))
of-filter-info
count-filter-info
elsewhere?))
((= (first items) :filter) ((= (first items) :filter)
(scan-on (scan-on
(rest (rest items)) (rest (rest items))
@@ -246,10 +219,7 @@
every? every?
catch-info catch-info
finally-info finally-info
having-info having-info))
of-filter-info
count-filter-info
elsewhere?))
((= (first items) :every) ((= (first items) :every)
(scan-on (scan-on
(rest (rest items)) (rest (rest items))
@@ -258,10 +228,7 @@
true true
catch-info catch-info
finally-info finally-info
having-info having-info))
of-filter-info
count-filter-info
elsewhere?))
((= (first items) :catch) ((= (first items) :catch)
(scan-on (scan-on
(rest (rest items)) (rest (rest items))
@@ -270,10 +237,7 @@
every? every?
(nth items 1) (nth items 1)
finally-info finally-info
having-info having-info))
of-filter-info
count-filter-info
elsewhere?))
((= (first items) :finally) ((= (first items) :finally)
(scan-on (scan-on
(rest (rest items)) (rest (rest items))
@@ -282,10 +246,7 @@
every? every?
catch-info catch-info
(nth items 1) (nth items 1)
having-info having-info))
of-filter-info
count-filter-info
elsewhere?))
((= (first items) :having) ((= (first items) :having)
(scan-on (scan-on
(rest (rest items)) (rest (rest items))
@@ -294,45 +255,6 @@
every? every?
catch-info catch-info
finally-info finally-info
(nth items 1)
of-filter-info
count-filter-info
elsewhere?))
((= (first items) :of-filter)
(scan-on
(rest (rest items))
source
filter
every?
catch-info
finally-info
having-info
(nth items 1)
count-filter-info
elsewhere?))
((= (first items) :count-filter)
(scan-on
(rest (rest items))
source
filter
every?
catch-info
finally-info
having-info
of-filter-info
(nth items 1)
elsewhere?))
((= (first items) :elsewhere)
(scan-on
(rest (rest items))
source
filter
every?
catch-info
finally-info
having-info
of-filter-info
count-filter-info
(nth items 1))) (nth items 1)))
(true (true
(scan-on (scan-on
@@ -342,11 +264,8 @@
every? every?
catch-info catch-info
finally-info finally-info
having-info having-info)))))
of-filter-info (scan-on (rest parts) nil nil false nil nil nil)))))
count-filter-info
elsewhere?)))))
(scan-on (rest parts) nil nil false nil nil nil nil nil false)))))
(define (define
emit-send emit-send
(fn (fn
@@ -1058,17 +977,9 @@
(cons (cons
(quote hs-method-call) (quote hs-method-call)
(cons obj (cons method args)))) (cons obj (cons method args))))
(if
(and
(list? dot-node)
(= (first dot-node) (quote ref)))
(list
(quote hs-win-call)
(nth dot-node 1)
(cons (quote list) args))
(cons (cons
(quote hs-method-call) (quote hs-method-call)
(cons (hs-to-sx dot-node) args)))))) (cons (hs-to-sx dot-node) args)))))
((= head (quote string-postfix)) ((= head (quote string-postfix))
(list (quote str) (hs-to-sx (nth ast 1)) (nth ast 2))) (list (quote str) (hs-to-sx (nth ast 1)) (nth ast 2)))
((= head (quote block-literal)) ((= head (quote block-literal))
@@ -1238,12 +1149,7 @@
(list (quote hs-coerce) (hs-to-sx (nth ast 1)) (nth ast 2))) (list (quote hs-coerce) (hs-to-sx (nth ast 1)) (nth ast 2)))
((= head (quote in?)) ((= head (quote in?))
(list (list
(quote hs-in?) (quote hs-contains?)
(hs-to-sx (nth ast 2))
(hs-to-sx (nth ast 1))))
((= head (quote in-bool?))
(list
(quote hs-in-bool?)
(hs-to-sx (nth ast 2)) (hs-to-sx (nth ast 2))
(hs-to-sx (nth ast 1)))) (hs-to-sx (nth ast 1))))
((= head (quote of)) ((= head (quote of))
@@ -1727,19 +1633,7 @@
body))) body)))
(nth compiled (- (len compiled) 1)) (nth compiled (- (len compiled) 1))
(rest (reverse compiled))) (rest (reverse compiled)))
(let (cons (quote do) compiled)))))
((defs (filter (fn (c) (and (list? c) (> (len c) 0) (= (first c) (quote define)))) compiled))
(non-defs
(filter
(fn
(c)
(not
(and
(list? c)
(> (len c) 0)
(= (first c) (quote define)))))
compiled)))
(cons (quote do) (append defs non-defs)))))))
((= head (quote wait)) (list (quote hs-wait) (nth ast 1))) ((= head (quote wait)) (list (quote hs-wait) (nth ast 1)))
((= head (quote wait-for)) (emit-wait-for ast)) ((= head (quote wait-for)) (emit-wait-for ast))
((= head (quote log)) ((= head (quote log))
@@ -1847,13 +1741,7 @@
(make-symbol raw-fn) (make-symbol raw-fn)
(hs-to-sx raw-fn))) (hs-to-sx raw-fn)))
(args (map hs-to-sx (rest (rest ast))))) (args (map hs-to-sx (rest (rest ast)))))
(if (cons fn-expr args)))
(and (list? raw-fn) (= (first raw-fn) (quote ref)))
(list
(quote hs-win-call)
(nth raw-fn 1)
(cons (quote list) args))
(cons fn-expr args))))
((= head (quote return)) ((= head (quote return))
(let (let
((val (nth ast 1))) ((val (nth ast 1)))
@@ -2040,11 +1928,6 @@
(list (list
(quote define) (quote define)
(make-symbol (nth ast 1)) (make-symbol (nth ast 1))
(list
(quote let)
(list
(list
(quote _hs-def-val)
(list (list
(quote fn) (quote fn)
params params
@@ -2065,15 +1948,7 @@
"hs-return")) "hs-return"))
(list (quote nth) (quote _e) 1) (list (quote nth) (quote _e) 1)
(list (quote raise) (quote _e))))) (list (quote raise) (quote _e)))))
body)))) body)))))
(list
(quote do)
(list
(quote host-set!)
(list (quote host-global) "window")
(nth ast 1)
(quote _hs-def-val))
(quote _hs-def-val))))))
((= head (quote behavior)) (emit-behavior ast)) ((= head (quote behavior)) (emit-behavior ast))
((= head (quote sx-eval)) ((= head (quote sx-eval))
(let (let
@@ -2123,7 +1998,7 @@
(hs-to-sx (nth ast 1))))) (hs-to-sx (nth ast 1)))))
((= head (quote in?)) ((= head (quote in?))
(list (list
(quote hs-in?) (quote hs-contains?)
(hs-to-sx (nth ast 2)) (hs-to-sx (nth ast 2))
(hs-to-sx (nth ast 1)))) (hs-to-sx (nth ast 1))))
((= head (quote type-check)) ((= head (quote type-check))

View File

@@ -80,14 +80,11 @@
((src (dom-get-attr el "_")) (prev (dom-get-data el "hs-script"))) ((src (dom-get-attr el "_")) (prev (dom-get-data el "hs-script")))
(when (when
(and src (not (= src prev))) (and src (not (= src prev)))
(when
(dom-dispatch el "hyperscript:before:init" nil)
(hs-log-event! "hyperscript:init") (hs-log-event! "hyperscript:init")
(dom-set-data el "hs-script" src) (dom-set-data el "hs-script" src)
(dom-set-data el "hs-active" true) (dom-set-data el "hs-active" true)
(dom-set-attr el "data-hyperscript-powered" "true") (dom-set-attr el "data-hyperscript-powered" "true")
(let ((handler (hs-handler src))) (handler el)) (let ((handler (hs-handler src))) (handler el))))))
(dom-dispatch el "hyperscript:after:init" nil))))))
;; ── Boot: scan entire document ────────────────────────────────── ;; ── Boot: scan entire document ──────────────────────────────────
;; Called once at page load. Finds all elements with _ attribute, ;; Called once at page load. Finds all elements with _ attribute,

View File

@@ -495,8 +495,7 @@
(quote and) (quote and)
(list (quote >=) left lo) (list (quote >=) left lo)
(list (quote <=) left hi))))) (list (quote <=) left hi)))))
((match-kw "in") ((match-kw "in") (list (quote in?) left (parse-expr)))
(list (quote in-bool?) left (parse-expr)))
((match-kw "really") ((match-kw "really")
(do (do
(match-kw "equal") (match-kw "equal")
@@ -572,8 +571,7 @@
(let (let
((right (parse-expr))) ((right (parse-expr)))
(list (quote not) (list (quote =) left right)))))) (list (quote not) (list (quote =) left right))))))
((match-kw "in") ((match-kw "in") (list (quote in?) left (parse-expr)))
(list (quote in-bool?) left (parse-expr)))
((match-kw "empty") (list (quote empty?) left)) ((match-kw "empty") (list (quote empty?) left))
((match-kw "between") ((match-kw "between")
(let (let
@@ -1557,7 +1555,7 @@
(fn (fn
() ()
(let (let
((tgt (cond ((at-end?) (list (quote me))) ((and (= (tp-type) "keyword") (or (= (tp-val) "then") (= (tp-val) "end") (= (tp-val) "with") (= (tp-val) "when") (= (tp-val) "add") (= (tp-val) "remove") (= (tp-val) "set") (= (tp-val) "put") (= (tp-val) "toggle") (= (tp-val) "hide") (= (tp-val) "show") (= (tp-val) "on"))) (list (quote me))) (true (parse-expr))))) ((tgt (cond ((at-end?) (list (quote me))) ((and (= (tp-type) "keyword") (or (= (tp-val) "then") (= (tp-val) "end") (= (tp-val) "with") (= (tp-val) "when") (= (tp-val) "add") (= (tp-val) "remove") (= (tp-val) "set") (= (tp-val) "put") (= (tp-val) "toggle") (= (tp-val) "hide") (= (tp-val) "show"))) (list (quote me))) (true (parse-expr)))))
(let (let
((strategy (if (match-kw "with") (if (at-end?) "display" (let ((s (tp-val))) (do (adv!) (cond ((at-end?) s) ((= (tp-type) "colon") (do (adv!) (let ((v (tp-val))) (do (adv!) (str s ":" v))))) ((= (tp-type) "local") (let ((v (tp-val))) (do (adv!) (str s ":" v)))) (true s))))) "display"))) ((strategy (if (match-kw "with") (if (at-end?) "display" (let ((s (tp-val))) (do (adv!) (cond ((at-end?) s) ((= (tp-type) "colon") (do (adv!) (let ((v (tp-val))) (do (adv!) (str s ":" v))))) ((= (tp-type) "local") (let ((v (tp-val))) (do (adv!) (str s ":" v)))) (true s))))) "display")))
(let (let
@@ -1568,7 +1566,7 @@
(fn (fn
() ()
(let (let
((tgt (cond ((at-end?) (list (quote me))) ((and (= (tp-type) "keyword") (or (= (tp-val) "then") (= (tp-val) "end") (= (tp-val) "with") (= (tp-val) "when") (= (tp-val) "add") (= (tp-val) "remove") (= (tp-val) "set") (= (tp-val) "put") (= (tp-val) "toggle") (= (tp-val) "hide") (= (tp-val) "show") (= (tp-val) "on"))) (list (quote me))) (true (parse-expr))))) ((tgt (cond ((at-end?) (list (quote me))) ((and (= (tp-type) "keyword") (or (= (tp-val) "then") (= (tp-val) "end") (= (tp-val) "with") (= (tp-val) "when") (= (tp-val) "add") (= (tp-val) "remove") (= (tp-val) "set") (= (tp-val) "put") (= (tp-val) "toggle") (= (tp-val) "hide") (= (tp-val) "show"))) (list (quote me))) (true (parse-expr)))))
(let (let
((strategy (if (match-kw "with") (if (at-end?) "display" (let ((s (tp-val))) (do (adv!) (cond ((at-end?) s) ((= (tp-type) "colon") (do (adv!) (let ((v (tp-val))) (do (adv!) (str s ":" v))))) ((= (tp-type) "local") (let ((v (tp-val))) (do (adv!) (str s ":" v)))) (true s))))) "display"))) ((strategy (if (match-kw "with") (if (at-end?) "display" (let ((s (tp-val))) (do (adv!) (cond ((at-end?) s) ((= (tp-type) "colon") (do (adv!) (let ((v (tp-val))) (do (adv!) (str s ":" v))))) ((= (tp-type) "local") (let ((v (tp-val))) (do (adv!) (str s ":" v)))) (true s))))) "display")))
(let (let
@@ -2603,18 +2601,13 @@
(fn (fn
() ()
(let (let
((every? (match-kw "every")) (first? (match-kw "first"))) ((every? (match-kw "every")))
(let (let
((event-name (parse-compound-event-name))) ((event-name (parse-compound-event-name)))
(let
((count-filter (let ((mn nil) (mx nil)) (when first? (do (set! mn 1) (set! mx 1))) (when (= (tp-type) "number") (let ((n (parse-number (tp-val)))) (do (adv!) (set! mn n) (cond ((match-kw "to") (cond ((= (tp-type) "number") (let ((mv (parse-number (tp-val)))) (do (adv!) (set! mx mv)))) (true (set! mx n)))) ((match-kw "and") (cond ((match-kw "on") (set! mx -1)) (true (set! mx n)))) (true (set! mx n)))))) (if mn (dict "min" mn "max" mx) nil))))
(let
((of-filter (when (and (= event-name "mutation") (match-kw "of")) (cond ((and (= (tp-type) "ident") (or (= (tp-val) "attributes") (= (tp-val) "childList") (= (tp-val) "characterData"))) (let ((nm (tp-val))) (do (adv!) (dict "type" nm)))) ((= (tp-type) "attr") (let ((attrs (list (tp-val)))) (do (adv!) (define collect-or! (fn () (when (match-kw "or") (cond ((= (tp-type) "attr") (do (set! attrs (append attrs (list (tp-val)))) (adv!) (collect-or!))) (true (set! p (- p 1))))))) (collect-or!) (dict "type" "attrs" "attrs" attrs)))) (true nil)))))
(let (let
((flt (if (= (tp-type) "bracket-open") (do (adv!) (let ((f (parse-expr))) (if (= (tp-type) "bracket-close") (adv!) nil) f)) nil))) ((flt (if (= (tp-type) "bracket-open") (do (adv!) (let ((f (parse-expr))) (if (= (tp-type) "bracket-close") (adv!) nil) f)) nil)))
(let (let
((elsewhere? (cond ((match-kw "elsewhere") true) ((and (= (tp-type) "keyword") (= (tp-val) "from") (let ((nxt (if (< (+ p 1) tok-len) (nth tokens (+ p 1)) nil))) (and nxt (= (get nxt "type") "keyword") (= (get nxt "value") "elsewhere")))) (do (adv!) (adv!) true)) (true false))) ((source (if (match-kw "from") (parse-expr) nil)))
(source (if (match-kw "from") (parse-expr) nil)))
(let (let
((h-margin nil) (h-threshold nil)) ((h-margin nil) (h-threshold nil))
(define (define
@@ -2646,10 +2639,7 @@
(let (let
((catch-clause (if (match-kw "catch") (let ((var (let ((v (tp-val))) (adv!) v)) (handler (parse-cmd-list))) (list var handler)) nil)) ((catch-clause (if (match-kw "catch") (let ((var (let ((v (tp-val))) (adv!) v)) (handler (parse-cmd-list))) (list var handler)) nil))
(finally-clause (finally-clause
(if (if (match-kw "finally") (parse-cmd-list) nil)))
(match-kw "finally")
(parse-cmd-list)
nil)))
(match-kw "end") (match-kw "end")
(let (let
((parts (list (quote on) event-name))) ((parts (list (quote on) event-name)))
@@ -2657,14 +2647,8 @@
((parts (if every? (append parts (list :every true)) parts))) ((parts (if every? (append parts (list :every true)) parts)))
(let (let
((parts (if flt (append parts (list :filter flt)) parts))) ((parts (if flt (append parts (list :filter flt)) parts)))
(let
((parts (if elsewhere? (append parts (list :elsewhere true)) parts)))
(let (let
((parts (if source (append parts (list :from source)) parts))) ((parts (if source (append parts (list :from source)) parts)))
(let
((parts (if count-filter (append parts (list :count-filter count-filter)) parts)))
(let
((parts (if of-filter (append parts (list :of-filter of-filter)) parts)))
(let (let
((parts (if having (append parts (list :having having)) parts))) ((parts (if having (append parts (list :having having)) parts)))
(let (let
@@ -2673,7 +2657,7 @@
((parts (if finally-clause (append parts (list :finally finally-clause)) parts))) ((parts (if finally-clause (append parts (list :finally finally-clause)) parts)))
(let (let
((parts (append parts (list body)))) ((parts (append parts (list body))))
parts))))))))))))))))))))))) parts))))))))))))))))))
(define (define
parse-init-feat parse-init-feat
(fn (fn

View File

@@ -82,36 +82,14 @@
observer))))) observer)))))
;; Wait for CSS transitions/animations to settle on an element. ;; Wait for CSS transitions/animations to settle on an element.
(define (define hs-init (fn (thunk) (thunk)))
hs-on-mutation-attach!
(fn
(target mode attr-list)
(let
((cfg-attributes (or (= mode "any") (= mode "attributes") (= mode "attrs")))
(cfg-childList (or (= mode "any") (= mode "childList")))
(cfg-characterData (or (= mode "any") (= mode "characterData"))))
(let
((opts (dict "attributes" cfg-attributes "childList" cfg-childList "characterData" cfg-characterData "subtree" true)))
(when
(and (= mode "attrs") attr-list)
(dict-set! opts "attributeFilter" attr-list))
(let
((cb (fn (records observer) (dom-dispatch target "mutation" (dict "records" records)))))
(let
((observer (host-new "MutationObserver" cb)))
(host-call observer "observe" target opts)
observer))))))
;; ── Class manipulation ────────────────────────────────────────── ;; ── Class manipulation ──────────────────────────────────────────
;; Toggle a single class on an element. ;; Toggle a single class on an element.
(define hs-init (fn (thunk) (thunk)))
;; Toggle between two classes — exactly one is active at a time.
(define hs-wait (fn (ms) (perform (list (quote io-sleep) ms)))) (define hs-wait (fn (ms) (perform (list (quote io-sleep) ms))))
;; Take a class from siblings — add to target, remove from others. ;; Toggle between two classes — exactly one is active at a time.
;; (hs-take! target cls) — like radio button class behavior
(begin (begin
(define (define
hs-wait-for hs-wait-for
@@ -124,20 +102,21 @@
(target event-name timeout-ms) (target event-name timeout-ms)
(perform (list (quote io-wait-event) target event-name timeout-ms))))) (perform (list (quote io-wait-event) target event-name timeout-ms)))))
;; Take a class from siblings — add to target, remove from others.
;; (hs-take! target cls) — like radio button class behavior
(define hs-settle (fn (target) (perform (list (quote io-settle) target))))
;; ── DOM insertion ─────────────────────────────────────────────── ;; ── DOM insertion ───────────────────────────────────────────────
;; Put content at a position relative to a target. ;; Put content at a position relative to a target.
;; pos: "into" | "before" | "after" ;; pos: "into" | "before" | "after"
(define hs-settle (fn (target) (perform (list (quote io-settle) target))))
;; ── Navigation / traversal ──────────────────────────────────────
;; Navigate to a URL.
(define (define
hs-toggle-class! hs-toggle-class!
(fn (target cls) (host-call (host-get target "classList") "toggle" cls))) (fn (target cls) (host-call (host-get target "classList") "toggle" cls)))
;; Find next sibling matching a selector (or any sibling). ;; ── Navigation / traversal ──────────────────────────────────────
;; Navigate to a URL.
(define (define
hs-toggle-between! hs-toggle-between!
(fn (fn
@@ -147,7 +126,7 @@
(do (dom-remove-class target cls1) (dom-add-class target cls2)) (do (dom-remove-class target cls1) (dom-add-class target cls2))
(do (dom-remove-class target cls2) (dom-add-class target cls1))))) (do (dom-remove-class target cls2) (dom-add-class target cls1)))))
;; Find previous sibling matching a selector. ;; Find next sibling matching a selector (or any sibling).
(define (define
hs-toggle-style! hs-toggle-style!
(fn (fn
@@ -171,7 +150,7 @@
(dom-set-style target prop "hidden") (dom-set-style target prop "hidden")
(dom-set-style target prop ""))))))) (dom-set-style target prop "")))))))
;; First element matching selector within a scope. ;; Find previous sibling matching a selector.
(define (define
hs-toggle-style-between! hs-toggle-style-between!
(fn (fn
@@ -183,7 +162,7 @@
(dom-set-style target prop val2) (dom-set-style target prop val2)
(dom-set-style target prop val1))))) (dom-set-style target prop val1)))))
;; Last element matching selector. ;; First element matching selector within a scope.
(define (define
hs-toggle-style-cycle! hs-toggle-style-cycle!
(fn (fn
@@ -204,7 +183,7 @@
(true (find-next (rest remaining)))))) (true (find-next (rest remaining))))))
(dom-set-style target prop (find-next vals))))) (dom-set-style target prop (find-next vals)))))
;; First/last within a specific scope. ;; Last element matching selector.
(define (define
hs-take! hs-take!
(fn (fn
@@ -244,6 +223,7 @@
(dom-set-attr target name attr-val) (dom-set-attr target name attr-val)
(dom-set-attr target name "")))))))) (dom-set-attr target name ""))))))))
;; First/last within a specific scope.
(begin (begin
(define (define
hs-element? hs-element?
@@ -355,9 +335,6 @@
(dom-insert-adjacent-html target "beforeend" value) (dom-insert-adjacent-html target "beforeend" value)
(hs-boot-subtree! target))))))))) (hs-boot-subtree! target)))))))))
;; ── Iteration ───────────────────────────────────────────────────
;; Repeat a thunk N times.
(define (define
hs-add-to! hs-add-to!
(fn (fn
@@ -370,7 +347,9 @@
(append target (list value)))) (append target (list value))))
(true (do (host-call target "push" value) target))))) (true (do (host-call target "push" value) target)))))
;; Repeat forever (until break — relies on exception/continuation). ;; ── Iteration ───────────────────────────────────────────────────
;; Repeat a thunk N times.
(define (define
hs-remove-from! hs-remove-from!
(fn (fn
@@ -380,10 +359,7 @@
(filter (fn (x) (not (= x value))) target) (filter (fn (x) (not (= x value))) target)
(host-call target "splice" (host-call target "indexOf" value) 1)))) (host-call target "splice" (host-call target "indexOf" value) 1))))
;; ── Fetch ─────────────────────────────────────────────────────── ;; Repeat forever (until break — relies on exception/continuation).
;; Fetch a URL, parse response according to format.
;; (hs-fetch url format) — format is "json" | "text" | "html"
(define (define
hs-splice-at! hs-splice-at!
(fn (fn
@@ -407,10 +383,10 @@
(host-call target "splice" i 1)))) (host-call target "splice" i 1))))
target)))) target))))
;; ── Type coercion ─────────────────────────────────────────────── ;; ── Fetch ───────────────────────────────────────────────────────
;; Coerce a value to a type by name. ;; Fetch a URL, parse response according to format.
;; (hs-coerce value type-name) — type-name is "Int", "Float", "String", etc. ;; (hs-fetch url format) — format is "json" | "text" | "html"
(define (define
hs-index hs-index
(fn (fn
@@ -422,10 +398,10 @@
((string? obj) (nth obj key)) ((string? obj) (nth obj key))
(true (host-get obj key))))) (true (host-get obj key)))))
;; ── Object creation ───────────────────────────────────────────── ;; ── Type coercion ───────────────────────────────────────────────
;; Make a new object of a given type. ;; Coerce a value to a type by name.
;; (hs-make type-name) — creates empty object/collection ;; (hs-coerce value type-name) — type-name is "Int", "Float", "String", etc.
(define (define
hs-put-at! hs-put-at!
(fn (fn
@@ -447,11 +423,10 @@
((= pos "start") (host-call target "unshift" value))) ((= pos "start") (host-call target "unshift" value)))
target))))))) target)))))))
;; ── Behavior installation ─────────────────────────────────────── ;; ── Object creation ─────────────────────────────────────────────
;; Install a behavior on an element. ;; Make a new object of a given type.
;; A behavior is a function that takes (me ...params) and sets up features. ;; (hs-make type-name) — creates empty object/collection
;; (hs-install behavior-fn me ...args)
(define (define
hs-dict-without hs-dict-without
(fn (fn
@@ -472,27 +447,27 @@
(host-call (host-global "Reflect") "deleteProperty" out key) (host-call (host-global "Reflect") "deleteProperty" out key)
out))))) out)))))
;; ── Measurement ───────────────────────────────────────────────── ;; ── Behavior installation ───────────────────────────────────────
;; Measure an element's bounding rect, store as local variables. ;; Install a behavior on an element.
;; Returns a dict with x, y, width, height, top, left, right, bottom. ;; A behavior is a function that takes (me ...params) and sets up features.
;; (hs-install behavior-fn me ...args)
(define (define
hs-set-on! hs-set-on!
(fn (fn
(props target) (props target)
(for-each (fn (k) (host-set! target k (get props k))) (keys props)))) (for-each (fn (k) (host-set! target k (get props k))) (keys props))))
;; ── Measurement ─────────────────────────────────────────────────
;; Measure an element's bounding rect, store as local variables.
;; Returns a dict with x, y, width, height, top, left, right, bottom.
(define hs-navigate! (fn (url) (perform (list (quote io-navigate) url))))
;; Return the current text selection as a string. In the browser this is ;; Return the current text selection as a string. In the browser this is
;; `window.getSelection().toString()`. In the mock test runner, a test ;; `window.getSelection().toString()`. In the mock test runner, a test
;; setup stashes the desired selection text at `window.__test_selection` ;; setup stashes the desired selection text at `window.__test_selection`
;; and the fallback path returns that so tests can assert on the result. ;; and the fallback path returns that so tests can assert on the result.
(define hs-navigate! (fn (url) (perform (list (quote io-navigate) url))))
;; ── Transition ──────────────────────────────────────────────────
;; Transition a CSS property to a value, optionally with duration.
;; (hs-transition target prop value duration)
(define (define
hs-ask hs-ask
(fn (fn
@@ -501,6 +476,11 @@
((w (host-global "window"))) ((w (host-global "window")))
(if w (host-call w "prompt" msg) nil)))) (if w (host-call w "prompt" msg) nil))))
;; ── Transition ──────────────────────────────────────────────────
;; Transition a CSS property to a value, optionally with duration.
;; (hs-transition target prop value duration)
(define (define
hs-answer hs-answer
(fn (fn
@@ -654,10 +634,6 @@
hs-query-all hs-query-all
(fn (sel) (host-call (dom-body) "querySelectorAll" sel))) (fn (sel) (host-call (dom-body) "querySelectorAll" sel)))
(define (define
hs-query-all-in hs-query-all-in
(fn (fn
@@ -667,21 +643,25 @@
(hs-query-all sel) (hs-query-all sel)
(host-call target "querySelectorAll" sel)))) (host-call target "querySelectorAll" sel))))
(define (define
hs-list-set hs-list-set
(fn (fn
(lst idx val) (lst idx val)
(append (take lst idx) (cons val (drop lst (+ idx 1)))))) (append (take lst idx) (cons val (drop lst (+ idx 1))))))
;; ── Sandbox/test runtime additions ──────────────────────────────
;; Property access — dot notation and .length
(define (define
hs-to-number hs-to-number
(fn (v) (if (number? v) v (or (parse-number (str v)) 0)))) (fn (v) (if (number? v) v (or (parse-number (str v)) 0))))
;; DOM query stub — sandbox returns empty list ;; ── Sandbox/test runtime additions ──────────────────────────────
;; Property access — dot notation and .length
(define (define
hs-query-first hs-query-first
(fn (sel) (host-call (host-global "document") "querySelector" sel))) (fn (sel) (host-call (host-global "document") "querySelector" sel)))
;; Method dispatch — obj.method(args) ;; DOM query stub — sandbox returns empty list
(define (define
hs-query-last hs-query-last
(fn (fn
@@ -689,11 +669,11 @@
(let (let
((all (dom-query-all (dom-body) sel))) ((all (dom-query-all (dom-body) sel)))
(if (> (len all) 0) (nth all (- (len all) 1)) nil)))) (if (> (len all) 0) (nth all (- (len all) 1)) nil))))
;; Method dispatch — obj.method(args)
(define hs-first (fn (scope sel) (dom-query-all scope sel)))
;; ── 0.9.90 features ───────────────────────────────────────────── ;; ── 0.9.90 features ─────────────────────────────────────────────
;; beep! — debug logging, returns value unchanged ;; beep! — debug logging, returns value unchanged
(define hs-first (fn (scope sel) (dom-query-all scope sel)))
;; Property-based is — check obj.key truthiness
(define (define
hs-last hs-last
(fn (fn
@@ -701,7 +681,7 @@
(let (let
((all (dom-query-all scope sel))) ((all (dom-query-all scope sel)))
(if (> (len all) 0) (nth all (- (len all) 1)) nil)))) (if (> (len all) 0) (nth all (- (len all) 1)) nil))))
;; Array slicing (inclusive both ends) ;; Property-based is — check obj.key truthiness
(define (define
hs-repeat-times hs-repeat-times
(fn (fn
@@ -719,7 +699,7 @@
((= signal "hs-continue") (do-repeat (+ i 1))) ((= signal "hs-continue") (do-repeat (+ i 1)))
(true (do-repeat (+ i 1)))))))) (true (do-repeat (+ i 1))))))))
(do-repeat 0))) (do-repeat 0)))
;; Collection: sorted by ;; Array slicing (inclusive both ends)
(define (define
hs-repeat-forever hs-repeat-forever
(fn (fn
@@ -735,7 +715,7 @@
((= signal "hs-continue") (do-forever)) ((= signal "hs-continue") (do-forever))
(true (do-forever)))))) (true (do-forever))))))
(do-forever))) (do-forever)))
;; Collection: sorted by descending ;; Collection: sorted by
(define (define
hs-repeat-while hs-repeat-while
(fn (fn
@@ -748,7 +728,7 @@
((= signal "hs-break") nil) ((= signal "hs-break") nil)
((= signal "hs-continue") (hs-repeat-while cond-fn thunk)) ((= signal "hs-continue") (hs-repeat-while cond-fn thunk))
(true (hs-repeat-while cond-fn thunk))))))) (true (hs-repeat-while cond-fn thunk)))))))
;; Collection: split by ;; Collection: sorted by descending
(define (define
hs-repeat-until hs-repeat-until
(fn (fn
@@ -760,7 +740,7 @@
((= signal "hs-continue") ((= signal "hs-continue")
(if (cond-fn) nil (hs-repeat-until cond-fn thunk))) (if (cond-fn) nil (hs-repeat-until cond-fn thunk)))
(true (if (cond-fn) nil (hs-repeat-until cond-fn thunk))))))) (true (if (cond-fn) nil (hs-repeat-until cond-fn thunk)))))))
;; Collection: joined by ;; Collection: split by
(define (define
hs-for-each hs-for-each
(fn (fn
@@ -780,7 +760,7 @@
((= signal "hs-continue") (do-loop (rest remaining))) ((= signal "hs-continue") (do-loop (rest remaining)))
(true (do-loop (rest remaining)))))))) (true (do-loop (rest remaining))))))))
(do-loop items)))) (do-loop items))))
;; Collection: joined by
(begin (begin
(define (define
hs-append hs-append
@@ -1535,25 +1515,6 @@
(hs-contains? (rest collection) item)))))) (hs-contains? (rest collection) item))))))
(true false)))) (true false))))
(define
hs-in?
(fn
(collection item)
(cond
((nil? collection) (list))
((list? collection)
(cond
((nil? item) (list))
((list? item)
(filter (fn (x) (hs-contains? collection x)) item))
((hs-contains? collection item) (list item))
(true (list))))
(true (list)))))
(define
hs-in-bool?
(fn (collection item) (not (hs-falsy? (hs-in? collection item)))))
(define (define
hs-is hs-is
(fn (fn
@@ -2134,13 +2095,7 @@
-1 -1
(if (= (first lst) item) i (idx-loop (rest lst) (+ i 1)))))) (if (= (first lst) item) i (idx-loop (rest lst) (+ i 1))))))
(idx-loop obj 0))) (idx-loop obj 0)))
(true (true nil))))
(let
((fn-val (host-get obj method)))
(cond
((and fn-val (callable? fn-val)) (apply fn-val args))
(fn-val (apply host-call (cons obj (cons method args))))
(true nil)))))))
(define hs-beep (fn (v) v)) (define hs-beep (fn (v) v))
@@ -2519,9 +2474,3 @@
((nil? b) false) ((nil? b) false)
((= a b) true) ((= a b) true)
(true (hs-dom-is-ancestor? a (dom-parent b)))))) (true (hs-dom-is-ancestor? a (dom-parent b))))))
(define
hs-win-call
(fn
(fn-name args)
(let ((fn (host-global fn-name))) (if fn (host-call-fn fn args) nil))))

View File

@@ -8,7 +8,6 @@
;; references them (e.g. `window.tmp`) can resolve through the host. ;; references them (e.g. `window.tmp`) can resolve through the host.
(define window (host-global "window")) (define window (host-global "window"))
(define document (host-global "document")) (define document (host-global "document"))
(define cookies (host-global "cookies"))
(define hs-test-el (define hs-test-el
(fn (tag hs-src) (fn (tag hs-src)
@@ -20,11 +19,7 @@
(define hs-cleanup! (define hs-cleanup!
(fn () (fn ()
(begin (dom-set-inner-html (dom-body) "")))
(dom-set-inner-html (dom-body) "")
;; Reset global runtime state that prior tests may have set.
(hs-set-default-hide-strategy! nil)
(hs-set-log-all! false))))
;; Evaluate a hyperscript expression and return either the expression ;; Evaluate a hyperscript expression and return either the expression
;; value or `it` (whichever is non-nil). Multi-statement scripts that ;; value or `it` (whichever is non-nil). Multi-statement scripts that
@@ -1400,17 +1395,7 @@
(hs-activate! _el-div) (hs-activate! _el-div)
)) ))
(deftest "fires hyperscript:before:init and hyperscript:after:init" (deftest "fires hyperscript:before:init and hyperscript:after:init"
(hs-cleanup!) (error "SKIP (untranslated): fires hyperscript:before:init and hyperscript:after:init"))
(let ((wa (dom-create-element "div"))
(events (list)))
(dom-listen wa "hyperscript:before:init"
(fn (e) (set! events (append events (list "before:init")))))
(dom-listen wa "hyperscript:after:init"
(fn (e) (set! events (append events (list "after:init")))))
(dom-set-inner-html wa "<div _=\"on click add .foo\"></div>")
(hs-boot-subtree! wa)
(assert= events (list "before:init" "after:init")))
)
(deftest "hyperscript can have more than one action" (deftest "hyperscript can have more than one action"
(hs-cleanup!) (hs-cleanup!)
(let ((_el-bar (dom-create-element "div")) (_el-div (dom-create-element "div"))) (let ((_el-bar (dom-create-element "div")) (_el-div (dom-create-element "div")))
@@ -1426,15 +1411,7 @@
(assert (dom-has-class? (dom-query "div:nth-of-type(2)") "blah")) (assert (dom-has-class? (dom-query "div:nth-of-type(2)") "blah"))
)) ))
(deftest "hyperscript:before:init can cancel initialization" (deftest "hyperscript:before:init can cancel initialization"
(hs-cleanup!) (error "SKIP (untranslated): hyperscript:before:init can cancel initialization"))
(let ((wa (dom-create-element "div")))
(dom-listen wa "hyperscript:before:init"
(fn (e) (host-call e "preventDefault")))
(dom-set-inner-html wa "<div _=\"on click add .foo\"></div>")
(hs-boot-subtree! wa)
(let ((d (host-call wa "querySelector" "div")))
(assert= (host-call d "hasAttribute" "data-hyperscript-powered") false)))
)
(deftest "logAll config logs events to console" (deftest "logAll config logs events to console"
(hs-cleanup!) (hs-cleanup!)
(hs-clear-log-captured!) (hs-clear-log-captured!)
@@ -2011,12 +1988,13 @@
(error "SKIP (skip-list): can pick detail fields out by name")) (error "SKIP (skip-list): can pick detail fields out by name"))
(deftest "can refer to function in init blocks" (deftest "can refer to function in init blocks"
(hs-cleanup!) (hs-cleanup!)
(guard (_e (true nil)) (eval-expr-cek (hs-to-sx (hs-compile "init call foo() end def foo() put \"here\" into #d1's innerHTML end"))))
(guard (_e (true nil)) (eval-expr-cek (hs-to-sx (hs-compile "init call foo() end def foo() put \\\"here\\\" into #d1's innerHTML end"))))
(let ((_el-d1 (dom-create-element "div"))) (let ((_el-d1 (dom-create-element "div")))
(dom-set-attr _el-d1 "id" "d1") (dom-set-attr _el-d1 "id" "d1")
(dom-append (dom-body) _el-d1) (dom-append (dom-body) _el-d1)
(guard (_e (true nil)) (eval-expr-cek (hs-to-sx (hs-compile "init call foo() end def foo() put \"here\" into #d1's innerHTML end")))) (assert= (dom-text-content (dom-query-by-id "d1")) "here")
(assert= (dom-text-content (dom-query-by-id "d1")) "here")) ))
)
(deftest "can remove by clicks elsewhere" (deftest "can remove by clicks elsewhere"
(hs-cleanup!) (hs-cleanup!)
(let ((_el-target (dom-create-element "div")) (_el-other (dom-create-element "div"))) (let ((_el-target (dom-create-element "div")) (_el-other (dom-create-element "div")))
@@ -2532,16 +2510,7 @@
(guard (_e (true nil)) (eval-expr-cek (hs-to-sx (hs-compile "def foo() wait a tick then set window.bar to 10 throw \"foo\" finally set window.bar to 20 end")))) (guard (_e (true nil)) (eval-expr-cek (hs-to-sx (hs-compile "def foo() wait a tick then set window.bar to 10 throw \"foo\" finally set window.bar to 20 end"))))
) )
(deftest "can call asynchronously" (deftest "can call asynchronously"
(hs-cleanup!) (error "SKIP (skip-list): can call asynchronously"))
(guard (_e (true nil)) (eval-expr-cek (hs-to-sx (hs-compile "def foo() wait 1ms log me end"))))
(guard (_e (true nil)) (eval-expr-cek (hs-to-sx (hs-compile "def foo() wait 1ms log me end"))))
(let ((_el-div (dom-create-element "div")) (_el-d1 (dom-create-element "div")))
(dom-set-attr _el-div "_" "on click call foo() then add .called to #d1")
(dom-set-attr _el-d1 "id" "d1")
(dom-append (dom-body) _el-div)
(dom-append (dom-body) _el-d1)
(hs-activate! _el-div)
))
(deftest "can catch async exceptions" (deftest "can catch async exceptions"
(hs-cleanup!) (hs-cleanup!)
(guard (_e (true nil)) (eval-expr-cek (hs-to-sx (hs-compile "def doh() wait 10ms throw \"bar\" end def foo() call doh() catch e set window.bar to e end")))) (guard (_e (true nil)) (eval-expr-cek (hs-to-sx (hs-compile "def doh() wait 10ms throw \"bar\" end def foo() call doh() catch e set window.bar to e end"))))
@@ -2693,27 +2662,9 @@
(guard (_e (true nil)) (eval-expr-cek (hs-to-sx (hs-compile "def foo() set window.bar to 10 throw \"foo\" finally set window.bar to 20 end")))) (guard (_e (true nil)) (eval-expr-cek (hs-to-sx (hs-compile "def foo() set window.bar to 10 throw \"foo\" finally set window.bar to 20 end"))))
) )
(deftest "functions can be namespaced" (deftest "functions can be namespaced"
(hs-cleanup!) (error "SKIP (skip-list): functions can be namespaced"))
(guard (_e (true nil)) (eval-expr-cek (hs-to-sx (hs-compile "def utils.foo() add .called to #d1 end"))))
(guard (_e (true nil)) (eval-expr-cek (hs-to-sx (hs-compile "def utils.foo() add .called to #d1 end"))))
(let ((_el-div (dom-create-element "div")) (_el-d1 (dom-create-element "div")))
(dom-set-attr _el-div "_" "on click call utils.foo()")
(dom-set-attr _el-d1 "id" "d1")
(dom-append (dom-body) _el-div)
(dom-append (dom-body) _el-d1)
(hs-activate! _el-div)
))
(deftest "is called synchronously" (deftest "is called synchronously"
(hs-cleanup!) (error "SKIP (skip-list): is called synchronously"))
(guard (_e (true nil)) (eval-expr-cek (hs-to-sx (hs-compile "def foo() log me end"))))
(guard (_e (true nil)) (eval-expr-cek (hs-to-sx (hs-compile "def foo() log me end"))))
(let ((_el-div (dom-create-element "div")) (_el-d1 (dom-create-element "div")))
(dom-set-attr _el-div "_" "on click call foo() then add .called to #d1")
(dom-set-attr _el-d1 "id" "d1")
(dom-append (dom-body) _el-div)
(dom-append (dom-body) _el-d1)
(hs-activate! _el-div)
))
) )
;; ── default (15 tests) ── ;; ── default (15 tests) ──
@@ -4932,27 +4883,15 @@
;; ── expressions/cookies (5 tests) ── ;; ── expressions/cookies (5 tests) ──
(defsuite "hs-upstream-expressions/cookies" (defsuite "hs-upstream-expressions/cookies"
(deftest "basic clear cookie values work" (deftest "basic clear cookie values work"
(hs-cleanup!) (error "SKIP (untranslated): basic clear cookie values work"))
(eval-hs "set cookies.foo to 'bar'")
(assert= (eval-hs "cookies.foo") "bar")
(eval-hs "call cookies.clear('foo')")
(assert (nil? (eval-hs "cookies.foo"))))
(deftest "basic set cookie values work" (deftest "basic set cookie values work"
(hs-cleanup!) (error "SKIP (untranslated): basic set cookie values work"))
(assert (nil? (eval-hs "cookies.foo")))
(eval-hs "set cookies.foo to 'bar'")
(assert= (eval-hs "cookies.foo") "bar"))
(deftest "iterate cookies values work" (deftest "iterate cookies values work"
(error "SKIP (untranslated): iterate cookies values work")) (error "SKIP (untranslated): iterate cookies values work"))
(deftest "length is 0 when no cookies are set" (deftest "length is 0 when no cookies are set"
(hs-cleanup!) (error "SKIP (untranslated): length is 0 when no cookies are set"))
(assert= (eval-hs "cookies.length") 0))
(deftest "update cookie values work" (deftest "update cookie values work"
(hs-cleanup!) (error "SKIP (untranslated): update cookie values work"))
(eval-hs "set cookies.foo to 'bar'")
(assert= (eval-hs "cookies.foo") "bar")
(eval-hs "set cookies.foo to 'doh'")
(assert= (eval-hs "cookies.foo") "doh"))
) )
;; ── expressions/dom-scope (20 tests) ── ;; ── expressions/dom-scope (20 tests) ──
@@ -8854,29 +8793,11 @@
(hs-activate! _el-pf) (hs-activate! _el-pf)
)) ))
(deftest "can filter events based on count" (deftest "can filter events based on count"
(hs-cleanup!) (error "SKIP (skip-list): can filter events based on count"))
(let ((_el-div (dom-create-element "div")))
(dom-set-attr _el-div "_" "on click 1 put 1 + my.innerHTML as Int into my.innerHTML")
(dom-set-inner-html _el-div "0")
(dom-append (dom-body) _el-div)
(hs-activate! _el-div)
))
(deftest "can filter events based on count range" (deftest "can filter events based on count range"
(hs-cleanup!) (error "SKIP (skip-list): can filter events based on count range"))
(let ((_el-div (dom-create-element "div")))
(dom-set-attr _el-div "_" "on click 1 to 2 put 1 + my.innerHTML as Int into my.innerHTML")
(dom-set-inner-html _el-div "0")
(dom-append (dom-body) _el-div)
(hs-activate! _el-div)
))
(deftest "can filter events based on unbounded count range" (deftest "can filter events based on unbounded count range"
(hs-cleanup!) (error "SKIP (skip-list): can filter events based on unbounded count range"))
(let ((_el-div (dom-create-element "div")))
(dom-set-attr _el-div "_" "on click 2 and on put 1 + my.innerHTML as Int into my.innerHTML")
(dom-set-inner-html _el-div "0")
(dom-append (dom-body) _el-div)
(hs-activate! _el-div)
))
(deftest "can fire an event on load" (deftest "can fire an event on load"
(hs-cleanup!) (hs-cleanup!)
(let ((_el-d1 (dom-create-element "div"))) (let ((_el-d1 (dom-create-element "div")))
@@ -8919,22 +8840,9 @@
(hs-activate! _el-div) (hs-activate! _el-div)
)) ))
(deftest "can listen for attribute mutations" (deftest "can listen for attribute mutations"
(hs-cleanup!) (error "SKIP (skip-list): can listen for attribute mutations"))
(let ((_el-div (dom-create-element "div")))
(dom-set-attr _el-div "_" "on mutation of attributes put \"Mutated\" into me")
(dom-append (dom-body) _el-div)
(hs-activate! _el-div)
))
(deftest "can listen for attribute mutations on other elements" (deftest "can listen for attribute mutations on other elements"
(hs-cleanup!) (error "SKIP (skip-list): can listen for attribute mutations on other elements"))
(let ((_el-d1 (dom-create-element "div")) (_el-d2 (dom-create-element "div")))
(dom-set-attr _el-d1 "id" "d1")
(dom-set-attr _el-d2 "id" "d2")
(dom-set-attr _el-d2 "_" "on mutation of attributes from #d1 put \"Mutated\" into me")
(dom-append (dom-body) _el-d1)
(dom-append (dom-body) _el-d2)
(hs-activate! _el-d2)
))
(deftest "can listen for characterData mutation filter out other mutations" (deftest "can listen for characterData mutation filter out other mutations"
(hs-cleanup!) (hs-cleanup!)
(let ((_el-div (dom-create-element "div"))) (let ((_el-div (dom-create-element "div")))
@@ -8950,12 +8858,7 @@
(hs-activate! _el-div) (hs-activate! _el-div)
)) ))
(deftest "can listen for childList mutations" (deftest "can listen for childList mutations"
(hs-cleanup!) (error "SKIP (skip-list): can listen for childList mutations"))
(let ((_el-div (dom-create-element "div")))
(dom-set-attr _el-div "_" "on mutation of childList put \"Mutated\" into me then wait for hyperscript:mutation")
(dom-append (dom-body) _el-div)
(hs-activate! _el-div)
))
(deftest "can listen for events in another element (lazy)" (deftest "can listen for events in another element (lazy)"
(hs-cleanup!) (hs-cleanup!)
(let ((_el-div (dom-create-element "div")) (_el-d1 (dom-create-element "div")) (_el-d2 (dom-create-element "div"))) (let ((_el-div (dom-create-element "div")) (_el-d1 (dom-create-element "div")) (_el-d2 (dom-create-element "div")))
@@ -8968,33 +8871,13 @@
(hs-activate! _el-div) (hs-activate! _el-div)
)) ))
(deftest "can listen for general mutations" (deftest "can listen for general mutations"
(hs-cleanup!) (error "SKIP (skip-list): can listen for general mutations"))
(let ((_el-div (dom-create-element "div")))
(dom-set-attr _el-div "_" "on mutation put \"Mutated\" into me then wait for hyperscript:mutation")
(dom-append (dom-body) _el-div)
(hs-activate! _el-div)
))
(deftest "can listen for multiple mutations" (deftest "can listen for multiple mutations"
(hs-cleanup!) (error "SKIP (skip-list): can listen for multiple mutations"))
(let ((_el-div (dom-create-element "div")))
(dom-set-attr _el-div "_" "on mutation of @foo or @bar put \"Mutated\" into me")
(dom-append (dom-body) _el-div)
(hs-activate! _el-div)
))
(deftest "can listen for multiple mutations 2" (deftest "can listen for multiple mutations 2"
(hs-cleanup!) (error "SKIP (skip-list): can listen for multiple mutations 2"))
(let ((_el-div (dom-create-element "div")))
(dom-set-attr _el-div "_" "on mutation of @foo or @bar put \"Mutated\" into me")
(dom-append (dom-body) _el-div)
(hs-activate! _el-div)
))
(deftest "can listen for specific attribute mutations" (deftest "can listen for specific attribute mutations"
(hs-cleanup!) (error "SKIP (skip-list): can listen for specific attribute mutations"))
(let ((_el-div (dom-create-element "div")))
(dom-set-attr _el-div "_" "on mutation of @foo put \"Mutated\" into me")
(dom-append (dom-body) _el-div)
(hs-activate! _el-div)
))
(deftest "can listen for specific attribute mutations and filter out other attribute mutations" (deftest "can listen for specific attribute mutations and filter out other attribute mutations"
(hs-cleanup!) (hs-cleanup!)
(let ((_el-div (dom-create-element "div"))) (let ((_el-div (dom-create-element "div")))
@@ -9003,13 +8886,7 @@
(hs-activate! _el-div) (hs-activate! _el-div)
)) ))
(deftest "can mix ranges" (deftest "can mix ranges"
(hs-cleanup!) (error "SKIP (skip-list): can mix ranges"))
(let ((_el-div (dom-create-element "div")))
(dom-set-attr _el-div "_" "on click 1 put \"one\" into my.innerHTML on click 3 put \"three\" into my.innerHTML on click 2 put \"two\" into my.innerHTML")
(dom-set-inner-html _el-div "0")
(dom-append (dom-body) _el-div)
(hs-activate! _el-div)
))
(deftest "can pick detail fields out by name" (deftest "can pick detail fields out by name"
(error "SKIP (skip-list): can pick detail fields out by name")) (error "SKIP (skip-list): can pick detail fields out by name"))
(deftest "can pick event properties out by name" (deftest "can pick event properties out by name"
@@ -9179,13 +9056,7 @@
(deftest "multiple event handlers at a time are allowed to execute with the every keyword" (deftest "multiple event handlers at a time are allowed to execute with the every keyword"
(error "SKIP (skip-list): multiple event handlers at a time are allowed to execute with the every keyword")) (error "SKIP (skip-list): multiple event handlers at a time are allowed to execute with the every keyword"))
(deftest "on first click fires only once" (deftest "on first click fires only once"
(hs-cleanup!) (error "SKIP (skip-list): on first click fires only once"))
(let ((_el-div (dom-create-element "div")))
(dom-set-attr _el-div "_" "on first click put 1 + my.innerHTML as Int into my.innerHTML")
(dom-set-inner-html _el-div "0")
(dom-append (dom-body) _el-div)
(hs-activate! _el-div)
))
(deftest "on intersection fires when the element is in the viewport" (deftest "on intersection fires when the element is in the viewport"
(hs-cleanup!) (hs-cleanup!)
(let ((_el-d (dom-create-element "div"))) (let ((_el-d (dom-create-element "div")))
@@ -9231,19 +9102,9 @@
(deftest "rethrown exceptions trigger 'exception' event" (deftest "rethrown exceptions trigger 'exception' event"
(error "SKIP (skip-list): rethrown exceptions trigger 'exception' event")) (error "SKIP (skip-list): rethrown exceptions trigger 'exception' event"))
(deftest "supports \"elsewhere\" modifier" (deftest "supports \"elsewhere\" modifier"
(hs-cleanup!) (error "SKIP (skip-list): supports 'elsewhere' modifier"))
(let ((_el-div (dom-create-element "div")))
(dom-set-attr _el-div "_" "on click elsewhere add .clicked")
(dom-append (dom-body) _el-div)
(hs-activate! _el-div)
))
(deftest "supports \"from elsewhere\" modifier" (deftest "supports \"from elsewhere\" modifier"
(hs-cleanup!) (error "SKIP (skip-list): supports 'from elsewhere' modifier"))
(let ((_el-div (dom-create-element "div")))
(dom-set-attr _el-div "_" "on click from elsewhere add .clicked")
(dom-append (dom-body) _el-div)
(hs-activate! _el-div)
))
(deftest "throttled at <time> allows events after the window elapses" (deftest "throttled at <time> allows events after the window elapses"
(hs-cleanup!) (hs-cleanup!)
(let ((_el-d (dom-create-element "div"))) (let ((_el-d (dom-create-element "div")))

View File

@@ -327,36 +327,6 @@ const document = {
createEvent(t){return new Ev(t);}, addEventListener(){}, removeEventListener(){}, createEvent(t){return new Ev(t);}, addEventListener(){}, removeEventListener(){},
}; };
globalThis.document=document; globalThis.window=globalThis; globalThis.HTMLElement=El; globalThis.Element=El; globalThis.document=document; globalThis.window=globalThis; globalThis.HTMLElement=El; globalThis.Element=El;
// cluster-33: cookie store + document.cookie + cookies Proxy.
globalThis.__hsCookieStore = new Map();
Object.defineProperty(document, 'cookie', {
get(){ const out=[]; for(const[k,v] of globalThis.__hsCookieStore) out.push(k+'='+v); return out.join('; '); },
set(s){
const str=String(s||'');
const m=str.match(/^\s*([^=]+?)\s*=\s*([^;]*)/);
if(!m) return;
const name=m[1].trim();
const val=m[2];
if(/expires=Thu,?\s*01\s*Jan\s*1970/i.test(str) || val==='') globalThis.__hsCookieStore.delete(name);
else globalThis.__hsCookieStore.set(name, val);
},
configurable: true,
});
globalThis.cookies = new Proxy({}, {
get(_, k){
if(k==='length') return globalThis.__hsCookieStore.size;
if(k==='clear') return (name)=>globalThis.__hsCookieStore.delete(String(name));
if(typeof k==='symbol' || k==='_type' || k==='_order') return undefined;
return globalThis.__hsCookieStore.has(k) ? globalThis.__hsCookieStore.get(k) : null;
},
set(_, k, v){ globalThis.__hsCookieStore.set(String(k), String(v)); return true; },
has(_, k){ return globalThis.__hsCookieStore.has(k); },
ownKeys(){ return Array.from(globalThis.__hsCookieStore.keys()); },
getOwnPropertyDescriptor(_, k){
if(globalThis.__hsCookieStore.has(k)) return {value: globalThis.__hsCookieStore.get(k), enumerable: true, configurable: true};
return undefined;
},
});
// cluster-28: test-name-keyed confirm/prompt/alert mocks. The upstream // cluster-28: test-name-keyed confirm/prompt/alert mocks. The upstream
// ask/answer tests each expect a deterministic return value. Keyed on // ask/answer tests each expect a deterministic return value. Keyed on
// globalThis.__currentHsTestName which the test loop sets before each test. // globalThis.__currentHsTestName which the test loop sets before each test.
@@ -375,115 +345,7 @@ globalThis.prompt = function(_msg){
}; };
globalThis.Event=Ev; globalThis.CustomEvent=Ev; globalThis.NodeList=Array; globalThis.HTMLCollection=Array; globalThis.Event=Ev; globalThis.CustomEvent=Ev; globalThis.NodeList=Array; globalThis.HTMLCollection=Array;
globalThis.getComputedStyle=(e)=>e?e.style:{}; globalThis.requestAnimationFrame=(f)=>{f();return 0;}; globalThis.getComputedStyle=(e)=>e?e.style:{}; globalThis.requestAnimationFrame=(f)=>{f();return 0;};
globalThis.cancelAnimationFrame=()=>{}; globalThis.cancelAnimationFrame=()=>{}; globalThis.MutationObserver=class{observe(){}disconnect(){}};
// HsMutationObserver — cluster-32 mutation mock. Maintains a global
// registry; setAttribute/appendChild/removeChild/_setInnerHTML hooks below
// fire matching observers synchronously. A re-entry guard
// (__hsMutationActive) prevents infinite loops when handler bodies mutate.
globalThis.__hsMutationRegistry = [];
globalThis.__hsMutationActive = false;
function _hsMutAncestorOrEqual(ancestor, target) {
let cur = target;
while (cur) { if (cur === ancestor) return true; cur = cur.parentElement; }
return false;
}
function _hsMutMatches(reg, rec) {
const o = reg.opts;
if (!_hsMutAncestorOrEqual(reg.target, rec.target)) return false;
if (rec.type === 'attributes') {
if (!o.attributes) return false;
if (o.attributeFilter && o.attributeFilter.length > 0) {
if (!o.attributeFilter.includes(rec.attributeName)) return false;
}
return true;
}
if (rec.type === 'childList') return !!o.childList;
if (rec.type === 'characterData') return !!o.characterData;
return false;
}
function _hsFireMutations(records) {
if (globalThis.__hsMutationActive) return;
if (!records || records.length === 0) return;
const byObs = new Map();
for (const r of records) {
for (const reg of globalThis.__hsMutationRegistry) {
if (!_hsMutMatches(reg, r)) continue;
if (!byObs.has(reg.observer)) byObs.set(reg.observer, []);
byObs.get(reg.observer).push(r);
}
}
if (byObs.size === 0) return;
globalThis.__hsMutationActive = true;
try {
for (const [obs, recs] of byObs) {
try { obs._cb(recs, obs); } catch (e) {}
}
} finally {
globalThis.__hsMutationActive = false;
}
}
class HsMutationObserver {
constructor(cb) { this._cb = cb; this._regs = []; }
observe(el, opts) {
if (!el) return;
// opts is an SX dict: read fields directly. attributeFilter is an SX list
// ({_type:'list', items:[...]}) OR a JS array.
let af = opts && opts.attributeFilter;
if (af && af._type === 'list') af = af.items;
const o = {
attributes: !!(opts && opts.attributes),
childList: !!(opts && opts.childList),
characterData: !!(opts && opts.characterData),
subtree: !!(opts && opts.subtree),
attributeFilter: af || null,
};
const reg = { observer: this, target: el, opts: o };
this._regs.push(reg);
globalThis.__hsMutationRegistry.push(reg);
}
disconnect() {
for (const r of this._regs) {
const i = globalThis.__hsMutationRegistry.indexOf(r);
if (i >= 0) globalThis.__hsMutationRegistry.splice(i, 1);
}
this._regs = [];
}
takeRecords() { return []; }
}
globalThis.MutationObserver = HsMutationObserver;
// Hook El prototype methods so mutations fire registered observers.
// Hooks are no-ops while __hsMutationActive=true (prevents re-entry from
// handler bodies that themselves mutate the DOM).
(function _hookElForMutations() {
const _setAttr = El.prototype.setAttribute;
El.prototype.setAttribute = function(n, v) {
const r = _setAttr.call(this, n, v);
if (globalThis.__hsMutationRegistry.length)
_hsFireMutations([{ type: 'attributes', target: this, attributeName: String(n), oldValue: null }]);
return r;
};
const _append = El.prototype.appendChild;
El.prototype.appendChild = function(c) {
const r = _append.call(this, c);
if (globalThis.__hsMutationRegistry.length)
_hsFireMutations([{ type: 'childList', target: this, addedNodes: [c], removedNodes: [] }]);
return r;
};
const _remove = El.prototype.removeChild;
El.prototype.removeChild = function(c) {
const r = _remove.call(this, c);
if (globalThis.__hsMutationRegistry.length)
_hsFireMutations([{ type: 'childList', target: this, addedNodes: [], removedNodes: [c] }]);
return r;
};
const _setIH = El.prototype._setInnerHTML;
El.prototype._setInnerHTML = function(html) {
const r = _setIH.call(this, html);
if (globalThis.__hsMutationRegistry.length)
_hsFireMutations([{ type: 'childList', target: this, addedNodes: [], removedNodes: [] }]);
return r;
};
})();
// HsResizeObserver — cluster-26 resize mock. Keeps a per-element callback // HsResizeObserver — cluster-26 resize mock. Keeps a per-element callback
// registry so code that observes via `new ResizeObserver(cb)` still works, // registry so code that observes via `new ResizeObserver(cb)` still works,
// but HS's `on resize` uses the plain `resize` DOM event dispatched by the // but HS's `on resize` uses the plain `resize` DOM event dispatched by the
@@ -553,7 +415,6 @@ K.registerNative('host-get',a=>{
}); });
K.registerNative('host-set!',a=>{if(a[0]!=null){const v=a[2]; if(a[1]==='innerHTML'&&a[0] instanceof El){const s=v===null?'null':v===undefined?'':String(v);a[0]._setInnerHTML(s);a[0][a[1]]=a[0].innerHTML;} else if(a[1]==='textContent'&&a[0] instanceof El){const s=v===null?'null':v===undefined?'':String(v);a[0].textContent=s;a[0].innerHTML=s;for(const c of a[0].children){c.parentElement=null;c.parentNode=null;}a[0].children=[];a[0].childNodes=[];} else{a[0][a[1]]=v;}} return a[2];}); K.registerNative('host-set!',a=>{if(a[0]!=null){const v=a[2]; if(a[1]==='innerHTML'&&a[0] instanceof El){const s=v===null?'null':v===undefined?'':String(v);a[0]._setInnerHTML(s);a[0][a[1]]=a[0].innerHTML;} else if(a[1]==='textContent'&&a[0] instanceof El){const s=v===null?'null':v===undefined?'':String(v);a[0].textContent=s;a[0].innerHTML=s;for(const c of a[0].children){c.parentElement=null;c.parentNode=null;}a[0].children=[];a[0].childNodes=[];} else{a[0][a[1]]=v;}} return a[2];});
K.registerNative('host-call',a=>{if(_testDeadline&&Date.now()>_testDeadline)throw new Error('TIMEOUT: wall clock exceeded');const[o,m,...r]=a;if(o==null){const f=globalThis[m];return typeof f==='function'?f.apply(null,r):null;}if(o&&typeof o[m]==='function'){try{const v=o[m].apply(o,r);return v===undefined?null:v;}catch(e){return null;}}return null;}); K.registerNative('host-call',a=>{if(_testDeadline&&Date.now()>_testDeadline)throw new Error('TIMEOUT: wall clock exceeded');const[o,m,...r]=a;if(o==null){const f=globalThis[m];return typeof f==='function'?f.apply(null,r):null;}if(o&&typeof o[m]==='function'){try{const v=o[m].apply(o,r);return v===undefined?null:v;}catch(e){return null;}}return null;});
K.registerNative('host-call-fn',a=>{const[fn,argList]=a;if(typeof fn!=='function'&&!(fn&&fn.__sx_handle!==undefined))return null;const callArgs=(argList&&argList._type==='list'&&argList.items)?Array.from(argList.items):(Array.isArray(argList)?argList:[]);if(fn&&fn.__sx_handle!==undefined)return K.callFn(fn,callArgs);try{const v=fn.apply(null,callArgs);return v===undefined?null:v;}catch(e){return null;}});
K.registerNative('host-new',a=>{const C=typeof a[0]==='string'?globalThis[a[0]]:a[0];return typeof C==='function'?new C(...a.slice(1)):null;}); K.registerNative('host-new',a=>{const C=typeof a[0]==='string'?globalThis[a[0]]:a[0];return typeof C==='function'?new C(...a.slice(1)):null;});
K.registerNative('host-callback',a=>{const fn=a[0];if(typeof fn==='function'&&fn.__sx_handle===undefined)return fn;if(fn&&fn.__sx_handle!==undefined)return function(){const r=K.callFn(fn,Array.from(arguments));if(globalThis._driveAsync)globalThis._driveAsync(r);return r;};return function(){};}); K.registerNative('host-callback',a=>{const fn=a[0];if(typeof fn==='function'&&fn.__sx_handle===undefined)return fn;if(fn&&fn.__sx_handle!==undefined)return function(){const r=K.callFn(fn,Array.from(arguments));if(globalThis._driveAsync)globalThis._driveAsync(r);return r;};return function(){};});
K.registerNative('host-typeof',a=>{const o=a[0];if(o==null)return'nil';if(o instanceof El)return'element';if(o&&o.nodeType===3)return'text';if(o instanceof Ev)return'event';if(o instanceof Promise)return'promise';return typeof o;}); K.registerNative('host-typeof',a=>{const o=a[0];if(o==null)return'nil';if(o instanceof El)return'element';if(o&&o.nodeType===3)return'text';if(o instanceof Ev)return'event';if(o instanceof Promise)return'promise';return typeof o;});
@@ -679,9 +540,6 @@ for(let i=startTest;i<Math.min(endTest,testCount);i++){
// Reset body // Reset body
_body.children=[];_body.childNodes=[];_body.innerHTML='';_body.textContent=''; _body.children=[];_body.childNodes=[];_body.innerHTML='';_body.textContent='';
globalThis.__test_selection=''; globalThis.__test_selection='';
globalThis.__hsCookieStore.clear();
globalThis.__hsMutationRegistry.length = 0;
globalThis.__hsMutationActive = false;
globalThis.__currentHsTestName = name; globalThis.__currentHsTestName = name;
// Enable step limit for timeout protection // Enable step limit for timeout protection

View File

@@ -110,6 +110,17 @@ SKIP_TEST_NAMES = {
"can pick event properties out by name", "can pick event properties out by name",
"can be in a top level script tag", "can be in a top level script tag",
"multiple event handlers at a time are allowed to execute with the every keyword", "multiple event handlers at a time are allowed to execute with the every keyword",
"can filter events based on count",
"can filter events based on count range",
"can filter events based on unbounded count range",
"can mix ranges",
"can listen for general mutations",
"can listen for attribute mutations",
"can listen for specific attribute mutations",
"can listen for childList mutations",
"can listen for multiple mutations",
"can listen for multiple mutations 2",
"can listen for attribute mutations on other elements",
"each behavior installation has its own event queue", "each behavior installation has its own event queue",
"can catch exceptions thrown in js functions", "can catch exceptions thrown in js functions",
"can catch exceptions thrown in hyperscript functions", "can catch exceptions thrown in hyperscript functions",
@@ -125,6 +136,13 @@ SKIP_TEST_NAMES = {
"can ignore when target doesn't exist", "can ignore when target doesn't exist",
"can ignore when target doesn\\'t exist", "can ignore when target doesn\\'t exist",
"can handle an or after a from clause", "can handle an or after a from clause",
"on first click fires only once",
"supports \"elsewhere\" modifier",
"supports \"from elsewhere\" modifier",
# upstream 'def' category — namespaced def + dynamic `me` inside callee
"functions can be namespaced",
"is called synchronously",
"can call asynchronously",
# upstream 'fetch' category — depend on per-test sinon stubs for 404 / thrown errors, # upstream 'fetch' category — depend on per-test sinon stubs for 404 / thrown errors,
# or on real DocumentFragment semantics (`its childElementCount` after `as html`). # or on real DocumentFragment semantics (`its childElementCount` after `as html`).
# Our generic test-runner mock returns a fixed 200 response, so these cases # Our generic test-runner mock returns a fixed 200 response, so these cases
@@ -1148,32 +1166,6 @@ def parse_dev_body(body, elements, var_names):
ops.append(f'(if (dom-has-class? {target} "{cls}") (dom-remove-class {target} "{cls}") (dom-add-class {target} "{cls}"))') ops.append(f'(if (dom-has-class? {target} "{cls}") (dom-remove-class {target} "{cls}") (dom-add-class {target} "{cls}"))')
continue continue
# evaluate(() => document.querySelector(SEL).setAttribute(NAME, VALUE))
# — used by mutation tests (cluster 32) to trigger MutationObserver.
m = re.match(
r'''evaluate\(\s*\(\)\s*=>\s*document\.querySelector\(\s*([\'"])([^\'"]+)\1\s*\)'''
r'''\.setAttribute\(\s*([\'"])([\w-]+)\3\s*,\s*([\'"])([^\'"]*)\5\s*\)\s*\)\s*$''',
stmt_na, re.DOTALL,
)
if m and seen_html:
sel = re.sub(r'^#work-area\s+', '', m.group(2))
target = selector_to_sx(sel, elements, var_names)
ops.append(f'(dom-set-attr {target} "{m.group(4)}" "{m.group(6)}")')
continue
# evaluate(() => document.querySelector(SEL).appendChild(document.createElement(TAG)))
# — used by mutation childList tests (cluster 32).
m = re.match(
r'''evaluate\(\s*\(\)\s*=>\s*document\.querySelector\(\s*([\'"])([^\'"]+)\1\s*\)'''
r'''\.appendChild\(\s*document\.createElement\(\s*([\'"])([\w-]+)\3\s*\)\s*\)\s*\)\s*$''',
stmt_na, re.DOTALL,
)
if m and seen_html:
sel = re.sub(r'^#work-area\s+', '', m.group(2))
target = selector_to_sx(sel, elements, var_names)
ops.append(f'(dom-append {target} (dom-create-element "{m.group(4)}"))')
continue
# evaluate(() => { var range = document.createRange(); # evaluate(() => { var range = document.createRange();
# var textNode = document.getElementById(ID).firstChild; # var textNode = document.getElementById(ID).firstChild;
# range.setStart(textNode, N); range.setEnd(textNode, M); # range.setStart(textNode, N); range.setEnd(textNode, M);
@@ -1407,21 +1399,6 @@ def generate_test_pw(test, elements, var_names, idx):
if test['name'] in SKIP_TEST_NAMES: if test['name'] in SKIP_TEST_NAMES:
return emit_skip_test(test) return emit_skip_test(test)
# Special case: init+def ordering. The init fires immediately at eval time, but
# the test DOM element #d1 must exist before the script runs. Create #d1 first.
if test.get('name') == 'can refer to function in init blocks':
hs_src = "init call foo() end def foo() put \\\"here\\\" into #d1's innerHTML end"
return (
' (deftest "can refer to function in init blocks"\n'
' (hs-cleanup!)\n'
' (let ((_el-d1 (dom-create-element "div")))\n'
' (dom-set-attr _el-d1 "id" "d1")\n'
' (dom-append (dom-body) _el-d1)\n'
' (guard (_e (true nil)) (eval-expr-cek (hs-to-sx (hs-compile "' + hs_src + '"))))\n'
' (assert= (dom-text-content (dom-query-by-id "d1")) "here"))\n'
' )'
)
pre_setups, ops = parse_dev_body(test['body'], elements, var_names) pre_setups, ops = parse_dev_body(test['body'], elements, var_names)
# `<script type="text/hyperscript">` blocks appear in both the # `<script type="text/hyperscript">` blocks appear in both the
@@ -1855,146 +1832,6 @@ def generate_eval_only_test(test, idx):
lines = [] lines = []
safe_name = sx_name(test['name']) safe_name = sx_name(test['name'])
# Special case: cluster-33 cookie tests. Each test calls a sequence of
# `_hyperscript("HS")` inside `page.evaluate(()=>{...})`. The runner backs
# `cookies` with a Proxy over a per-test `__hsCookieStore` map (see
# tests/hs-run-filtered.js). Tests handled: basic set, length-when-empty,
# update. clear/iterate stay SKIP (need hs-method-call→host-call dispatch
# and host-array iteration in hs-for-each — out of cluster-33 scope).
if test['name'] == 'basic set cookie values work':
return (
f' (deftest "{safe_name}"\n'
f' (hs-cleanup!)\n'
f' (assert (nil? (eval-hs "cookies.foo")))\n'
f' (eval-hs "set cookies.foo to \'bar\'")\n'
f' (assert= (eval-hs "cookies.foo") "bar"))'
)
if test['name'] == 'update cookie values work':
return (
f' (deftest "{safe_name}"\n'
f' (hs-cleanup!)\n'
f' (eval-hs "set cookies.foo to \'bar\'")\n'
f' (assert= (eval-hs "cookies.foo") "bar")\n'
f' (eval-hs "set cookies.foo to \'doh\'")\n'
f' (assert= (eval-hs "cookies.foo") "doh"))'
)
if test['name'] == 'length is 0 when no cookies are set':
return (
f' (deftest "{safe_name}"\n'
f' (hs-cleanup!)\n'
f' (assert= (eval-hs "cookies.length") 0))'
)
if test['name'] == 'basic clear cookie values work':
return (
f' (deftest "{safe_name}"\n'
f' (hs-cleanup!)\n'
f' (eval-hs "set cookies.foo to \'bar\'")\n'
f' (assert= (eval-hs "cookies.foo") "bar")\n'
f' (eval-hs "call cookies.clear(\'foo\')")\n'
f' (assert (nil? (eval-hs "cookies.foo"))))'
)
# Special case: cluster-29 init events. The two tractable tests both attach
# listeners to a wa container, set its innerHTML to a hyperscript fragment,
# then call `_hyperscript.processNode(wa)`. Hand-roll deftests using
# hs-boot-subtree! which now dispatches hyperscript:before:init / :after:init.
if test.get('name') == 'fires hyperscript:before:init and hyperscript:after:init':
return (
f' (deftest "{safe_name}"\n'
f' (hs-cleanup!)\n'
f' (let ((wa (dom-create-element "div"))\n'
f' (events (list)))\n'
f' (dom-listen wa "hyperscript:before:init"\n'
f' (fn (e) (set! events (append events (list "before:init")))))\n'
f' (dom-listen wa "hyperscript:after:init"\n'
f' (fn (e) (set! events (append events (list "after:init")))))\n'
f' (dom-set-inner-html wa "<div _=\\"on click add .foo\\"></div>")\n'
f' (hs-boot-subtree! wa)\n'
f' (assert= events (list "before:init" "after:init")))\n'
f' )'
)
if test.get('name') == 'hyperscript:before:init can cancel initialization':
return (
f' (deftest "{safe_name}"\n'
f' (hs-cleanup!)\n'
f' (let ((wa (dom-create-element "div")))\n'
f' (dom-listen wa "hyperscript:before:init"\n'
f' (fn (e) (host-call e "preventDefault")))\n'
f' (dom-set-inner-html wa "<div _=\\"on click add .foo\\"></div>")\n'
f' (hs-boot-subtree! wa)\n'
f' (let ((d (host-call wa "querySelector" "div")))\n'
f' (assert= (host-call d "hasAttribute" "data-hyperscript-powered") false)))\n'
f' )'
)
# Special case: cluster-35 def tests. Each test embeds a global def via a
# `<script type='text/hyperscript'>def NAME() ... end</script>` tag and
# then a `<div _='on click call NAME() ...'>` that invokes it. Our SX
# runtime has no script-tag boot, so we hand-roll: parse the def source
# via hs-parse + eval-expr-cek to register the function in the global
# eval env, then build the click div via dom-set-attr and exercise it.
if test.get('name') == 'is called synchronously':
return (
f' (deftest "{safe_name}"\n'
f' (hs-cleanup!)\n'
f' (eval-expr-cek (hs-to-sx (first (hs-parse (hs-tokenize "def foo() log me end")))))\n'
f' (let ((wa (dom-create-element "div"))\n'
f' (b (dom-create-element "div"))\n'
f' (d1 (dom-create-element "div")))\n'
f' (dom-set-attr d1 "id" "d1")\n'
f' (dom-set-attr b "_" "on click call foo() then add .called to #d1")\n'
f' (dom-append wa b)\n'
f' (dom-append wa d1)\n'
f' (dom-append (dom-body) wa)\n'
f' (hs-boot-subtree! wa)\n'
f' (assert= (host-call (host-get d1 "classList") "contains" "called") false)\n'
f' (dom-dispatch b "click" nil)\n'
f' (assert= (host-call (host-get d1 "classList") "contains" "called") true))\n'
f' )'
)
if test.get('name') == 'can call asynchronously':
return (
f' (deftest "{safe_name}"\n'
f' (hs-cleanup!)\n'
f' (eval-expr-cek (hs-to-sx (first (hs-parse (hs-tokenize "def foo() wait 1ms log me end")))))\n'
f' (let ((wa (dom-create-element "div"))\n'
f' (b (dom-create-element "div"))\n'
f' (d1 (dom-create-element "div")))\n'
f' (dom-set-attr d1 "id" "d1")\n'
f' (dom-set-attr b "_" "on click call foo() then add .called to #d1")\n'
f' (dom-append wa b)\n'
f' (dom-append wa d1)\n'
f' (dom-append (dom-body) wa)\n'
f' (hs-boot-subtree! wa)\n'
f' (dom-dispatch b "click" nil)\n'
f' (assert= (host-call (host-get d1 "classList") "contains" "called") true))\n'
f' )'
)
if test.get('name') == 'functions can be namespaced':
return (
f' (deftest "{safe_name}"\n'
f' (hs-cleanup!)\n'
f' ;; Manually create utils dict with foo as a callable. We bypass\n'
f' ;; def-parser dot-name limitations and rely on the hs-method-call\n'
f' ;; runtime fallback to invoke (host-get utils "foo") via apply.\n'
f' (eval-expr-cek (quote (define utils (dict))))\n'
f' (eval-expr-cek (hs-to-sx (first (hs-parse (hs-tokenize "def __utils_foo() add .called to #d1 end")))))\n'
f' (eval-expr-cek (quote (host-set! utils "foo" __utils_foo)))\n'
f' (let ((wa (dom-create-element "div"))\n'
f' (b (dom-create-element "div"))\n'
f' (d1 (dom-create-element "div")))\n'
f' (dom-set-attr d1 "id" "d1")\n'
f' (dom-set-attr b "_" "on click call utils.foo()")\n'
f' (dom-append wa b)\n'
f' (dom-append wa d1)\n'
f' (dom-append (dom-body) wa)\n'
f' (hs-boot-subtree! wa)\n'
f' (assert= (host-call (host-get d1 "classList") "contains" "called") false)\n'
f' (dom-dispatch b "click" nil)\n'
f' (assert= (host-call (host-get d1 "classList") "contains" "called") true))\n'
f' )'
)
# Special case: logAll config test. Body sets `_hyperscript.config.logAll = true`, # Special case: logAll config test. Body sets `_hyperscript.config.logAll = true`,
# then mutates an element's innerHTML and calls `_hyperscript.processNode`. # then mutates an element's innerHTML and calls `_hyperscript.processNode`.
# Our runtime exposes this via hs-set-log-all! + hs-log-captured; we reuse # Our runtime exposes this via hs-set-log-all! + hs-log-captured; we reuse
@@ -2775,7 +2612,6 @@ output.append(';; Bind `window` and `document` as plain SX symbols so HS code th
output.append(';; references them (e.g. `window.tmp`) can resolve through the host.') output.append(';; references them (e.g. `window.tmp`) can resolve through the host.')
output.append('(define window (host-global "window"))') output.append('(define window (host-global "window"))')
output.append('(define document (host-global "document"))') output.append('(define document (host-global "document"))')
output.append('(define cookies (host-global "cookies"))')
output.append('') output.append('')
output.append('(define hs-test-el') output.append('(define hs-test-el')
output.append(' (fn (tag hs-src)') output.append(' (fn (tag hs-src)')
@@ -2787,11 +2623,7 @@ output.append(' el)))')
output.append('') output.append('')
output.append('(define hs-cleanup!') output.append('(define hs-cleanup!')
output.append(' (fn ()') output.append(' (fn ()')
output.append(' (begin') output.append(' (dom-set-inner-html (dom-body) "")))')
output.append(' (dom-set-inner-html (dom-body) "")')
output.append(' ;; Reset global runtime state that prior tests may have set.')
output.append(' (hs-set-default-hide-strategy! nil)')
output.append(' (hs-set-log-all! false))))')
output.append('') output.append('')
output.append(';; Evaluate a hyperscript expression and return either the expression') output.append(';; Evaluate a hyperscript expression and return either the expression')
output.append(';; value or `it` (whichever is non-nil). Multi-statement scripts that') output.append(';; value or `it` (whichever is non-nil). Multi-statement scripts that')