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: