feat(fls): read and write a project's own applications as their beans

A project's own applications (classes under projekt/anwendungen, installed
on a host) are no longer left out. The decoder now keeps the Java type of
every primitive — at setters, constructor and call arguments, also through
shared ids like Integer0 — and which properties an object's own getter
made. beanCapture.ts turns a decoded bean back into what the writer writes,
with the node's system software as a mark the writer replaces by the
system software it writes; what the format cannot say is refused, not
guessed (an unresolved value, a Field, a factory-made object, an object a
getter reaches elsewhere in the file). The reader keeps own applications as
captured beans; the corpus no longer warns.

The capture first took a setter with another object's getter result for an
own getter; the round-trip test found it. The Java oracle did not show
whether an application runs on its node's system software, so a mutation
writing the mark instead survived; it now reports onItsSystem (true for all
192 applications of the corpus).

Checked by Java: from the files written again, FILIUS loads Accumulator and
Broadcast installed, switched on and on their node's system software, as
in the original. The round-trip test covers every primitive, null, a
class, an array, shared objects, getter chains, constructed elements, map
reads and static constants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 09:43:16 +00:00
co-authored by Claude Opus 5.5
parent 95c5b7d97b
commit 3548652687
18 changed files with 1444 additions and 404 deletions
+76 -10
View File
@@ -45,8 +45,18 @@ export interface JavaObject {
readonly factoryMethod: string | undefined;
/** Set through setters, `Field.set` or created lazily by a getter. */
readonly properties: Map<string, JavaValue>;
/** Method calls in order (`add`, `put`, …), with their arguments. */
readonly calls: { method: string; args: JavaValue[] }[];
/** The Java type of each property set to a primitive (`int`, `long`, …): numbers alone do not say. */
readonly propertyKinds: Map<string, PrimitiveKind>;
/**
* The properties whose value this object's own getter made (a placeholder
* the file's statements then changed) — not set to an object from
* elsewhere.
*/
readonly propertiesFromGetters: Set<string>;
/** The Java types of the constructor's arguments, where they are primitives. */
readonly argKinds: (PrimitiveKind | undefined)[];
/** Method calls in order (`add`, `put`, …), with their arguments and those arguments' primitive types. */
readonly calls: { method: string; args: JavaValue[]; kinds?: (PrimitiveKind | undefined)[] }[];
/**
* Elements the object already held before the file's statements — a list
* its constructor filled, reached by `<void index="i">` on a getter's
@@ -62,6 +72,23 @@ export interface JavaObject {
readonly fromGetter: boolean;
}
/** The primitive types XMLEncoder writes as elements of their own. */
export type PrimitiveKind = 'boolean' | 'byte' | 'char' | 'short' | 'int' | 'long' | 'float' | 'double';
const PRIMITIVE_TAGS = new Set<string>(['boolean', 'byte', 'char', 'short', 'int', 'long', 'float', 'double']);
/** A wrapper's id prefix, as XMLEncoder names a shared value (Integer0), and its primitive. */
const KIND_OF_ID_PREFIX: Readonly<Record<string, PrimitiveKind>> = {
Boolean: 'boolean',
Byte: 'byte',
Character: 'char',
Short: 'short',
Integer: 'int',
Long: 'long',
Float: 'float',
Double: 'double',
};
export interface JavaArray {
readonly kind: 'array';
readonly componentClass: string;
@@ -262,6 +289,9 @@ function newObject(fields: Partial<JavaObject> & { className: string }): JavaObj
staticField: fields.staticField,
factoryMethod: fields.factoryMethod,
properties: new Map(),
propertyKinds: new Map(),
propertiesFromGetters: new Set(),
argKinds: [],
calls: [],
presetElements: new Map(),
presetEntries: new Map(),
@@ -271,6 +301,15 @@ function newObject(fields: Partial<JavaObject> & { className: string }): JavaObj
class Decoder {
private readonly ids = new Map<string, JavaValue>();
/** The primitive type of each id that names a primitive value. */
private readonly idKinds = new Map<string, PrimitiveKind>();
/** The primitive type an argument element stands for, if any. */
private kindOf(element: Element): PrimitiveKind | undefined {
if (PRIMITIVE_TAGS.has(element.name)) return element.name as PrimitiveKind;
const idref = element.attributes.idref;
return idref === undefined ? undefined : this.idKinds.get(idref);
}
topLevel(java: Element): JavaValue[] {
return java.children.map((child) => this.expression(child));
@@ -332,6 +371,10 @@ class Decoder {
private primitive(element: Element, value: JavaValue): JavaValue {
this.store(element.attributes.id, value);
const id = element.attributes.id;
// An element of its own (<int>), or a constant named by its wrapper (Integer0).
const kind = PRIMITIVE_TAGS.has(element.name) ? (element.name as PrimitiveKind) : KIND_OF_ID_PREFIX[id?.replace(/\d+$/, '') ?? ''];
if (id !== undefined && kind !== undefined) this.idKinds.set(id, kind);
return value;
}
@@ -382,7 +425,10 @@ class Decoder {
factoryMethod: attributes.method,
});
this.store(attributes.id, object);
for (const arg of args) object.args.push(this.expression(arg));
for (const arg of args) {
object.args.push(this.expression(arg));
object.argKinds.push(this.kindOf(arg));
}
this.applyStatements(
element.children.filter((child) => child.name === 'void'),
object,
@@ -400,12 +446,14 @@ class Decoder {
private statement(element: Element, target: JavaValue | undefined): JavaValue {
const attributes = element.attributes;
const args: JavaValue[] = [];
const kinds: (PrimitiveKind | undefined)[] = [];
const nested: Element[] = [];
for (const child of element.children) {
if (child.name === 'void') {
nested.push(child);
} else {
args.push(this.expression(child));
kinds.push(this.kindOf(child));
}
}
@@ -416,17 +464,23 @@ class Decoder {
value =
args.length === 0
? this.getProperty(target, attributes.property, attributes.id)
: this.setProperty(target, attributes.property, args[0]!);
: this.setProperty(target, attributes.property, args[0]!, kinds[0]);
} else if (attributes.index !== undefined) {
const index = Number.parseInt(attributes.index, 10);
value = args.length === 0 ? this.getIndex(target, index, attributes.id) : this.setIndex(target, index, args[0]!);
} else if (attributes.method !== undefined) {
value = this.call(target, attributes.method, args, attributes.id);
value = this.call(target, attributes.method, args, attributes.id, kinds);
} else if (attributes.idref !== undefined) {
value = this.ids.get(attributes.idref) ?? null;
}
this.store(attributes.id, value);
// A getter's primitive result, shared by id: its wrapper names the type.
const prefix = attributes.id?.replace(/\d+$/, '');
const kind = prefix === undefined ? undefined : KIND_OF_ID_PREFIX[prefix];
if (kind !== undefined && (typeof value === 'number' || typeof value === 'bigint' || typeof value === 'boolean')) {
this.idKinds.set(attributes.id!, kind);
}
this.applyStatements(nested, value);
return value;
}
@@ -439,10 +493,16 @@ class Decoder {
return newObject({ className, id, args, factoryMethod: method });
}
private call(target: JavaValue | undefined, method: string, args: JavaValue[], id: string | undefined): JavaValue {
private call(
target: JavaValue | undefined,
method: string,
args: JavaValue[],
id: string | undefined,
kinds: (PrimitiveKind | undefined)[] = [],
): JavaValue {
if (typeof target === 'object' && target !== null && (target as JavaField).kind === 'field') {
const field = target as JavaField;
if (method === 'set' && args.length === 2) return this.setProperty(args[0]!, field.name, args[1]!);
if (method === 'set' && args.length === 2) return this.setProperty(args[0]!, field.name, args[1]!, kinds[1]);
if (method === 'get' && args.length === 1) return this.getProperty(args[0]!, field.name, id);
}
if (args.length === 0) {
@@ -450,15 +510,20 @@ class Decoder {
if (property !== undefined) return this.getProperty(target, property, id);
}
if (isJavaObject(target)) {
target.calls.push({ method, args });
target.calls.push(kinds.some((kind) => kind !== undefined) ? { method, args, kinds } : { method, args });
if (method === 'get' && args.length === 1 && typeof args[0] === 'number') return this.getIndex(target, args[0], id);
if (method === 'get' && args.length === 1 && typeof args[0] === 'string') return this.getEntry(target, args[0], id);
}
return null;
}
private setProperty(target: JavaValue | undefined, property: string, value: JavaValue): JavaValue {
if (isJavaObject(target)) target.properties.set(property, value);
private setProperty(target: JavaValue | undefined, property: string, value: JavaValue, kind?: PrimitiveKind): JavaValue {
if (isJavaObject(target)) {
target.properties.set(property, value);
target.propertiesFromGetters.delete(property);
if (kind === undefined) target.propertyKinds.delete(property);
else target.propertyKinds.set(property, kind);
}
return null;
}
@@ -482,6 +547,7 @@ class Decoder {
}
const placeholder = newObject({ className: prefix ?? 'unknown', id, fromGetter: true });
target.properties.set(property, placeholder);
target.propertiesFromGetters.add(property);
return placeholder;
}