Generate SSD detector from typed Kotlin

ober

c6dcf0f846fe91a03c5b49a3282d7a21374926fe

diff --git a/.build.yml b/.build.yml
index 9dc5a6a..cdc3b7f 100644
--- a/.build.yml
+++ b/.build.yml
@@ -5,7 +5,7 @@ packages:
   - make=4.4.1-r4
 sources:
   # Build dependency: full immutable commit, mirrored in dependencies.lock.json.
-  - "https://git.sr.ht/~lisp/jerboa#fccc76e702602c0538c28935ef7b5e228947bded"
+  - "https://git.sr.ht/~lisp/jerboa#a62bac2471f691b7087ea3594db3d375b2097cfd"
   # The second source is the build subject selected by the SourceHut submitter.
   - https://git.sr.ht/~lisp/jerboa-android
 tasks:
@@ -14,6 +14,6 @@ tasks:
       test "$(apk info -v chez-scheme)" = chez-scheme-10.3.0-r2
       test "$(apk info -v git)" = git-2.54.0-r0
       test "$(apk info -v make)" = make-4.4.1-r4
-      test "$(git -C ../jerboa rev-parse HEAD)" = fccc76e702602c0538c28935ef7b5e228947bded
-      test "$(git -C ../jerboa rev-parse 'HEAD^{tree}')" = 041beddb9852658fbd7865cc1073272583513867
+      test "$(git -C ../jerboa rev-parse HEAD)" = a62bac2471f691b7087ea3594db3d375b2097cfd
+      test "$(git -C ../jerboa rev-parse 'HEAD^{tree}')" = a1e7cd426ed7957d29801409eb5e56207ea5e1a3
       JERBOA="chez --libdirs .:../jerboa/lib --script" make test
diff --git a/dependencies.lock.json b/dependencies.lock.json
index 1e6fd49..44bb927 100644
--- a/dependencies.lock.json
+++ b/dependencies.lock.json
@@ -11,8 +11,8 @@
   "generator_runtime": {
     "name": "jerboa",
     "repository": "https://git.sr.ht/~lisp/jerboa",
-    "commit": "fccc76e702602c0538c28935ef7b5e228947bded",
-    "tree": "041beddb9852658fbd7865cc1073272583513867"
+    "commit": "a62bac2471f691b7087ea3594db3d375b2097cfd",
+    "tree": "a1e7cd426ed7957d29801409eb5e56207ea5e1a3"
   },
   "assurance_tools": {
     "osv_scanner": {
diff --git a/scripts/verify-supply-chain.sh b/scripts/verify-supply-chain.sh
index 200f26f..e8fa80c 100755
--- a/scripts/verify-supply-chain.sh
+++ b/scripts/verify-supply-chain.sh
@@ -3,8 +3,8 @@ set -eu
 
 repo=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd -P)
 lock="$repo/dependencies.lock.json"
-jerboa_commit=fccc76e702602c0538c28935ef7b5e228947bded # gitsafe:ignore
-jerboa_tree=041beddb9852658fbd7865cc1073272583513867 # gitsafe:ignore
+jerboa_commit=a62bac2471f691b7087ea3594db3d375b2097cfd # gitsafe:ignore
+jerboa_tree=a1e7cd426ed7957d29801409eb5e56207ea5e1a3 # gitsafe:ignore
 gradle_sha=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78
 jdk_macos_sha=8fa1eff40bb637a33613b2ccb8b12c70dc3661cc22cf8e784943715769a05336
 jdk_linux_sha=d8afc263758141a66e0e3aafc321e783f7016696f4eaea067d340a269037d331
diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index 778a78b..c57896b 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -3422,7 +3422,7 @@
                                   (float32 w)
                                   (float32 h)
                                   "android-contour")))))))))))))
-        (def (detectorComponentCells (components : (List Component))
+        (def (detectorComponentCells (components : (MutableList Component))
                                      (detector : String)) : (MutableList SsdCell)
           (for/fold ((out (mutable-list-empty SsdCell)))
                     ((i (in-range (int32 0) (list-size components))))
@@ -3432,7 +3432,7 @@
                   (begin
                     (mutable-list-add! out (nullable-get cell))
                     out)))))
-        (def (detectorToneCells (components : (List Component))
+        (def (detectorToneCells (components : (MutableList Component))
                                 (gray : IntArray)
                                 (width : Int32)
                                 (height : Int32)) : (MutableList SsdCell)
@@ -3444,7 +3444,7 @@
                   (begin
                     (mutable-list-add! out (nullable-get cell))
                     out)))))
