Pārlūkot izejas kodu

[haxe] Plan for serializer generator

Mario Zechner 1 mēnesi atpakaļ
vecāks
revīzija
108f9bf355
3 mainītis faili ar 1014 papildinājumiem un 12 dzēšanām
  1. 76 8
      tests/package-lock.json
  2. 927 0
      tests/plan-haxe.md
  3. 11 4
      todos/todos.md

+ 76 - 8
tests/package-lock.json

@@ -20,6 +20,7 @@
       "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.1.2.tgz",
       "integrity": "sha512-yq8ZZuKuBVDgAS76LWCfFKHSYIAgqkxVB3mGVVpOe2vSkUTs7xG46zXZeNPRNVjiJuw0SZ3+J2rXiYx0RUpfGg==",
       "dev": true,
+      "license": "MIT OR Apache-2.0",
       "bin": {
         "biome": "bin/biome"
       },
@@ -49,6 +50,7 @@
         "arm64"
       ],
       "dev": true,
+      "license": "MIT OR Apache-2.0",
       "optional": true,
       "os": [
         "darwin"
@@ -65,6 +67,7 @@
         "x64"
       ],
       "dev": true,
+      "license": "MIT OR Apache-2.0",
       "optional": true,
       "os": [
         "darwin"
@@ -81,6 +84,7 @@
         "arm64"
       ],
       "dev": true,
+      "license": "MIT OR Apache-2.0",
       "optional": true,
       "os": [
         "linux"
@@ -97,6 +101,7 @@
         "arm64"
       ],
       "dev": true,
+      "license": "MIT OR Apache-2.0",
       "optional": true,
       "os": [
         "linux"
@@ -113,6 +118,7 @@
         "x64"
       ],
       "dev": true,
+      "license": "MIT OR Apache-2.0",
       "optional": true,
       "os": [
         "linux"
@@ -129,6 +135,7 @@
         "x64"
       ],
       "dev": true,
+      "license": "MIT OR Apache-2.0",
       "optional": true,
       "os": [
         "linux"
@@ -145,6 +152,7 @@
         "arm64"
       ],
       "dev": true,
+      "license": "MIT OR Apache-2.0",
       "optional": true,
       "os": [
         "win32"
@@ -161,6 +169,7 @@
         "x64"
       ],
       "dev": true,
+      "license": "MIT OR Apache-2.0",
       "optional": true,
       "os": [
         "win32"
@@ -177,6 +186,7 @@
         "ppc64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "aix"
@@ -193,6 +203,7 @@
         "arm"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "android"
@@ -209,6 +220,7 @@
         "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "android"
@@ -225,6 +237,7 @@
         "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "android"
@@ -241,6 +254,7 @@
         "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "darwin"
@@ -257,6 +271,7 @@
         "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "darwin"
@@ -273,6 +288,7 @@
         "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "freebsd"
@@ -289,6 +305,7 @@
         "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "freebsd"
@@ -305,6 +322,7 @@
         "arm"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -321,6 +339,7 @@
         "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -337,6 +356,7 @@
         "ia32"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -353,6 +373,7 @@
         "loong64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -369,6 +390,7 @@
         "mips64el"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -385,6 +407,7 @@
         "ppc64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -401,6 +424,7 @@
         "riscv64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -417,6 +441,7 @@
         "s390x"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -433,6 +458,7 @@
         "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -449,6 +475,7 @@
         "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "netbsd"
@@ -465,6 +492,7 @@
         "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "netbsd"
@@ -481,6 +509,7 @@
         "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "openbsd"
@@ -497,6 +526,7 @@
         "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "openbsd"
@@ -513,6 +543,7 @@
         "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "openharmony"
@@ -529,6 +560,7 @@
         "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "sunos"
@@ -545,6 +577,7 @@
         "arm64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "win32"
@@ -561,6 +594,7 @@
         "ia32"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "win32"
@@ -577,6 +611,7 @@
         "x64"
       ],
       "dev": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "win32"
@@ -606,6 +641,7 @@
       "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.9.tgz",
       "integrity": "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "undici-types": "~6.21.0"
       }
@@ -614,6 +650,7 @@
       "version": "5.4.1",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
       "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
+      "license": "MIT",
       "engines": {
         "node": "^12.17.0 || ^14.13 || >=16.0.0"
       },
@@ -625,6 +662,7 @@
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
       "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+      "license": "ISC",
       "engines": {
         "node": ">=10"
       }
@@ -633,6 +671,7 @@
       "version": "11.1.0",
       "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
       "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
+      "license": "MIT",
       "engines": {
         "node": ">=16"
       }
