From b8e931745c222fba428a068843242e739073bbf6 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Tue, 1 Sep 2026 13:37:15 +1000 Subject: [PATCH 1/4] Fix V1.TryCompileExpression() nameResolver argument nullability --- src/Seq.Syntax/Compatibility/V1.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Seq.Syntax/Compatibility/V1.cs b/src/Seq.Syntax/Compatibility/V1.cs index 4bae914..51ac436 100644 --- a/src/Seq.Syntax/Compatibility/V1.cs +++ b/src/Seq.Syntax/Compatibility/V1.cs @@ -24,7 +24,6 @@ using Seq.Syntax.Templates.Compilation.NameResolution; using Seq.Syntax.Templates.Encoding; using Seq.Syntax.Templates.Parsing; -using Seq.Syntax.Templates.Themes; namespace Seq.Syntax.Compatibility; @@ -37,7 +36,7 @@ public static class V1 public static bool TryCompileExpression( string expression, CultureInfo? formatProvider, - NameResolver nameResolver, + NameResolver? nameResolver, [MaybeNullWhen(false)] out CompiledExpression result, [MaybeNullWhen(true)] out string error) { @@ -59,7 +58,7 @@ public static bool TryCompileExpression( return true; } - /// + /// public static bool TryParseTemplate( string template, CultureInfo? culture, From 6de0f20054ba59beb92f730b83f1cfb98d25180f Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Tue, 1 Sep 2026 14:06:40 +1000 Subject: [PATCH 2/4] Add support for `@Data`, `ToJson()`, and `FromJson()`. Assisted-by: Claude:claude-fable-5 --- src/Seq.Syntax/Diagnostics.cs | 3 + .../Linq/LinqExpressionCompiler.cs | 3 +- .../Expressions/KeywordProperties.cs | 6 + .../Expressions/Runtime/RuntimeOperators.cs | 23 ++++ .../Cases/expression-evaluation-cases.asv | 28 ++++ .../Cases/harness-baseline-cases.asv | 126 ------------------ .../Expressions/IntrinsicsTests.cs | 20 +++ 7 files changed, 82 insertions(+), 127 deletions(-) delete mode 100644 test/Seq.Syntax.Tests/Cases/harness-baseline-cases.asv diff --git a/src/Seq.Syntax/Diagnostics.cs b/src/Seq.Syntax/Diagnostics.cs index f7eb1f0..cacabfa 100644 --- a/src/Seq.Syntax/Diagnostics.cs +++ b/src/Seq.Syntax/Diagnostics.cs @@ -50,6 +50,9 @@ public static class ErrorKinds // A regular expression driven to its match timeout by adversarial input. public const string RegexTimeout = "regex_timeout"; + // A `FromJson()` argument that couldn't be parsed as JSON. + public const string InvalidJson = "invalid_json"; + // A comparison or render abandoned because the data nested too deeply for the stack. public const string RecursionDepth = "recursion_depth"; } diff --git a/src/Seq.Syntax/Expressions/Compilation/Linq/LinqExpressionCompiler.cs b/src/Seq.Syntax/Expressions/Compilation/Linq/LinqExpressionCompiler.cs index 4d52067..28d46aa 100644 --- a/src/Seq.Syntax/Expressions/Compilation/Linq/LinqExpressionCompiler.cs +++ b/src/Seq.Syntax/Expressions/Compilation/Linq/LinqExpressionCompiler.cs @@ -533,9 +533,10 @@ protected override ExpressionBody Transform(AmbientNameExpression px) return Splice(context => Intrinsics.GetPropertyValue(context, "@ra")); case KeywordProperties.Scope: return Splice(context => Intrinsics.GetPropertyValue(context, "@sa")); + case KeywordProperties.Data: + return Splice(context => KeywordProperties.GetData(context.Document)); case KeywordProperties.Arrived: case KeywordProperties.Document: - case KeywordProperties.Data: return UndefinedConstant; } diff --git a/src/Seq.Syntax/Expressions/KeywordProperties.cs b/src/Seq.Syntax/Expressions/KeywordProperties.cs index cb37d37..3400481 100644 --- a/src/Seq.Syntax/Expressions/KeywordProperties.cs +++ b/src/Seq.Syntax/Expressions/KeywordProperties.cs @@ -118,6 +118,12 @@ public static EvaluationResult GetProperties(JsonObject eventJson) return properties; } + // The complete event document, verbatim. + public static EvaluationResult GetData(JsonObject eventJson) + { + return Values.Clone(eventJson); + } + public static EvaluationResult GetStart(JsonObject eventJson) { return GetTimestampField(eventJson, "@st") is { } dto diff --git a/src/Seq.Syntax/Expressions/Runtime/RuntimeOperators.cs b/src/Seq.Syntax/Expressions/Runtime/RuntimeOperators.cs index 187ffa3..0d3a7d9 100644 --- a/src/Seq.Syntax/Expressions/Runtime/RuntimeOperators.cs +++ b/src/Seq.Syntax/Expressions/Runtime/RuntimeOperators.cs @@ -551,6 +551,29 @@ public static EvaluationResult UriEncode(string value) return JsonValue.Create(Uri.EscapeDataString(value)); } + public static EvaluationResult ToJson(JsonNode? value) + { + // Serializes over the *inserted* form of a typed scalar: `Values.Clone` degrades a level + // to its string moniker and rejects pre-encoded `unsafe()` output. Nodes nested within + // containers were already degraded when they were inserted. + var node = value is JsonValue ? Values.Clone(value) : value; + return JsonValue.Create(node?.ToJsonString() ?? "null"); + } + + public static EvaluationResult FromJson(string json) + { + try + { + // `Parse` returns null for the JSON literal `null`. + return EvaluationResult.Defined(JsonNode.Parse(json)); + } + catch (JsonException) + { + Diagnostics.RecordSuppressedError(Diagnostics.ErrorKinds.InvalidJson); + return EvaluationResult.Undefined; + } + } + public static EvaluationResult IsSpan(JsonObject eventJson) { return ScalarBoolean(eventJson.ContainsKey("@tr") && diff --git a/test/Seq.Syntax.Tests/Cases/expression-evaluation-cases.asv b/test/Seq.Syntax.Tests/Cases/expression-evaluation-cases.asv index d28d196..59eb40c 100644 --- a/test/Seq.Syntax.Tests/Cases/expression-evaluation-cases.asv +++ b/test/Seq.Syntax.Tests/Cases/expression-evaluation-cases.asv @@ -341,9 +341,37 @@ uriencode(undefined()) ⇶ undefined() uriencode('') ⇶ '' uriencode(' ') ⇶ '%20' +// JSON serialization +tojson(42) ⇶ '42' +tojson('a') ⇶ '"a"' +tojson(true) ⇶ 'true' +tojson(null) ⇶ 'null' +tojson(undefined()) ⇶ undefined() +tojson([1, 'b', null]) ⇶ '[1,"b",null]' +tojson({a: 1}) ⇶ '{"a":1}' +tojson(@Level) ⇶ '"Information"' + +// JSON deserialization +fromjson('{"a": [1, null, "x"]}') ⇶ {a: [1, null, 'x']} +fromjson(' true ') ⇶ true +fromjson('null') ⇶ null +fromjson('') ⇶ undefined() +fromjson('{"a":') ⇶ undefined() +fromjson(42) ⇶ undefined() +fromjson(null) ⇶ undefined() +fromjson(undefined()) ⇶ undefined() +fromjson(tojson({a: [1, 'b']})) ⇶ {a: [1, 'b']} + tostring(@Level, 'u3') ⇶ 'INF' tostring(@Elapsed) ⇶ '00:10:00' +// The whole event document via @Data (@Document is deprecated and thus left undefined). +@Data['@mt'] ⇶ @mt +@Data['User']['Name'] ⇶ 'nblumhardt' +@Data['@Nonexistent'] ⇶ undefined() +@Document ⇶ undefined() +@Arrived ⇶ undefined() + // Typed values keep their string forms when inserted into constructed containers [@Level][0] ⇶ 'Information' {l: @Level}['l'] ⇶ 'Information' diff --git a/test/Seq.Syntax.Tests/Cases/harness-baseline-cases.asv b/test/Seq.Syntax.Tests/Cases/harness-baseline-cases.asv deleted file mode 100644 index 0d522dd..0000000 --- a/test/Seq.Syntax.Tests/Cases/harness-baseline-cases.asv +++ /dev/null @@ -1,126 +0,0 @@ -// Baseline characterization cases, run over the event JSON corpus by `Seq.Syntax.Tests.Harness` -// and the in-process snapshot test. Three columns: case id ⇶ expression|template ⇶ text. -// -// Keyword properties, `@x` spelling (raw JSON reads after the migration) and keyword spelling. -at-l ⇶ expression ⇶ @l -level ⇶ expression ⇶ @Level -at-t ⇶ expression ⇶ @t -timestamp ⇶ expression ⇶ @Timestamp -at-m ⇶ expression ⇶ @m -message ⇶ expression ⇶ @Message -at-mt ⇶ expression ⇶ @mt -messagetemplate ⇶ expression ⇶ @MessageTemplate -at-x ⇶ expression ⇶ @x -exception ⇶ expression ⇶ @Exception -at-i ⇶ expression ⇶ @i -eventtype ⇶ expression ⇶ @EventType -at-r ⇶ expression ⇶ @r -at-r-first ⇶ expression ⇶ @r[0] -at-seqid ⇶ expression ⇶ @seqid -id ⇶ expression ⇶ @Id -at-tr ⇶ expression ⇶ @tr -traceid ⇶ expression ⇶ @TraceId -at-sp ⇶ expression ⇶ @sp -spanid ⇶ expression ⇶ @SpanId -at-ps ⇶ expression ⇶ @ps -parentid ⇶ expression ⇶ @ParentId -at-sk ⇶ expression ⇶ @sk -spankind ⇶ expression ⇶ @SpanKind -at-st ⇶ expression ⇶ @st -start ⇶ expression ⇶ @Start -at-ra ⇶ expression ⇶ @ra -resource ⇶ expression ⇶ @Resource -at-sa ⇶ expression ⇶ @sa -scope ⇶ expression ⇶ @Scope -elapsed ⇶ expression ⇶ @Elapsed -arrived ⇶ expression ⇶ @Arrived -document ⇶ expression ⇶ @Document -data ⇶ expression ⇶ @Data -at-p ⇶ expression ⇶ @p -properties ⇶ expression ⇶ @Properties - -// Timestamp behavior: default rendering, .NET format strings, and compositions. -timestamp-default ⇶ template ⇶ {@Timestamp} -at-t-default ⇶ template ⇶ {@t} -timestamp-format ⇶ template ⇶ {@Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} -timestamp-utc ⇶ expression ⇶ UtcDateTime(@Timestamp) -at-t-utc ⇶ expression ⇶ UtcDateTime(@t) -timestamp-unix-ms ⇶ expression ⇶ TotalMilliseconds(FromUnixEpoch(@Timestamp)) -start-unix-ms ⇶ expression ⇶ TotalMilliseconds(FromUnixEpoch(@Start)) -elapsed-ms ⇶ expression ⇶ TotalMilliseconds(@Elapsed) -elapsed-ms-round ⇶ expression ⇶ Round(TotalMilliseconds(@Elapsed), 0) - -// Level behavior: monikers, comparisons, absent-level defaulting. -level-default ⇶ template ⇶ {@Level} -at-l-default ⇶ template ⇶ {@l} -level-u3 ⇶ template ⇶ {@Level:u3} -at-l-u3 ⇶ template ⇶ {@l:u3} -level-w ⇶ template ⇶ {@Level:w} -level-t4 ⇶ template ⇶ {@Level:t4} -level-aligned ⇶ template ⇶ |{@Level,11}|{@Level,-11}| -level-tostring-u3 ⇶ expression ⇶ ToString(@Level, 'u3') -level-eq-information ⇶ expression ⇶ @Level = 'Information' -at-l-eq-warning ⇶ expression ⇶ @l = 'Warning' -at-l-in ⇶ expression ⇶ @l in ['Warning', 'Error', 'OK'] - -// Message, exception, and event type. -message-hole ⇶ template ⇶ {@Message} -at-m-hole ⇶ template ⇶ {@m} -at-mt-hole ⇶ template ⇶ {@mt} -exception-hole ⇶ template ⇶ {@Exception} -at-x-hole ⇶ template ⇶ {@x} -eventtype-x8 ⇶ template ⇶ {@EventType:x8} -at-i-x8 ⇶ template ⇶ {@i:x8} -eventtype-tostring ⇶ expression ⇶ ToString(@EventType, 'x8') - -// Properties object, direct reads, and rest(). -properties-member ⇶ expression ⇶ @Properties['Application'] -at-p-member ⇶ expression ⇶ @p['Application'] -plain-member ⇶ expression ⇶ Application -properties-hole ⇶ template ⇶ {@Properties} -at-p-hole ⇶ template ⇶ {@p} -rest-hole ⇶ template ⇶ {rest()} -rest-deep-hole ⇶ template ⇶ {rest(true)} -each-properties ⇶ template ⇶ {#each k, v in @Properties}{k}={v}{#delimit}, {#end} - -// Spans. -isspan ⇶ expression ⇶ IsSpan() -isrootspan ⇶ expression ⇶ IsRootSpan() - -// Construction, spreads, indexing, wildcards. -object-build ⇶ expression ⇶ {ts: @t, level: @l, msg: @m, x: @x} -object-spread ⇶ expression ⇶ {source: 'corpus', ..@Properties} -array-build ⇶ expression ⇶ [@l, @sk, 42, null] -nested-index ⇶ expression ⇶ @ra['service']['name'] -nested-accessor ⇶ expression ⇶ @Resource.service.name -wildcard-any ⇶ expression ⇶ @r[?] is not null - -// TypeOf and TagOf. -typeof-at-t ⇶ expression ⇶ TypeOf(@t) -typeof-timestamp ⇶ expression ⇶ TypeOf(@Timestamp) -typeof-at-l ⇶ expression ⇶ TypeOf(@l) -typeof-level ⇶ expression ⇶ TypeOf(@Level) -typeof-at-ra ⇶ expression ⇶ TypeOf(@ra) -typeof-at-r ⇶ expression ⇶ TypeOf(@r) -typeof-member ⇶ expression ⇶ TypeOf(Application) -typeof-undefined ⇶ expression ⇶ TypeOf(undefined()) -tagof-tagged ⇶ expression ⇶ TagOf(Tagged) - -// General functions over document data. -coalesce-sk-l ⇶ expression ⇶ Coalesce(@sk, @l, 'none') -length-mt ⇶ expression ⇶ Length(@mt) -substring-mt ⇶ expression ⇶ Substring(@mt, 0, 10) -contains-mt ⇶ expression ⇶ Contains(@mt, 'a') ci -mt-like ⇶ expression ⇶ @mt like '%request%' -concat-l-sk ⇶ expression ⇶ Concat(Coalesce(@l, '-'), Coalesce(@sk, '-')) -tolower-mt ⇶ expression ⇶ ToLower(@mt) -isdefined-x ⇶ expression ⇶ IsDefined(@x) -at-x-is-null ⇶ expression ⇶ @x is null - -// Template directives. -if-level ⇶ template ⇶ {#if @l = 'Warning'}warning{#else}other{#end} -if-exception ⇶ template ⇶ {#if @x is not null}has-exception{#end} -if-isspan ⇶ template ⇶ {#if IsSpan()}span:{@SpanKind}{#else}event{#end} -each-renderings ⇶ template ⇶ {#each r in @r}[{r}]{#else}none{#end} -number-format ⇶ template ⇶ {Length(@mt):000.0} -literal-mix ⇶ template ⇶ [{@t}] {@l:u3} {@m} ({@i}) diff --git a/test/Seq.Syntax.Tests/Expressions/IntrinsicsTests.cs b/test/Seq.Syntax.Tests/Expressions/IntrinsicsTests.cs index 08c48a5..4763a59 100644 --- a/test/Seq.Syntax.Tests/Expressions/IntrinsicsTests.cs +++ b/test/Seq.Syntax.Tests/Expressions/IntrinsicsTests.cs @@ -28,4 +28,24 @@ public void EventTypeFallsBackToMessageTemplateHash() Assert.True(Values.TryGetClrValue(node, out var i)); Assert.Equal(Seq.Syntax.Expressions.Compilation.Linq.EventIdHash.Compute("Hello, {Name}!"), i); } + + [Fact] + public void DataIsACloneOfTheEventDocument() + { + var evt = new JsonObject + { + ["@mt"] = "Hello, {Name}!", + ["Name"] = "World" + }; + + var data = KeywordProperties.GetData(evt); + Assert.True(data.TryGetValue(out var node)); + var clone = Assert.IsType(node); + Assert.NotSame(evt, clone); + Assert.True(JsonNode.DeepEquals(evt, clone)); + + clone["Name"] = "Modified"; + Assert.True(Values.TryGetString(evt["Name"], out var original)); + Assert.Equal("World", original); + } } From ca5d2b1772d3aaf45923ea6f2537e1c35e121376 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Tue, 1 Sep 2026 14:21:27 +1000 Subject: [PATCH 3/4] README fixup --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b4cc6c3..cf40950 100644 --- a/README.md +++ b/README.md @@ -102,14 +102,14 @@ var custom = new AnsiTheme((AnsiTheme)TemplateTheme.Literate, new Dictionary{@Message}

", - escaper: TemplateOutputEscaper.Html); + encoder: TemplateOutputEncoder.Html); ``` Where an event property is known to contain trusted, well-formed HTML, `{unsafe(Markup)}` From 09a5bf42aa4175d12c4676de6c7928fa3fcd7220 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Tue, 1 Sep 2026 14:22:13 +1000 Subject: [PATCH 4/4] README fixup --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cf40950..5d8d5c5 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ terminal output: ```csharp var template = new ExpressionTemplate( "[{@Timestamp:HH:mm:ss} {@Level:u3}] {@Message}\n{@Exception}", - theme: TemplateTheme.Code); + encoder: TemplateOutputEncoder.Ansi(TemplateTheme.Code)); ``` Themes can be customized by overriding the styles of a base theme: