Files
rose-ash/spec/tests/test-foreign.sx
giles 000f285ae8 Step 11: define-foreign FFI + transpiler mutable globals fix
FFI: define-foreign special form in evaluator — registry, param parser,
kwargs parser, binding resolver, type checker, lambda builder, dispatcher.
Generates callable lambdas that route through foreign-dispatch to host-call.
24 tests in test-foreign.sx (registry, parsing, resolution, type checking).

Transpiler: fix mutable global ref emission — ml-emit-define now emits
both X_ref = ref <init> and X_ = <init> for starred globals (was missing
the ref definition entirely, broke retranspilation). Add *provide-batch-depth*,
*provide-batch-queue*, *provide-subscribers* to mutable globals list.

Evaluator: add missing (define *provide-batch-queue* (list)) and
(define *provide-subscribers* (dict)) — were only in hand-edited sx_ref.ml.

Known: 36 bind-tracking + 8 capability test failures on retranspilation
(pre-existing transpiler local-ref shadowing bug, not caused by FFI).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:22:33 +00:00

179 lines
6.1 KiB
Plaintext

;; FFI tests — define-foreign, *foreign-registry*, foreign-dispatch
(defsuite
"foreign-registry-basic"
(deftest
"define-foreign registers in *foreign-registry*"
(define-foreign my-abs (x :as number) :returns :number :js "Math.abs")
(assert (foreign-registered? "my-abs")))
(deftest
"foreign-lookup returns spec dict"
(define-foreign my-floor (x :as number) :returns :number :js "Math.floor")
(let
((spec (foreign-lookup "my-floor")))
(assert= (get spec "name") "my-floor")
(assert= (get spec "js") "Math.floor")
(assert= (get spec "returns") "number")))
(deftest
"foreign-names includes registered names"
(define-foreign my-ceil (x :as number) :returns :number :js "Math.ceil")
(assert (contains? (foreign-names) "my-ceil")))
(deftest
"define-foreign creates callable lambda"
(define-foreign my-round (x :as number) :returns :number :js "Math.round")
(assert (lambda? my-round)))
(deftest
"multiple define-foreign coexist"
(define-foreign ff-a () :js "Date.now")
(define-foreign ff-b (s :as string) :js "parseInt")
(assert (foreign-registered? "ff-a"))
(assert (foreign-registered? "ff-b"))))
(defsuite
"foreign-param-parsing"
(deftest
"single param with type"
(define-foreign fp-one (url :as string) :js "encodeURI")
(let
((spec (foreign-lookup "fp-one")))
(let
((params (get spec "params")))
(assert= (len params) 1)
(assert= (get (first params) "name") "url")
(assert= (get (first params) "type") "string"))))
(deftest
"multiple params with types"
(define-foreign fp-two (base :as string radix :as number) :js "parseInt")
(let
((spec (foreign-lookup "fp-two")))
(let
((params (get spec "params")))
(assert= (len params) 2)
(assert= (get (first params) "name") "base")
(assert= (get (first params) "type") "string")
(assert= (get (nth params 1) "name") "radix")
(assert= (get (nth params 1) "type") "number"))))
(deftest
"no params"
(define-foreign fp-none () :js "Date.now")
(let
((spec (foreign-lookup "fp-none")))
(assert= (len (get spec "params")) 0)))
(deftest
"param without :as defaults to any"
(define-foreign fp-any (x) :js "String")
(let
((spec (foreign-lookup "fp-any")))
(let
((params (get spec "params")))
(assert= (get (first params) "type") "any"))))
(deftest
"callback param type"
(define-foreign fp-cb (handler :as callback) :js "setTimeout")
(let
((spec (foreign-lookup "fp-cb")))
(assert= (get (first (get spec "params")) "type") "callback"))))
(defsuite
"foreign-binding-resolution"
(deftest
"dotted binding splits into object + method"
(let
((resolved (foreign-resolve-binding "localStorage.getItem")))
(assert= (get resolved "object") "localStorage")
(assert= (get resolved "method") "getItem")))
(deftest
"simple binding has nil object"
(let
((resolved (foreign-resolve-binding "parseInt")))
(assert= (get resolved "object") nil)
(assert= (get resolved "method") "parseInt")))
(deftest
"deep dotted binding preserves object path"
(let
((resolved (foreign-resolve-binding "window.navigator.language")))
(assert= (get resolved "object") "window.navigator")
(assert= (get resolved "method") "language")))
(deftest
"single segment is method only"
(let
((resolved (foreign-resolve-binding "alert")))
(assert= (get resolved "object") nil)
(assert= (get resolved "method") "alert"))))
(defsuite
"foreign-kwargs"
(deftest
"returns keyword parsed correctly"
(define-foreign fk-ret (x :as number) :returns :number :js "Math.abs")
(assert= (get (foreign-lookup "fk-ret") "returns") "number"))
(deftest
"doc keyword stored"
(define-foreign fk-doc () :js "Date.now" :doc "Get current timestamp")
(assert= (get (foreign-lookup "fk-doc") "doc") "Get current timestamp"))
(deftest
"capability keyword stored"
(define-foreign
fk-cap
(url :as string)
:returns :promise
:js "window.fetch"
:capability :network)
(assert= (get (foreign-lookup "fk-cap") "capability") "network"))
(deftest
"promise return type"
(define-foreign
fk-async
(url :as string)
:returns :promise
:js "window.fetch")
(assert= (get (foreign-lookup "fk-async") "returns") "promise")))
(defsuite
"foreign-build-lambda"
(deftest
"sync foreign builds non-perform lambda"
(define-foreign fbl-sync (x :as number) :returns :number :js "Math.abs")
(let
((spec (foreign-lookup "fbl-sync")))
(let
((expr (foreign-build-lambda spec)))
(assert (list? expr))
(assert= (symbol-name (first expr)) "fn"))))
(deftest
"async foreign builds perform-wrapping lambda"
(define-foreign
fbl-async
(url :as string)
:returns :promise
:js "window.fetch")
(let
((spec (foreign-lookup "fbl-async")))
(let
((expr (foreign-build-lambda spec)))
(assert (list? expr))
(let
((body (nth expr 2)))
(assert= (symbol-name (first body)) "perform"))))))
(defsuite
"foreign-type-checking"
(deftest
"foreign-check-args accepts correct types"
(foreign-check-args "test" (list {:type "number" :name "x"}) (list 42))
(assert true))
(deftest
"foreign-check-args rejects wrong type"
(let
((err (guard (e (#t (error-message e))) (foreign-check-args "test" (list {:type "number" :name "x"}) (list "not-a-number")) nil)))
(assert (contains? err "expected number"))))
(deftest
"foreign-check-args accepts any type"
(foreign-check-args "test" (list {:type "any" :name "x"}) (list "hello"))
(foreign-check-args "test" (list {:type "any" :name "x"}) (list 42))
(assert true))
(deftest
"foreign-check-args rejects too few args"
(let
((err (guard (e (#t (error-message e))) (foreign-check-args "test" (list {:type "number" :name "x"} {:type "number" :name "y"}) (list 1)) nil)))
(assert (contains? err "expected 2 args")))))