@@ -641,13 +680,15 @@
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/commandpost/-/commandpost-1.4.0.tgz",
       "integrity": "sha512-aE2Y4MTFJ870NuB/+2z1cXBhSBBzRydVVjzhFC4gtenEhpnj15yu0qptWGJsO9YGrcPZ3ezX8AWb1VA391MKpQ==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/editorconfig": {
       "version": "0.15.3",
       "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz",
       "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "commander": "^2.19.0",
         "lru-cache": "^4.1.5",
@@ -662,7 +703,8 @@
       "version": "2.20.3",
       "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
       "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/esbuild": {
       "version": "0.25.8",
@@ -670,6 +712,7 @@
       "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==",
       "dev": true,
       "hasInstallScript": true,
+      "license": "MIT",
       "bin": {
         "esbuild": "bin/esbuild"
       },
@@ -709,6 +752,7 @@
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
       "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+      "license": "ISC",
       "dependencies": {
         "minipass": "^3.0.0"
       },
@@ -720,6 +764,7 @@
       "version": "3.3.6",
       "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
       "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+      "license": "ISC",
       "dependencies": {
         "yallist": "^4.0.0"
       },
@@ -733,6 +778,7 @@
       "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
       "dev": true,
       "hasInstallScript": true,
+      "license": "MIT",
       "optional": true,
       "os": [
         "darwin"
@@ -746,6 +792,7 @@
       "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",
       "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "resolve-pkg-maps": "^1.0.0"
       },
@@ -758,6 +805,7 @@
       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
       "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "pseudomap": "^1.0.2",
         "yallist": "^2.1.2"
@@ -767,12 +815,14 @@
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
       "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==",
-      "dev": true
+      "dev": true,
+      "license": "ISC"
     },
     "node_modules/minipass": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
       "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
+      "license": "ISC",
       "engines": {
         "node": ">=8"
       }
@@ -781,6 +831,7 @@
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
       "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+      "license": "MIT",
       "dependencies": {
         "minipass": "^3.0.0",
         "yallist": "^4.0.0"
@@ -793,6 +844,7 @@
       "version": "3.3.6",
       "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
       "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+      "license": "ISC",
       "dependencies": {
         "yallist": "^4.0.0"
       },
@@ -804,6 +856,7 @@
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
       "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+      "license": "MIT",
       "bin": {
         "mkdirp": "bin/cmd.js"
       },
@@ -815,6 +868,7 @@
       "version": "1.15.0",
       "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz",
       "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==",
+      "license": "MIT",
       "engines": {
         "node": ">=0.12.0"
       },
@@ -827,13 +881,15 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
       "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==",
-      "dev": true
+      "dev": true,
+      "license": "ISC"
     },
     "node_modules/resolve-pkg-maps": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
       "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
       "dev": true,
+      "license": "MIT",
       "funding": {
         "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
       }
@@ -843,6 +899,7 @@
       "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
       "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
       "dev": true,
+      "license": "ISC",
       "bin": {
         "semver": "bin/semver"
       }
@@ -851,12 +908,14 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz",
       "integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==",
-      "dev": true
+      "dev": true,
+      "license": "ISC"
     },
     "node_modules/tar": {
       "version": "6.2.1",
       "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
       "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
+      "license": "ISC",
       "dependencies": {
         "chownr": "^2.0.0",
         "fs-minipass": "^2.0.0",
@@ -874,6 +933,7 @@
       "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz",
       "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "esbuild": "~0.25.0",
         "get-tsconfig": "^4.7.5"
@@ -893,6 +953,7 @@
       "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
       "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
       "dev": true,
+      "license": "Apache-2.0",
       "peer": true,
       "bin": {
         "tsc": "bin/tsc",
@@ -907,6 +968,7 @@
       "resolved": "https://registry.npmjs.org/typescript-formatter/-/typescript-formatter-7.2.2.tgz",
       "integrity": "sha512-V7vfI9XArVhriOTYHPzMU2WUnm5IMdu9X/CPxs8mIMGxmTBFpDABlbkBka64PZJ9/xgQeRpK8KzzAG4MPzxBDQ==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "commandpost": "^1.0.0",
         "editorconfig": "^0.15.0"
@@ -925,12 +987,14 @@
       "version": "6.21.0",
       "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
       "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/vscode-jsonrpc": {
       "version": "8.2.1",
       "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz",
       "integrity": "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==",
+      "license": "MIT",
       "engines": {
         "node": ">=14.0.0"
       }
@@ -939,6 +1003,7 @@
       "version": "3.17.5",
       "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
       "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
+      "license": "MIT",
       "dependencies": {
         "vscode-jsonrpc": "8.2.0",
         "vscode-languageserver-types": "3.17.5"
@@ -948,6 +1013,7 @@
       "version": "8.2.0",
       "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
       "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
+      "license": "MIT",
       "engines": {
         "node": ">=14.0.0"
       }
@@ -955,12 +1021,14 @@
     "node_modules/vscode-languageserver-types": {
       "version": "3.17.5",
       "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
-      "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="
+      "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
+      "license": "MIT"
     },
     "node_modules/yallist": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+      "license": "ISC"
     }
   }
 }

+ 927 - 0
tests/plan-haxe.md

@@ -0,0 +1,927 @@
+# Haxe Serializer Generator Implementation Plan
+
+## Overview
+
+This document outlines the complete implementation plan for adding Haxe support to the Spine runtime testing infrastructure. The goal is to generate a Haxe serializer that produces identical JSON output to the existing Java and C++ serializers, enabling cross-runtime compatibility testing.
+
+## Current System Architecture
+
+The existing system consists of three layers:
+
+1. **SerializerIR Generation** (`tests/src/generate-serializer-ir.ts`)
+   - Analyzes Java API to create intermediate representation
+   - Outputs `tests/output/serializer-ir.json` with type and property metadata
+
+2. **Language-Specific Generators** 
+   - `tests/src/generate-java-serializer.ts` - Java implementation
+   - `tests/src/generate-cpp-serializer.ts` - C++ implementation  
+   - **Missing**: `tests/src/generate-haxe-serializer.ts`
+
+3. **HeadlessTest Applications**
+   - `spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/HeadlessTest.java`
+   - `spine-cpp/tests/HeadlessTest.cpp`
+   - **Missing**: `spine-haxe/tests/HeadlessTest.hx`
+
+4. **Test Runner** (`tests/src/headless-test-runner.ts`)
+   - Orchestrates building and running tests
+   - Compares outputs for consistency
+   - Currently supports: Java, C++
+   - **Needs**: Haxe support
+
+## SerializerIR Structure Reference
+
+Based on `tests/src/generate-serializer-ir.ts:10-80`:
+
+```typescript
+interface SerializerIR {
+    publicMethods: PublicMethod[];     // Entry point methods
+    writeMethods: WriteMethod[];       // Type-specific serializers  
+    enumMappings: { [enumName: string]: { [javaValue: string]: string } };
+}
+
+interface WriteMethod {
+    name: string;                      // writeSkeletonData, writeBone, etc.
+    paramType: string;                 // Full Java class name
+    properties: Property[];            // Fields to serialize
+    isAbstractType: boolean;           // Needs instanceof chain
+    subtypeChecks?: SubtypeCheck[];    // For abstract types
+}
+
+type Property = Primitive | Object | Enum | Array | NestedArray;
+```
+
+## Implementation Plan
+
+### 1. Generate Haxe Serializer (`tests/src/generate-haxe-serializer.ts`)
+
+Create the generator following the pattern from existing generators:
+
+```typescript
+#!/usr/bin/env tsx
+
+import * as fs from 'fs';
+import * as path from 'path';
+import { fileURLToPath } from 'url';
+import type { Property, SerializerIR } from './generate-serializer-ir';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+function transformType(javaType: string): string {
+    // Java → Haxe type mappings
+    const primitiveMap: Record<string, string> = {
+        'String': 'String',
+        'int': 'Int',
+        'float': 'Float', 
+        'boolean': 'Bool',
+        'short': 'Int',
+        'byte': 'Int',
+        'double': 'Float',
+        'long': 'Int'
+    };
+
+    // Remove package prefixes and map primitives
+    const simpleName = javaType.includes('.') ? javaType.split('.').pop()! : javaType;
+    
+    if (primitiveMap[simpleName]) {
+        return primitiveMap[simpleName];
+    }
+
+    // Handle arrays: Java T[] → Haxe Array<T>
+    if (simpleName.endsWith('[]')) {
+        const baseType = simpleName.slice(0, -2);
+        return `Array<${transformType(baseType)}>`;
+    }
+
+    // Java Array<T> stays Array<T> in Haxe
+    if (simpleName.startsWith('Array<')) {
+        return simpleName;
+    }
+
+    // Object types: keep class name, remove package
+    return simpleName;
+}
+
+function mapJavaGetterToHaxeField(javaGetter: string, objName: string): string {
+    // Map Java getter methods to Haxe field access
+    // Based on analysis of existing Haxe classes in spine-haxe/spine-haxe/spine/
+    
+    if (javaGetter.endsWith('()')) {
+        const methodName = javaGetter.slice(0, -2);
+        
+        // Remove get/is prefix and convert to camelCase field
+        if (methodName.startsWith('get')) {
+            const fieldName = methodName.slice(3);
+            const haxeField = fieldName.charAt(0).toLowerCase() + fieldName.slice(1);
+            return `${objName}.${haxeField}`;
+        }
+        
+        if (methodName.startsWith('is')) {
+            const fieldName = methodName.slice(2);
+            const haxeField = fieldName.charAt(0).toLowerCase() + fieldName.slice(1);
+            return `${objName}.${haxeField}`;
+        }
+        
+        // Some methods might be direct field names
+        return `${objName}.${methodName}`;
+    }
+    
+    // Direct field access (already in correct format)
+    return `${objName}.${javaGetter}`;
+}
+
+function generatePropertyCode(property: Property, indent: string, enumMappings: { [enumName: string]: { [javaValue: string]: string } }): string[] {
+    const lines: string[] = [];
+    const accessor = mapJavaGetterToHaxeField(property.getter, 'obj');
+
+    switch (property.kind) {
+        case "primitive":
+            lines.push(`${indent}json.writeValue(${accessor});`);
+            break;
+
+        case "object":
+            if (property.isNullable) {
+                lines.push(`${indent}if (${accessor} == null) {`);
+                lines.push(`${indent}    json.writeNull();`);
+                lines.push(`${indent}} else {`);
+                lines.push(`${indent}    ${property.writeMethodCall}(${accessor});`);
+                lines.push(`${indent}}`);
+            } else {
+                lines.push(`${indent}${property.writeMethodCall}(${accessor});`);
+            }
+            break;
+
+        case "enum": {
+            const enumName = property.enumName;
+            const enumMap = enumMappings[enumName];
+            
+            if (property.isNullable) {
+                lines.push(`${indent}if (${accessor} == null) {`);
+                lines.push(`${indent}    json.writeNull();`);
+                lines.push(`${indent}} else {`);
+            }
+
+            if (enumMap && Object.keys(enumMap).length > 0) {
+                // Generate switch statement for enum mapping
+                lines.push(`${indent}${property.isNullable ? '    ' : ''}switch (${accessor}) {`);
+                
+                for (const [javaValue, haxeValue] of Object.entries(enumMap)) {
+                    lines.push(`${indent}${property.isNullable ? '    ' : ''}    case ${haxeValue}: json.writeValue("${javaValue}");`);
+                }
+                
+                lines.push(`${indent}${property.isNullable ? '    ' : ''}    default: json.writeValue("unknown");`);
+                lines.push(`${indent}${property.isNullable ? '    ' : ''}}`);
+            } else {
+                // Fallback using Type.enumConstructor or similar
+                lines.push(`${indent}${property.isNullable ? '    ' : ''}json.writeValue(Type.enumConstructor(${accessor}));`);
+            }
+            
+            if (property.isNullable) {
+                lines.push(`${indent}}`);
+            }
+            break;
+        }
+
+        case "array": {
+            if (property.isNullable) {
+                lines.push(`${indent}if (${accessor} == null) {`);
+                lines.push(`${indent}    json.writeNull();`);
+                lines.push(`${indent}} else {`);
+                lines.push(`${indent}    json.writeArrayStart();`);
+                lines.push(`${indent}    for (item in ${accessor}) {`);
+            } else {
+                lines.push(`${indent}json.writeArrayStart();`);
+                lines.push(`${indent}for (item in ${accessor}) {`);
+            }
+
+            const itemIndent = property.isNullable ? `${indent}        ` : `${indent}    `;
+            if (property.elementKind === "primitive") {
+                lines.push(`${itemIndent}json.writeValue(item);`);
+            } else {
+                lines.push(`${itemIndent}${property.writeMethodCall}(item);`);
+            }
+
+            if (property.isNullable) {
+                lines.push(`${indent}    }`);
+                lines.push(`${indent}    json.writeArrayEnd();`);
+                lines.push(`${indent}}`);
+            } else {
+                lines.push(`${indent}}`);
+                lines.push(`${indent}json.writeArrayEnd();`);
+            }
+            break;
+        }
+
+        case "nestedArray": {
+            if (property.isNullable) {
+                lines.push(`${indent}if (${accessor} == null) {`);
+                lines.push(`${indent}    json.writeNull();`);
+                lines.push(`${indent}} else {`);
+            }
+
+            const outerIndent = property.isNullable ? `${indent}    ` : indent;
+            lines.push(`${outerIndent}json.writeArrayStart();`);
+            lines.push(`${outerIndent}for (nestedArray in ${accessor}) {`);
+            lines.push(`${outerIndent}    if (nestedArray == null) {`);
+            lines.push(`${outerIndent}        json.writeNull();`);
+            lines.push(`${outerIndent}    } else {`);
+            lines.push(`${outerIndent}        json.writeArrayStart();`);
+            lines.push(`${outerIndent}        for (elem in nestedArray) {`);
+            lines.push(`${outerIndent}            json.writeValue(elem);`);
+            lines.push(`${outerIndent}        }`);
+            lines.push(`${outerIndent}        json.writeArrayEnd();`);
+            lines.push(`${outerIndent}    }`);
+            lines.push(`${outerIndent}}`);
+            lines.push(`${outerIndent}json.writeArrayEnd();`);
+
+            if (property.isNullable) {
+                lines.push(`${indent}}`);
+            }
+            break;
+        }
+    }
+
+    return lines;
+}
+
+function generateHaxeFromIR(ir: SerializerIR): string {
+    const haxeOutput: string[] = [];
+
+    // Generate Haxe file header
+    haxeOutput.push('package spine.utils;');
+    haxeOutput.push('');
+    haxeOutput.push('import haxe.ds.StringMap;');
+    haxeOutput.push('import spine.*;');
+    haxeOutput.push('import spine.animation.*;');
+    haxeOutput.push('import spine.attachments.*;');
+    haxeOutput.push('');
+    haxeOutput.push('class SkeletonSerializer {');
+    haxeOutput.push('    private var visitedObjects:StringMap<String> = new StringMap();');
+    haxeOutput.push('    private var nextId:Int = 1;');
+    haxeOutput.push('    private var json:JsonWriter;');
+    haxeOutput.push('');
+    haxeOutput.push('    public function new() {}');
+    haxeOutput.push('');
+
+    // Generate public methods
+    for (const method of ir.publicMethods) {
+        const haxeParamType = transformType(method.paramType);
+        haxeOutput.push(`    public function ${method.name}(${method.paramName}:${haxeParamType}):String {`);
+        haxeOutput.push('        visitedObjects = new StringMap();');
+        haxeOutput.push('        nextId = 1;');
+        haxeOutput.push('        json = new JsonWriter();');
+        haxeOutput.push(`        ${method.writeMethodCall}(${method.paramName});`);
+        haxeOutput.push('        return json.getString();');
+        haxeOutput.push('    }');
+        haxeOutput.push('');
+    }
+
+    // Generate write methods
+    for (const method of ir.writeMethods) {
+        const shortName = method.paramType.split('.').pop();
+        const haxeType = transformType(method.paramType);
+
+        haxeOutput.push(`    private function ${method.name}(obj:${haxeType}):Void {`);
+
+        if (method.isAbstractType) {
+            // Handle abstract types with Std.isOfType chain (Haxe equivalent of instanceof)
+            if (method.subtypeChecks && method.subtypeChecks.length > 0) {
+                let first = true;
+                for (const subtype of method.subtypeChecks) {
+                    const subtypeHaxeName = transformType(subtype.typeName);
+
+                    if (first) {
+                        haxeOutput.push(`        if (Std.isOfType(obj, ${subtypeHaxeName})) {`);
+                        first = false;
+                    } else {
+                        haxeOutput.push(`        } else if (Std.isOfType(obj, ${subtypeHaxeName})) {`);
+                    }
+                    haxeOutput.push(`            ${subtype.writeMethodCall}(cast(obj, ${subtypeHaxeName}));`);
+                }
+                haxeOutput.push('        } else {');
+                haxeOutput.push(`            throw new spine.SpineException("Unknown ${shortName} type");`);
+                haxeOutput.push('        }');
+            } else {
+                haxeOutput.push('        json.writeNull(); // No concrete implementations after filtering exclusions');
+            }
+        } else {
+            // Handle concrete types - add cycle detection
+            haxeOutput.push('        if (visitedObjects.exists(obj)) {');
+            haxeOutput.push('            json.writeValue(visitedObjects.get(obj));');
+            haxeOutput.push('            return;');
+            haxeOutput.push('        }');
+
+            // Generate reference string
+            const nameGetter = method.properties.find(p => 
+                (p.kind === 'object' || p.kind === "primitive") &&
+                p.getter === 'getName()' &&
+                p.valueType === 'String'
+            );
+
+            if (nameGetter) {
+                const nameAccessor = mapJavaGetterToHaxeField('getName()', 'obj');
+                haxeOutput.push(`        var refString = ${nameAccessor} != null ? "<${shortName}-" + ${nameAccessor} + ">" : "<${shortName}-" + (nextId++) + ">";`);
+            } else {
+                haxeOutput.push(`        var refString = "<${shortName}-" + (nextId++) + ">";`);
+            }
+            haxeOutput.push('        visitedObjects.set(obj, refString);');
+            haxeOutput.push('');
+
+            haxeOutput.push('        json.writeObjectStart();');
+
+            // Write reference string and type
+            haxeOutput.push('        json.writeName("refString");');
+            haxeOutput.push('        json.writeValue(refString);');
+            haxeOutput.push('        json.writeName("type");');
+            haxeOutput.push(`        json.writeValue("${shortName}");`);
+
+            // Write properties
+            for (const property of method.properties) {
+                haxeOutput.push('');
+                haxeOutput.push(`        json.writeName("${property.name}");`);
+                const propertyLines = generatePropertyCode(property, '        ', ir.enumMappings);
+                haxeOutput.push(...propertyLines);
+            }
+
+            haxeOutput.push('');
+            haxeOutput.push('        json.writeObjectEnd();');
+        }
+
+        haxeOutput.push('    }');
+        haxeOutput.push('');
+    }
+
+    // Add helper methods for special types (following C++ pattern)
+    haxeOutput.push('    // Helper methods for special types');
+    haxeOutput.push('    private function writeColor(obj:spine.Color):Void {');
+    haxeOutput.push('        if (obj == null) {');
+    haxeOutput.push('            json.writeNull();');
+    haxeOutput.push('        } else {');
+    haxeOutput.push('            json.writeObjectStart();');
+    haxeOutput.push('            json.writeName("r");');
+    haxeOutput.push('            json.writeValue(obj.r);');
+    haxeOutput.push('            json.writeName("g");');
+    haxeOutput.push('            json.writeValue(obj.g);');
+    haxeOutput.push('            json.writeName("b");');
+    haxeOutput.push('            json.writeValue(obj.b);');
+    haxeOutput.push('            json.writeName("a");');
+    haxeOutput.push('            json.writeValue(obj.a);');
+    haxeOutput.push('            json.writeObjectEnd();');
+    haxeOutput.push('        }');
+    haxeOutput.push('    }');
+    haxeOutput.push('');
+
+    haxeOutput.push('}');
+
+    return haxeOutput.join('\n');
+}
+
+async function main(): Promise<void> {
+    try {
+        // Read the IR file
+        const irFile = path.resolve(__dirname, '../output/serializer-ir.json');
+        if (!fs.existsSync(irFile)) {
+            console.error('Serializer IR not found. Run generate-serializer-ir.ts first.');
+            process.exit(1);
+        }
+
+        const ir: SerializerIR = JSON.parse(fs.readFileSync(irFile, 'utf8'));
+
+        // Generate Haxe serializer from IR
+        const haxeCode = generateHaxeFromIR(ir);
+
+        // Write the Haxe file
+        const haxeFile = path.resolve(
+            __dirname,
+            '../../spine-haxe/spine-haxe/spine/utils/SkeletonSerializer.hx'
+        );
+
+        fs.mkdirSync(path.dirname(haxeFile), { recursive: true });
+        fs.writeFileSync(haxeFile, haxeCode);
+
+        console.log(`Generated Haxe serializer from IR: ${haxeFile}`);
+        console.log(`- ${ir.publicMethods.length} public methods`);
+        console.log(`- ${ir.writeMethods.length} write methods`);
+        console.log(`- ${Object.keys(ir.enumMappings).length} enum mappings`);
+
+    } catch (error: any) {
+        console.error('Error:', error.message);
+        console.error('Stack:', error.stack);
+        process.exit(1);
+    }
+}
+
+// Allow running as a script or importing the function
+if (import.meta.url === `file://${process.argv[1]}`) {
+    main();
+}
+
+export { generateHaxeFromIR };
+```
+
+### 2. JsonWriter Helper Class (`spine-haxe/spine-haxe/spine/utils/JsonWriter.hx`)
+
+Based on the pattern from `spine-cpp/tests/JsonWriter.h`, create a Haxe equivalent:
+
+```haxe
+package spine.utils;
+
+enum JsonContext {
+    Object;
+    Array;
+}
+
+class JsonWriter {
+    private var buffer:StringBuf = new StringBuf();
+    private var needsComma:Bool = false;
+    private var contexts:Array<JsonContext> = [];
+
+    public function new() {
+        buffer = new StringBuf();
+        needsComma = false;
+        contexts = [];
+    }
+
+    public function writeObjectStart():Void {
+        writeCommaIfNeeded();
+        buffer.add("{");
+        contexts.push(Object);
+        needsComma = false;
+    }
+
+    public function writeObjectEnd():Void {
+        buffer.add("}");
+        contexts.pop();
+        needsComma = true;
+    }
+
+    public function writeArrayStart():Void {
+        writeCommaIfNeeded();
+        buffer.add("[");
+        contexts.push(Array);
+        needsComma = false;
+    }
+
+    public function writeArrayEnd():Void {
+        buffer.add("]");
+        contexts.pop();
+        needsComma = true;
+    }
+
+    public function writeName(name:String):Void {
+        writeCommaIfNeeded();
+        buffer.add('"${escapeString(name)}":');
+        needsComma = false;
+    }
+
+    public function writeValue(value:Dynamic):Void {
+        writeCommaIfNeeded();
+        
+        if (value == null) {
+            buffer.add("null");
+        } else if (Std.isOfType(value, String)) {
+            buffer.add('"${escapeString(cast(value, String))}"');
+        } else if (Std.isOfType(value, Bool)) {
+            buffer.add(value ? "true" : "false");
+        } else if (Std.isOfType(value, Float) || Std.isOfType(value, Int)) {
+            // Ensure consistent float formatting (C locale style)
+            buffer.add(Std.string(value));
+        } else {
+            buffer.add(Std.string(value));
+        }
+        
+        needsComma = true;
+    }
+
+    public function writeNull():Void {
+        writeCommaIfNeeded();
+        buffer.add("null");
+        needsComma = true;
+    }
+
+    public function getString():String {
+        return buffer.toString();
+    }
+
+    private function writeCommaIfNeeded():Void {
+        if (needsComma) {
+            buffer.add(",");
+        }
+    }
+
+    private function escapeString(str:String):String {
+        // Escape special characters for JSON
+        str = StringTools.replace(str, "\\", "\\\\");
+        str = StringTools.replace(str, '"', '\\"');
+        str = StringTools.replace(str, "\n", "\\n");
+        str = StringTools.replace(str, "\r", "\\r");
+        str = StringTools.replace(str, "\t", "\\t");
+        return str;
+    }
+}
+```
+
+### 3. Haxe HeadlessTest Application (`spine-haxe/tests/HeadlessTest.hx`)
+
+Following the pattern from existing HeadlessTest implementations:
+
+```haxe
+package;
+
+import spine.*;
+import spine.atlas.TextureAtlas;
+import spine.atlas.TextureAtlasPage;
+import spine.atlas.TextureLoader;
+import spine.attachments.AtlasAttachmentLoader;
+import spine.animation.*;
+import spine.utils.SkeletonSerializer;
+import sys.io.File;
+import haxe.io.Bytes;
+
+// Mock texture loader that doesn't require actual texture loading
+class MockTextureLoader implements TextureLoader {
+    public function new() {}
+    
+    public function load(page:TextureAtlasPage, path:String):Void {
+        // Set mock dimensions - no actual texture loading needed
+        page.width = 1024;
+        page.height = 1024;
+        page.texture = {}; // Empty object as mock texture
+    }
+    
+    public function unload(texture:Dynamic):Void {
+        // Nothing to unload in headless mode
+    }
+}
+
+class HeadlessTest {
+    static function main():Void {
+        var args = Sys.args();
+        
+        if (args.length < 2) {
+            Sys.stderr().writeString("Usage: HeadlessTest <skeleton-path> <atlas-path> [animation-name]\n");
+            Sys.exit(1);
+        }
+        
+        var skeletonPath = args[0];
+        var atlasPath = args[1]; 
+        var animationName = args.length >= 3 ? args[2] : null;
+        
+        try {
+            // Load atlas with mock texture loader
+            var textureLoader = new MockTextureLoader();
+            var atlasContent = File.getContent(atlasPath);
+            var atlas = new TextureAtlas(atlasContent, textureLoader);
+            
+            // Load skeleton data
+            var skeletonData:SkeletonData;
+            var attachmentLoader = new AtlasAttachmentLoader(atlas);
+            
+            if (StringTools.endsWith(skeletonPath, ".json")) {
+                var loader = new SkeletonJson(attachmentLoader);
+                var jsonContent = File.getContent(skeletonPath);
+                skeletonData = loader.readSkeletonData(jsonContent);
+            } else {
+                var loader = new SkeletonBinary(attachmentLoader);
+                var binaryContent = File.getBytes(skeletonPath);
+                skeletonData = loader.readSkeletonData(binaryContent);
+            }
+            
+            // Create serializer
+            var serializer = new SkeletonSerializer();
+            
+            // Print skeleton data
+            Sys.println("=== SKELETON DATA ===");
+            Sys.println(serializer.serializeSkeletonData(skeletonData));
+            
+            // Create skeleton instance
+            var skeleton = new Skeleton(skeletonData);
+            
+            // Handle animation if provided
+            var state:AnimationState = null;
+            if (animationName != null) {
+                var stateData = new AnimationStateData(skeletonData);
+                state = new AnimationState(stateData);
+                
+                var animation = skeletonData.findAnimation(animationName);
+                if (animation == null) {
+                    Sys.stderr().writeString('Animation not found: $animationName\n');
+                    Sys.exit(1);
+                }
+                
+                state.setAnimation(0, animation, true);
+                state.update(0.016);
+                state.apply(skeleton);
+            }
+            
+            // Update world transforms (following the pattern from other HeadlessTests)
+            skeleton.updateWorldTransform(Physics.update);
+            
+            // Print skeleton state
+            Sys.println("\n=== SKELETON STATE ===");
+            Sys.println(serializer.serializeSkeleton(skeleton));
+            
+            // Print animation state if present
+            if (state != null) {
+                Sys.println("\n=== ANIMATION STATE ===");
+                Sys.println(serializer.serializeAnimationState(state));
+            }
+            
+        } catch (e:Dynamic) {
+            Sys.stderr().writeString('Error: $e\n');
+            Sys.exit(1);
+        }
+    }
+}
+```
+
+### 4. Build Script (`spine-haxe/build-headless-test.sh`)
+
+```bash
+#!/bin/bash
+
+# Build Haxe HeadlessTest for cross-platform execution
+# Following pattern from spine-cpp/build.sh
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "$SCRIPT_DIR"
+
+echo "Building Haxe HeadlessTest..."
+
+# Clean previous build
+rm -rf build/headless-test
+
+# Create build directory
+mkdir -p build
+
+# Compile HeadlessTest to C++ for performance and consistency with other runtimes
+haxe \
+    -cp spine-haxe \
+    -cp tests \
+    -main HeadlessTest \
+    -cpp build/headless-test \
+    -D HXCPP_QUIET
+
+# Make executable
+chmod +x build/headless-test/HeadlessTest
+
+echo "Build complete: build/headless-test/HeadlessTest"
+```
+
+### 5. Test Runner Integration (`tests/src/headless-test-runner.ts`)
+
+Add Haxe support to the existing test runner. Key changes needed:
+
+```typescript
+// Line 207: Update supported languages
+if (!['cpp', 'haxe'].includes(language)) {
+    log_detail(`Invalid target language: ${language}. Must be cpp or haxe`);
+    process.exit(1);
+}
+
+// Add needsHaxeBuild function (similar to needsCppBuild at line 96)
+function needsHaxeBuild(): boolean {
+    const haxeDir = join(SPINE_ROOT, 'spine-haxe');
+    const buildDir = join(haxeDir, 'build');
+    const headlessTest = join(buildDir, 'headless-test', 'HeadlessTest');
+
+    try {
+        // Check if executable exists
+        if (!existsSync(headlessTest)) return true;
+
+        // Get executable modification time
+        const execTime = statSync(headlessTest).mtime.getTime();
+
+        // Check Haxe source files
+        const haxeSourceTime = getNewestFileTime(join(haxeDir, 'spine-haxe'), '*.hx');
+        const testSourceTime = getNewestFileTime(join(haxeDir, 'tests'), '*.hx');
+        const buildScriptTime = getNewestFileTime(haxeDir, 'build-headless-test.sh');
+
+        const newestSourceTime = Math.max(haxeSourceTime, testSourceTime, buildScriptTime);
+
+        return newestSourceTime > execTime;
+    } catch {
+        return true;
+    }
+}
+
+// Add executeHaxe function (similar to executeCpp at line 321)
+function executeHaxe(args: TestArgs): string {
+    const haxeDir = join(SPINE_ROOT, 'spine-haxe');
+    const testsDir = join(haxeDir, 'tests');
+
+    if (!existsSync(testsDir)) {
+        log_detail(`Haxe tests directory not found: ${testsDir}`);
+        process.exit(1);
+    }
+
+    // Check if we need to build
+    if (needsHaxeBuild()) {
+        log_action('Building Haxe HeadlessTest');
+        try {
+            execSync('./build-headless-test.sh', {
+                cwd: haxeDir,
+                stdio: ['inherit', 'pipe', 'inherit']
+            });
+            log_ok();
+        } catch (error: any) {
+            log_fail();
+            log_detail(`Haxe build failed: ${error.message}`);
+            process.exit(1);
+        }
+    }
+
+    // Run the headless test
+    const testArgs = [args.skeletonPath, args.atlasPath];
+    if (args.animationName) {
+        testArgs.push(args.animationName);
+    }
+
+    const buildDir = join(haxeDir, 'build');
+    const headlessTest = join(buildDir, 'headless-test', 'HeadlessTest');
+
+    if (!existsSync(headlessTest)) {
+        log_detail(`Haxe headless-test executable not found: ${headlessTest}`);
+        process.exit(1);
+    }
+
+    log_action('Running Haxe HeadlessTest');
+    try {
+        const output = execSync(`${headlessTest} ${testArgs.join(' ')}`, {
+            encoding: 'utf8',
+            maxBuffer: 50 * 1024 * 1024 // 50MB buffer for large output
+        });
+        log_ok();
+        return output;
+    } catch (error: any) {
+        log_fail();
+        log_detail(`Haxe execution failed: ${error.message}`);
+        process.exit(1);
+    }
+}
+
+// Update runTestsForFiles function around line 525 to handle Haxe
+if (language === 'cpp') {
+    targetOutput = executeCpp(testArgs);
+} else if (language === 'haxe') {
+    targetOutput = executeHaxe(testArgs);
+} else {
+    log_detail(`Unsupported target language: ${language}`);
+    process.exit(1);
+}
+```
+
+### 6. Build Integration (`tests/generate-serializers.sh`)
+
+Update the serializer generation script to include Haxe:
+
+```bash
+# Add after C++ generation
+echo "Generating Haxe serializer..."
+tsx tests/src/generate-haxe-serializer.ts
+
+echo "Type checking Haxe serializer..."
+cd spine-haxe && haxe -cp spine-haxe --no-output -main spine.utils.SkeletonSerializer
+cd ..
+```
+
+### 7. File Structure Summary
+
+```
+spine-haxe/
+├── spine-haxe/spine/utils/
+│   ├── SkeletonSerializer.hx  (generated)
+│   └── JsonWriter.hx          (helper class)
+├── tests/
+│   └── HeadlessTest.hx        (console application)
+├── build-headless-test.sh     (build script)
+└── build/headless-test/       (compiled executable)
+    └── HeadlessTest
+
+tests/src/
+├── generate-haxe-serializer.ts (new generator)
+└── headless-test-runner.ts     (updated with Haxe support)
+```
+
+## Type Checking and Validation
+
+### Compilation Validation
+
+Add type checking to the generator to ensure generated code compiles:
+
+```typescript
+import { execSync } from 'child_process';
+
+async function validateGeneratedHaxeCode(haxeCode: string, outputPath: string): Promise<void> {
+    // Write code to file
+    fs.writeFileSync(outputPath, haxeCode);
+    
+    try {
+        // Attempt compilation without output (type check only)
+        execSync('haxe -cp spine-haxe --no-output -main spine.utils.SkeletonSerializer', {
+            cwd: path.resolve(__dirname, '../../spine-haxe'),
+            stdio: 'pipe'
+        });
+        
+        console.log('✓ Generated Haxe serializer compiles successfully');
+        
+    } catch (error: any) {
+        fs.unlinkSync(outputPath);
+        throw new Error(`Generated Haxe serializer failed to compile:\n${error.message}`);
+    }
+}
+
+// Call in main() after generating code
+await validateGeneratedHaxeCode(haxeCode, haxeFile);
+```
+
+## Key Implementation Notes
+
+### Java → Haxe Property Mapping
+
+Based on analysis of `spine-haxe/spine-haxe/spine/` classes:
+
+- `obj.getName()` → `obj.name`
+- `obj.getBones()` → `obj.bones` 
+- `obj.isActive()` → `obj.active`
+- `obj.getColor()` → `obj.color`
+
+### Enum Handling
+
+Haxe enums are different from Java enums. Use `Type.enumConstructor()` to get string representation:
+
+```haxe
+// For enum serialization
+json.writeValue(Type.enumConstructor(obj.blendMode));
+```
+
+### Array Handling 
+
+Haxe uses `Array<T>` syntax similar to Java, but iteration is different:
+
+```haxe
+// Haxe iteration
+for (item in obj.bones) {
+    writeBone(item);
+}
+```
+
+### Null Safety
+
+Haxe has explicit null checking:
+
+```haxe
+if (obj.skin == null) {
+    json.writeNull();
+} else {
+    writeSkin(obj.skin);
+}
+```
+
+## Testing and Verification
+
+### Cross-Runtime Consistency
+
+The test runner will automatically:
+
+1. Build all three runtimes (Java, C++, Haxe)
+2. Run identical test cases on same skeleton files
+3. Compare JSON outputs for exact matches
+4. Report any differences
+
+### Manual Testing
+
+```bash
+# Generate all serializers
+./tests/generate-serializers.sh
+
+# Test specific skeleton with all runtimes
+tsx tests/src/headless-test-runner.ts cpp -s spineboy idle
+tsx tests/src/headless-test-runner.ts haxe -s spineboy idle
+
+# Compare outputs
+diff tests/output/skeleton-data-cpp-json.json tests/output/skeleton-data-haxe-json.json
+```
+
+## Implementation Checklist
+
+- [ ] Create `tests/src/generate-haxe-serializer.ts`
+- [ ] Create `spine-haxe/spine-haxe/spine/utils/JsonWriter.hx`
+- [ ] Create `spine-haxe/tests/HeadlessTest.hx`
+- [ ] Create `spine-haxe/build-headless-test.sh`
+- [ ] Update `tests/src/headless-test-runner.ts` with Haxe support
+- [ ] Update `tests/generate-serializers.sh`
+- [ ] Test with existing skeleton examples
+- [ ] Verify JSON output matches Java/C++ exactly
+- [ ] Add to CI pipeline
+
+## Expected Benefits
+
+1. **Cross-Runtime Testing**: Verify Haxe runtime behavior matches Java/C++
+2. **Debugging Support**: Unified JSON format for inspection across all runtimes  
+3. **API Consistency**: Ensure Haxe API changes don't break compatibility
+4. **Quality Assurance**: Automated verification of serialization correctness
+5. **Development Velocity**: Fast detection of runtime-specific issues
+
+This implementation follows the established patterns while adapting to Haxe's specific language features and build system.

+ 11 - 4
todos/todos.md

@@ -1,8 +1,15 @@
 - Port C++ SkeletonRenderer and RenderCommands to all runtimes
     - Will be used to snapshottesting via HeadlessTest, see also tests/
     - Can go into main package in all core runtimes, except for spine-libgdx, where it must go next to SkeletonSerializer in spine-libgdx-tests
-- Generate language bindings in spine-c/codegen
-    - Use CClassOrStruct, CEnum that get generated from spine-cpp-types.json and generate
-        - Swift
-        - Dart
+- Fix Dart NativeArray wrt to resize/add/remove. Current impl is wonky. Either make it read-only or support full mutabiliy (prefer latter)
+- Generate bindings for Swift from spine-c generate() like dart-writer.ts
+- Generate Godot wrappers from C++ types and/or spine-c generate() (unlike dart-writer.ts)?
+- headless-test improvements
+    - should take cli args for ad-hoc testing
+    - if none are given, should execute a set of (regression) tests and output individual test snapshots one after the other as jsonl
+    - All headless tests must have the same test suite
+    - test runner must know how to deal with this mode
+- Add serializer generator for Haxe (see tests/plan-haxe.md for a full plan)
+- Add serializer generator for C#
+- Add serializer generator for TypeScript
 - spine-c/codegen type extractor should also report typedefs like typedef long long PropertyId; so primitive type to some name, and we need to handle that in the codegen