From 393f21e89d51716a10d902402e1a84b6e70bfb2c Mon Sep 17 00:00:00 2001
From: "Olof Lagerkvist (LTRData)"
Date: Tue, 8 Sep 2026 20:47:37 +0200
Subject: [PATCH 1/3] Migrate netexpr to the modern expression pipeline
---
.github/workflows/netexpr-review.yml | 78 +++++++++++++++++++
...ctory.build.props => Directory.Build.props | 0
MathTools.slnx | 1 +
docs/netexpr-migration.md | 57 ++++++++++++++
netexpr.Review/Program.cs | 57 ++++++++++++++
netexpr.Review/netexpr.Review.csproj | 12 +++
netexpr/Program.vb | 57 +++++++++-----
netexpr/netexpr.vbproj | 2 +-
8 files changed, 245 insertions(+), 19 deletions(-)
create mode 100644 .github/workflows/netexpr-review.yml
rename Directory.build.props => Directory.Build.props (100%)
create mode 100644 docs/netexpr-migration.md
create mode 100644 netexpr.Review/Program.cs
create mode 100644 netexpr.Review/netexpr.Review.csproj
diff --git a/.github/workflows/netexpr-review.yml b/.github/workflows/netexpr-review.yml
new file mode 100644
index 0000000..78ba9a2
--- /dev/null
+++ b/.github/workflows/netexpr-review.yml
@@ -0,0 +1,78 @@
+name: netexpr package review
+
+on:
+ push:
+ branches: [master, experimental/math-expression-migration]
+ paths: [netexpr/**, netexpr.Review/**, Directory.Build.props, .github/workflows/netexpr-review.yml]
+ pull_request:
+ paths: [netexpr/**, netexpr.Review/**, Directory.Build.props, .github/workflows/netexpr-review.yml]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ review:
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-24.04, windows-2025]
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 20
+ env:
+ LocalNuGetPath: ${{ runner.temp }}/netexpr-feed
+ NUGET_PACKAGES: ${{ runner.temp }}/netexpr-packages
+ DOTNET_CLI_TELEMETRY_OPTOUT: 1
+ defaults:
+ run:
+ shell: pwsh
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ path: consumer
+ - uses: actions/checkout@v4
+ with:
+ repository: LTRData/Library
+ ref: 162d02a80c456e2c782eaad86a6f132db7e2f704
+ path: library
+ - uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: 10.0.x
+ - name: Configure the shared package feed
+ run: |
+ New-Item -ItemType Directory -Force $env:LocalNuGetPath | Out-Null
+ @'
+
+
+
+
+
+
+
+
+
+
+
+
+ '@ | Set-Content NuGet.Config
+ - name: Build Library packages
+ run: |
+ foreach ($name in @('LTRData.Extensions', 'LTRData.MathExpression')) {
+ $project = "library/$name/$name.csproj"
+ dotnet restore $project --configfile NuGet.Config
+ if ($LASTEXITCODE) { throw "Restore failed: $project" }
+ dotnet build $project -c Release --no-restore
+ if ($LASTEXITCODE) { throw "Package build failed: $project" }
+ }
+ - name: Build every netexpr target
+ run: |
+ dotnet restore consumer/netexpr/netexpr.vbproj --configfile NuGet.Config
+ if ($LASTEXITCODE) { throw 'Consumer restore failed' }
+ dotnet build consumer/netexpr/netexpr.vbproj -c Release --no-restore
+ if ($LASTEXITCODE) { throw 'Consumer build failed' }
+ - name: Run command-line scenarios
+ run: |
+ dotnet restore consumer/netexpr.Review/netexpr.Review.csproj --configfile NuGet.Config
+ if ($LASTEXITCODE) { throw 'Review restore failed' }
+ dotnet run --project consumer/netexpr.Review/netexpr.Review.csproj -c Release --no-restore
+ if ($LASTEXITCODE) { throw 'Review failed' }
diff --git a/Directory.build.props b/Directory.Build.props
similarity index 100%
rename from Directory.build.props
rename to Directory.Build.props
diff --git a/MathTools.slnx b/MathTools.slnx
index 758f2b1..091d33c 100644
--- a/MathTools.slnx
+++ b/MathTools.slnx
@@ -7,4 +7,5 @@
+
diff --git a/docs/netexpr-migration.md b/docs/netexpr-migration.md
new file mode 100644
index 0000000..db1b506
--- /dev/null
+++ b/docs/netexpr-migration.md
@@ -0,0 +1,57 @@
+# netexpr modern expression API review
+
+The migration uses `MathParser`, `MathBinder`, the standard symbol catalog and
+interpreted evaluation from `LTRData.MathExpression` 1.1.0-preview.1. All existing
+targets remain: net35, net40, net8.0, net9.0 and net10.0.
+
+Formula arguments are joined as before. Variables are prompted in first-use order,
+with case-insensitive `name=value` assignments and invariant numeric syntax.
+Repeated names share a slot; reassignment updates it while other values are pending.
+Blank input is ignored; EOF with missing values returns a useful error. Parse and
+binding errors include diagnostic codes and source spans.
+
+The modern language has right-associative power (`2^3^2` = 512), with power before
+unary sign (`-2^2` = -4). `^` and `**` both mean power. Shift/bitwise syntax and the
+old parser's accidental precedence rules are not retained. The authoritative
+[language specification](https://github.com/LTRData/Library/blob/experimental/math-expression-redesign/docs/math-expression-redesign/language-specification.md)
+describes functions, constants and implicit multiplication.
+
+Output and the historical numeric exit code are retained. The printed result is
+invariant; the exit code uses VB `CInt` rounding (ties to even), or -1 for errors,
+non-finite values or conversion overflow. A nonzero result is therefore not a
+conventional process-success code, and a shell may truncate it.
+
+## Build through the local feed
+
+Set `LocalNuGetPath` to a shared package directory. In the Library experimental
+checkout, build these projects in Release, which produces their NuGet packages:
+
+```sh
+dotnet build LTRData.Extensions/LTRData.Extensions.csproj -c Release
+dotnet build LTRData.MathExpression/LTRData.MathExpression.csproj -c Release
+```
+
+Configure this repository's local NuGet.Config to include the shared directory,
+then build and run the review executable:
+
+```sh
+dotnet restore netexpr/netexpr.vbproj --force-evaluate
+dotnet build netexpr/netexpr.vbproj -c Release --no-restore
+dotnet run --project netexpr.Review/netexpr.Review.csproj -c Release
+```
+
+The review executable calls the real command-line entry point with redirected
+streams under Swedish culture, checking 13 scenarios. It returns zero on success,
+independently of the calculator's numeric exit-code convention. The only
+ProjectReference is between projects in this repository; Library is consumed
+through packages. `Directory.Build.props` now has the standard filename casing so
+Unix builds apply the same common settings.
+
+The focused `netexpr package review` workflow repeats the chain on Linux and
+Windows. It uses a pinned Library commit, a shared output feed, package source
+mapping and a fresh cache. Nothing is published to a NuGet server. For equivalent
+local configuration, see Library's
+[local package workflow](https://github.com/LTRData/Library/blob/experimental/math-expression-redesign/docs/local-package-workflow.md).
+
+Try your own saved formulas and shell scripts, particularly any that use old
+operator syntax or assume a conventional zero-on-success exit code.
diff --git a/netexpr.Review/Program.cs b/netexpr.Review/Program.cs
new file mode 100644
index 0000000..8e7a8d5
--- /dev/null
+++ b/netexpr.Review/Program.cs
@@ -0,0 +1,57 @@
+using System.Globalization;
+
+var originalOut = Console.Out;
+var originalError = Console.Error;
+var originalInput = Console.In;
+var originalCulture = CultureInfo.CurrentCulture;
+var count = 0;
+try
+{
+ // Formula syntax, parameter numbers and results remain invariant on a Swedish host.
+ CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("sv-SE");
+ Check(["2^3^2"], "", 512, "512");
+ Check(["-2^2"], "", -4, "-4");
+ Check(["1", "+", "2", "*", "3"], "", 7, "7");
+ Check(["atan2(0, 1)"], "", 0, "0");
+ Check([".5 x"], "X=4\n", 2, "2");
+ Check(["b+a+B"], "\nz=1\nb=wrong\nb=2\nB=3\na=4\n", 10, "10",
+ "Parameter z not part of expression.", "Invalid parameter value b=wrong", prompt: "b, a");
+ Check(["a+b"], "a=3\n", -1, null, "Input ended before all parameter values were supplied.");
+ Check(["sin(1,2)"], "", -1, null, "MATH300");
+ Check(["1 << 10"], "", -1, null, "MATH");
+ Check(["(1+2"], "", -1, null, "MATH");
+ Check(["2.5"], "", 2, "2.5");
+ Check(["sqrt(-1)"], "", -1, "NaN");
+ Check([], "", -1, null, "Syntax:");
+ originalOut.WriteLine($"PASS: {count} CLI scenarios, invariant culture, variables, diagnostics and numeric exit codes.");
+ return 0;
+}
+finally
+{
+ Console.SetOut(originalOut);
+ Console.SetError(originalError);
+ Console.SetIn(originalInput);
+ CultureInfo.CurrentCulture = originalCulture;
+}
+
+void Check(string[] arguments, string input, int exitCode, string? lastLine,
+ string? error = null, string? secondError = null, string? prompt = null)
+{
+ using var output = new StringWriter(CultureInfo.InvariantCulture);
+ using var errors = new StringWriter(CultureInfo.InvariantCulture);
+ using var reader = new StringReader(input);
+ Console.SetOut(output);
+ Console.SetError(errors);
+ Console.SetIn(reader);
+ var actual = netexpr.Program.Main(arguments);
+ if (actual != exitCode) throw new Exception($"Exit code for {string.Join(' ', arguments)}: expected {exitCode}, got {actual}. {errors}");
+ if (lastLine is not null && output.ToString().TrimEnd().Split('\n')[^1].TrimEnd('\r') != lastLine)
+ throw new Exception($"Unexpected output: {output}");
+ foreach (var expected in new[] { error, secondError })
+ if (expected is not null && !errors.ToString().Contains(expected, StringComparison.Ordinal))
+ throw new Exception($"Missing diagnostic {expected}: {errors}");
+ if (error is null && errors.GetStringBuilder().Length != 0) throw new Exception(errors.ToString());
+ if (prompt is not null && !output.ToString().Contains("Enter values for parameters: " + prompt + Environment.NewLine))
+ throw new Exception($"Unexpected variable order or duplicate variable: {output}");
+ count++;
+}
diff --git a/netexpr.Review/netexpr.Review.csproj b/netexpr.Review/netexpr.Review.csproj
new file mode 100644
index 0000000..8e97c9c
--- /dev/null
+++ b/netexpr.Review/netexpr.Review.csproj
@@ -0,0 +1,12 @@
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
diff --git a/netexpr/Program.vb b/netexpr/Program.vb
index 011164b..372999b 100644
--- a/netexpr/Program.vb
+++ b/netexpr/Program.vb
@@ -1,5 +1,4 @@
Imports System.Globalization
-Imports System.Linq.Expressions
Imports LTRData.Extensions.Formatting
Imports LTRData.MathExpression
@@ -29,21 +28,39 @@ Public Module Program
Return -1
End If
- Dim exprParser As New MathExpressionParser(CultureInfo.InvariantCulture)
- Dim parameters As ParameterExpression() = Nothing
+ Dim parse = MathParser.Default.Parse(String.Join(" ", args))
+ If Not parse.Success Then
+ Return ReportDiagnostics(parse.Diagnostics)
+ End If
+
+ Dim binding = MathBinder.Bind(parse.Root, MathSymbolCatalog.Standard)
+ If Not binding.Success Then
+ Return ReportDiagnostics(binding.Diagnostics)
+ End If
- Dim expr = exprParser.ParseExpression(String.Join(" ", args), parameters)
+ Dim expression = binding.Expression
+ Dim parameters = expression.Variables
+ Dim values(parameters.Count - 1) As Double
+ Dim supplied(parameters.Count - 1) As Boolean
+ Dim remaining = parameters.Count
- If parameters.Length > 0 Then
+ If remaining > 0 Then
Console.WriteLine($"Enter values for parameters: {parameters.Select(Function(p) p.Name).Join(", ")}")
End If
- Dim paramValues As New Dictionary(Of String, KeyValuePair(Of ParameterExpression, Double))
+ While remaining > 0
+ Dim input = Console.ReadLine()
+ If input Is Nothing Then
+ Console.Error.WriteLine("Input ended before all parameter values were supplied.")
+ Return -1
+ End If
- While Not parameters.All(Function(p) paramValues.ContainsKey(p.Name))
+ Dim line = input.Split(separator, StringSplitOptions.RemoveEmptyEntries)
+ If line.Length = 0 Then
+ Continue While
+ End If
- Dim line = Console.ReadLine().Split(separator, StringSplitOptions.RemoveEmptyEntries)
- Dim param = parameters.FirstOrDefault(Function(p) p.Name = line(0))
+ Dim param = parameters.FirstOrDefault(Function(p) String.Equals(p.Name, line(0), StringComparison.OrdinalIgnoreCase))
If param Is Nothing Then
Console.Error.WriteLine($"Parameter {line(0)} not part of expression.")
Continue While
@@ -62,18 +79,15 @@ Public Module Program
Continue While
End If
- paramValues.Add(param.Name, New KeyValuePair(Of ParameterExpression, Double)(param, value))
+ values(param.Slot) = value
+ If Not supplied(param.Slot) Then
+ supplied(param.Slot) = True
+ remaining -= 1
+ End If
End While
-#If NETFRAMEWORK AndAlso Not NET40_OR_GREATER Then
- Dim lambda = Expression.Lambda(expr, paramValues.Values.Select(Function(v) v.Key).ToArray()).Compile()
-#Else
- Dim lambda = Expression.Lambda(expr, paramValues.Values.Select(Function(v) v.Key)).Compile()
-#End If
-
- Dim values = paramValues.Values.Select(Function(v) CObj(v.Value)).ToArray()
- Dim returnValue = CDbl(lambda.DynamicInvoke(values))
+ Dim returnValue = expression.Evaluate(values)
Console.WriteLine(returnValue.ToString(NumberFormatInfo.InvariantInfo))
@@ -92,4 +106,11 @@ Public Module Program
End Function
+ Private Function ReportDiagnostics(diagnostics As IEnumerable(Of MathDiagnostic)) As Integer
+ For Each diagnostic In diagnostics
+ Console.Error.WriteLine(diagnostic.ToString())
+ Next
+ Return -1
+ End Function
+
End Module
diff --git a/netexpr/netexpr.vbproj b/netexpr/netexpr.vbproj
index df99c88..c3356a6 100644
--- a/netexpr/netexpr.vbproj
+++ b/netexpr/netexpr.vbproj
@@ -8,7 +8,7 @@
-
+
From b194a2c0a97df91f68d650d5173eb52de00daff3 Mon Sep 17 00:00:00 2001
From: "Olof Lagerkvist (LTRData)"
Date: Tue, 8 Sep 2026 20:50:45 +0200
Subject: [PATCH 2/3] Use available workflow context for package directories
---
.github/workflows/netexpr-review.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/netexpr-review.yml b/.github/workflows/netexpr-review.yml
index 78ba9a2..10af9a3 100644
--- a/.github/workflows/netexpr-review.yml
+++ b/.github/workflows/netexpr-review.yml
@@ -20,8 +20,8 @@ jobs:
runs-on: ${{ matrix.os }}
timeout-minutes: 20
env:
- LocalNuGetPath: ${{ runner.temp }}/netexpr-feed
- NUGET_PACKAGES: ${{ runner.temp }}/netexpr-packages
+ LocalNuGetPath: ${{ github.workspace }}/.package-review/netexpr-feed
+ NUGET_PACKAGES: ${{ github.workspace }}/.package-review/netexpr-packages
DOTNET_CLI_TELEMETRY_OPTOUT: 1
defaults:
run:
From 6c8a6ffbf821504e1d017ba1967140898eef752d Mon Sep 17 00:00:00 2001
From: "Olof Lagerkvist (LTRData)"
Date: Tue, 8 Sep 2026 22:45:10 +0200
Subject: [PATCH 3/3] Consume stable math expressions in netexpr review
---
.github/workflows/netexpr-review.yml | 2 +-
docs/netexpr-migration.md | 7 ++++---
netexpr/netexpr.vbproj | 2 +-
3 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/netexpr-review.yml b/.github/workflows/netexpr-review.yml
index 10af9a3..7e7d793 100644
--- a/.github/workflows/netexpr-review.yml
+++ b/.github/workflows/netexpr-review.yml
@@ -33,7 +33,7 @@ jobs:
- uses: actions/checkout@v4
with:
repository: LTRData/Library
- ref: 162d02a80c456e2c782eaad86a6f132db7e2f704
+ ref: 0e853ea1abdc3d4484695db1e026ebf88535ae42
path: library
- uses: actions/setup-dotnet@v5
with:
diff --git a/docs/netexpr-migration.md b/docs/netexpr-migration.md
index db1b506..d1a348f 100644
--- a/docs/netexpr-migration.md
+++ b/docs/netexpr-migration.md
@@ -1,7 +1,7 @@
# netexpr modern expression API review
The migration uses `MathParser`, `MathBinder`, the standard symbol catalog and
-interpreted evaluation from `LTRData.MathExpression` 1.1.0-preview.1. All existing
+interpreted evaluation from `LTRData.MathExpression` 1.1.0. All existing
targets remain: net35, net40, net8.0, net9.0 and net10.0.
Formula arguments are joined as before. Variables are prompted in first-use order,
@@ -53,5 +53,6 @@ mapping and a fresh cache. Nothing is published to a NuGet server. For equivalen
local configuration, see Library's
[local package workflow](https://github.com/LTRData/Library/blob/experimental/math-expression-redesign/docs/local-package-workflow.md).
-Try your own saved formulas and shell scripts, particularly any that use old
-operator syntax or assume a conventional zero-on-success exit code.
+The application owner has built and tested this migration successfully. Saved
+formulas and shell scripts remain useful review cases, particularly any that use
+old operator syntax or assume a conventional zero-on-success exit code.
diff --git a/netexpr/netexpr.vbproj b/netexpr/netexpr.vbproj
index c3356a6..7b71160 100644
--- a/netexpr/netexpr.vbproj
+++ b/netexpr/netexpr.vbproj
@@ -8,7 +8,7 @@
-
+