Skip to content

Latest commit

 

History

History
437 lines (342 loc) · 23.2 KB

File metadata and controls

437 lines (342 loc) · 23.2 KB

Changelog

[1.7.0] — 2026-04-17

Added

  • Anvil.Net.Query: new project — extracts AnvilNode tree, AnvilDoc, AnvilSchema, AnvilWriter, AnvilVarsState, scripting interfaces from Anvil.Net.Api (FR-2604-anvl-net-017)
  • Anvil.Net.Scripting: new project — extracts AnvilScriptEngine, IAnvilScriptEngine, IAnvilScriptingPlugin from Anvil.Net.Api (FR-2604-anvl-net-018)
  • Anvil.Net.Serializer: new project — extracts AnvilSerializer, AnvilCodeGen, IAnvilValueConverter, all Anvil*Attribute types and codegen from Anvil.Net.Api (FR-2604-anvl-net-019)
  • Anvil.Net.Api: zero-source façade — references Query, Scripting, Serializer; no owned source files
  • AnvilScriptingRegistry.TryAutoLoad: path-based LoadFrom probe with EnsureRegistered reflection call for reliable plugin activation in test hosts
  • AslNodeKind.Ternary: ternary conditional expression cond ? then : else (BL-035-asl, FR-2604-anvl-net-014)
  • AslParser.ParseExpr: ternary '?' check at minPrec==0 — lowest precedence, right-associative chaining
  • AslEvaluator: Ternary branch evaluates cond.Truthy, short-circuits unevaluated branch
  • TernaryTests: TN01–TN10 (true/false branches, int/string literals, expression branches, right-assoc chain, module argument)
  • AslNodeKind.IndexAccess: index access collection[index] for List and Map values (BL-038-asl, FR-2604-anvl-net-015)
  • AslParser.ParsePostfix: '[' branch chains after any primary expression — handles inline literal, variable, and computed index
  • AslEvaluator.EvalIndexAccess: List bounds-checked (Null on miss), Map key coercion (string or long.ToString)
  • IndexTests: IX01–IX10 (first/last/variable index, out-of-bounds, negative, string list, computed index, map bracket, split-result, inline literal)
  • schema.enum: @[schema.enum] attribute on parenthesised declaration registers a named enumeration type in schema context (FR-2604-anvl-net-016)
  • SchemaTypes.EnumTypeNode: new schema AST node (TypeDeclKind.Enum) with Name + Members list
  • AnvilSchemaValidator: two-pass enum registry — pass 1 collects @[schema.enum] declarations, pass 2 resolves field type references against enum registry
  • AnvilErrorCode.SchemaEnumValueMismatch = 4704: data field value is not a declared member of the named enum type
  • SchemaTests TC-17 to TC-21: valid member passes, invalid raises 4704, unknown type name raises 4602, primitive regression, multi-enum no cross-contamination

[1.6.0] — 2026-04-16

