diff --git a/_doc/manual.md b/_doc/manual.md index 434cace..96b8cff 100644 --- a/_doc/manual.md +++ b/_doc/manual.md @@ -880,31 +880,75 @@ function foo() ### Compiler-assisted field mapping -For dedicated state classes, the compiler provides two save/load intrinsics that expand into direct field -accesses: +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. ```wurst +import MagicFunctions + class PlayerState int level = 1 string name = "" function save(FieldWriter writer) - __wurst_forFields((fieldName, value) -> writer.write(fieldName, value)) + forFields((fieldName, value) -> writer.write(fieldName, value)) function load(FieldReader reader) - __wurst_mapFields((fieldName, value) -> reader.read(fieldName, value)) + mapFields((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. + +Both functions also accept an explicit target. The target is evaluated exactly once: + +```wurst +forFields(state, (fieldName, value) -> writer.write(fieldName, value)) +mapFields(state, (fieldName, value) -> reader.read(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. + +Use `newInstance()` when a specialized generic function needs to construct its concrete result type: + +```wurst +function loadState(FieldReader reader) returns T + let result = newInstance() + mapFields(result, (fieldName, oldValue) -> reader.read(fieldName, oldValue)) + return result ``` -`__wurst_forFields` invokes the callback once for every non-static instance field. The callback receives the -field name and current value and must produce a statement. `__wurst_mapFields` invokes a reader callback and -assigns its result back to each field. Leave the two callback parameter types inferred; overload the reader or -writer for the field types used by the state class. +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. Keep serializable state in a small class with mutable instance fields and keep the persistence -codec separate from the rest of the game logic. See the [Save and Load tutorial](/tutorials/saveload.html) for +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. +Identifiers beginning with `__wurst` are reserved for compiler-generated internals and must not be declared by +user code. + ### Array Members Wurstscript supports sized arrays as classmembers by translating it to SIZE times arrays and then resolve the array in a get/set function via binary search. diff --git a/_tutorials/saveload.md b/_tutorials/saveload.md index 360d034..9e9fc45 100644 --- a/_tutorials/saveload.md +++ b/_tutorials/saveload.md @@ -64,32 +64,61 @@ 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. Use the -compiler intrinsics `__wurst_forFields` when writing fields and `__wurst_mapFields` when reading them: +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: ```wurst +import MagicFunctions + class PlayerState int level = 1 string name = "" function save(FieldWriter writer) - __wurst_forFields((fieldName, value) -> writer.write(fieldName, value)) + forFields((fieldName, value) -> writer.write(fieldName, value)) function load(FieldReader reader) - __wurst_mapFields((fieldName, value) -> reader.read(fieldName, value)) + mapFields((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. -`__wurst_forFields` is for statement callbacks. `__wurst_mapFields` uses the callback result to assign each -field, so the reader should return the value to store. Leave both callback parameter types inferred and use -the reader/writer overload matching each field type. Static fields are not included. +`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. + +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)) +``` + +For a generic load wrapper, `newInstance()` 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)) + return state +``` -The `__wurst_` names are compiler intrinsics; ordinary user functions named `forFields` or `mapFields` are -unaffected. Keep these state classes focused on their serializable data (normally direct mutable instance -fields) and keep the persistence codec separate from the game logic. +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 +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 +also avoid nested modules with sibling fields sharing the same name; prefer direct fields or ordinary inheritance. ## Usage