Describe the bug
ScreenplayPrinter can emit .play text that does not parse back, silently breaking the round-trip guarantee documented in printing.md ("Compiling and printing are inverses"). Two independent defects, both in ScreenplaySyntaxText.Literal (Source/DotNET/Screenplay/Printing/ScreenplaySyntaxText.cs:144-156):
1. Non-double numeric literals are formatted culture-dependently
static string Literal(object? value) => value switch
{
null => "null",
bool boolean => boolean ? "true" : "false",
string text => $"\"{text}\"",
double number => Number(number),
_ => value.ToString() ?? string.Empty // ← decimal, float, int, long all land here
};
Number(double) correctly uses CultureInfo.InvariantCulture, but every other numeric type falls through to the bare value.ToString(), which honours the current culture. On a machine with a comma decimal separator (nb-NO, de-DE, fr-FR…), new LiteralExpressionSyntax(5.5m) prints 5,5, which then fails to parse.
2. String literals are never escaped
string text => $"\"{text}\"" interpolates the raw value between quotes. Any " in a description, validation message, label or tag terminates the literal early and produces invalid output. The message regex in particular (\bmessage\s+(?:"([^"]*)"|...)) cannot match a message containing a quote.
Both defects are invisible to the printer's own specs because they only manifest with a comma-decimal culture, or with a quote in the input.
Steps to reproduce
Culture-dependent numbers:
- Run under a culture with a comma decimal separator, e.g.
CultureInfo.CurrentCulture = new CultureInfo("nb-NO");
- Build a tree containing
new LiteralExpressionSyntax(5.5m, SourceLocation.Start) (for example as the value of a ValidationRuleSyntax).
new ScreenplayPrinter().Print(application) → output contains 5,5.
- Feed that output to
ScreenplayCompiler.Compile → parse error.
Unescaped strings:
- Build any node with a string containing a quote, e.g. a command
description of He said "hello".
- Print it →
description "He said "hello"".
- Compile that output → parse error.
Expected behavior
Printing any valid syntax tree produces text that compiles with zero diagnostics, and reprinting the compiled tree returns identical text — for every culture and for any string content.
Actual behavior
Output is culture-dependent for non-double numerics, and structurally invalid for strings containing ". In both cases the failure surfaces later, at parse time, rather than at construction.
Environment
- OS: macOS 15 (reproduces on any comma-decimal culture)
- .NET version: 10.0.x
- Screenplay version: 1.5.2
Additional context
Found while building Cratis.Arc.Screenplay (Arc repo), which constructs syntax trees programmatically and prints them. We are currently working around both by coercing every numeric to double and stripping quotes from all strings before construction — but the printer is the right place to fix this, since every programmatic producer will otherwise hit the same trap.
Suggested fix
- Route all numeric types through invariant formatting — extend the
switch to cover decimal, float, int, long etc. explicitly, or add an IFormattable arm passing CultureInfo.InvariantCulture, so the culture-dependent value.ToString() fallback is never reached for numbers.
- Escape
" (and any other character meaningful to the literal grammar) when rendering string literals, and unescape symmetrically in the lexer so the round trip holds.
Acceptance criteria
Describe the bug
ScreenplayPrintercan emit.playtext that does not parse back, silently breaking the round-trip guarantee documented in printing.md ("Compiling and printing are inverses"). Two independent defects, both inScreenplaySyntaxText.Literal(Source/DotNET/Screenplay/Printing/ScreenplaySyntaxText.cs:144-156):1. Non-
doublenumeric literals are formatted culture-dependentlyNumber(double)correctly usesCultureInfo.InvariantCulture, but every other numeric type falls through to the barevalue.ToString(), which honours the current culture. On a machine with a comma decimal separator (nb-NO, de-DE, fr-FR…),new LiteralExpressionSyntax(5.5m)prints5,5, which then fails to parse.2. String literals are never escaped
string text => $"\"{text}\""interpolates the raw value between quotes. Any"in a description, validation message, label or tag terminates the literal early and produces invalid output. The message regex in particular (\bmessage\s+(?:"([^"]*)"|...)) cannot match a message containing a quote.Both defects are invisible to the printer's own specs because they only manifest with a comma-decimal culture, or with a quote in the input.
Steps to reproduce
Culture-dependent numbers:
CultureInfo.CurrentCulture = new CultureInfo("nb-NO");new LiteralExpressionSyntax(5.5m, SourceLocation.Start)(for example as the value of aValidationRuleSyntax).new ScreenplayPrinter().Print(application)→ output contains5,5.ScreenplayCompiler.Compile→ parse error.Unescaped strings:
descriptionofHe said "hello".description "He said "hello"".Expected behavior
Printing any valid syntax tree produces text that compiles with zero diagnostics, and reprinting the compiled tree returns identical text — for every culture and for any string content.
Actual behavior
Output is culture-dependent for non-
doublenumerics, and structurally invalid for strings containing". In both cases the failure surfaces later, at parse time, rather than at construction.Environment
Additional context
Found while building
Cratis.Arc.Screenplay(Arc repo), which constructs syntax trees programmatically and prints them. We are currently working around both by coercing every numeric todoubleand stripping quotes from all strings before construction — but the printer is the right place to fix this, since every programmatic producer will otherwise hit the same trap.Suggested fix
switchto coverdecimal,float,int,longetc. explicitly, or add anIFormattablearm passingCultureInfo.InvariantCulture, so the culture-dependentvalue.ToString()fallback is never reached for numbers."(and any other character meaningful to the literal grammar) when rendering string literals, and unescape symmetrically in the lexer so the round trip holds.Acceptance criteria
decimal,float,intandlongliterals print identically regardless ofCurrentCulture.CurrentCulturetonb-NO(or similar) and asserts a print → compile → reprint round trip on a tree containing a fractional literal."round-trip through print → compile → reprint unchanged.description, validationmessage, screenlabelandtagvalues.