Skip to content

Make the expression visitor an internal instance class - #100

Draft
HowardvanRooijen wants to merge 1 commit into
feature/expression-tree-improvementsfrom
feature/expression-visitor-instance
Draft

HowardvanRooijen wants to merge 1 commit into
feature/expression-tree-improvementsfrom
feature/expression-visitor-instance

Conversation

@HowardvanRooijen

@HowardvanRooijen HowardvanRooijen commented Sep 1, 2026

Copy link
Copy Markdown
Member

The first of the two follow-ups #99 listed: turn ExpressionVisitor into an internal instance
class. Behaviour-preserving; the visibility change is a deliberate break for the major version.

The change

ExpressionVisitor was a public static class whose every method threaded four arguments -
(Context context, Environment environment, …, ParameterExpression param) - of which three are
invariant for the whole translation of one constraint. It is now an internal sealed class that
holds those three as readonly fields, constructed once by a static Translate entry point:

internal static Expr Translate(Context context, Environment environment, Expression expression, ParameterExpression param)
    => new ExpressionVisitor(context, environment, param).Visit(expression);

so the Visit* methods take only the node they translate. Theorem calls Translate at its two
sites; nothing else changes.

Two details kept deliberately:

  • The dispatch lambdas still take the Context (static (ctx, a, b) => ctx.MkAdd(…)), so they
    remain non-capturing and are cached by the compiler in a static field rather than allocated per
    node. Making context a field did not tempt them into closures.
  • ToUtcTicks stays internal static - it depends on nothing instance-level and Theorem's
    read path refers to it by name.

Why internal

The class was public "for historical reasons," and its own doc said there was no supported
reason to call it. Since #82 the package ships its types and their IntelliSense, so that
non-API was visible to consumers. The .NET 10 major version is the window to stop exposing it.
Confirmed nothing outside the assembly referenced it - the only mentions in tests are line-number
comments, not calls - so this breaks no caller in the repository, and Theorem (same assembly)
is unaffected.

Verification

  • dotnet build solutions/Z3.Linq.slnx -c Release - clean, TreatWarningsAsErrors and
    documentation generation on; ExpressionVisitor is now internal sealed
  • 290/290 locally - unchanged from Translate ternaries, and stop crashing on bitwise and real modulo #99, which is the verification for a behaviour-preserving
    refactor: same tests, same results, only the shape and visibility moved
  • ./build.ps1 -Configuration Release - 46 tasks, 0 errors, 0 warnings
  • Coverage 89.2% -> 89.3% line (844 of 945); branch unchanged at 76.0% (543 of 714)

No new tests: there is no new behaviour to pin, and the existing suite already exercises every
Visit* path. A reflection test asserting the type is internal would only restate the compiler's
own enforcement.

The other follow-up, still open

Bit-vector symbols - what would make integer bitwise (&/|/^) and shifts actually work
rather than be refused, as #99 diagnosed them. That is a feature in its own right, with a design
decision to make first (how a caller opts a symbol into a fixed-width bit-vector, and its width),
so it is not folded in here.

Release note

Releases remain on hold under #60 until Microsoft.Z3 5.x reaches nuget.org, so this reaches main
but not consumers. The public-API surface shrinks by one type (ExpressionVisitor), which the
major version accommodates.

ExpressionVisitor was a public static class that threaded the Z3 context,
the environment, and the constraint lambda's parameter through every
method - four arguments on each, of which three are invariant for the
whole translation. It is now an internal sealed class that holds those
three as readonly fields, set once by a static Translate entry point, so
the Visit* methods take only the node they translate.

The dispatch lambdas still take the context, so they stay non-capturing
and compiler-cached rather than allocating per node. Behaviour is
unchanged - the 290 tests pass as before; this is a shape and
visibility change, the visibility being the point: the class was public
"for historical reasons" with no supported reason to call it, and the
.NET 10 major version is the moment to stop shipping it as API. Nothing
outside the assembly referenced it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

Test Results

  1 files  ±0    1 suites  ±0   17s ⏱️ +8s
279 tests ±0  279 ✅ ±0  0 💤 ±0  0 ❌ ±0 
290 runs  ±0  290 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 81c8b1d. ± Comparison against base commit 03bc794.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant