From 89a080c19f087e4a0221d636b06af82bb1b306fd Mon Sep 17 00:00:00 2001 From: Frotty Date: Tue, 11 Aug 2026 13:00:36 +0200 Subject: [PATCH] Document serialization compiler intrinsics --- _doc/manual.md | 95 ++++++++++++++++++++++++------------------ _tutorials/saveload.md | 38 ++++++++--------- 2 files changed, 72 insertions(+), 61 deletions(-) diff --git a/_doc/manual.md b/_doc/manual.md index 96b8cff..7c9075b 100644 --- a/_doc/manual.md +++ b/_doc/manual.md @@ -880,9 +880,10 @@ function foo() ### Compiler-assisted field mapping -For dedicated state classes, import `MagicFunctions` to use compiler-assisted field iteration and generic -construction. These operations expand to ordinary constructors and direct field accesses; serialization formats, -hashes, and storage remain standard-library concerns. +Import `MagicFunctions` to use the compiler-provided `wurstForFields`, `wurstMapFields`, and +`wurstNewInstance()` helpers. Their `@compilerintrinsic` declarations provide completion, hover information, +and definition navigation. Calls are replaced at compile time with ordinary field accesses or constructor calls; +the declarations themselves do not remain in generated Jass or Lua. ```wurst import MagicFunctions @@ -892,59 +893,71 @@ class PlayerState string name = "" function save(FieldWriter writer) - forFields((fieldName, value) -> writer.write(fieldName, value)) + wurstForFields((fieldName, value) -> writer.write(fieldName, value)) function load(FieldReader reader) - mapFields((fieldName, value) -> reader.read(fieldName, value)) + wurstMapFields((fieldName, value) -> reader.read(fieldName, value)) ``` -`forFields` invokes the callback once for every accessible, non-static instance field. This includes inherited, -module-injected, readonly, and constant fields. The callback receives the field key and current value and must -produce a statement. `mapFields` assigns each callback result back to its field, so it includes only accessible, -mutable instance fields. Module field keys are qualified when necessary to disambiguate equal names. +`wurstForFields` emits one callback invocation for each accessible, non-static instance field. This includes +inherited, module-injected, readonly, and constant fields. The callback must produce a statement. +`wurstMapFields` assigns each callback result back to its field, so every visited field must be mutable; a readonly +or constant field produces a compile-time diagnostic. Static fields are never visited. -Both functions also accept an explicit target. The target is evaluated exactly once: +The callback receives a field key and the field's current value. The value parameter has a different concrete type +for each generated invocation, despite the `int` placeholder shown by the tooling interface. Leave callback +parameter types inferred and use overloads for the field types your mapper supports. Field iteration itself is +shallow: nested classes, tuples, collections, nullable values, and other composite types require matching library +or user-provided overloads. The compiler does not recursively serialize them. + +Both operations also accept an explicit target as their first argument. Class targets are evaluated exactly once. +Class and tuple targets are supported. A tuple passed to `wurstMapFields` must be a variable so the compiler can +write the mapped tuple back once after updating its components. ```wurst -forFields(state, (fieldName, value) -> writer.write(fieldName, value)) -mapFields(state, (fieldName, value) -> reader.read(fieldName, value)) -``` +tuple Position(int x, int y) + +function savePosition(Position position, FieldWriter writer) + wurstForFields(position, (fieldName, value) -> writer.write(fieldName, value)) -Leave callback parameter types inferred and overload the reader or writer for every field type used by the state -class. An applicable ordinary visible overload with one of these names is resolved normally and is not treated as -compiler magic. +function loadPosition(Position position, FieldReader reader) returns Position + var result = position + wurstMapFields(result, (fieldName, value) -> reader.read(fieldName, value)) + return result +``` -Use `newInstance()` when a specialized generic function needs to construct its concrete result type: +Use `wurstNewInstance()` in a generic loader when the concrete result type is known at specialization time: ```wurst function loadState(FieldReader reader) returns T - let result = newInstance() - mapFields(result, (fieldName, oldValue) -> reader.read(fieldName, oldValue)) + let result = wurstNewInstance() + wurstMapFields(result, (fieldName, oldValue) -> reader.read(fieldName, oldValue)) return result ``` -At each concrete call such as `loadState(reader)`, the compiler specializes the required path and -lowers `newInstance()` to its normal zero-argument constructor. `T` must resolve to a concrete, -non-abstract class with an accessible zero-argument constructor. Interfaces, handles, primitives, tuples, -unresolved type parameters, and classes without a usable constructor are rejected. - -Keep a generic loader in the free-function form shown above. On Lua, a method cannot currently combine type -parameters from its generic owning class with additional type parameters declared by the method itself. -Likewise, do not call `newInstance()` from a generic class constructor. Construct the state in the loader and -initialize nested state explicitly afterward. - -On Lua, do not invoke a generic-construction method directly on a freshly constructed generic receiver. Prefer the -free loader above, or store the receiver in a typed local first. Multi-parameter generic-interface dispatch is also -outside this loader contract; use one construction type parameter. `newInstance()` is for runtime Jass/Lua -construction and is not supported inside `compiletime(...)` expressions. Field mapping also does not support -nested modules whose sibling submodules declare fields with the same name; use direct fields, inheritance, or -unique shallow module field names for dedicated state classes. - -These are compile-time transformations, not runtime reflection, and generate equivalent direct accesses in both -Jass and Lua. They generate no runtime registry, type-name lookup, or reflection metadata. Keep serializable state -in small, dedicated classes, avoid unsupported field kinds such as static fields, and keep persistence codecs and -format migration separate from the state model. See the [Save and Load tutorial](/tutorials/saveload.html) for -integration with Warcraft III's file API. +The helper invokes the normal accessible zero-argument constructor of a concrete, non-abstract class. It does not +allocate an uninitialized object or look up a class by name. Constructor initializers run normally, which lets a +serialization library retain defaults for fields missing from older records. + +These helpers provide no wire format, stable field IDs, versioning, migration policy, integrity checks, runtime +reflection metadata, or type registry. Those remain library concerns. In particular, field keys are source names, +not stable persisted identities; a library must translate them to its own schema identity if renames need to remain +compatible. + +The original unprefixed `forFields`, `mapFields`, and `newInstance()` spellings remain available as compatibility +fallbacks. New code should use the `wurst`-prefixed names to avoid accidental collisions. If an applicable ordinary +function with the same name is visible, normal overload resolution selects that function instead of the compiler +operation. + +For Lua, keep generic construction in a free function with one construction type parameter. Do not combine type +parameters from a generic owning class with independent method type parameters, call `wurstNewInstance()` from +a generic class constructor, invoke a generic-construction method directly on a freshly constructed generic +receiver, or use multi-parameter generic-interface dispatch. `wurstNewInstance()` is a runtime Jass/Lua helper +and is not supported inside `compiletime(...)`. Field mapping does not support nested modules whose sibling +submodules declare equal field names. + +See the [Save and Load tutorial](/tutorials/saveload.html) for integration with Warcraft III's file API and the +standard library serialization layers. Identifiers beginning with `__wurst` are reserved for compiler-generated internals and must not be declared by user code. diff --git a/_tutorials/saveload.md b/_tutorials/saveload.md index 9e9fc45..f50b65d 100644 --- a/_tutorials/saveload.md +++ b/_tutorials/saveload.md @@ -64,8 +64,8 @@ The `serialize()` function returns a `ChunkedString`, which then can be passed t ## Compiler-assisted field mapping -For small, dedicated state classes, Wurst can generate the repetitive field mapping for you. Import -`MagicFunctions`, then use `forFields` when writing fields and `mapFields` when reading them: +For small, dedicated state classes, Wurst can generate repetitive field mapping. Import `MagicFunctions`, then +use the canonical `wurstForFields` helper when writing and `wurstMapFields` when reading: ```wurst import MagicFunctions @@ -75,49 +75,47 @@ class PlayerState string name = "" function save(FieldWriter writer) - forFields((fieldName, value) -> writer.write(fieldName, value)) + wurstForFields((fieldName, value) -> writer.write(fieldName, value)) function load(FieldReader reader) - mapFields((fieldName, value) -> reader.read(fieldName, value)) + wurstMapFields((fieldName, value) -> reader.read(fieldName, value)) ``` The callback receives the field name as a `string` and the current field value. The compiler expands these calls into ordinary direct field accesses, so there is no runtime reflection or metadata lookup. The same source works for both Jass and Lua. -`forFields` is for statement callbacks. It includes accessible non-static fields, including inherited, -module-injected, readonly, and constant state. `mapFields` uses the callback result to assign each field, so the -reader should return the value to store; readonly and constant fields are therefore excluded from mapping. Leave -both callback parameter types inferred and use a reader/writer overload for each field type. Module field keys are -qualified when equal names need disambiguation. +`wurstForFields` visits accessible non-static fields, including inherited, module-injected, readonly, and constant +state. `wurstMapFields` assigns each callback result back, so all visited fields must be mutable. Leave both callback +parameter types inferred and provide reader/writer overloads for every field type. Mapping is shallow: nested +objects, tuples, collections, and other composite values need a matching library or user-provided codec. The explicit-target forms work outside the state class and evaluate the target exactly once: ```wurst -forFields(state, (fieldName, value) -> writer.write(fieldName, value)) -mapFields(state, (fieldName, value) -> reader.read(fieldName, value)) +wurstForFields(state, (fieldName, value) -> writer.write(fieldName, value)) +wurstMapFields(state, (fieldName, value) -> reader.read(fieldName, value)) ``` -For a generic load wrapper, `newInstance()` constructs the specialized concrete class through its normal +For a generic load wrapper, `wurstNewInstance()` constructs the specialized concrete class through its normal accessible zero-argument constructor: ```wurst function loadState(FieldReader reader) returns T - let state = newInstance() - mapFields(state, (fieldName, oldValue) -> reader.read(fieldName, oldValue)) + let state = wurstNewInstance() + wurstMapFields(state, (fieldName, oldValue) -> reader.read(fieldName, oldValue)) return state ``` This works for Jass and Lua without runtime reflection, a type registry, or a type-id switch. `T` must resolve to a concrete, non-abstract class with an accessible zero-argument constructor. Keep state classes focused on data and -keep the persistence codec, schema versioning, validation, and migrations separate from construction and field -mapping. Applicable ordinary visible overloads named `forFields`, `mapFields`, or `newInstance` still resolve -normally. Keep the generic loader as a free function: on Lua, a method cannot currently combine type parameters -from its generic owning class with additional type parameters declared by the method itself. Do not call -`newInstance()` from a generic class constructor; construct the state in the loader and initialize nested state +keep the persistence codec, stable field identity, schema versioning, validation, and migrations separate from +construction and field mapping. Keep the generic loader as a free function: on Lua, a method cannot currently +combine type parameters from its generic owning class with additional type parameters declared by the method itself. Do not call +`wurstNewInstance()` from a generic class constructor; construct the state in the loader and initialize nested state explicitly afterward. Avoid calling generic-construction methods directly on freshly constructed generic receivers on Lua, and keep the loader to one construction type parameter rather than multi-parameter generic-interface -dispatch. `newInstance()` is not supported inside `compiletime(...)` expressions. Dedicated state classes should +dispatch. `wurstNewInstance()` is not supported inside `compiletime(...)` expressions. Dedicated state classes should also avoid nested modules with sibling fields sharing the same name; prefer direct fields or ordinary inheritance.