Syntax tree compatibility
The syntax tree is a public API. Every consumer that compiles Screenplay to something - a code generator, a designer, a documentation tool - is written against these types, so what the tree promises across versions decides how much work a Screenplay upgrade costs them.
This page states those promises. It is deliberately narrow: a guarantee you cannot rely on is worse than one that was never made.
What the tree is
Section titled “What the tree is”Every node is a positional record deriving from SyntaxNode, which carries a SourceLocation. Nodes are immutable, compare by value, and support with expressions. A construct with several forms - an expression, a policy condition, a projection block, a screen directive, a projection mapping, a projection key, a validation block, a constraint - is an abstract base record with a concrete record per form.
That shape is stable. Nodes will stay records, stay immutable, and keep deriving from SyntaxNode.
What is guaranteed
Section titled “What is guaranteed”A node kind can be added. New constructs arrive as new record types. If the new type derives from an existing abstract base, it starts appearing in collections you already read - see the caveat below. If you consume the tree through ScreenplaySyntaxWalker, a new kind arrives as a new Visit method with a default implementation that walks its children, so your subclass compiles unchanged and keeps walking. That is the property the walker exists to give you.
An unknown form never throws. The walker dispatches abstract bases with a fallback: a concrete form it does not recognize is passed to VisitNode and then left alone. A tool compiled against an older Screenplay does not fault on a document using a newer construct - it silently does not act on it, which is the recoverable failure.
Enum members keep their numeric values. SliceType, ValidationRuleKind, ComparisonOperator, LogicalOperator, AutoMapMode and CaptureWhenKind gain members at the end and never renumber existing ones. This one matters more than it looks: the C# compiler inlines an enum constant into the consuming assembly, so renumbering a member changes behavior in an already-compiled consumer with no error anywhere - the value it was built with silently means something else. New members are therefore always appended.
public const values never change. The diagnostic codes are public const string, and a const inlines into consumers exactly the way an enum member does. A code’s value is fixed once published; a diagnostic that changes meaning gets a new code rather than a new value on the old one. The same rule applies to every other published constant, such as QuerySyntax.ObservableModifier and PropertySyntax.IdentifierModifier.
A node grows by init-only property, not by a new parameter. A node gaining an optional value gains it as an init property on the record body, so the primary constructor and Deconstruct keep the exact signature they were published with. Positional construction you already wrote keeps compiling, with expressions reach the new value like any other, and it participates in equality and ToString the same way a parameter would.
Appending a trailing optional parameter instead looks equivalent and is not. It is source compatible and binary breaking: the compiler emits a different primary constructor and a different Deconstruct, so an assembly built against the previous version calls a member that no longer exists and fails at run time with a missing method - with no error at compile time anywhere. It has shipped once, Cratis.Arc.Screenplay built against 1.5.2 breaking against 1.7.0 exactly this way. Package validation now fails the build that would repeat it, so the guarantee is enforced rather than intended.
What is not guaranteed
Section titled “What is not guaranteed”The positional parameter list. Inserting a parameter, removing one, promoting an optional one to required, or appending a new one is a breaking change. The first three break source and binary; appending breaks binary only, which is the worse of the two to discover. Any of them is allowed in a major release and is enumerated in the release notes when it happens. If you construct nodes yourself, prefer named arguments and object initializers; if you consume them, prefer property access over deconstruction.
Exhaustive switch over an abstract base. A switch on ExpressionSyntax or ProjectionBlockSyntax with an arm per known form compiles today and still compiles tomorrow, but a form added later falls through. C# cannot check exhaustiveness over a type hierarchy, so this is a silent behavior change, not a compile error. Always write a default arm, or override the walker’s base-kind method (VisitExpression, VisitProjectionBlock, …) so the fallback is handled for you.
A collection’s element types. IEnumerable<ProjectionBlockSyntax> can start yielding a form your code has never seen. Code that casts elements to a concrete type rather than pattern-matching them will fault.
Hand-written traversal. Nothing protects a consumer that walks the tree with its own loops. A node gaining a child collection is invisible to it - the tree grew and the consumer silently stopped covering the document. This is the concrete reason to prefer the walker: a hand-written walk has no place for the language to tell you it changed.
The safe way to consume the tree
Section titled “The safe way to consume the tree”- Derive from
ScreenplaySyntaxWalkerrather than writing the loops. - Override the narrowest methods that answer your question, and call
baseunless you mean to prune. - Pattern-match node forms; never cast, and always have a fallback.
- Treat an unrecognized form as “not my concern”, not as an error.
- Read diagnostics by
Code, never by message text.
A consumer that does these five things is affected by a Screenplay release only when a construct it actively handles changes shape.
See also
Section titled “See also”- Visitors and traversal - the walker and the root visitor interfaces.
- Diagnostics - the diagnostic code catalogue and its stability rules.
- Printing and generating - constructing nodes yourself and rendering them back to text.