Route Kotlin through the structural matcher

ober

de6d2cf11d8acc603ca14a9435eafc4892ef07db

diff --git a/HANDOFF_OPUS_4_8.md b/HANDOFF_OPUS_4_8.md
index 60b8138..593ed84 100644
--- a/HANDOFF_OPUS_4_8.md
+++ b/HANDOFF_OPUS_4_8.md
@@ -7,7 +7,7 @@ Packaged Semgrep oracle: `/Users/user/.local/bin/semgrep`
 Branch: `java-structural-migration` (off `main`; carries java+scala+rust); Go
 and PHP migrations already merged.
 
-## Summary: Go, PHP, Java, Scala, Rust, Ruby all on the real structural matcher
+## Summary: Go, PHP, Java, Scala, Rust, Ruby, Kotlin on the structural matcher
 
 Five languages migrated off the regex generic-matcher approximation onto the
 real tree-sitter structural matcher; `generic-language?` (scan.ss ~809) no
@@ -16,7 +16,7 @@ compiled into `jerboa-treesitter`: go/php/scala/rust (ABI 15), java (ABI 14,
 within the runtime's 13-15 window); scala/rust/php carry external scanners,
 go/java are parser-only. `make test` 321/321; smoke 15/15 (parse case per lang).
 Per-language normalize-oracle fixture results: java 42/42, scala 18/18,
-rust 11/11, ruby 4/4, php 29/29, go 16/16.
+rust 11/11, ruby 4/4, kotlin 4/4, php 29/29, go 16/16.
 
 ### Java/Scala/Rust migration (branch `java-structural-migration`)
 
@@ -124,8 +124,10 @@ The two remaining fixtures are blocked by the same gap that limits ~12
 languages, so closing them is the tip of the larger work:
 
 1. **AST matching for the "generic" languages (the big one).**
-   python/js/ts/go/php/java/scala/rust/ruby now use the tree-sitter structural
-   matcher. C#, Swift, Dart, Kotlin, C, C++ remain `generic-language?`
+   python/js/ts/go/php/java/scala/rust/ruby/kotlin now use the tree-sitter
+   structural matcher. C#, Swift, Dart, C, C++ remain (Dart grammar is vendored
+   but deferred: bare patterns parse as top-level declarations, needing Go-style
+   scaffolding) `generic-language?`
    (scan.ss ~809) and use the regex-based `scan-generic-pattern`. That matcher
    approximates patterns with regexes and cannot express several Semgrep
    constructs. Real tree-sitter grammars for these languages would replace the
diff --git a/lib/semgrep/match/structural.sls b/lib/semgrep/match/structural.sls
index 4fc841b..9e9ae46 100644
--- a/lib/semgrep/match/structural.sls
+++ b/lib/semgrep/match/structural.sls
@@ -583,6 +583,10 @@
          [(string=? language "ruby")
           (or (string=? type "program")
               (string=? type "expression_statement"))]
