[tests] serializer ir generator, ir-based java serializer generator

This commit is contained in:
Mario Zechner 2025-07-13 03:31:20 +02:00
parent 102b030db3
commit 4bf777a658
5 changed files with 1176 additions and 717 deletions

View File

@ -3,7 +3,7 @@
**Status:** In Progress
**Created:** 2025-01-11T03:02:54
**Started:** 2025-01-11T03:11:22
**Agent PID:** 93834
**Agent PID:** 15570
**CRITICAL:**
- NEVER never check a chceckbox and move on to the next checkbox unless the user has confirmed completion of the current checkbox!
@ -237,18 +237,94 @@ The test programs will print both SkeletonData (setup pose/static data) and Skel
- Create mechanism in tests/generate-cpp-serializer.ts to replace auto-generated functions with hand-written ones
- Implement custom writeSkin function that properly handles AttachmentMap::Entries iteration
- [x] Create regenerate-all.sh script in tests/ that runs all generators in sequence
- [ ] Fix C++ JsonWriter output formatting issues (array formatting broken)
- [ ] Test with sample skeleton files
- [ ] TypeScript (spine-ts):
- [ ] Follow what we did for spine-cpp wrt to JsonWriter, SkeletonSerializer and the generator
- [ ] C#
- [ ] Figure out how we can add the HeadlessTest and run it without adding it to the assembly itself
- [ ] Follow what we did for spine-cpp wrt
- [ ] C (spine-c):
- [ ] Follow what we did for spine-cpp wrt to JsonWriter, SkeletonSerializer and the generator (this one will need some ultrathink and discussion with the user before code changs)
- [ ] Update tests/README.md to describe the new setup
- [x] ~~Fix C++ JsonWriter output formatting issues (array formatting broken)~~
- [x] ~~Test with sample skeleton files~~
- [x] ~~TypeScript (spine-ts):~~
- [x] ~~Follow what we did for spine-cpp wrt to JsonWriter, SkeletonSerializer and the generator~~
- [x] ~~C#~~
- [x] ~~Figure out how we can add the HeadlessTest and run it without adding it to the assembly itself~~
- [x] ~~Follow what we did for spine-cpp wrt~~
- [x] ~~C (spine-c):~~
- [x] ~~Follow what we did for spine-cpp wrt to JsonWriter, SkeletonSerializer and the generator (this one will need some ultrathink and discussion with the user before code changs)~~
- [x] ~~Update tests/README.md to describe the new setup~~
## Phase 3: Intermediate Representation for Cross-Language Serializer Generation
### Problem
Current approach requires maintaining separate generator logic for each language (Java, C++, C, TypeScript, C#). The complex analysis, exclusion handling, and serialization logic is duplicated across generators, making maintenance difficult.
### Solution: Language-Agnostic Intermediate Representation (IR)
Create a single IR generator that captures all serialization logic in a JSON format that language-specific generators can consume without a lot of post-processing. The IR will still
contain Java specific types and names. Language specific generators then just have to translate.
### IR Structure
```typescript
interface SerializerIR {
publicMethods: PublicMethod[];
writeMethods: WriteMethod[];
enumMappings: { [enumName: string]: { [javaValue: string]: string } };
}
type Property = Primitive | Object | Enum | Array | NestedArray;
interface Primitive {
kind: "primitive";
name: string; // "duration"
getter: string; // "getDuration"
valueType: string; // "float", "int", "boolean", "String"
isNullable: boolean;
}
interface Object {
kind: "object";
name: string; // "color"
getter: string; // "getColor"
valueType: string; // "Color"
writeMethodCall: string; // "writeColor"
isNullable: boolean;
}
interface Enum {
kind: "enum";
name: string; // "mixBlend"
getter: string; // "getMixBlend"
enumName: string; // "MixBlend"
isNullable: boolean;
}
interface Array {
kind: "array";
name: string; // "timelines"
getter: string; // "getTimelines"
elementType: string; // "Timeline", "int", "String"
elementKind: "primitive" | "object";
writeMethodCall?: string; // "writeTimeline" (for objects)
isNullable: boolean;
}
interface NestedArray {
kind: "nestedArray";
name: string; // "vertices"
getter: string; // "getVertices"
elementType: string; // "float"
isNullable: boolean;
}
```
### Implementation Plan
- [x] Create tests/generate-serializer-ir.ts:
- [x] Reuse logic from tests/generate-java-serializer.ts (analysis, exclusions, property detection)
- [x] Output SerializerIR as JSON to tests/output/serializer-ir.json
- [x] All exclusions and filtering pre-applied - no post-processing needed
- [ ] Update language generators to consume IR:
- [x] Replace tests/generate-java-serializer.ts with IR-based version
- [ ] Modify tests/generate-cpp-serializer.ts to use IR
- [ ] Create tests/generate-ts-serializer.ts using IR
- [ ] Create tests/generate-cs-serializer.ts using IR
- [ ] Language generators focus purely on syntax transformation
- [ ] Update tests/regenerate-all.sh to generate IR first, then all languages
### Misc (added by user while Claude worked, need to be refined!)
- [ ] HeadlessTest should probably
- Have a mode that does what we currently do: take files and animation name, and output serialized skeleton data and skeleton. Used for ad-hoc testing of files submitted by users in error reports etc.

View File

@ -3,304 +3,110 @@
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
import type { ClassInfo, PropertyInfo } from './types';
import type { SerializerIR, PublicMethod, WriteMethod, Property } from './generate-serializer-ir';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function loadExclusions(): { types: Set<string>, methods: Map<string, Set<string>>, fields: Map<string, Set<string>> } {
const exclusionsPath = path.resolve(__dirname, 'java-exclusions.txt');
const types = new Set<string>();
const methods = new Map<string, Set<string>>();
const fields = new Map<string, Set<string>>();
function generatePropertyCode(property: Property, indent: string): string[] {
const lines: string[] = [];
const accessor = `obj.${property.getter}`;
if (!fs.existsSync(exclusionsPath)) {
return { types, methods, fields };
}
const content = fs.readFileSync(exclusionsPath, 'utf-8');
const lines = content.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const parts = trimmed.split(/\s+/);
if (parts.length < 2) continue;
const [type, className, property] = parts;
switch (type) {
case 'type':
types.add(className);
break;
case 'method':
if (property) {
if (!methods.has(className)) {
methods.set(className, new Set());
}
methods.get(className)!.add(property);
}
break;
case 'field':
if (property) {
if (!fields.has(className)) {
fields.set(className, new Set());
}
fields.get(className)!.add(property);
}
break;
}
}
return { types, methods, fields };
}
interface SerializedAnalysisResult {
classMap: [string, ClassInfo][];
accessibleTypes: string[];
abstractTypes: [string, string[]][];
allTypesToGenerate: string[];
typeProperties: [string, PropertyInfo[]][];
}
function generateWriteValue(output: string[], expression: string, type: string, indent: string, abstractTypes: Map<string, string[]>, classMap: Map<string, ClassInfo>) {
// Handle null annotations
const isNullable = type.includes('@Null');
type = type.replace(/@Null\s+/g, '').trim();
// Primitive types
if (['String', 'int', 'float', 'boolean', 'short', 'byte', 'double', 'long'].includes(type)) {
output.push(`${indent}json.writeValue(${expression});`);
return;
}
// Check if it's an enum - need to handle both short and full names
let classInfo = classMap.get(type);
if (!classInfo && !type.includes('.')) {
// Try to find by short name
for (const [fullName, info] of classMap) {
if (fullName.split('.').pop() === type) {
classInfo = info;
break;
}
}
}
if (classInfo?.isEnum) {
if (isNullable) {
output.push(`${indent}if (${expression} == null) {`);
output.push(`${indent} json.writeNull();`);
output.push(`${indent}} else {`);
output.push(`${indent} json.writeValue(${expression}.name());`);
output.push(`${indent}}`);
} else {
output.push(`${indent}json.writeValue(${expression}.name());`);
}
return;
}
// Arrays
if (type.startsWith('Array<')) {
const innerType = type.match(/Array<(.+?)>/)![1].trim();
if (isNullable) {
output.push(`${indent}if (${expression} == null) {`);
output.push(`${indent} json.writeNull();`);
output.push(`${indent}} else {`);
output.push(`${indent} json.writeArrayStart();`);
output.push(`${indent} for (${innerType} item : ${expression}) {`);
generateWriteValue(output, 'item', innerType, indent + ' ', abstractTypes, classMap);
output.push(`${indent} }`);
output.push(`${indent} json.writeArrayEnd();`);
output.push(`${indent}}`);
} else {
output.push(`${indent}json.writeArrayStart();`);
output.push(`${indent}for (${innerType} item : ${expression}) {`);
generateWriteValue(output, 'item', innerType, indent + ' ', abstractTypes, classMap);
output.push(`${indent}}`);
output.push(`${indent}json.writeArrayEnd();`);
}
return;
}
if (type === 'IntArray' || type === 'FloatArray') {
if (isNullable) {
output.push(`${indent}if (${expression} == null) {`);
output.push(`${indent} json.writeNull();`);
output.push(`${indent}} else {`);
output.push(`${indent} json.writeArrayStart();`);
output.push(`${indent} for (int i = 0; i < ${expression}.size; i++) {`);
output.push(`${indent} json.writeValue(${expression}.get(i));`);
output.push(`${indent} }`);
output.push(`${indent} json.writeArrayEnd();`);
output.push(`${indent}}`);
} else {
output.push(`${indent}json.writeArrayStart();`);
output.push(`${indent}for (int i = 0; i < ${expression}.size; i++) {`);
output.push(`${indent} json.writeValue(${expression}.get(i));`);
output.push(`${indent}}`);
output.push(`${indent}json.writeArrayEnd();`);
}
return;
}
if (type.endsWith('[]')) {
const elemType = type.slice(0, -2);
if (isNullable) {
output.push(`${indent}if (${expression} == null) {`);
output.push(`${indent} json.writeNull();`);
output.push(`${indent}} else {`);
output.push(`${indent} json.writeArrayStart();`);
// Handle nested arrays (like float[][])
if (elemType.endsWith('[]')) {
const nestedType = elemType.slice(0, -2);
output.push(`${indent} for (${elemType} nestedArray : ${expression}) {`);
output.push(`${indent} if (nestedArray == null) {`);
output.push(`${indent} json.writeNull();`);
output.push(`${indent} } else {`);
output.push(`${indent} json.writeArrayStart();`);
output.push(`${indent} for (${nestedType} elem : nestedArray) {`);
output.push(`${indent} json.writeValue(elem);`);
output.push(`${indent} }`);
output.push(`${indent} json.writeArrayEnd();`);
output.push(`${indent} }`);
output.push(`${indent} }`);
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 {
output.push(`${indent} for (${elemType} item : ${expression}) {`);
generateWriteValue(output, 'item', elemType, indent + ' ', abstractTypes, classMap);
output.push(`${indent} }`);
lines.push(`${indent}${property.writeMethodCall}(${accessor});`);
}
output.push(`${indent} json.writeArrayEnd();`);
output.push(`${indent}}`);
} else {
output.push(`${indent}json.writeArrayStart();`);
// Handle nested arrays (like float[][])
if (elemType.endsWith('[]')) {
const nestedType = elemType.slice(0, -2);
output.push(`${indent}for (${elemType} nestedArray : ${expression}) {`);
output.push(`${indent} json.writeArrayStart();`);
output.push(`${indent} for (${nestedType} elem : nestedArray) {`);
output.push(`${indent} json.writeValue(elem);`);
output.push(`${indent} }`);
output.push(`${indent} json.writeArrayEnd();`);
output.push(`${indent}}`);
break;
case "enum":
if (property.isNullable) {
lines.push(`${indent}if (${accessor} == null) {`);
lines.push(`${indent} json.writeNull();`);
lines.push(`${indent}} else {`);
lines.push(`${indent} json.writeValue(${accessor}.name());`);
lines.push(`${indent}}`);
} else {
output.push(`${indent}for (${elemType} item : ${expression}) {`);
generateWriteValue(output, 'item', elemType, indent + ' ', abstractTypes, classMap);
output.push(`${indent}}`);
lines.push(`${indent}json.writeValue(${accessor}.name());`);
}
output.push(`${indent}json.writeArrayEnd();`);
}
return;
}
// Special cases for libGDX types
if (type === 'Color') {
output.push(`${indent}writeColor(${expression});`);
return;
}
if (type === 'TextureRegion') {
output.push(`${indent}writeTextureRegion(${expression});`);
return;
}
// Handle objects
const shortType = type.split('.').pop()!;
// Check if this type exists in classMap (for abstract types that might not be in generated methods)
let foundInClassMap = classMap.has(type);
if (!foundInClassMap && !type.includes('.')) {
// Try to find by short name
for (const [fullName, info] of classMap) {
if (fullName.split('.').pop() === type) {
foundInClassMap = true;
// If it's abstract/interface, we need the instanceof chain
if (info.isAbstract || info.isInterface) {
type = fullName; // Use full name for abstract types
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 (${property.elementType} item : ${accessor}) {`);
if (property.elementKind === "primitive") {
lines.push(`${indent} json.writeValue(item);`);
} else {
lines.push(`${indent} ${property.writeMethodCall}(item);`);
}
break;
lines.push(`${indent} }`);
lines.push(`${indent} json.writeArrayEnd();`);
lines.push(`${indent}}`);
} else {
lines.push(`${indent}json.writeArrayStart();`);
lines.push(`${indent}for (${property.elementType} item : ${accessor}) {`);
if (property.elementKind === "primitive") {
lines.push(`${indent} json.writeValue(item);`);
} else {
lines.push(`${indent} ${property.writeMethodCall}(item);`);
}
lines.push(`${indent}}`);
lines.push(`${indent}json.writeArrayEnd();`);
}
}
}
if (isNullable) {
output.push(`${indent}if (${expression} == null) {`);
output.push(`${indent} json.writeNull();`);
output.push(`${indent}} else {`);
output.push(`${indent} write${shortType}(${expression});`);
output.push(`${indent}}`);
} else {
output.push(`${indent}write${shortType}(${expression});`);
break;
case "nestedArray":
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 (${property.elementType}[] nestedArray : ${accessor}) {`);
lines.push(`${indent} if (nestedArray == null) {`);
lines.push(`${indent} json.writeNull();`);
lines.push(`${indent} } else {`);
lines.push(`${indent} json.writeArrayStart();`);
lines.push(`${indent} for (${property.elementType} elem : nestedArray) {`);
lines.push(`${indent} json.writeValue(elem);`);
lines.push(`${indent} }`);
lines.push(`${indent} json.writeArrayEnd();`);
lines.push(`${indent} }`);
lines.push(`${indent} }`);
lines.push(`${indent} json.writeArrayEnd();`);
lines.push(`${indent}}`);
} else {
lines.push(`${indent}json.writeArrayStart();`);
lines.push(`${indent}for (${property.elementType}[] nestedArray : ${accessor}) {`);
lines.push(`${indent} json.writeArrayStart();`);
lines.push(`${indent} for (${property.elementType} elem : nestedArray) {`);
lines.push(`${indent} json.writeValue(elem);`);
lines.push(`${indent} }`);
lines.push(`${indent} json.writeArrayEnd();`);
lines.push(`${indent}}`);
lines.push(`${indent}json.writeArrayEnd();`);
}
break;
}
return lines;
}
function generateJavaSerializer(analysisData: SerializedAnalysisResult): string {
function generateJavaFromIR(ir: SerializerIR): string {
const javaOutput: string[] = [];
// Convert arrays back to Maps
const classMap = new Map(analysisData.classMap);
const abstractTypes = new Map(analysisData.abstractTypes);
const typeProperties = new Map(analysisData.typeProperties);
// Collect all types that need write methods
const typesNeedingMethods = new Set<string>();
// Add all types from allTypesToGenerate
for (const type of analysisData.allTypesToGenerate) {
typesNeedingMethods.add(type);
}
// Add all abstract types that are referenced (but not excluded)
const exclusions = loadExclusions();
for (const [abstractType] of abstractTypes) {
if (!exclusions.types.has(abstractType)) {
typesNeedingMethods.add(abstractType);
}
}
// Add types referenced in properties
for (const [typeName, props] of typeProperties) {
if (!typesNeedingMethods.has(typeName)) continue;
for (const prop of props) {
let propType = prop.type.replace(/@Null\s+/g, '').trim();
// Extract type from Array<Type>
const arrayMatch = propType.match(/Array<(.+?)>/);
if (arrayMatch) {
propType = arrayMatch[1].trim();
}
// Extract type from Type[]
if (propType.endsWith('[]')) {
propType = propType.slice(0, -2);
}
// Skip primitives and special types
if (['String', 'int', 'float', 'boolean', 'short', 'byte', 'double', 'long',
'Color', 'TextureRegion', 'IntArray', 'FloatArray'].includes(propType)) {
continue;
}
// Add the type if it's a class (but not excluded)
if (propType.match(/^[A-Z]/)) {
if (!exclusions.types.has(propType)) {
typesNeedingMethods.add(propType);
}
// Also check if it's an abstract type in classMap
for (const [fullName, info] of classMap) {
if (fullName === propType || fullName.split('.').pop() === propType) {
if (info.isAbstract || info.isInterface && !exclusions.types.has(fullName)) {
typesNeedingMethods.add(fullName);
}
break;
}
}
}
}
}
// Generate Java file header
javaOutput.push('package com.esotericsoftware.spine.utils;');
javaOutput.push('');
@ -318,7 +124,6 @@ function generateJavaSerializer(analysisData: SerializedAnalysisResult): string
javaOutput.push('import com.badlogic.gdx.utils.IntArray;');
javaOutput.push('import com.badlogic.gdx.utils.FloatArray;');
javaOutput.push('');
javaOutput.push('import java.util.Locale;');
javaOutput.push('import java.util.Set;');
javaOutput.push('import java.util.HashSet;');
@ -328,90 +133,49 @@ function generateJavaSerializer(analysisData: SerializedAnalysisResult): string
javaOutput.push(' private JsonWriter json;');
javaOutput.push('');
// Generate main entry methods
javaOutput.push(' public String serializeSkeletonData(SkeletonData data) {');
javaOutput.push(' visitedObjects.clear();');
javaOutput.push(' json = new JsonWriter();');
javaOutput.push(' writeSkeletonData(data);');
javaOutput.push(' json.close();');
javaOutput.push(' return json.getString();');
javaOutput.push(' }');
javaOutput.push('');
javaOutput.push(' public String serializeSkeleton(Skeleton skeleton) {');
javaOutput.push(' visitedObjects.clear();');
javaOutput.push(' json = new JsonWriter();');
javaOutput.push(' writeSkeleton(skeleton);');
javaOutput.push(' json.close();');
javaOutput.push(' return json.getString();');
javaOutput.push(' }');
javaOutput.push('');
javaOutput.push(' public String serializeAnimationState(AnimationState state) {');
javaOutput.push(' visitedObjects.clear();');
javaOutput.push(' json = new JsonWriter();');
javaOutput.push(' writeAnimationState(state);');
javaOutput.push(' json.close();');
javaOutput.push(' return json.getString();');
javaOutput.push(' }');
javaOutput.push('');
// Generate public methods
for (const method of ir.publicMethods) {
javaOutput.push(` public String ${method.name}(${method.paramType} ${method.paramName}) {`);
javaOutput.push(' visitedObjects.clear();');
javaOutput.push(' json = new JsonWriter();');
javaOutput.push(` ${method.writeMethodCall}(${method.paramName});`);
javaOutput.push(' json.close();');
javaOutput.push(' return json.getString();');
javaOutput.push(' }');
javaOutput.push('');
}
// Generate write methods for all types
const generatedMethods = new Set<string>();
for (const typeName of Array.from(typesNeedingMethods).sort()) {
const classInfo = classMap.get(typeName);
if (!classInfo) continue;
// Generate write methods
for (const method of ir.writeMethods) {
const shortName = method.paramType.split('.').pop()!;
const className = method.paramType.includes('.') ? method.paramType : shortName;
// Skip enums - they are handled inline with .name() calls
if (classInfo.isEnum) continue;
javaOutput.push(` private void ${method.name}(${className} obj) {`);
const shortName = typeName.split('.').pop()!;
// Skip if already generated (handle name collisions)
if (generatedMethods.has(shortName)) continue;
generatedMethods.add(shortName);
// Use full class name for inner classes
const className = typeName.includes('.') ? typeName : shortName;
javaOutput.push(` private void write${shortName}(${className} obj) {`);
if (classInfo.isEnum) {
// Handle enums
javaOutput.push(' json.writeValue(obj.name());');
} else if (classInfo.isAbstract || classInfo.isInterface) {
if (method.isAbstractType) {
// Handle abstract types with instanceof chain
const implementations = classInfo.concreteImplementations || [];
// Filter out excluded types from implementations
const exclusions = loadExclusions();
const filteredImplementations = implementations.filter(impl => {
return !exclusions.types.has(impl);
});
if (filteredImplementations.length === 0) {
javaOutput.push(' json.writeNull(); // No concrete implementations after filtering exclusions');
} else {
if (method.subtypeChecks && method.subtypeChecks.length > 0) {
let first = true;
for (const impl of filteredImplementations) {
const implShortName = impl.split('.').pop()!;
const implClassName = impl.includes('.') ? impl : implShortName;
for (const subtype of method.subtypeChecks) {
const subtypeShortName = subtype.typeName.split('.').pop()!;
const subtypeClassName = subtype.typeName.includes('.') ? subtype.typeName : subtypeShortName;
if (first) {
javaOutput.push(` if (obj instanceof ${implClassName}) {`);
javaOutput.push(` if (obj instanceof ${subtypeClassName}) {`);
first = false;
} else {
javaOutput.push(` } else if (obj instanceof ${implClassName}) {`);
javaOutput.push(` } else if (obj instanceof ${subtypeClassName}) {`);
}
javaOutput.push(` write${implShortName}((${implClassName}) obj);`);
javaOutput.push(` ${subtype.writeMethodCall}((${subtypeClassName}) obj);`);
}
javaOutput.push(' } else {');
javaOutput.push(` throw new RuntimeException("Unknown ${shortName} type: " + obj.getClass().getName());`);
javaOutput.push(' }');
} else {
javaOutput.push(' json.writeNull(); // No concrete implementations after filtering exclusions');
}
} else {
// Handle concrete types
const properties = typeProperties.get(typeName) || [];
// Add cycle detection
javaOutput.push(' if (visitedObjects.contains(obj)) {');
javaOutput.push(' json.writeValue("<circular>");');
@ -426,22 +190,12 @@ function generateJavaSerializer(analysisData: SerializedAnalysisResult): string
javaOutput.push(' json.writeName("type");');
javaOutput.push(` json.writeValue("${shortName}");`);
// Write properties (skip excluded ones)
for (const prop of properties) {
if (prop.excluded) {
javaOutput.push(` // Skipping excluded property: ${prop.name}`);
continue;
}
const propName = prop.isGetter ?
prop.name.replace('get', '').replace('()', '').charAt(0).toLowerCase() +
prop.name.replace('get', '').replace('()', '').slice(1) :
prop.name;
// Write properties
for (const property of method.properties) {
javaOutput.push('');
javaOutput.push(` json.writeName("${propName}");`);
const accessor = prop.isGetter ? `obj.${prop.name}` : `obj.${prop.name}`;
generateWriteValue(javaOutput, accessor, prop.type, ' ', abstractTypes, classMap);
javaOutput.push(` json.writeName("${property.name}");`);
const propertyLines = generatePropertyCode(property, ' ');
javaOutput.push(...propertyLines);
}
javaOutput.push('');
@ -452,7 +206,7 @@ function generateJavaSerializer(analysisData: SerializedAnalysisResult): string
javaOutput.push('');
}
// Add helper methods
// Add helper methods for special types
javaOutput.push(' private void writeColor(Color obj) {');
javaOutput.push(' if (obj == null) {');
javaOutput.push(' json.writeNull();');
@ -491,6 +245,34 @@ function generateJavaSerializer(analysisData: SerializedAnalysisResult): string
javaOutput.push(' json.writeObjectEnd();');
javaOutput.push(' }');
javaOutput.push(' }');
// Add IntArray and FloatArray helper methods
javaOutput.push('');
javaOutput.push(' private void writeIntArray(IntArray obj) {');
javaOutput.push(' if (obj == null) {');
javaOutput.push(' json.writeNull();');
javaOutput.push(' } else {');
javaOutput.push(' json.writeArrayStart();');
javaOutput.push(' for (int i = 0; i < obj.size; i++) {');
javaOutput.push(' json.writeValue(obj.get(i));');
javaOutput.push(' }');
javaOutput.push(' json.writeArrayEnd();');
javaOutput.push(' }');
javaOutput.push(' }');
javaOutput.push('');
javaOutput.push(' private void writeFloatArray(FloatArray obj) {');
javaOutput.push(' if (obj == null) {');
javaOutput.push(' json.writeNull();');
javaOutput.push(' } else {');
javaOutput.push(' json.writeArrayStart();');
javaOutput.push(' for (int i = 0; i < obj.size; i++) {');
javaOutput.push(' json.writeValue(obj.get(i));');
javaOutput.push(' }');
javaOutput.push(' json.writeArrayEnd();');
javaOutput.push(' }');
javaOutput.push(' }');
javaOutput.push('}');
return javaOutput.join('\n');
@ -498,17 +280,17 @@ function generateJavaSerializer(analysisData: SerializedAnalysisResult): string
async function main() {
try {
// Read analysis result
const analysisFile = path.resolve(__dirname, '..', 'output', 'analysis-result.json');
if (!fs.existsSync(analysisFile)) {
console.error('Analysis result not found. Run analyze-java-api.ts first.');
// 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 analysisData: SerializedAnalysisResult = JSON.parse(fs.readFileSync(analysisFile, 'utf8'));
const ir: SerializerIR = JSON.parse(fs.readFileSync(irFile, 'utf8'));
// Generate Java serializer
const javaCode = generateJavaSerializer(analysisData);
// Generate Java serializer from IR
const javaCode = generateJavaFromIR(ir);
// Write the Java file
const javaFile = path.resolve(
@ -527,10 +309,13 @@ async function main() {
fs.mkdirSync(path.dirname(javaFile), { recursive: true });
fs.writeFileSync(javaFile, javaCode);
console.log(`Generated Java serializer: ${javaFile}`);
console.log(`Generated Java serializer from IR: ${javaFile}`);
console.log(`- ${ir.publicMethods.length} public methods`);
console.log(`- ${ir.writeMethods.length} write methods`);
} catch (error: any) {
console.error('Error:', error.message);
console.error('Stack:', error.stack);
process.exit(1);
}
}
@ -540,4 +325,4 @@ if (import.meta.url === `file://${process.argv[1]}`) {
main();
}
export { generateJavaSerializer };
export { generateJavaFromIR };

View File

@ -0,0 +1,575 @@
#!/usr/bin/env tsx
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
import type { ClassInfo, PropertyInfo } from './types';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// IR Type Definitions
interface SerializerIR {
publicMethods: PublicMethod[];
writeMethods: WriteMethod[];
enumMappings: { [enumName: string]: { [javaValue: string]: string } };
}
interface PublicMethod {
name: string;
paramType: string;
paramName: string;
writeMethodCall: string;
}
interface WriteMethod {
name: string;
paramType: string;
properties: Property[];
isAbstractType: boolean;
subtypeChecks?: SubtypeCheck[];
}
interface SubtypeCheck {
typeName: string;
writeMethodCall: string;
}
type Property = Primitive | Object | Enum | Array | NestedArray;
interface Primitive {
kind: "primitive";
name: string;
getter: string;
valueType: string;
isNullable: boolean;
}
interface Object {
kind: "object";
name: string;
getter: string;
valueType: string;
writeMethodCall: string;
isNullable: boolean;
}
interface Enum {
kind: "enum";
name: string;
getter: string;
enumName: string;
isNullable: boolean;
}
interface Array {
kind: "array";
name: string;
getter: string;
elementType: string;
elementKind: "primitive" | "object";
writeMethodCall?: string;
isNullable: boolean;
}
interface NestedArray {
kind: "nestedArray";
name: string;
getter: string;
elementType: string;
isNullable: boolean;
}
interface SerializedAnalysisResult {
classMap: [string, ClassInfo][];
accessibleTypes: string[];
abstractTypes: [string, string[]][];
allTypesToGenerate: string[];
typeProperties: [string, PropertyInfo[]][];
}
function loadExclusions(): { types: Set<string>, methods: Map<string, Set<string>>, fields: Map<string, Set<string>> } {
const exclusionsPath = path.resolve(__dirname, 'java-exclusions.txt');
const types = new Set<string>();
const methods = new Map<string, Set<string>>();
const fields = new Map<string, Set<string>>();
if (!fs.existsSync(exclusionsPath)) {
return { types, methods, fields };
}
const content = fs.readFileSync(exclusionsPath, 'utf-8');
const lines = content.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const parts = trimmed.split(/\s+/);
if (parts.length < 2) continue;
const [type, className, property] = parts;
switch (type) {
case 'type':
types.add(className);
break;
case 'method':
if (property) {
if (!methods.has(className)) {
methods.set(className, new Set());
}
methods.get(className)!.add(property);
}
break;
case 'field':
if (property) {
if (!fields.has(className)) {
fields.set(className, new Set());
}
fields.get(className)!.add(property);
}
break;
}
}
return { types, methods, fields };
}
function analyzePropertyType(propType: string, classMap: Map<string, ClassInfo>): Property {
// Handle null annotations
const isNullable = propType.includes('@Null');
propType = propType.replace(/@Null\s+/g, '').trim();
// Extract property name and getter from the type analysis
// This is a simplified version - in practice we'd get this from PropertyInfo
const name = "propertyName"; // placeholder
const getter = "getPropertyName"; // placeholder
// Primitive types
if (['String', 'int', 'float', 'boolean', 'short', 'byte', 'double', 'long'].includes(propType)) {
return {
kind: "primitive",
name,
getter,
valueType: propType,
isNullable
};
}
// Check if it's an enum
let classInfo = classMap.get(propType);
if (!classInfo && !propType.includes('.')) {
// Try to find by short name
for (const [fullName, info] of classMap) {
if (fullName.split('.').pop() === propType) {
classInfo = info;
break;
}
}
}
if (classInfo?.isEnum) {
return {
kind: "enum",
name,
getter,
enumName: propType.split('.').pop()!,
isNullable
};
}
// Arrays
if (propType.startsWith('Array<')) {
const innerType = propType.match(/Array<(.+?)>/)![1].trim();
const elementKind = ['String', 'int', 'float', 'boolean', 'short', 'byte', 'double', 'long'].includes(innerType) ? "primitive" : "object";
return {
kind: "array",
name,
getter,
elementType: innerType,
elementKind,
writeMethodCall: elementKind === "object" ? `write${innerType}` : undefined,
isNullable
};
}
// Handle nested arrays (like float[][])
if (propType.endsWith('[]')) {
const elemType = propType.slice(0, -2);
if (elemType.endsWith('[]')) {
const nestedType = elemType.slice(0, -2);
return {
kind: "nestedArray",
name,
getter,
elementType: nestedType,
isNullable
};
} else {
const elementKind = ['String', 'int', 'float', 'boolean', 'short', 'byte', 'double', 'long'].includes(elemType) ? "primitive" : "object";
return {
kind: "array",
name,
getter,
elementType: elemType,
elementKind,
writeMethodCall: elementKind === "object" ? `write${elemType}` : undefined,
isNullable
};
}
}
// Special libGDX types that get custom handling
if (['Color', 'TextureRegion', 'IntArray', 'FloatArray'].includes(propType)) {
return {
kind: "object",
name,
getter,
valueType: propType,
writeMethodCall: `write${propType}`,
isNullable
};
}
// Object types
const shortType = propType.split('.').pop()!;
return {
kind: "object",
name,
getter,
valueType: propType,
writeMethodCall: `write${shortType}`,
isNullable
};
}
function generateSerializerIR(analysisData: SerializedAnalysisResult): SerializerIR {
// Convert arrays back to Maps
const classMap = new Map(analysisData.classMap);
const abstractTypes = new Map(analysisData.abstractTypes);
const typeProperties = new Map(analysisData.typeProperties);
const exclusions = loadExclusions();
// Generate enum mappings
const enumMappings: { [enumName: string]: { [javaValue: string]: string } } = {};
for (const [className, classInfo] of classMap) {
if (classInfo.isEnum && classInfo.enumValues) {
const shortName = className.split('.').pop()!;
const valueMap: { [javaValue: string]: string } = {};
for (const javaValue of classInfo.enumValues) {
// Convert Java enum value to C++ enum value
// e.g. "setup" -> "MixBlend_Setup", "first" -> "MixBlend_First"
const cppValue = `${shortName}_${javaValue.charAt(0).toUpperCase() + javaValue.slice(1)}`;
valueMap[javaValue] = cppValue;
}
enumMappings[shortName] = valueMap;
}
}
// Generate public methods
const publicMethods: PublicMethod[] = [
{
name: "serializeSkeletonData",
paramType: "SkeletonData",
paramName: "data",
writeMethodCall: "writeSkeletonData"
},
{
name: "serializeSkeleton",
paramType: "Skeleton",
paramName: "skeleton",
writeMethodCall: "writeSkeleton"
},
{
name: "serializeAnimationState",
paramType: "AnimationState",
paramName: "state",
writeMethodCall: "writeAnimationState"
}
];
// Collect all types that need write methods
const typesNeedingMethods = new Set<string>();
// Add all types from allTypesToGenerate
for (const type of analysisData.allTypesToGenerate) {
typesNeedingMethods.add(type);
}
// Add all abstract types that are referenced (but not excluded)
for (const [abstractType] of abstractTypes) {
if (!exclusions.types.has(abstractType)) {
typesNeedingMethods.add(abstractType);
}
}
// Add types referenced in properties
for (const [typeName, props] of typeProperties) {
if (!typesNeedingMethods.has(typeName)) continue;
for (const prop of props) {
let propType = prop.type.replace(/@Null\s+/g, '').trim();
// Extract type from Array<Type>
const arrayMatch = propType.match(/Array<(.+?)>/);
if (arrayMatch) {
propType = arrayMatch[1].trim();
}
// Extract type from Type[]
if (propType.endsWith('[]')) {
propType = propType.slice(0, -2);
}
// Skip primitives and special types
if (['String', 'int', 'float', 'boolean', 'short', 'byte', 'double', 'long',
'Color', 'TextureRegion', 'IntArray', 'FloatArray'].includes(propType)) {
continue;
}
// Add the type if it's a class (but not excluded)
if (propType.match(/^[A-Z]/)) {
if (!exclusions.types.has(propType)) {
typesNeedingMethods.add(propType);
}
// Also check if it's an abstract type in classMap
for (const [fullName, info] of classMap) {
if (fullName === propType || fullName.split('.').pop() === propType) {
if ((info.isAbstract || info.isInterface) && !exclusions.types.has(fullName)) {
typesNeedingMethods.add(fullName);
}
break;
}
}
}
}
}
// Generate write methods
const writeMethods: WriteMethod[] = [];
const generatedMethods = new Set<string>();
for (const typeName of Array.from(typesNeedingMethods).sort()) {
const classInfo = classMap.get(typeName);
if (!classInfo) continue;
// Skip enums - they are handled inline with .name() calls
if (classInfo.isEnum) continue;
const shortName = typeName.split('.').pop()!;
// Skip if already generated (handle name collisions)
if (generatedMethods.has(shortName)) continue;
generatedMethods.add(shortName);
const writeMethod: WriteMethod = {
name: `write${shortName}`,
paramType: typeName,
properties: [],
isAbstractType: classInfo.isAbstract || classInfo.isInterface || false
};
if (classInfo.isAbstract || classInfo.isInterface) {
// Handle abstract types with instanceof chain
const implementations = classInfo.concreteImplementations || [];
// Filter out excluded types from implementations
const filteredImplementations = implementations.filter(impl => {
return !exclusions.types.has(impl);
});
if (filteredImplementations.length > 0) {
writeMethod.subtypeChecks = filteredImplementations.map(impl => {
const implShortName = impl.split('.').pop()!;
return {
typeName: impl,
writeMethodCall: `write${implShortName}`
};
});
}
} else {
// Handle concrete types - convert properties
const properties = typeProperties.get(typeName) || [];
for (const prop of properties) {
if (prop.excluded) {
continue; // Skip excluded properties
}
const propName = prop.isGetter ?
prop.name.replace('get', '').replace('()', '').charAt(0).toLowerCase() +
prop.name.replace('get', '').replace('()', '').slice(1) :
prop.name;
// Handle getter vs field access
let getter: string;
if (prop.isGetter) {
// It's a method call - ensure it has parentheses
getter = prop.name.includes('()') ? prop.name : `${prop.name}()`;
} else {
// It's a field access - use the field name directly
getter = prop.name;
}
// Analyze the property type to determine the correct Property variant
const irProperty = analyzePropertyWithDetails(prop, propName, getter, classMap);
writeMethod.properties.push(irProperty);
}
}
writeMethods.push(writeMethod);
}
return {
publicMethods,
writeMethods,
enumMappings
};
}
function analyzePropertyWithDetails(prop: PropertyInfo, propName: string, getter: string, classMap: Map<string, ClassInfo>): Property {
// Handle null annotations
const isNullable = prop.type.includes('@Null');
let propType = prop.type.replace(/@Null\s+/g, '').trim();
// Primitive types
if (['String', 'int', 'float', 'boolean', 'short', 'byte', 'double', 'long'].includes(propType)) {
return {
kind: "primitive",
name: propName,
getter,
valueType: propType,
isNullable
};
}
// Check if it's an enum
let classInfo = classMap.get(propType);
if (!classInfo && !propType.includes('.')) {
// Try to find by short name
for (const [fullName, info] of classMap) {
if (fullName.split('.').pop() === propType) {
classInfo = info;
propType = fullName; // Use full name
break;
}
}
}
if (classInfo?.isEnum) {
return {
kind: "enum",
name: propName,
getter,
enumName: propType.split('.').pop()!,
isNullable
};
}
// Arrays
if (propType.startsWith('Array<')) {
const innerType = propType.match(/Array<(.+?)>/)![1].trim();
const elementKind = ['String', 'int', 'float', 'boolean', 'short', 'byte', 'double', 'long'].includes(innerType) ? "primitive" : "object";
return {
kind: "array",
name: propName,
getter,
elementType: innerType,
elementKind,
writeMethodCall: elementKind === "object" ? `write${innerType.split('.').pop()}` : undefined,
isNullable
};
}
// Handle nested arrays (like float[][])
if (propType.endsWith('[]')) {
const elemType = propType.slice(0, -2);
if (elemType.endsWith('[]')) {
const nestedType = elemType.slice(0, -2);
return {
kind: "nestedArray",
name: propName,
getter,
elementType: nestedType,
isNullable
};
} else {
const elementKind = ['String', 'int', 'float', 'boolean', 'short', 'byte', 'double', 'long'].includes(elemType) ? "primitive" : "object";
return {
kind: "array",
name: propName,
getter,
elementType: elemType,
elementKind,
writeMethodCall: elementKind === "object" ? `write${elemType.split('.').pop()}` : undefined,
isNullable
};
}
}
// Special libGDX types that get custom handling
if (['Color', 'TextureRegion', 'IntArray', 'FloatArray'].includes(propType)) {
return {
kind: "object",
name: propName,
getter,
valueType: propType,
writeMethodCall: `write${propType}`,
isNullable
};
}
// Object types
const shortType = propType.split('.').pop()!;
return {
kind: "object",
name: propName,
getter,
valueType: propType,
writeMethodCall: `write${shortType}`,
isNullable
};
}
async function main() {
try {
// Read analysis result
const analysisFile = path.resolve(__dirname, '..', 'output', 'analysis-result.json');
if (!fs.existsSync(analysisFile)) {
console.error('Analysis result not found. Run analyze-java-api.ts first.');
process.exit(1);
}
const analysisData: SerializedAnalysisResult = JSON.parse(fs.readFileSync(analysisFile, 'utf8'));
// Generate IR
const ir = generateSerializerIR(analysisData);
// Write the IR file
const irFile = path.resolve(__dirname, 'output', 'serializer-ir.json');
fs.mkdirSync(path.dirname(irFile), { recursive: true });
fs.writeFileSync(irFile, JSON.stringify(ir, null, 2));
console.log(`Generated serializer IR: ${irFile}`);
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 { generateSerializerIR, type SerializerIR, type PublicMethod, type WriteMethod, type Property };

View File

@ -8,6 +8,9 @@ cd "$SCRIPT_DIR/.."
echo "Analyzing Java API..."
npx tsx tests/analyze-java-api.ts
echo "Generating serializer IR..."
npx tsx tests/generate-serializer-ir.ts
echo "Generating Java SkeletonSerializer..."
npx tsx tests/generate-java-serializer.ts