Added

  • AslNodeKind.MapLiteral: map literal syntax { key: value, … } in expression position (BL-036-asl, FR-2604-anvl-net-011)
  • AslParser.ParsePrimary: { key: expr } lookahead disambiguates map literal from block; empty {} supported
  • AslEvaluator.EvalMapLiteral: builds Dictionary<string,AslValue> from alternating StringLiteral/expr child pairs
  • AslNode.StringValue: string? field for pre-processed escape content; evaluator uses it in preference to span
  • AslParser.ProcessEscapes: \n \t \r \" \\ processed at parse time in all string literals (BL-034-asl, FR-2604-anvl-net-012)
  • AslParser.ParseInterpolatedString: $"…{ident}…" produces left-associative BinaryExpr(Add) chain seeded with empty StringLiteral (BL-039-asl, FR-2604-anvl-net-012)
  • AnvilErrorCode.AslUnterminatedString = 4513: unterminated interpolated string or unclosed hole
  • MapTests: ML01–ML08 (empty/single/multi-entry map literal, map as argument, dot access, nested access, missing key, truthiness)
  • StringTests: ES01–ES05 (escape sequences) + IS01–IS06 (interpolated string: single/multi hole, no-hole, int coercion, local-from-$-ref, escape inside interpolation)
  • AnvilAttrAttribute: POCO property → @[attribute] binding for serializer (FR-2604-anvl-net-010)
  • [AnvilAttr(key)]: Maps property to block-level @[key=value] during deserialization/serialization
  • AnvilSerializer.DeserializeObject: [AnvilAttr] binding loop calls node.Attribute(key) + CoerceValue
  • AnvilSerializer.FormatAttributes: collects [AnvilAttr] properties, emits @[key="value", ...] before := operator
  • AnvilSerializer.SerializeToWriter: attribute emission integrated for both inheritance chain and flat emit paths
  • Test coverage: AnvilSerializerAttrTests.cs TC-1 through TC-10 (read/write paths, coercion, round-trip, escaping)
  • AnvilSchema (.asch) — Phase 2 enforcement layer (FR-2604-anvl-net-013)
  • Parser: @[schema] module attribute recognized; IsSchemaContext flag set; RHS bare-word tokens interpreted as type names in schema context
  • SchemaTypes.cs: PrimitiveTypeNode, ArrayTypeNode, BlockTypeNode, TupleTypeNode, AnyTypeNode, SchemaField — schema AST (260 lines)
  • AnvilSchemaValidator: document traversal, type matching, ValidateField, ValidateArrayElement, ValidatePathField, ValidateWithDefaults
  • AnvilSchemaOptions: SchemaEnforcementMode (Strict | Advisory); Strict throws AnvilSchemaException, Advisory populates ValidationResult.Warnings
  • AnvilSchemaResult: resolved document + Defaults dict + IsValid delegate; indexer injects default values for absent optional fields
  • AnvilSchemaPathResolver: 4-step resolution — absolute → relative-to-file → schema.path search entries → polaris/schemas/ fallback
  • Anvil.LoadWithSchema(dataPath, schemaPath, options): explicit schema binding for CI, runtime registry, offline validation
  • AnvilErrorCode.SchemaTypeMismatch = 4701: data field value type does not match declared schema type
  • AnvilErrorCode.SchemaPathInvalid = 4702: field @[schema.path] value is structurally invalid (empty, null bytes, or illegal path chars); no filesystem access
  • AnvilErrorCode.SchemaArrayElementTypeMismatch = 4703: per-element array type validation
  • Test coverage: SchemaTests TC-1 through TC-16 (schema context, Advisory/Strict modes, missing fields, table, path resolver, polaris fallback, defaults, tuples, type mismatch, array elements, nested blocks, any type, path structure, security boundary)
  • docs/asch-authoring-guide.md: .asch file authoring guide with full type vocabulary, field attributes, path resolution, enforcement modes, and security properties

Changed

  • EvalExpr switch: added MapLiteral case routing to EvalMapLiteral
  • EvalExpr switch StringLiteral case: uses node.StringValue when non-null (escape-processed value)
  • AslParser $ branch: $" routes to ParseInterpolatedString before VarRef fallback
  • Parser: removed AttributesNotAllowedOnType restriction on top-level scalar statements — @[k=v] is now valid on any statement (required for .asch field attribute syntax)

Fixed

  • BL-040-asl seam hardening: removed ExternalLookup fallthrough from EvalIdent; bare identifiers now resolve from local ASL scope only — $ sigil required to cross ANVL/ASL boundary (SM01–SM04)
  • IN02_05 SyntaxInquiryTests: updated to use $existing (was relying on removed bare-ident fallthrough)
  • AnvilNode.Attributes: nested field attributes now exposed correctly via _field.AttribStart/AttribCount (was missing nested block attribute access)

Security

  • SchemaTypeMismatch (4701) rejects type confusion at the schema boundary — crafted strings cannot coerce to int/float/bool silently (TC-16: SQL injection analogue)
  • SchemaArrayElementTypeMismatch (4703) validates every element independently — a bad element in position N cannot hide behind valid elements in 0..N-1
  • SchemaPathInvalid (4702) validates path structure without File.Exists — no TOCTOU race; existence is audited at point of use by the caller
  • Strict mode enforces hard type boundaries on document load — validation failure throws before data reaches application code
  • any type is an explicit opt-out, not the default — every undeclared field requires deliberate author action to skip validation
  • Advisory mode returns structured AnvilValidationError objects with machine-readable codes — warnings are inspectable, not silent

[1.5.1] — 2026-04-15

