Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions .github/workflows/netexpr-review.yml
Original file line number Diff line number Diff line change
@@ -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: ${{ github.workspace }}/.package-review/netexpr-feed
NUGET_PACKAGES: ${{ github.workspace }}/.package-review/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: 0e853ea1abdc3d4484695db1e026ebf88535ae42
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
@'
<configuration>
<packageSources>
<clear />
<add key="local" value="%LocalNuGetPath%" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
<packageSourceMapping>
<clear />
<packageSource key="local"><package pattern="LTRData.*" /></packageSource>
<packageSource key="nuget.org"><package pattern="*" /></packageSource>
</packageSourceMapping>
</configuration>
'@ | 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' }
File renamed without changes.
1 change: 1 addition & 0 deletions MathTools.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
<Project Path="coordtool/coordtool.csproj" />
<Project Path="luhn/luhn.csproj" />
<Project Path="netexpr/netexpr.vbproj" />
<Project Path="netexpr.Review/netexpr.Review.csproj" />
</Solution>
58 changes: 58 additions & 0 deletions docs/netexpr-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# netexpr modern expression API review

The migration uses `MathParser`, `MathBinder`, the standard symbol catalog and
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,
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).

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.
57 changes: 57 additions & 0 deletions netexpr.Review/Program.cs
Original file line number Diff line number Diff line change
@@ -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++;
}
12 changes: 12 additions & 0 deletions netexpr.Review/netexpr.Review.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../netexpr/netexpr.vbproj" />
</ItemGroup>
</Project>
57 changes: 39 additions & 18 deletions netexpr/Program.vb
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
Imports System.Globalization
Imports System.Linq.Expressions
Imports LTRData.Extensions.Formatting
Imports LTRData.MathExpression

Expand Down Expand Up @@ -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
Expand All @@ -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))

Expand All @@ -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
2 changes: 1 addition & 1 deletion netexpr/netexpr.vbproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

<ItemGroup>
<PackageReference Include="LTRData.Extensions" Version="*" />
<PackageReference Include="LTRData.MathExpression" Version="*" />
<PackageReference Include="LTRData.MathExpression" Version="1.1.0" />
</ItemGroup>

</Project>
Loading