process cases of ALT as special rules

This commit is contained in:
Youmu Liu
2026-09-10 18:17:09 +08:00
parent 8755185e78
commit f372f83171
18 changed files with 486 additions and 166 deletions
+28 -6
View File
@@ -1,19 +1,37 @@
<Project>
<!-- Native builds are opt-in for executable hosts, not repeated by every library project. -->
<Target Name="BuildNativeLibrary"
BeforeTargets="PrepareForBuild"
Condition="'$(BuildNativeBeforeManaged)'=='true' And '$(DesignTimeBuild)'!='true'">
<PropertyGroup>
<NativeCMakePreset Condition="'$(NativeCMakePreset)'=='' And '$(Configuration)'=='Debug'">clang-debug</NativeCMakePreset>
<NativeCMakePreset Condition="'$(NativeCMakePreset)'==''">clang-release</NativeCMakePreset>
<NativeCMakeBuildDir Condition="'$(NativeCMakeBuildDir)'==''">$(SolutionRoot)cmake_build/$(NativeCMakePreset)</NativeCMakeBuildDir>
<_NativeCMakeOutputDir>$([System.String]::Copy('$(NativeBuildDir)').Replace('\', '/'))</_NativeCMakeOutputDir>
</PropertyGroup>
<Exec Command="cmake --preset &quot;$(NativeCMakePreset)&quot; -S &quot;$(SolutionRoot)src/Nitload.Native&quot; -B &quot;$(NativeCMakeBuildDir)&quot; &quot;-DNIF_BUILD_OUTPUT_DIR:PATH=$(_NativeCMakeOutputDir)&quot; &quot;-DNIF_EXPECTED_RID:STRING=$(BuildRid)&quot;" />
<Exec Command="cmake --build &quot;$(NativeCMakeBuildDir)&quot; --target &quot;$(NativeLibName)&quot;" />
<Error Condition="!Exists('$(NativeBuildDir)$(NativeFileName)')"
Text="CMake did not produce the expected native library: $(NativeBuildDir)$(NativeFileName). Check the target OS, architecture and output name." />
</Target>
<!-- ════════════════════════════════════════════════════════ -->
<!-- 编译后:把 C# 输出拷贝到 bin/<rid>/ 目录 -->
<!-- 替代原来的 xcopy PostBuild -->
<!-- ════════════════════════════════════════════════════════ -->
<Target Name="CopyOutputToBuildDir"
AfterTargets="Build"
Condition="'$(DisablePostBuildCopy)'!='true'">
Condition="'$(DisablePostBuildCopy)'!='true' And '$(MSBuildProjectExtension)'=='.csproj' And '$(DesignTimeBuild)'!='true'">
<MakeDir Directories="$(ManagedBuildDir)" />
<!-- 拷贝所有编译输出(DLL + PDB + XML + deps.json) -->
<ItemGroup>
<_OutputFiles Include="$(OutputPath)**\*.*"
Exclude="$(OutputPath)**\*.user" />
<!-- Native artifacts flow from CMake to hosts only. Never copy an old deployed DLL
back over the authoritative CMake output, including pre-rename artifacts. -->
<_OutputFiles Include="$(TargetDir)**\*.*"
Exclude="$(TargetDir)**\*.user;$(TargetDir)**\$(NativeLibName).*;$(TargetDir)**\lib$(NativeLibName).*;$(TargetDir)**\StgSharp.Native.*;$(TargetDir)**\libStgSharp.Native.*" />
</ItemGroup>
<Copy SourceFiles="@(_OutputFiles)"
@@ -30,7 +48,11 @@
<!-- ════════════════════════════════════════════════════════ -->
<Target Name="CopyNativeToProjectOutput"
AfterTargets="Build"
Condition="'$(DisableNativeCopy)'!='true'">
BeforeTargets="CopyOutputToBuildDir"
Condition="'$(DisableNativeCopy)'!='true' And '$(MSBuildProjectExtension)'=='.csproj' And '$(DesignTimeBuild)'!='true'">
<Error Text="Native library not found at $(NativeBuildDir)$(NativeFileName). Build Nitload.Native with CMake, or enable BuildNativeBeforeManaged."
Condition="'$(RequireNativeLibrary)'=='true' And !Exists('$(NativeBuildDir)$(NativeFileName)')" />
<!-- native 库文件 -->
<ItemGroup>
@@ -45,12 +67,12 @@
</ItemGroup>
<Copy SourceFiles="@(_NativeLibFile);@(_NativeSymFile)"
DestinationFolder="$(OutputPath)"
DestinationFolder="$(TargetDir)"
SkipUnchangedFiles="true"
Condition="'@(_NativeLibFile)'!='' Or '@(_NativeSymFile)'!=''" />
<Message Importance="high"
Text="[Nitload] Native $(NativeFileName) ($(BuildRid)) → $(OutputPath)"
Text="[Nitload] Native $(NativeFileName) ($(BuildRid)) → $(TargetDir)"
Condition="'@(_NativeLibFile)'!=''" />
<Warning Text="[Nitload] Native library not found at $(NativeBuildDir)$(NativeFileName). Build Nitload.Native first!"
+1 -1
View File
@@ -161,7 +161,7 @@ Nitload is currently in development and requires compilation from source. **NuGe
dotnet build Nitload.sln
```
Managed builds copy an existing native binary into their output directory. They do not invoke CMake automatically, so build `Nitload.Native` first when native functionality is required.
Managed library builds copy an existing native binary into their output directory. Build `Nitload.Native` first when native functionality is required. The `StgSharpDebug` executable is an exception: its project enables an incremental CMake build before managed compilation and requires the native DLL for the normal runtime path.
### Build Configuration
@@ -1785,11 +1785,9 @@ namespace Nitload.Graphics.OpenGL
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Uniform(int location, bool transpose, GMatrix44<float> value)
{
fixed (GMatrix44<float>* __value = &value)
{
__context->glUniformMatrix4fv(location, 1, transpose ? (byte)1 : (byte)0, (float*)__value);
}
__context->glUniformMatrix4fv(location, 1, transpose ? (byte)1 : (byte)0, (float*)&value);
}
/// <summary>
/// <para>Forwards to the native OpenGL entry point.</para>
@@ -633,11 +633,9 @@ namespace Nitload.Graphics.OpenGL
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ProgramUniform(GlHandle program, int location, bool transpose, GMatrix44<float> value)
{
fixed (GMatrix44<float>* __value = &value)
{
__context->glProgramUniformMatrix4fv(program.Value, location, 1, transpose ? (byte)1 : (byte)0, (float*)__value);
__context->glProgramUniformMatrix4fv(program.Value, location, 1, transpose ? (byte)1 : (byte)0, (float*)&value);
}
}
/// <summary>
/// <para>Forwards to the native OpenGL entry point.</para>
+12 -9
View File
@@ -42,10 +42,11 @@ namespace Nitload.Graphics.OpenGL
{
ContextHandle = Marshal.AllocHGlobal(Marshal.SizeOf<OpenglContext>());
OpenglContext* contextID = (OpenglContext*)ContextHandle;
contextID->glGetString = (delegate*<uint,byte*>)IntPtr.Zero;
contextID->glGetString = (delegate* unmanaged[Cdecl]<uint,byte*>)IntPtr.Zero;
}
if (GL == null) {
_context = OpenGLFunction.BuildGlFunctionPackage(ContextHandle);
if (GL == null)
{
// _context = OpenGLFunction.BuildGlFunctionPackage(ContextHandle);
}
}
@@ -60,7 +61,8 @@ namespace Nitload.Graphics.OpenGL
public unsafe void MakeAsCurrentContext()
{
GraphicFramework.glfwMakeContextCurrent(CanvasHandle);
OpenGLFunction.CurrentGL = this.GL;
// OpenGLFunction.CurrentGL = this.GL;
}
/// <inheritdoc />
@@ -73,7 +75,8 @@ namespace Nitload.Graphics.OpenGL
SwapBuffers();
renderToThread[this] = ThreadHelper.EmptyThread;
GraphicFramework.glfwMakeContextCurrent(IntPtr.Zero);
OpenGLFunction.CurrentGL = null;
// OpenGLFunction.CurrentGL = null;
}
/// <inheritdoc />
@@ -121,7 +124,7 @@ namespace Nitload.Graphics.OpenGL
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected sealed override void NativeCameraViewRange(
Radius fieldOfRange,
Vec2 offset,
Vec2<float> offset,
(float frontDepth, float backDepth) viewDepth
)
{
@@ -130,9 +133,9 @@ namespace Nitload.Graphics.OpenGL
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected sealed override void NativeCameraViewTarget(
Vec3 position,
Vec3 direction,
Vec3 up
Vec3<float> position,
Vec3<float> direction,
Vec3<float> up
)
{
NativeCamera.SetViewDirection(position, direction, up);
+6 -6
View File
@@ -16,14 +16,15 @@ namespace Nitload.Graphics
public class PlainGeometryMesh
{
private readonly PlainGeometry _shape;
private readonly Vec2[] _textureCoordinates;
private readonly Vec2<float>[] _textureCoordinates;
private readonly Image<Rgba8> _textureImage;
private readonly PlainGeometry _shape;
public PlainGeometryMesh(
PlainGeometry shape,
Image<Rgba8> texture,
Vec2[] texCoord
Vec2<float>[] texCoord
)
{
ArgumentNullException.ThrowIfNull(shape);
@@ -40,14 +41,13 @@ namespace Nitload.Graphics
_textureCoordinates = texCoord;
}
public Vec4[] VertexArray => _shape.VertexStream;
public Vec3<float>[] VertexArray => _shape.VertexStream;
public Image<Rgba8> TextureImage => _textureImage;
public ReadOnlySpan<int> MeshIndices => _shape.VertexIndices;
public ReadOnlySpan<Vec4> TextureCoord => MemoryMarshal.Cast<Vec2, Vec4>(
_textureCoordinates);
public ReadOnlySpan<Vec2<float>> TextureCoord => _textureCoordinates;
public PlainGeometryMesh Resize(
PlainGeometry newShape
+5 -5
View File
@@ -5,10 +5,10 @@
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using Nitload.Common.Timing;
using Nitload.Mathematics;
using Nitload.Mathematics.Numeric.Graphics;
using Nitload.Mathematics.Numeric.Graphics;
using Nitload.Common.Timing;
using System;
using System.Collections.Generic;
@@ -123,14 +123,14 @@ namespace Nitload.Graphics
protected abstract void NativeCameraViewRange(
Radius fieldOfRange,
Vec2 offset,
Vec2<float> offset,
(float frontDepth, float backDepth) viewDepth
);
protected abstract void NativeCameraViewTarget(
Vec3 position,
Vec3 direction,
Vec3 up
Vec3<float> position,
Vec3<float> direction,
Vec3<float> up
);
#endregion
@@ -24,12 +24,11 @@ namespace Nitload.Mathematics.Numeric.Graphics
private Buffer _buffer;
public Vec4<T> this[
int x
internal unsafe ref Vec4<T> this[
int x
]
{
readonly get => _buffer[x];
set => _buffer[x] = value;
get => ref Unsafe.Add(ref Unsafe.As<GMatrix44<T>, Vec4<T>>(ref this), x);
}
public T this[
@@ -178,6 +177,75 @@ namespace Nitload.Mathematics.Numeric.Graphics
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GMatrix44<T> operator -(
GMatrix44<T> left,
GMatrix44<T> right
)
{
Unsafe.SkipInit(out GMatrix44<T> result);
result._buffer[0] = left._buffer[0] - right._buffer[0];
result._buffer[1] = left._buffer[1] - right._buffer[1];
result._buffer[2] = left._buffer[2] - right._buffer[2];
result._buffer[3] = left._buffer[3] - right._buffer[3];
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GMatrix44<T> operator *(
GMatrix44<T> left,
T right
)
{
Unsafe.SkipInit(out GMatrix44<T> result);
result._buffer[0] = left._buffer[0] * right;
result._buffer[1] = left._buffer[1] * right;
result._buffer[2] = left._buffer[2] * right;
result._buffer[3] = left._buffer[3] * right;
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GMatrix44<T> operator *(
T left,
GMatrix44<T> right
)
{
return right * left;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GMatrix44<T> operator *(
GMatrix44<T> left,
GMatrix44<T> right
)
{
Unsafe.SkipInit(out GMatrix44<T> result);
result._buffer[0] = left._buffer[0] + right._buffer[0];
result._buffer[1] = left._buffer[1] + right._buffer[1];
result._buffer[2] = left._buffer[2] + right._buffer[2];
result._buffer[3] = left._buffer[3] + right._buffer[3];
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GMatrix44<T> operator +(
GMatrix44<T> left,
GMatrix44<T> right
)
{
Unsafe.SkipInit(out GMatrix44<T> result);
result._buffer[0] = left._buffer[0] + right._buffer[0];
result._buffer[1] = left._buffer[1] + right._buffer[1];
result._buffer[2] = left._buffer[2] + right._buffer[2];
result._buffer[3] = left._buffer[3] + right._buffer[3];
return result;
}
[InlineArray(4)]
private struct Buffer
{
+22 -4
View File
@@ -70,7 +70,13 @@ else()
set(NIF_ARCH_TAG ${NIF_RESOLVED_ARCH_NAME})
endif()
set(NIF_BUILD_OUTPUT_DIR ${NIF_SOLUTION_ROOT}/bin/${NIF_OS_TAG}-${NIF_ARCH_TAG})
set(NIF_EXPECTED_RID "" CACHE STRING "Optional managed target RID to validate before deployment")
if(NIF_EXPECTED_RID AND NOT NIF_EXPECTED_RID STREQUAL "${NIF_OS_TAG}-${NIF_ARCH_TAG}")
message(FATAL_ERROR "Native target ${NIF_OS_TAG}-${NIF_ARCH_TAG} does not match managed target ${NIF_EXPECTED_RID}. Use a matching compiler/toolchain and a separate build directory.")
endif()
set(NIF_BUILD_OUTPUT_DIR "${NIF_SOLUTION_ROOT}/bin/${NIF_OS_TAG}-${NIF_ARCH_TAG}"
CACHE PATH "Native artifact directory shared with managed build deployment")
# Compile-time intermediates (native PDB / .obj side files) live inside the CMake
# build tree (cmake_build/<preset>) so they get cleaned up with it.
set(NIF_OBJECT_ROOT_DIR ${CMAKE_CURRENT_BINARY_DIR}/obj/${PROJECT_NAME})
@@ -86,9 +92,21 @@ source_group(TREE ${CMAKE_CURRENT_LIST_DIR} FILES ${NIF_NATIVE_PROJECT_FILES})
set_target_properties(${PROJECT_NAME} PROPERTIES
C_STANDARD 11
C_STANDARD_REQUIRED ON
RUNTIME_OUTPUT_DIRECTORY ${NIF_BUILD_OUTPUT_DIR}
LIBRARY_OUTPUT_DIRECTORY ${NIF_BUILD_OUTPUT_DIR}
ARCHIVE_OUTPUT_DIRECTORY ${NIF_BUILD_OUTPUT_DIR})
OUTPUT_NAME "Nitload.Native"
PDB_NAME "Nitload.Native"
DEBUG_POSTFIX ""
RELEASE_POSTFIX ""
RELWITHDEBINFO_POSTFIX ""
MINSIZEREL_POSTFIX ""
RUNTIME_OUTPUT_DIRECTORY "${NIF_BUILD_OUTPUT_DIR}"
LIBRARY_OUTPUT_DIRECTORY "${NIF_BUILD_OUTPUT_DIR}"
ARCHIVE_OUTPUT_DIRECTORY "${NIF_BUILD_OUTPUT_DIR}"
PDB_OUTPUT_DIRECTORY "${NIF_BUILD_OUTPUT_DIR}")
# P/Invoke loads Nitload.Native.dll on Windows, including GNU-style Clang toolchains.
if(WIN32)
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")
endif()
foreach(_config Debug Release RelWithDebInfo MinSizeRel)
string(TOUPPER ${_config} _config_upper)
+4 -3
View File
@@ -85,9 +85,10 @@
<!-- 把 ARM64 → arm64,x64 保持 x64 -->
<_ArchTag Condition="'$(Platform)'=='x64'">x64</_ArchTag>
<_ArchTag Condition="'$(Platform)'=='ARM64'">arm64</_ArchTag>
<!-- 输出到 cmake_build/win_<arch>/,与 CMake 目录约定保持一致 -->
<OutDir>$(SolutionDir)cmake_build\win_$(_ArchTag)\</OutDir>
<IntDir>$(SolutionDir)obj\$(ProjectName)\$(Configuration)\$(Platform)\</IntDir>
<!-- Keep the navigation project's artifact paths aligned with CMake and managed deployment. -->
<TargetName>Nitload.Native</TargetName>
<OutDir>$(NativeBuildDir)</OutDir>
<IntDir>$(SolutionRoot)obj\$(ProjectName)\$(Configuration)\$(Platform)\</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<RunCodeAnalysis>true</RunCodeAnalysis>
+26
View File
@@ -6,6 +6,32 @@ Though this project do has a .vcxproj file,
you can only edit but not compile in visul studio.
Currently you can only compile this lib via Clang and cmake.
## Build output and managed deployment
From the repository root:
```powershell
cmake --preset clang-debug -S src/Nitload.Native
cmake --build cmake_build/clang-debug --target Nitload.Native
```
CMake writes `Nitload.Native.dll` and its linker PDB to `bin/win-x64` on Windows x64
(Linux uses `bin/linux-x64/libNitload.Native.so`). Debug and release presets currently
share this platform directory; the most recent native build is the deployed version.
`NIF_BUILD_OUTPUT_DIR` can override the destination. Managed builds pass their expected
RID as `NIF_EXPECTED_RID` so an incompatible native target is rejected before deployment.
`StgSharpDebug.csproj` enables `BuildNativeBeforeManaged` and `RequireNativeLibrary`.
Its build runs the incremental CMake build (`clang-debug` for Debug, `clang-release`
otherwise), then deploys the native library to the executable's actual `TargetDir`.
Other managed projects copy an existing native artifact without invoking CMake.
Managed-output staging never copies deployed native files back over the CMake artifact.
To use a prebuilt native artifact, pass `-p:BuildNativeBeforeManaged=false`. A matching
Clang/Ninja environment and the local GLFW static library are required for native builds.
Cross-target builds require a matching toolchain/preset; changing a managed platform alone
does not cross-compile the C library.
## Code Layout
Conventions:
@@ -39,13 +39,13 @@ namespace Nitload.RegularAnalysis.Text
GenerateConcat(root, sc, func_list, context, exit_label);
break;
case RegexElementLabel.COUNT:
GenerateCount(root, sc, func_list, context);
GenerateCount(root, sc, func_list, context, exit_label);
break;
case RegexElementLabel.GROUP_BEGIN:
GenerateGroup(root, sc, func_list, context);
GenerateGroup(root, sc, func_list, context, exit_label);
break;
case RegexElementLabel.ALT:
GenerateAlt(root, sc, func_list, context);
GenerateAlt(root, sc, func_list, context, exit_label);
break;
default:
@@ -167,17 +167,20 @@ namespace Nitload.RegularAnalysis.Text
)
{
RegexCharSetPayload payload = value.PayloadAs<RegexCharSetPayload>();
string[] rules = new string[payload.Set.Count];
for (int i = 0; i < rules.Length; i++) {
rules[i] = GenerateCharSetExpression(payload.Set[i]);
}
// Combine rule-level complements before applying the enclosing charset complement.
// An empty union accepts nothing; its complement accepts any available character.
string predicate = rules.Length == 0 ? "false" : string.Join(" || ", rules);
string mismatch = payload.Accept ? $"!({predicate})" : $"({predicate})";
string remain_span = context.RemainSpan;
_ = sc.AppendLine($"if ({remain_span}.IsEmpty || {mismatch})")
_ = sc.AppendLine($"if ({remain_span}.IsEmpty ||")
.AppendLine(payload.Accept ? " !(" : " (");
if (payload.Set.Count == 0) {
_ = sc.AppendLine(" false");
}
for (int i = 0; i < payload.Set.Count; i++)
{
string suffix = i + 1 < payload.Set.Count ? " ||" : string.Empty;
_ = sc.AppendLine($" {GenerateCharSetExpression(payload.Set[i])}{suffix}");
}
_ = sc.AppendLine(" ))")
.AppendLine("{")
.AppendLine($"goto {exit.GetLabelName()};")
.AppendLine("}")
@@ -244,9 +247,6 @@ namespace Nitload.RegularAnalysis.Text
string required_chunk_index = $"__required_chunk_index_{scratch_scope_index}";
string additional_count = $"__additional_count_{scratch_scope_index}";
// TODO(NGRA): Replace this local COUNT exit with the inherited failure ExitLabel.
// The temporary goto preserves the former local exit but still lets siblings execute.
if (min <= flattened_unit_count_threshold)
{
if (min > 0)
@@ -428,9 +428,6 @@ namespace Nitload.RegularAnalysis.Text
string additional_count = $"__additional_count_{scratch_scope_index}";
string starts_with = $"{nameof(MemoryExtensions.StartsWith)}";
// TODO(NGRA): Replace this local COUNT exit with the inherited failure ExitLabel.
// The temporary goto preserves the former local exit but still lets siblings execute.
if (literal.Length > flattened_unit_count_threshold)
{
// One repetition is already larger than the flattening threshold. Match the
@@ -551,93 +548,28 @@ namespace Nitload.RegularAnalysis.Text
RegexAstNode node,
[NotNull] in SequenceEmitter<string> sc,
in List<SequenceEmitter<string>> func_list,
in SourceGenContext context
in SourceGenContext context,
in ExitLabel exit_label
)
{
ExitLabel success = context.RequestExitLabel(ExitMode.Goto);
RegexAstNode current = node.Right;
// TODO(NGRA-NAMING): Reserve exclusive names for future ALT cursor/capture snapshots;
// their lifetime extends through retry labels, not just the candidate's emitted block.
ExitLabel exit_label = context.RequestExitLabel(ExitMode.Goto);
// TODO(NGRA): Walk every normalized ALT candidate exactly once. The final traversal
// shape and failure continuation are not wired yet.
while (RegexAstNode.IsNullOrEmpty(current))
while (!RegexAstNode.IsNullOrEmpty(current))
{
RegexElementLabel op = current.Label & RegexElementLabel.SEQUENCE;
if (op != 0)
{
string code = current.SourceCode;
switch (op)
{
case RegexElementLabel.UNIT:
// TODO(NGRA): Guard against an empty remaining span before indexing it.
_ = sc.AppendLine(@$"if({context.RemainSpan}[0] == {FormatCharLiteral(code[0])})")
.AppendLine("{")
.AppendLine($@"{context.RemainSpan} = {context.RemainSpan}.Slice(1);")
.AppendLine($@"goto {exit_label};")
.AppendLine("}");
break;
case RegexElementLabel.UNIT_SET:
// TODO(NGRA): Guard the remaining span, handle empty/Any sets, and
// validate negated-set composition before emitting this predicate.
RegexCharSetPayload payload = current.PayloadAs<RegexCharSetPayload>();
IReadOnlyList<RegexCharSet> split = payload.Set;
bool accept = payload.Accept;
RegexCharSet _set = split[0];
_ = sc.AppendLine($"if({(accept ? string.Empty : "!(")} {GenerateCharSetExpression(_set)} ||");
for (int i = 1; i < split.Count - 1; i++)
{
_set = split[i];
_ = sc.AppendLine($" {GenerateCharSetExpression(_set)} ||");
}
_set = split[^1];
_ = sc.AppendLine($" {GenerateCharSetExpression(_set)} ){(accept ? string.Empty : ')')}")
.AppendLine("{")
.AppendLine($@"{context.RemainSpan} = {context.RemainSpan}.Slice(1);")
.AppendLine($@"goto {exit_label};")
.AppendLine("}");
break;
case RegexElementLabel.UNIT_SPAN:
// TODO(NGRA): Advance by code.Length rather than one character and
// preserve the ALT rollback cursor on failure.
string literal = FormatStringLiteral(code);
_ = sc.AppendLine(@$"if({context.RemainSpan}.{MemoryExtensions.StartsWith}({literal}))")
.AppendLine("{")
.AppendLine($@"{context.RemainSpan} = {context.RemainSpan}.Slice({code.Length});")
.AppendLine($@"goto {exit_label};")
.AppendLine("}");
break;
default:
// TODO(NGRA): Route unsupported ALT candidates through the recursive
// matcher or report a source-generation diagnostic.
break;
}
// TODO(NGRA): Advance to the next ALT candidate after a scalar candidate
// misses; the current sequence branch does not update current.
} else
{
GenerateMethodSource(current, sc, func_list, context);
current = current.Next;
}
GenerateAltCandidate(current, sc, func_list, context, success);
current = current.Next;
}
// TODO(NGRA): Emit the all-alternatives-failed continuation before the shared success
// label, including restoration of the entry cursor and capture state.
_ = sc.AppendLine($@"{exit_label}: {{ }}");
_ = sc.AppendLine($"goto {exit_label.GetLabelName()};")
.AppendLine($"{success.GetLabelName()}: ;");
}
private static void GenerateGroup(
RegexAstNode node,
[NotNull] in SequenceEmitter<string> sc,
in List<SequenceEmitter<string>> func_list,
in SourceGenContext context
in SourceGenContext context,
in ExitLabel exit_label
)
{
RegexAstNode left = node.Left;
@@ -648,14 +580,13 @@ namespace Nitload.RegularAnalysis.Text
// TODO(NGRA-NAMING): Reserve exclusive names for generated group snapshots and capture
// temporaries. A capture's user-provided name is not a unique generated local name.
// TODO(NGRA): Select the owned group body using Empty-sentinel checks, snapshot its
// start cursor, and restore it when the child matcher fails.
if (left is not null)
// TODO(NGRA): Snapshot the group start cursor and restore it when the child fails.
if (!RegexAstNode.IsNullOrEmpty(left))
{
GenerateMethodSource(left, sc, func_list, context);
GenerateMethodSource(left, sc, func_list, context, exit_label);
} else
if (right is not null) {
GenerateMethodSource(right, sc, func_list, context);
if (!RegexAstNode.IsNullOrEmpty(right)) {
GenerateMethodSource(right, sc, func_list, context, exit_label);
}
string group_name = config.Name;
@@ -685,7 +616,8 @@ namespace Nitload.RegularAnalysis.Text
RegexAstNode node,
[NotNull] in SequenceEmitter<string> sc,
in List<SequenceEmitter<string>> func_list,
in SourceGenContext context
in SourceGenContext context,
in ExitLabel exit_label
)
{
RegexAstNode value = RegexAstNode.IsNullOrEmpty(node.Left) ? node.Right : node.Left;
@@ -699,12 +631,11 @@ namespace Nitload.RegularAnalysis.Text
// TODO(NGRA-NAMING): Reserve exclusive names for future repetition/rollback state
// across complex COUNT retries; recursive children must use the same method pool.
ExitLabel exit_label = context.RequestExitLabel(ExitMode.Goto);
if ((value.Label & RegexElementLabel.SEQUENCE) == 0)
{
// TODO(NGRA): Emit min/max repetition, zero-width progress protection, greedy vs.
// lazy ordering, and per-attempt rollback for a complex COUNT body.
GenerateMethodSource(value, sc, func_list, context);
GenerateMethodSource(value, sc, func_list, context, exit_label);
} else
{
switch (value.Label & RegexElementLabel.SEQUENCE)
@@ -726,9 +657,6 @@ namespace Nitload.RegularAnalysis.Text
}
}
// TODO(NGRA): Connect COUNT success/failure continuations to the surrounding rollback
// scope; the label is currently only a placeholder.
_ = sc.AppendLine($@"{exit_label}: {{ }}");
}
#endregion
@@ -1,6 +1,6 @@
// -----------------------------------------------------------------------------
// file="TextRegexSourceGen.GenSpecial"
// Project: Nitload
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
@@ -24,11 +24,162 @@ namespace Nitload.RegularAnalysis.Text
// to the general path; returning true prevents that subtree from being emitted twice.
return node.Label switch
{
RegexElementLabel.CONCAT => TryGenerateFindConcat(node, sc, func_list, context, exit_label),
RegexElementLabel.CONCAT => TryGenerateFindConcat(node, sc, func_list, context,
exit_label),
_ => false,
};
}
#region ALT candidate rules
private static bool TryGetSimpleAltNode(
RegexAstNode node,
out RegexAstNode simple
)
{
simple = node;
while (simple.Label == RegexElementLabel.CONCAT)
{
bool left_empty = RegexAstNode.IsNullOrEmpty(simple.Left);
bool right_empty = RegexAstNode.IsNullOrEmpty(simple.Right);
if (left_empty == right_empty) {
return false;
}
simple = left_empty ? simple.Right : simple.Left;
}
// Inspect transparent wrappers without changing ownership or the candidate Next link.
// COUNT and GROUP are never peeled: repetition and capture semantics must survive.
return simple.Label is RegexElementLabel.UNIT or RegexElementLabel.UNIT_SET or
RegexElementLabel.UNIT_SPAN;
}
private static void GenerateAltCandidate(
RegexAstNode node,
in SequenceEmitter<string> sc,
in List<SequenceEmitter<string>> func_list,
in SourceGenContext context,
in ExitLabel success
)
{
if (TryGetSimpleAltNode(node, out RegexAstNode simple)) {
node = simple;
}
switch (node.Label)
{
case RegexElementLabel.UNIT:
GenerateAltUnit(node, sc, context, success);
break;
case RegexElementLabel.UNIT_SET:
GenerateAltUnitSet(node, sc, context, success);
break;
case RegexElementLabel.UNIT_SPAN:
GenerateAltUnitSpan(node, sc, context, success);
break;
case RegexElementLabel.CONCAT when RegexAstNode.IsNullOrEmpty(node.Left) &&
RegexAstNode.IsNullOrEmpty(node.Right):
case RegexElementLabel.EPSILON:
// An empty expression succeeds without consuming input.
_ = sc.AppendLine($"goto {success.GetLabelName()};");
break;
case RegexElementLabel.CONCAT:
case RegexElementLabel.COUNT:
case RegexElementLabel.GROUP_BEGIN:
case RegexElementLabel.ALT:
GenerateAltOperator(node, sc, func_list, context, success);
break;
default:
throw new NotSupportedException($"Unsupported ALT candidate: {node.Label}.");
}
}
private static void GenerateAltUnit(
RegexAstNode node,
in SequenceEmitter<string> sc,
in SourceGenContext context,
in ExitLabel success
)
{
string literal = node.Value;
if (literal.Length != 1) {
throw new InvalidOperationException("ALT UNIT requires one decoded character.");
}
_ = sc.AppendLine($"if (!{context.RemainSpan}.IsEmpty && {context.RemainSpan}[0] == {FormatCharLiteral(literal[0])})")
.AppendLine("{")
.AppendLine($"{context.RemainSpan} = {context.RemainSpan}.Slice(1);")
.AppendLine($"goto {success.GetLabelName()};")
.AppendLine("}");
}
private static void GenerateAltUnitSet(
RegexAstNode node,
in SequenceEmitter<string> sc,
in SourceGenContext context,
in ExitLabel success
)
{
RegexCharSetPayload payload = node.PayloadAs<RegexCharSetPayload>();
_ = sc.AppendLine($"if (!{context.RemainSpan}.IsEmpty &&")
.AppendLine(payload.Accept ? " (" : " !(");
if (payload.Set.Count == 0) {
_ = sc.AppendLine(" false");
}
for (int i = 0; i < payload.Set.Count; i++)
{
string suffix = i + 1 < payload.Set.Count ? " ||" : string.Empty;
_ = sc.AppendLine($" {GenerateCharSetExpression(payload.Set[i])}{suffix}");
}
_ = sc.AppendLine(" ))")
.AppendLine("{")
.AppendLine($"{context.RemainSpan} = {context.RemainSpan}.Slice(1);")
.AppendLine($"goto {success.GetLabelName()};")
.AppendLine("}");
}
private static void GenerateAltUnitSpan(
RegexAstNode node,
in SequenceEmitter<string> sc,
in SourceGenContext context,
in ExitLabel success
)
{
string literal = node.Value;
if (literal.Length == 0) {
throw new InvalidOperationException("ALT UNIT_SPAN requires a non-empty literal.");
}
string predicate = $"{context.RemainSpan}.{nameof(MemoryExtensions.StartsWith)}({FormatStringLiteral(literal)})";
_ = sc.AppendLine($"if ({predicate})")
.AppendLine("{")
.AppendLine($"{context.RemainSpan} = {context.RemainSpan}.Slice({literal.Length});")
.AppendLine($"goto {success.GetLabelName()};")
.AppendLine("}");
}
private static void GenerateAltOperator(
RegexAstNode node,
in SequenceEmitter<string> sc,
in List<SequenceEmitter<string>> func_list,
in SourceGenContext context,
in ExitLabel success
)
{
ExitLabel failure = context.RequestExitLabel(ExitMode.Goto);
string entry = $"__alt_entry_{context.RequestScratchScopeIndex()}";
_ = sc.AppendLine($"{ROS_char} {entry} = {context.RemainSpan};");
GenerateMethodSource(node, sc, func_list, context, failure);
_ = sc.AppendLine($"goto {success.GetLabelName()};")
.AppendLine($"{failure.GetLabelName()}:")
.AppendLine($"{context.RemainSpan} = {entry};");
// TODO(NGRA): Restore capture state once GROUP emission supports captures.
// Retrying an already successful candidate after a later continuation fails is separate.
}
#endregion
#region FIND concatenation
private static bool TryGenerateFindConcat(
@@ -78,7 +229,8 @@ namespace Nitload.RegularAnalysis.Text
{
// (?<name>.*)abc
if (RegexAstNode.IsNullOrEmpty(node) || node.Label != RegexElementLabel.CONCAT) {
if (RegexAstNode.IsNullOrEmpty(node) || node.Label != RegexElementLabel.CONCAT)
{
return false;
}
RegexAstNode group = node.Left;
@@ -106,6 +258,5 @@ namespace Nitload.RegularAnalysis.Text
// An escaped star is a literal and must not be classified as dot-star repetition.
#endregion
}
}
+1 -1
View File
@@ -28,4 +28,4 @@ cmake --build cmake_build/clang-release
dotnet build Nitload.sln
```
Managed builds copy an existing native binary when one is available; they do not invoke CMake automatically. Generated OpenGL files under `Nitload.Graphics/OpenGL/Generated` should be updated through the generator in `tools/Nitload.GenerateGL`, not edited by hand.
Managed library builds copy an existing native binary when one is available; they do not invoke CMake automatically. The `test/StgSharpDebug` executable explicitly enables the CMake build before managed compilation. Generated OpenGL files under `Nitload.Graphics/OpenGL/Generated` should be updated through the generator in `tools/Nitload.GenerateGL`, not edited by hand.
+32
View File
@@ -16,4 +16,36 @@ Run the active debug harness directly when investigating a subsystem:
dotnet run --project test/StgSharpDebug/StgSharpDebug.csproj
```
## NGRA generation preview
`StgSharpDebug/TestRegexGenerate.cs` contains a standalone debugging routine that calls the
existing NGRA emitter without Roslyn attribute binding. Select it from the debug `Main`:
```powershell
dotnet run --project test/StgSharpDebug/StgSharpDebug.csproj -- --regex-preview
```
This branch returns before `World` or native-memory initialization. Without the switch, the
existing debug flow is unchanged. You can also call `TestRegexGenerate.Run()` directly from
`Main` while debugging; pass a pattern to `Run("abc")` for a single preview.
The switch only selects runtime behavior; building the debug project still builds its normal
project references, including Graphics and Mathematics, and prepares the native library with
CMake. `BuildNativeBeforeManaged` defaults to true; Debug selects `clang-debug`, and other
configurations select `clang-release`. `RequireNativeLibrary` reports a missing DLL at build
time instead of allowing the default runtime path to fail later.
For managed-only preview work without a native toolchain or DLL, add
`-p:BuildNativeBeforeManaged=false -p:RequireNativeLibrary=false` before `--` in the run command.
Enter raw regular expressions without C# string delimiters. The routine prints only the emitted
code, without status headers or file output. Exceptions propagate directly to the caller/debugger.
Use `:exit` or end-of-input to leave the loop. Empty input is an empty regex. For one-shot mode:
```powershell
dotnet run --project test/StgSharpDebug/StgSharpDebug.csproj -- --regex-preview 'a{2,3}'
```
Set breakpoints in `TestRegexGenerate.cs` or any existing `Generate*` method. The output is a
method-body fragment; this routine does not compile or execute it.
New repeatable tests should use a test project and avoid machine-specific paths, external mutable data, and dependencies on previous test execution. Keep one-off investigations in `SingleFile` or the manual debug harness until their expected behavior is stable enough for automation.
+12 -10
View File
@@ -5,13 +5,13 @@
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using Nitload.Graphics;
using Nitload.Common.HighPerformance.Memory;
using Nitload.Common.Internal;
using Nitload.Common.Timing;
using Nitload.Common.Timing;
using Nitload.Graphics;
using Nitload.Mathematics;
using Nitload.Mathematics.Numeric;
using Nitload.Common.Timing;
using Nitload.Common.Timing;
using System;
using System.Runtime.InteropServices;
@@ -28,6 +28,13 @@ namespace Nitload
string[] args
)
{
// Select the managed-only emitter preview before initializing the normal runtime.
if (args.Length > 0 && args[0] == "--regex-preview")
{
Environment.ExitCode = TestRegexGenerate.Run(args[1..]);
return;
}
try
{
World.Initialize(new()
@@ -39,13 +46,8 @@ namespace Nitload
// .UseModule<GraphicModule>()
.InitializeModule();
_ = SlabAllocator.FromBuffer<int>(
(nuint)NativeMemory.AlignedAlloc(65536 * 4, 4),
65536,
buffer => NativeMemory.AlignedFree((void*)buffer)
);
TestRegexAnalyse.Run();
// TestRegexAnalyse.Run();
_ = TestRegexGenerate.Run("abc|123");
// TestHlsf();
+9 -1
View File
@@ -2,6 +2,10 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<BuildNativeBeforeManaged>true</BuildNativeBeforeManaged>
<RequireNativeLibrary>true</RequireNativeLibrary>
<!-- Let MSBuild run the incremental CMake build even when managed inputs are unchanged. -->
<DisableFastUpToDateCheck>true</DisableFastUpToDateCheck>
</PropertyGroup>
@@ -9,6 +13,11 @@
<None Remove="SSG_errorlog.log" />
</ItemGroup>
<ItemGroup>
<!-- The debug host calls the emitter directly and needs Roslyn at runtime. -->
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Nitload.Common\Nitload.Common.csproj" />
<ProjectReference Include="..\..\src\Nitload.Graphics\Nitload.Graphics.csproj" />
@@ -18,4 +27,3 @@
</ItemGroup>
</Project>
+65
View File
@@ -0,0 +1,65 @@
// -----------------------------------------------------------------------------
// file="TestRegexGenerate"
// Project: Nitload
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using Nitload.RegularAnalysis.Abstraction;
using Nitload.RegularAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Runtime.ExceptionServices;
using static Nitload.RegularAnalysis.Text.TextRegexSourceGen;
namespace Nitload
{
internal static class TestRegexGenerate
{
public static int Run(params string[] args)
{
if (args.Length > 1) {
throw new ArgumentException("Expected at most one regex pattern.", nameof(args));
}
if (args.Length == 1)
{
Print(args[0]);
return 0;
}
while (true)
{
Console.Write("regex> ");
string? pattern = Console.ReadLine();
if (pattern is null or ":exit") {
return 0;
}
Print(pattern);
}
}
private static void Print(string pattern)
{
TextRegexSource analysis = RegexAnalyzer.Analyze(pattern);
if (analysis.AnalyseException is not null) {
ExceptionDispatchInfo.Throw(analysis.AnalyseException);
}
SequenceEmitter<string> body = new();
List<SequenceEmitter<string>> functions = [];
SourceGenContext context = new();
ExitLabel exit = context.RequestExitLabel(ExitMode.Goto);
GenerateMethodSource(analysis.Ast.Root, body, functions, context, exit);
foreach (string line in body.ToList()) {
Console.WriteLine(line);
}
foreach (SequenceEmitter<string> function in functions)
{
foreach (string line in function.ToList()) {
Console.WriteLine(line);
}
}
}
}
}