From 3dd9690f278a1646dc2f07d7d7d6cc4b097fbc94 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 10 Aug 2026 16:17:15 +0200 Subject: [PATCH 1/6] Update generic field mapping documentation --- _doc/manual.md | 49 +++++++++++++++++++++++++++++++++--------- _tutorials/saveload.md | 42 +++++++++++++++++++++++++++--------- 2 files changed, 71 insertions(+), 20 deletions(-) diff --git a/_doc/manual.md b/_doc/manual.md index 434cace..03c31e0 100644 --- a/_doc/manual.md +++ b/_doc/manual.md @@ -880,29 +880,58 @@ 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 ordinary visible function 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. 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. ### Array Members diff --git a/_tutorials/saveload.md b/_tutorials/saveload.md index 360d034..1d3466a 100644 --- a/_tutorials/saveload.md +++ b/_tutorials/saveload.md @@ -64,32 +64,54 @@ 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. Ordinary visible functions named `forFields`, `mapFields`, or `newInstance` still resolve normally. ## Usage From 884f10162b8e221b07b2e635d32bbebc35c7ae45 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 10 Aug 2026 16:55:40 +0200 Subject: [PATCH 2/6] Clarify generic loader scope --- _doc/manual.md | 7 +++++-- _tutorials/saveload.md | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/_doc/manual.md b/_doc/manual.md index 03c31e0..f0ea68b 100644 --- a/_doc/manual.md +++ b/_doc/manual.md @@ -911,8 +911,8 @@ 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 ordinary visible function with one of these names is resolved normally and is not treated as compiler -magic. +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: @@ -928,6 +928,9 @@ lowers `newInstance()` to its normal zero-argument constructor. `T` 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. + 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 diff --git a/_tutorials/saveload.md b/_tutorials/saveload.md index 1d3466a..10887df 100644 --- a/_tutorials/saveload.md +++ b/_tutorials/saveload.md @@ -111,7 +111,9 @@ function loadState(FieldReader reader) returns T 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. Ordinary visible functions named `forFields`, `mapFields`, or `newInstance` still resolve normally. +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. ## Usage From bbe53a76c99fc2c90854bcc81816e4835e2df3a6 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 10 Aug 2026 17:27:59 +0200 Subject: [PATCH 3/6] Document generic constructor restriction --- _doc/manual.md | 2 ++ _tutorials/saveload.md | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/_doc/manual.md b/_doc/manual.md index f0ea68b..b714f1b 100644 --- a/_doc/manual.md +++ b/_doc/manual.md @@ -930,6 +930,8 @@ unresolved type parameters, and classes without a usable constructor are rejecte 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. 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 diff --git a/_tutorials/saveload.md b/_tutorials/saveload.md index 10887df..a321866 100644 --- a/_tutorials/saveload.md +++ b/_tutorials/saveload.md @@ -113,7 +113,9 @@ concrete, non-abstract class with an accessible zero-argument constructor. Keep 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. +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. ## Usage From 6b914c2c15312811c0e80db96acc6d8c8230a387 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 10 Aug 2026 18:06:35 +0200 Subject: [PATCH 4/6] Document reserved compiler namespace --- _doc/manual.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/_doc/manual.md b/_doc/manual.md index b714f1b..748259b 100644 --- a/_doc/manual.md +++ b/_doc/manual.md @@ -939,6 +939,9 @@ in small, dedicated classes, avoid unsupported field kinds such as static fields 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. From 42395298262d37de5b056f48a2409a46413b0e6a Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 10 Aug 2026 18:55:44 +0200 Subject: [PATCH 5/6] Document generic receiver and module limits --- _doc/manual.md | 5 +++++ _tutorials/saveload.md | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/_doc/manual.md b/_doc/manual.md index 748259b..b6a4982 100644 --- a/_doc/manual.md +++ b/_doc/manual.md @@ -933,6 +933,11 @@ parameters from its generic owning class with additional type parameters declare 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. 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 diff --git a/_tutorials/saveload.md b/_tutorials/saveload.md index a321866..27ba0e0 100644 --- a/_tutorials/saveload.md +++ b/_tutorials/saveload.md @@ -115,7 +115,9 @@ mapping. Applicable ordinary visible overloads named `forFields`, `mapFields`, o 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. +explicitly afterward. Avoid calling generic-construction methods directly on freshly constructed generic receivers +on Lua. Dedicated state classes should also avoid nested modules with sibling fields sharing the same name; prefer +direct fields or ordinary inheritance. ## Usage From 171f25e5bf2f24ac5db302bae771aaaf07f3336b Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 10 Aug 2026 19:11:30 +0200 Subject: [PATCH 6/6] Document final generic construction boundaries --- _doc/manual.md | 8 +++++--- _tutorials/saveload.md | 5 +++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/_doc/manual.md b/_doc/manual.md index b6a4982..96b8cff 100644 --- a/_doc/manual.md +++ b/_doc/manual.md @@ -934,9 +934,11 @@ Likewise, do not call `newInstance()` from a generic class constructor. Const 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. 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. +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 diff --git a/_tutorials/saveload.md b/_tutorials/saveload.md index 27ba0e0..9e9fc45 100644 --- a/_tutorials/saveload.md +++ b/_tutorials/saveload.md @@ -116,8 +116,9 @@ normally. Keep the generic loader as a free function: on Lua, a method cannot cu 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. Dedicated state classes should also avoid nested modules with sibling fields sharing the same name; prefer -direct fields or ordinary inheritance. +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