Added

  • AnvilBlobAttribute: Specialized AnvilMember attribute for blob properties with optional tag support (FR-2604-anvl-net-009)
  • [AnvilBlob(key, tag)]: Inherits from AnvilMemberAttribute, adds Tag property for @png/@webp/@md blob syntax
  • AnvilSerializer serialization: ToAnvil() now called during property serialization (completes round-trip)
  • WritePropertyField(): Check for [AnvilConverter] before other attribute dispatch (blob tag wrapping)
  • FormatPropEntry(): Check for [AnvilConverter] before other attribute dispatch (inline object path)
  • Blob tag wrapping: @tagbase64... for tagged blobs, base64... for untagged blobs
  • Test coverage: ConverterTests.cs VC10-VC14 (5 new tests: tagged/untagged blob serialization, round-trip, key override)

Changed

  • AnvilMemberAttribute: Changed from sealed class to public class (allows inheritance)
  • AnvilSerializer: WritePropertyField() now checks for [AnvilConverter] and calls ToAnvil()
  • AnvilSerializer: FormatPropEntry() now checks for [AnvilConverter] and calls ToAnvil()
  • Users Guide: Added 'Blob Serialization with [AnvilBlob]' section (150+ lines with Vellum use case)

Fixed

  • FR-2604-anvl-net-009: IAnvilValueConverter.ToAnvil() now called during serialization
  • byte[] with [AnvilConverter] now serializes as base64 string instead of 'System.Byte[]'
  • Vellum FontAtlasDescriptor now supports full serialize + deserialize round-trip for 70KB PNG atlas blobs
  • Serialization dispatch order: converter check now BEFORE array/tuple/map attribute checks

[1.5.0] — 2026-04-13

Added

  • IAnvilValueConverter: Zero-copy extensibility API for custom deserialization (FR-2604-anvl-net-008)
  • IAnvilValueConverter.FromAnvil(ReadOnlySpan, Type): Zero-allocation decode from AML source
  • IAnvilValueConverter.ToAnvil(object?, Type): Round-trip serialization support
  • [AnvilConverter(typeof(T))]: Property-level attribute for converter dispatch
  • AnvilSerializer.CoerceValue() integration: Converter dispatch before standard type coercion
  • Test coverage: ConverterTests.cs with 9 passing tests (VC01-VC09: base64, DateTime, round-trip, nested objects, error handling)
  • Users Guide: IAnvilValueConverter section with production-ready Base64Converter implementation

Changed

  • Parser: Parse-time quote stripping for Scalar and Blob values (internal implementation change)
  • ParseStatement(): Scalar values now use stripped position/length from ParseScalarValue()
  • ParseCollection(): Array/Tuple elements use stripped positions for Scalar/Blob elements
  • AnvilNode.AsString(): Unescape logic now only applies to Scalar type (blobs are raw content)
  • XML doc comments: AnvilError.Current → Anvil.LastError (BL-002 API consistency)
  • AnvilSchema.cs example code updated to use Anvil.LastError
  • All public API documentation now references the correct Anvil.LastError property

Fixed

  • FR-2604-anvl-net-008: 70KB+ base64 blobs now avoid LOH (Large Object Heap) via zero-copy AsSpan()
  • ParseStatement() position corruption: Scalar values no longer include surrounding quotes in metadata
  • Array/Tuple element positions now correctly exclude quotes (parse-time stripping)
  • Blob AsString() no longer incorrectly unescapes content (blobs are literal byte sequences)
  • BL-002: Reconciled dual error access pattern documentation
  • Removed incorrect references to non-existent AnvilError.Current property

[1.4.0] — 2026-04-12

Added

  • ModuleRegistryConcurrencyTests.cs: Thread-safety validation for AnvilModuleRegistry (BR-2604-anvl-net-002)
  • Test coverage: MRC01-MRC06 (6 passing tests: concurrent Register/TryGet/Clear, Task-based parallelism, xUnit parallel pattern)