-        (def (detectorContourCells (components : (List Component))
+        (def (detectorContourCells (components : (MutableList Component))
                                    (edge : BooleanArray)
                                    (width : Int32)
                                    (height : Int32)) : (MutableList SsdCell)
@@ -5102,7 +5102,7 @@
        "                        detectorNote = \", rust detector\""
        "                        detectorSource = if (forceDetect && existingTruthGroupCount > 0) \"forced-rust-plus-truth\" else \"rust-detector\""
        "                    } else {"
-       "                        detectedCells = SsdDetector.detectCells(bitmap)"
+       "                        detectedCells = detectCells(bitmap)"
        "                        detectorSource = if (forceDetect && existingTruthGroupCount > 0) \"forced-android-plus-truth\" else \"android-fallback\""
        "                    }"
        "                    ssdSessionAddDetectedCellsInsideArea(session, detectedCells)"
@@ -6060,81 +6060,131 @@
        "    }"
        "}"
        ))
-    (kotlin-file-lines "com/sfb/ssdreview/SsdDetector.kt"
-      (
-       "package com.sfb.ssdreview"
-       ""
-       "import android.graphics.Bitmap"
-       "object SsdDetector {"
-       "    fun detectCells(bitmap: Bitmap): MutableList<SsdCell> {"
-       "        val width = bitmap.width"
-       "        val height = bitmap.height"
-       "        val pixels = IntArray(width * height)"
-       "        bitmap.getPixels(pixels, 0, width, 0, 0, width, height)"
-       "        val gray = grayscale(pixels)"
-       "        val cells = mutableListOf<SsdCell>()"
-       ""
-       "        cells += detectorComponentCells("
-       "            detectColorComponents(width, height, pixels, \"android-color\"),"
-       "            \"android-color\""
-       "        )"
-       ""
-       "        cells += detectorToneCells("
-       "            detectToneComponents(width, height, gray, \"android-tone\"),"
-       "            gray,"
-       "            width,"
-       "            height"
-       "        )"
-       ""
-       "        var merged = mergeDuplicateCells(cells)"
-       "        if (merged.size < 50) {"
-       "            merged += contourCells(width, height, gray)"
-       "            merged = mergeDuplicateCells(merged)"
-       "        }"
-       "        return merged"
-       "    }"
-       ""
-       "    fun shrinkRectToInk(bitmap: Bitmap, rect: FloatArray): FloatArray? {"
-       "        return shrinkRectByMaskKind(bitmap, rect, 0)"
-       "    }"
-       ""
-       "    fun shrinkRectToBoxFill(bitmap: Bitmap, rect: FloatArray): FloatArray? {"
-       "        return shrinkRectByMaskKind(bitmap, rect, 1)"
-       "    }"
-       ""
-       "    private fun shrinkRectByMaskKind("
-       "        bitmap: Bitmap,"
-       "        rect: FloatArray,"
-       "        kind: Int"
-       "    ): FloatArray? {"
-       "        val x1 = rect[0].toInt().coerceIn(0, bitmap.width - 1)"
-       "        val y1 = rect[1].toInt().coerceIn(0, bitmap.height - 1)"
-       "        val x2 = rect[2].toInt().coerceIn(0, bitmap.width)"
-       "        val y2 = rect[3].toInt().coerceIn(0, bitmap.height)"
-       "        val w = x2 - x1"
-       "        val h = y2 - y1"
-       "        if (w < 4 || h < 4 || w * h > 1_200_000) return null"
-       "        val pixels = IntArray(w * h)"
-       "        bitmap.getPixels(pixels, 0, w, x1, y1, w, h)"
-       "        return if (kind == 0) {"
-       "            shrinkInkMaskBounds(pixels, w, h, x1, y1, bitmap.width, bitmap.height)"
-       "        } else {"
-       "            shrinkBoxFillMaskBounds(pixels, w, h, x1, y1, bitmap.width, bitmap.height)"
-       "        }"
-       "    }"
-       ""
-       "    private fun contourCells(width: Int, height: Int, gray: IntArray): List<SsdCell> {"
-       "        val edge = edgeMask(gray, width, height)"
-       "        return detectorContourCells("
-       "            detectEdgeComponents(width, height, edge, \"android-contour\"),"
-       "            edge,"
-       "            width,"
-       "            height"
-       "        )"
-       "    }"
-       ""
-       "}"
-       ))
+    (typed-kotlin-file "com/sfb/ssdreview/SsdDetector.kt"
+      (kotlin-imports (android graphics Bitmap))
+      (typed-library (com sfb ssdreview)
+        (export detectCells shrinkRectToInk shrinkRectToBoxFill)
+        (type Bitmap)
+        (type Int32)
+        (type IntArray)
+        (type Float32)
+        (type FloatArray)
+        (extern (bitmapWidth (bitmap : Bitmap)) : Int32
+          (kotlin-member-get width))
+        (extern (bitmapHeight (bitmap : Bitmap)) : Int32
+          (kotlin-member-get height))
+        (extern (bitmapGetPixels (bitmap : Bitmap)
+                                 (pixels : IntArray)
+                                 (offset : Int32)
+                                 (stride : Int32)
+                                 (x : Int32)
+                                 (y : Int32)
+                                 (width : Int32)
+                                 (height : Int32)) : Unit
+          (kotlin-member-call getPixels))
+        (extern (float32ToInt32 (value : Float32)) : Int32
+          (kotlin-member-call toInt))
+        (def (clampDetectorInt32 (value : Int32)
+                                 (floor : Int32)
+                                 (ceiling : Int32)) : Int32
+          (if (< value floor)
+            floor
+            (if (> value ceiling) ceiling value)))
+        (def (contourCells (width : Int32) (height : Int32) (gray : IntArray)) : (MutableList SsdCell)
+          (let ((edge (edgeMask gray width height)))
+            (detectorContourCells
+              (detectEdgeComponents width height edge "android-contour")
+              edge
+              width
+              height)))
+        (def (detectCells (bitmap : Bitmap)) : (MutableList SsdCell)
+          (let ((width (bitmapWidth bitmap))
+                (height (bitmapHeight bitmap)))
+            (let ((pixels
+                    (int-array-build
+                      (* width height)
+                      (index (int32 0)))))
+              (begin
+                (bitmapGetPixels
+                  bitmap
+                  pixels
+                  (int32 0)
+                  width
+                  (int32 0)
+                  (int32 0)
+                  width
+                  height)
+                (let ((gray (grayscale pixels))
+                      (cells (mutable-list-empty SsdCell)))
+                  (begin
+                    (mutable-list-add-all!
+                      cells
+                      (detectorComponentCells
+                        (detectColorComponents width height pixels "android-color")
+                        "android-color"))
+                    (mutable-list-add-all!
+                      cells
+                      (detectorToneCells
+                        (detectToneComponents width height gray "android-tone")
+                        gray
+                        width
+                        height))
+                    (let ((merged (mergeDuplicateCells cells)))
+                      (if (< (list-size merged) (int32 50))
+                        (begin
+                          (mutable-list-add-all! merged (contourCells width height gray))
+                          (mergeDuplicateCells merged))
+                        merged))))))))
+        (def (shrinkRectByMaskKind (bitmap : Bitmap)
+                                   (rect : FloatArray)
+                                   (kind : Int32)) : (Nullable FloatArray)
+          (let ((bitmapW (bitmapWidth bitmap))
+                (bitmapH (bitmapHeight bitmap)))
+            (let ((x1 (clampDetectorInt32
+                        (float32ToInt32 (float-array-ref rect (int32 0)))
+                        (int32 0)
+                        (- bitmapW (int32 1))))
+                  (y1 (clampDetectorInt32
+                        (float32ToInt32 (float-array-ref rect (int32 1)))
+                        (int32 0)
+                        (- bitmapH (int32 1))))
+                  (x2 (clampDetectorInt32
+                        (float32ToInt32 (float-array-ref rect (int32 2)))
+                        (int32 0)
+                        bitmapW))
+                  (y2 (clampDetectorInt32
+                        (float32ToInt32 (float-array-ref rect (int32 3)))
+                        (int32 0)
+                        bitmapH)))
+              (let ((w (- x2 x1))
+                    (h (- y2 y1)))
+                (if (or (or (< w (int32 4))
+                            (< h (int32 4)))
+                        (> (* w h) (int32 1200000)))
+                  (nullable-none FloatArray)
+                  (let ((pixels
+                          (int-array-build
+                            (* w h)
+                            (index (int32 0)))))
+                    (begin
+                      (bitmapGetPixels
+                        bitmap
+                        pixels
+                        (int32 0)
+                        w
+                        x1
+                        y1
+                        w
+                        h)
+                      (if (= kind (int32 0))
+                        (shrinkInkMaskBounds pixels w h x1 y1 bitmapW bitmapH)
+                        (shrinkBoxFillMaskBounds pixels w h x1 y1 bitmapW bitmapH)))))))))
+        (def (shrinkRectToInk (bitmap : Bitmap)
+                              (rect : FloatArray)) : (Nullable FloatArray)
+          (shrinkRectByMaskKind bitmap rect (int32 0)))
+        (def (shrinkRectToBoxFill (bitmap : Bitmap)
+                                  (rect : FloatArray)) : (Nullable FloatArray)
+          (shrinkRectByMaskKind bitmap rect (int32 1)))))
     (kotlin-file-lines "com/sfb/ssdreview/SsdVisionBridge.kt"
       (
        "package com.sfb.ssdreview"
@@ -6478,8 +6528,8 @@
 	       "            bboxForCells(matching)"
        "        } else {"
        "            bitmap?.let {"
-       "                SsdDetector.shrinkRectToBoxFill(it, rect)"
-       "                    ?: SsdDetector.shrinkRectToInk(it, rect)"
+       "                shrinkRectToBoxFill(it, rect)"
+       "                    ?: shrinkRectToInk(it, rect)"
        "            } ?: rect"
        "        }"
 	       "        onStatus?.invoke("