+         [(string=? language "kotlin")
+          (or (string=? type "source_file")
+              (string=? type "statement")
+              (string=? type "expression_statement"))]
          [else #f]))
   (def (normalized-pattern-root language root)
        (let loop ([current root] [owned '()])
diff --git a/lib/semgrep/parse/parse-target.sls b/lib/semgrep/parse/parse-target.sls
index de71d60..a81ed5a 100644
--- a/lib/semgrep/parse/parse-target.sls
+++ b/lib/semgrep/parse/parse-target.sls
@@ -29,6 +29,7 @@
            [(string=? canonical "scala") 'scala]
            [(string=? canonical "rust") 'rust]
            [(string=? canonical "ruby") 'ruby]
+           [(string=? canonical "kotlin") 'kotlin]
            [else
             (error 'parse-target-string
               "unsupported language in current MVP"
diff --git a/lib/semgrep/scan.sls b/lib/semgrep/scan.sls
index 6451762..5ba5659 100644
--- a/lib/semgrep/scan.sls
+++ b/lib/semgrep/scan.sls
@@ -748,7 +748,6 @@
              (string=? canonical "dart")
              (string=? canonical "move_on_aptos")
              (string=? canonical "julia")
-             (string=? canonical "kotlin")
              (string=? canonical "cpp"))))
   (def (c-language? language)
        (let ([canonical (or (canonical-language language)
@@ -31277,6 +31276,8 @@
           '("$L = $R" "$L[$I] = $R" "val $L = $R" "var $L = $R")]
          [(string=? language "rust")
           '("$L = $R" "$L[$I] = $R" "let $L = $R" "let mut $L = $R")]
+         [(string=? language "kotlin")
+          '("$L = $R" "$L[$I] = $R" "val $L = $R" "var $L = $R")]
          [else '("$L = $R" "$L[$I] = $R")]))
   (def (scan-implicit-assignment-propagators rule language
          path source target-root)
diff --git a/src/.jerbuild-hashes b/src/.jerbuild-hashes
index cea8124..6d5a28d 100644
--- a/src/.jerbuild-hashes
+++ b/src/.jerbuild-hashes
@@ -2,12 +2,12 @@
   ("src/semgrep/result.ss" . "22D23E40B49BA529")
   ("src/semgrep/output/json.ss" . "293881CFA2ADB7BC")
   ("src/semgrep/lang.ss" . "6982E07679D20836")
-  ("src/semgrep/parse/parse-target.ss" . "AD193DA8CA67B2B2")
-  ("src/semgrep/scan.ss" . "82F2DEBA214D9CFD")
-  ("src/semgrep/output/text.ss" . "BE476CB84B807FBA")
+  ("src/semgrep/parse/parse-target.ss" . "904B71D207617229")
+  ("src/semgrep/scan.ss" . "A994804D70D4F26E")
   ("src/semgrep/fix.ss" . "2E5B65B1FEF3B2B1")
-  ("src/semgrep/schema/lang.ss" . "CAE2CA859C9A9FD0")
+  ("src/semgrep/output/text.ss" . "BE476CB84B807FBA")
   ("src/semgrep/rule.ss" . "E12C108153C181FA")
-  ("src/semgrep/match/structural.ss" . "3CE2210D6FE588E4")
+  ("src/semgrep/schema/lang.ss" . "CAE2CA859C9A9FD0")
+  ("src/semgrep/match/structural.ss" . "6047821D83B32FCE")
   ("src/semgrep/main.ss" . "A4EC9E7F2A09D25E")
   ("src/semgrep/cli.ss" . "EBDC4B1DAD3F13CC"))
diff --git a/src/semgrep/match/structural.ss b/src/semgrep/match/structural.ss
index 4a41fb0..4d0feb9 100644
--- a/src/semgrep/match/structural.ss
+++ b/src/semgrep/match/structural.ss
@@ -607,6 +607,10 @@
     [(string=? language "ruby")
      (or (string=? type "program")
          (string=? type "expression_statement"))]
+    [(string=? language "kotlin")
+     (or (string=? type "source_file")
+         (string=? type "statement")
+         (string=? type "expression_statement"))]
     [else #f]))
 
 (def (normalized-pattern-root language root)
diff --git a/src/semgrep/parse/parse-target.ss b/src/semgrep/parse/parse-target.ss
index 02c38d9..1872685 100644
--- a/src/semgrep/parse/parse-target.ss
+++ b/src/semgrep/parse/parse-target.ss
@@ -21,6 +21,7 @@
       [(string=? canonical "scala") 'scala]
       [(string=? canonical "rust") 'rust]
       [(string=? canonical "ruby") 'ruby]
+      [(string=? canonical "kotlin") 'kotlin]
       [else
        (error 'parse-target-string "unsupported language in current MVP" language)])))
 
diff --git a/src/semgrep/scan.ss b/src/semgrep/scan.ss
index 0f334b8..b4d1b29 100644
--- a/src/semgrep/scan.ss
+++ b/src/semgrep/scan.ss
@@ -820,7 +820,6 @@
         (string=? canonical "dart")
         (string=? canonical "move_on_aptos")
         (string=? canonical "julia")
-        (string=? canonical "kotlin")
         (string=? canonical "cpp"))))
 
 (def (c-language? language)
@@ -31263,6 +31262,12 @@
        "$L[$I] = $R"
        "let $L = $R"
        "let mut $L = $R")]
+    [(string=? language "kotlin")
+     ;; Kotlin bindings are val/var property declarations.
+     '("$L = $R"
+       "$L[$I] = $R"
+       "val $L = $R"
+       "var $L = $R")]
     [else '("$L = $R"
             "$L[$I] = $R")]))