Fixed

  • BR-2604-anvl-net-004: AnvilNode.AsString() now returns unquoted string values
  • AsString() automatically strips surrounding quotes from string literals
  • AsString() unescapes standard escape sequences (\" \n \r \t \\)
  • Removed redundant .Trim('"') calls from AnvilSerializer, AnvilScriptEngine, and AnvilNodeExtensions
  • Users no longer need manual quote stripping — API returns semantic values

[1.3.0] — 2026-04-09

Added

  • ASL while loop: C-style while (cond) { body } control flow (BL-012)
  • AslNodeKind.While: While loop node type for ASL (v1.3.0)
  • AslParser: ParseWhileStatement() supporting while (expr) { stmts } syntax
  • AslEvaluator: EvalWhile() with break/continue support
  • Test coverage: WhileLoopTests.cs with 6 passing tests (WL01-WL06: simple, conditional, nested, mutation, break, continue)
  • Anonymous block attribute syntax: identifier @[attrs] { fields } (v1.1.0 enhancement)
  • Parser: LookaheadIsAnonObject() updated to detect optional @[...] attributes
  • Parser: ParseAnonObject() now parses attributes before object value
  • Test coverage: AB07-AB09 (anonymous block attributes, backward compatibility)
  • Anvil.ParseToContext(source, dialect?): Parse once, return reusable context (FR-2604-004)
  • Anvil.LoadToContext(path, dialect?): Load and parse file into reusable context
  • Anvil.Resolve(context, options?): Resolve pre-parsed context with optional merge strategy
  • AnvilContext.IsParsed flag: Prevents redundant parsing in context-reuse pattern
  • AnvilContext.SourcePath property: Enables import resolution for file-based contexts
  • Test coverage: ContextReuseTests.cs with 5 passing tests (CR01-CR05)
  • AnvilException: Abstract base class for all Anvil-specific exceptions (BL-001)
  • AnvilException constructors: (message), (message, inner) for consistent exception patterns
  • Test coverage: ExceptionBaseTests.cs with 8 passing tests (EB01-EB08: hierarchy, catch patterns, properties)

Changed

  • AslNodeKind.Break/Continue: Documentation updated to reflect for/while loop support
  • Test AB06: Anonymous blocks now support optional attributes (backward compatible)
  • AnvilResolverException: Now inherits from AnvilException (was Exception)
  • AnvilVarsException: Now inherits from AnvilException (was Exception)
  • AnvilValidationException: Now inherits from AnvilException (was Exception)

Fixed

  • BR-0409-002: AsString() now strips quotes and unescapes string literals
  • BR-2604-003: Anonymous blocks with attributes no longer require explicit := operator
  • AsString() returns "Player" instead of "\"Player\"" (ergonomics improvement)
  • Removed .Trim('"') workaround from 35 test assertions across 8 files

[1.2.1] — 2026-04-09

Added

  • AnvilElementMeta: FieldStart and FieldCount properties for object-typed array elements (BR-2604-anvl-net-001)
  • AnvilValue: ElemFieldStartsTemp and ElemFieldCountsTemp temporary arrays for parser metadata transfer
  • Parser: Object field metadata capture in ParseCollection (arrays/tuples)
  • Parser: Metadata transfer in ParseStatement for top-level array/tuple values
  • AnvilNode: NodeFromField array/tuple metadata transfer (BR-2604-anvl-net-001 fix)
  • AnvilNode: Integer indexer constructs valueMeta for object-typed elements (FR-2604-anvl-net-001)
  • Test coverage: InlineObjectTests.cs with 15 passing tests (IN01, IN04-IN12, IN14-IN15, IN17-IN19)

Fixed

  • BR-2604-anvl-net-001: Parser now persists object field metadata in AnvilElementMeta
  • Canonical array[i]['field'] access pattern now works for inline objects in arrays
  • Lattice font glyph metrics use case (IN-2604-anvil-001) resolved

[1.2.0] — 2026-04-06

Added

  • IAnvilArrayView interface: lazy array view abstraction with Count property and GetElement() method
  • ConcatenatedArrayView implementation: stores element metadata from base + derived arrays
  • AnvilValue.VirtualArrayView property: attaches lazy views to merged values
  • AnvilNode integration: indexer and NodeEnumerator check for VirtualArrayView before using Elements array
  • Test coverage: AC01-AC04 (base+derived concat, predicate filtering, multiple elements, cross-source)

Changed

  • MergeStrategies.ConcatArrays() now creates virtual array views (fully functional, resolves v1.1.0 limitation)
  • AnvilNode.NodeFromField() passes field reference to enable VirtualArrayView access
  • MergeStrategies.IsArray() null-safe check for empty array handling

[1.1.0] — 2026-04-06

Added

  • Anonymous block syntax (FR-2604-anvl-net-001): top-level immutable object declarations with bare identifier + brace
  • AnvilStatementType.AnonymousObject enum value
  • Parser: LookaheadIsAnonObject(), ParseAnonObject() methods
  • Resolver validation: inheritance from anonymous blocks throws AnvilResolverException
  • IMergeStrategy interface for custom inheritance field merging (FR-2604-anvl-net-002)
  • DefaultMergeStrategy singleton for replacement semantics
  • AnvilResolveOptions container for resolution configuration
  • Anvil.Parse(source, options) and Anvil.Load(path, options) overloads
  • MergeStrategies helper: ConcatArrays(), FilterBase(predicate), Compose(first, second)
  • Public API exposure: AnvilField, AnvilValue (previously internal)
  • Test coverage: AB01-AB06 (anonymous blocks), CM11 (inheritance rejection), MS01-MS05 (merge strategies)

[1.0.0-rc4] — 2026-03-20

Changed

  • Packaging: Switched to standard dotnet pack NuGet toolchain
  • NuGet metadata moved from nuspec + pack.py into Anvil.Net.Api.csproj
  • dist/1.0.0-rc4/ package produced via dotnet pack

Fixed

  • Interpolated string slot syntax: {ref} (bare name) instead of {.varname} (dot-prefix) - BR_0326_001
  • Parser now checks IsIdentifierStart immediately after { (no dot consumed)

[1.0.0-rc3] — 2026-03-08

Changed

  • SchemaValidator: zero-alloc happy path (lazy List init only on first error)
  • AslScope: inline 8-slot linear table (replaces Dictionary allocation per call frame)
  • SB03 (warm validate) drops from 656 B to 0 B allocated

Fixed

  • AslEvaluator SE09: runtime error check precedes return check (AslCallDepthExceeded now surfaces correctly)

[1.0.0-rc2] — 2026-03-08

Added

  • 11 new ASL parse error codes (450x range): AslExpectedLBrace through AslAssignLhsNoName

Changed

  • No-throw parsing policy enforced: AslParser and AslEvaluator no longer throw exceptions for flow control
  • All 13 throw statements replaced with AnvilError.Set() + return null
  • try/catch wrapper in Parse() removed

[1.0.0-rc1] — 2026-03-07

Added

  • Integration tests: IT01-IT05 (schema + validation + codegen pipeline end-to-end)

Changed

  • README updated: badges, Current State section rewritten, API file table extended
  • Docs pass: schema and code-gen usage snippets added

Removed

  • dist/: all pre-rc1 snapshot folders removed (dist/1.0.0-rc1/ is sole artifact)

[0.4.6-alpha] — 2026-03-07

Added

  • AnvilCodeGen: POCO code generation from AnvilSchema
  • CodeGenOptions: Namespace, NullableEnabled, UseRecords, Indent configuration
  • CSharpEmitter: internal StringBuilder-based renderer (no Roslyn dependency)
  • AnvilCodeGen.GenerateSource(AnvilSchema, options?) and GenerateSource(string, options?)

Changed

  • Schema Object types map to sealed class (or sealed record)
  • Schema Enum types map to C# enum
  • Schema Flags types map to [Flags] enum with power-of-2 values

[0.4.5-alpha] — 2026-03-08

Added

  • Schema validation: .asch schema documents define named types (Object, Enum, Flags)
  • AnvilSchema: Load(), Parse(), Types, Validate(), ValidateAll()
  • .asch extension registered as AnvilDialect.Aml
  • AnvilNode.TryGetOwn(string): scan own fields without following inheritance
  • SchemaTypeKind enum: Object, Enum, Flags
  • FieldRule, SchemaType, SchemaRuleset, AnvilSchema classes
  • Error codes: 4601-4606 (SchemaAttrMissing through SchemaValidationUnknownField)

[0.4.4-alpha] — 2026-03-07

Added

  • Multi-error validation mode: Anvil.Validate*(path/source/paths) entry points
  • AnvilValidationError struct: Code, Message, Line, Column, File
  • AnvilValidationResult class: IsValid, Errors, ThrowIfInvalid()
  • AnvilValidationException: thrown by ThrowIfInvalid(), carries full Result
  • Anvil.Validate(path), ValidateSource(source), ValidateAll(paths)

[0.4.3-alpha] — 2026-03-06

Added

  • AMP scalar arrays and tuples: [scalar, ...] and (scalar, ...) now valid in #!amp
  • Error code 4401 AmpArrayElementNotScalar: nesting forbidden in AMP
  • AnvilWriter AMP array/tuple support (Object nodes still throw)

Changed

  • @[ forbidden in AMP value positions (was already blocked at module/statement level)

[0.4.2-alpha] — 2026-03-06

Added

  • @[attr] on ASL function declarations: attributes preserved on function node
  • AnvilScriptEngine.FunctionNode(name): returns full AnvilNode? for named function
  • Host attribute-based discovery: FunctionNode(name)?.HasAttribute('hook')

Changed

  • ParseFunctionDef now accepts attrMeta and stores directly (root cause fix)

[0.4.1-alpha] — 2026-03-06

Added

  • for (init; cond; step) {} loop with break and continue
  • AslValueKind.List and AslValueKind.Map types
  • Inline list literals: [a, b, c] in any expression position
  • $key.field dotted-path access for AML object tree navigation
  • Built-in modules (auto-registered): sys_math, sys_str, sys_io
  • Error context: AnvilErrorState.FunctionName property
  • C-style ASL syntax: assignment uses =, optional ;, i++/i--, +=/-=/etc, else if chains
  • Flag-based control flow: return/break/continue use context flags (not exceptions)

Changed

  • Local variable assignment: := → = (inside function bodies)
  • Control flow eliminates ~177 exception allocations per fib(10) call

[0.3.0-alpha] — 2026-03-03

Added

  • Multi-file imports: import 'path' and import 'path' as alias
  • Cross-source statement lookup: root['alias.Key']
  • Cross-source VarRef resolution: .alias.varKey
  • Cross-source inheritance: LocalType:import.BaseType
  • AnvilImportSet: owns transitive import graph
  • AnvilContext.ImportDecls, HasImports, AddImportDecl
  • AnvilField.ExternalSourceData + CloneWithExternalSource
  • Error codes: 4201-4207 (import errors)
  • MergeBenchmarks: MB01-MB06 covering inheritance patterns

[0.2.3-alpha] — 2026-03-02

Added

  • O(1) statement hash index: lazy Dictionary<string,int> on root nodes
  • NodeEnumerator struct: value-type enumerator for zero-alloc foreach
  • Anvil.Net.Benchmarks project: BenchmarkDotNet 0.14.0 suite with 25 benchmarks
  • docs/Benchmarks.md: version-tracked benchmark results

Changed

  • TryGet(string) rewritten: direct hash path, no exception on miss
  • TryGet miss latency: ~11,000 ns → ~20 ns (540× faster)
  • TryGet miss allocation: 464 B → 0 B
  • TryGet hit latency: ~300 ns → ~191 ns
  • .gitignore cleanup: purged 163 tracked bin/obj artifacts

[0.2.2-alpha] — 2026-02-28

Added

  • [AnvilArray] attribute: maps POCO collection to Anvil array
  • [AnvilMap] attribute: maps Dictionary<string,T> to Anvil object
  • [AnvilTuple] / ValueTuple serialization support
  • AnvilWriter minification: AnvilWriterOptions.Minify = true (26% allocation reduction)
  • AnvilWriter dialect support: AMP enforcement, dialect-specific output

Changed

  • String quoting: AnvilWriter quotes special chars correctly
  • AnvilSerializer no longer over-eagerly quotes numeric strings

Fixed

  • Blob parser: AML/ASL backtick blobs now store AnvilValueType.Blob (was Scalar)

[0.2.1-alpha] — 2026-02-26

Added

  • AnvilWriter: serializes AnvilNode tree back to Anvil text
  • AnvilSerializer.Serialize(obj): POCO to Anvil text conversion
  • AnvilWriterOptions: configures pretty/minify, dialect, indentation, PreserveVars

[0.2.0-alpha] — 2026-02-24

Added

  • vars block: top-level vars { key := value ... } for document variables
  • VarRef (.ref): bare .identifier resolved from vars pool
  • Interpolated strings ($'...'): {.ref} slots resolved at AsString() time
  • ReadOnlyMemory backing: zero-alloc span access via AsSpan()/AsMemory()
  • Enum deserialization: AnvilSerializer supports C# enum properties
  • AnvilVarsState circular-reference detection at construction time