Skip to content

ScreenplayPrinter emits non-round-trippable output for non-double numerics and quoted strings #25

Description

@woksin

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:

  1. Run under a culture with a comma decimal separator, e.g. CultureInfo.CurrentCulture = new CultureInfo("nb-NO");
  2. Build a tree containing new LiteralExpressionSyntax(5.5m, SourceLocation.Start) (for example as the value of a ValidationRuleSyntax).
  3. new ScreenplayPrinter().Print(application) → output contains 5,5.
  4. Feed that output to ScreenplayCompiler.Compile → parse error.

Unescaped strings:

  1. Build any node with a string containing a quote, e.g. a command description of He said "hello".
  2. Print it → description "He said "hello"".
  3. 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

  • decimal, float, int and long literals print identically regardless of CurrentCulture.
  • A spec that sets CurrentCulture to nb-NO (or similar) and asserts a print → compile → reprint round trip on a tree containing a fractional literal.
  • Strings containing " round-trip through print → compile → reprint unchanged.
  • Round-trip specs cover description, validation message, screen label and tag values.
  • No change to output for input that is already valid today (no churn in existing expected-output specs).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingwait

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions