From f372f83171151bc155bd78879eef3d2517088e4c Mon Sep 17 00:00:00 2001 From: Youmu Liu <105042599+Nitload-NSI@users.noreply.github.com> Date: Thu, 10 Sep 2026 18:17:09 +0800 Subject: [PATCH] process cases of ALT as special rules --- Directory.Build.targets | 34 +++- ReadMe.md | 2 +- .../OpenGL/Generated/OpenGLFunction.Base.g.cs | 6 +- .../OpenGL/Generated/OpenGLFunction.v4_1.g.cs | 6 +- src/Nitload.Graphics/OpenGL/glRenderStream.cs | 21 ++- src/Nitload.Graphics/PlainGeometryMesh.cs | 12 +- src/Nitload.Graphics/RenderStream.cs | 10 +- .../Numeric/Graphics/GMatrix44_T.cs | 76 ++++++++- src/Nitload.Native/CMakeLists.txt | 26 ++- src/Nitload.Native/Nitload.Native.vcxproj | 7 +- src/Nitload.Native/README.md | 26 +++ .../Text/TextRegexSourceGen.GenClass.cs | 136 ++++----------- .../Text/TextRegexSourceGen.GenSpecial.cs | 159 +++++++++++++++++- src/README.md | 2 +- test/README.md | 32 ++++ test/StgSharpDebug/Program.cs | 22 +-- test/StgSharpDebug/StgSharpDebug.csproj | 10 +- test/StgSharpDebug/TestRegexGenerate.cs | 65 +++++++ 18 files changed, 486 insertions(+), 166 deletions(-) create mode 100644 test/StgSharpDebug/TestRegexGenerate.cs diff --git a/Directory.Build.targets b/Directory.Build.targets index 749f92a9..3bd7f0ac 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,19 +1,37 @@ + + + + clang-debug + clang-release + $(SolutionRoot)cmake_build/$(NativeCMakePreset) + <_NativeCMakeOutputDir>$([System.String]::Copy('$(NativeBuildDir)').Replace('\', '/')) + + + + + + + Condition="'$(DisablePostBuildCopy)'!='true' And '$(MSBuildProjectExtension)'=='.csproj' And '$(DesignTimeBuild)'!='true'"> - <_OutputFiles Include="$(OutputPath)**\*.*" - Exclude="$(OutputPath)**\*.user" /> + + <_OutputFiles Include="$(TargetDir)**\*.*" + Exclude="$(TargetDir)**\*.user;$(TargetDir)**\$(NativeLibName).*;$(TargetDir)**\lib$(NativeLibName).*;$(TargetDir)**\StgSharp.Native.*;$(TargetDir)**\libStgSharp.Native.*" /> + BeforeTargets="CopyOutputToBuildDir" + Condition="'$(DisableNativeCopy)'!='true' And '$(MSBuildProjectExtension)'=='.csproj' And '$(DesignTimeBuild)'!='true'"> + + @@ -45,12 +67,12 @@ value) { - fixed (GMatrix44* __value = &value) - { - __context->glUniformMatrix4fv(location, 1, transpose ? (byte)1 : (byte)0, (float*)__value); - } + __context->glUniformMatrix4fv(location, 1, transpose ? (byte)1 : (byte)0, (float*)&value); } + /// /// Forwards to the native OpenGL entry point. diff --git a/src/Nitload.Graphics/OpenGL/Generated/OpenGLFunction.v4_1.g.cs b/src/Nitload.Graphics/OpenGL/Generated/OpenGLFunction.v4_1.g.cs index 512d07be..997c6b9d 100644 --- a/src/Nitload.Graphics/OpenGL/Generated/OpenGLFunction.v4_1.g.cs +++ b/src/Nitload.Graphics/OpenGL/Generated/OpenGLFunction.v4_1.g.cs @@ -633,11 +633,9 @@ namespace Nitload.Graphics.OpenGL [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ProgramUniform(GlHandle program, int location, bool transpose, GMatrix44 value) { - fixed (GMatrix44* __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); } - } + /// /// Forwards to the native OpenGL entry point. diff --git a/src/Nitload.Graphics/OpenGL/glRenderStream.cs b/src/Nitload.Graphics/OpenGL/glRenderStream.cs index c2ab1c90..0f505671 100644 --- a/src/Nitload.Graphics/OpenGL/glRenderStream.cs +++ b/src/Nitload.Graphics/OpenGL/glRenderStream.cs @@ -42,10 +42,11 @@ namespace Nitload.Graphics.OpenGL { ContextHandle = Marshal.AllocHGlobal(Marshal.SizeOf()); OpenglContext* contextID = (OpenglContext*)ContextHandle; - contextID->glGetString = (delegate*)IntPtr.Zero; + contextID->glGetString = (delegate* unmanaged[Cdecl])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; } /// @@ -73,7 +75,8 @@ namespace Nitload.Graphics.OpenGL SwapBuffers(); renderToThread[this] = ThreadHelper.EmptyThread; GraphicFramework.glfwMakeContextCurrent(IntPtr.Zero); - OpenGLFunction.CurrentGL = null; + + // OpenGLFunction.CurrentGL = null; } /// @@ -121,7 +124,7 @@ namespace Nitload.Graphics.OpenGL [MethodImpl(MethodImplOptions.AggressiveInlining)] protected sealed override void NativeCameraViewRange( Radius fieldOfRange, - Vec2 offset, + Vec2 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 position, + Vec3 direction, + Vec3 up ) { NativeCamera.SetViewDirection(position, direction, up); diff --git a/src/Nitload.Graphics/PlainGeometryMesh.cs b/src/Nitload.Graphics/PlainGeometryMesh.cs index c2ebf751..213c9974 100644 --- a/src/Nitload.Graphics/PlainGeometryMesh.cs +++ b/src/Nitload.Graphics/PlainGeometryMesh.cs @@ -16,14 +16,15 @@ namespace Nitload.Graphics public class PlainGeometryMesh { - private readonly PlainGeometry _shape; - private readonly Vec2[] _textureCoordinates; + private readonly Vec2[] _textureCoordinates; private readonly Image _textureImage; + private readonly PlainGeometry _shape; + public PlainGeometryMesh( PlainGeometry shape, Image texture, - Vec2[] texCoord + Vec2[] texCoord ) { ArgumentNullException.ThrowIfNull(shape); @@ -40,14 +41,13 @@ namespace Nitload.Graphics _textureCoordinates = texCoord; } - public Vec4[] VertexArray => _shape.VertexStream; + public Vec3[] VertexArray => _shape.VertexStream; public Image TextureImage => _textureImage; public ReadOnlySpan MeshIndices => _shape.VertexIndices; - public ReadOnlySpan TextureCoord => MemoryMarshal.Cast( - _textureCoordinates); + public ReadOnlySpan> TextureCoord => _textureCoordinates; public PlainGeometryMesh Resize( PlainGeometry newShape diff --git a/src/Nitload.Graphics/RenderStream.cs b/src/Nitload.Graphics/RenderStream.cs index 5aec9383..ae515e43 100644 --- a/src/Nitload.Graphics/RenderStream.cs +++ b/src/Nitload.Graphics/RenderStream.cs @@ -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 offset, (float frontDepth, float backDepth) viewDepth ); protected abstract void NativeCameraViewTarget( - Vec3 position, - Vec3 direction, - Vec3 up + Vec3 position, + Vec3 direction, + Vec3 up ); #endregion diff --git a/src/Nitload.Mathematics/Numeric/Graphics/GMatrix44_T.cs b/src/Nitload.Mathematics/Numeric/Graphics/GMatrix44_T.cs index 4824e1e2..9db36e62 100644 --- a/src/Nitload.Mathematics/Numeric/Graphics/GMatrix44_T.cs +++ b/src/Nitload.Mathematics/Numeric/Graphics/GMatrix44_T.cs @@ -24,12 +24,11 @@ namespace Nitload.Mathematics.Numeric.Graphics private Buffer _buffer; - public Vec4 this[ - int x + internal unsafe ref Vec4 this[ + int x ] { - readonly get => _buffer[x]; - set => _buffer[x] = value; + get => ref Unsafe.Add(ref Unsafe.As, Vec4>(ref this), x); } public T this[ @@ -178,6 +177,75 @@ namespace Nitload.Mathematics.Numeric.Graphics } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static GMatrix44 operator -( + GMatrix44 left, + GMatrix44 right + ) + { + Unsafe.SkipInit(out GMatrix44 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 operator *( + GMatrix44 left, + T right + ) + { + Unsafe.SkipInit(out GMatrix44 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 operator *( + T left, + GMatrix44 right + ) + { + return right * left; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static GMatrix44 operator *( + GMatrix44 left, + GMatrix44 right + ) + { + Unsafe.SkipInit(out GMatrix44 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 operator +( + GMatrix44 left, + GMatrix44 right + ) + { + Unsafe.SkipInit(out GMatrix44 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 { diff --git a/src/Nitload.Native/CMakeLists.txt b/src/Nitload.Native/CMakeLists.txt index 223b0059..aeea2cc5 100644 --- a/src/Nitload.Native/CMakeLists.txt +++ b/src/Nitload.Native/CMakeLists.txt @@ -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/) 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) diff --git a/src/Nitload.Native/Nitload.Native.vcxproj b/src/Nitload.Native/Nitload.Native.vcxproj index ec4f2119..abe5b157 100644 --- a/src/Nitload.Native/Nitload.Native.vcxproj +++ b/src/Nitload.Native/Nitload.Native.vcxproj @@ -85,9 +85,10 @@ <_ArchTag Condition="'$(Platform)'=='x64'">x64 <_ArchTag Condition="'$(Platform)'=='ARM64'">arm64 - - $(SolutionDir)cmake_build\win_$(_ArchTag)\ - $(SolutionDir)obj\$(ProjectName)\$(Configuration)\$(Platform)\ + + Nitload.Native + $(NativeBuildDir) + $(SolutionRoot)obj\$(ProjectName)\$(Configuration)\$(Platform)\ true diff --git a/src/Nitload.Native/README.md b/src/Nitload.Native/README.md index 1b19e6fe..cd6e5b1f 100644 --- a/src/Nitload.Native/README.md +++ b/src/Nitload.Native/README.md @@ -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: diff --git a/src/Nitload.RegularAnalysis/Text/TextRegexSourceGen.GenClass.cs b/src/Nitload.RegularAnalysis/Text/TextRegexSourceGen.GenClass.cs index 26da0a18..39047e19 100644 --- a/src/Nitload.RegularAnalysis/Text/TextRegexSourceGen.GenClass.cs +++ b/src/Nitload.RegularAnalysis/Text/TextRegexSourceGen.GenClass.cs @@ -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(); - 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 sc, in List> 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(); - IReadOnlyList 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 sc, in List> 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 sc, in List> 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 diff --git a/src/Nitload.RegularAnalysis/Text/TextRegexSourceGen.GenSpecial.cs b/src/Nitload.RegularAnalysis/Text/TextRegexSourceGen.GenSpecial.cs index 1fbeb591..9deefd61 100644 --- a/src/Nitload.RegularAnalysis/Text/TextRegexSourceGen.GenSpecial.cs +++ b/src/Nitload.RegularAnalysis/Text/TextRegexSourceGen.GenSpecial.cs @@ -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 sc, + in List> 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 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 sc, + in SourceGenContext context, + in ExitLabel success + ) + { + RegexCharSetPayload payload = node.PayloadAs(); + _ = 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 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 sc, + in List> 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 { // (?.*)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 - } } diff --git a/src/README.md b/src/README.md index c18375f8..2960c206 100644 --- a/src/README.md +++ b/src/README.md @@ -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. diff --git a/test/README.md b/test/README.md index fa1aa32c..167dcd3e 100644 --- a/test/README.md +++ b/test/README.md @@ -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. diff --git a/test/StgSharpDebug/Program.cs b/test/StgSharpDebug/Program.cs index 0160e0e6..0ff354d0 100644 --- a/test/StgSharpDebug/Program.cs +++ b/test/StgSharpDebug/Program.cs @@ -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() .InitializeModule(); - _ = SlabAllocator.FromBuffer( - (nuint)NativeMemory.AlignedAlloc(65536 * 4, 4), - 65536, - buffer => NativeMemory.AlignedFree((void*)buffer) - ); - - TestRegexAnalyse.Run(); + // TestRegexAnalyse.Run(); + _ = TestRegexGenerate.Run("abc|123"); // TestHlsf(); diff --git a/test/StgSharpDebug/StgSharpDebug.csproj b/test/StgSharpDebug/StgSharpDebug.csproj index 3cd98ede..70048fe6 100644 --- a/test/StgSharpDebug/StgSharpDebug.csproj +++ b/test/StgSharpDebug/StgSharpDebug.csproj @@ -2,6 +2,10 @@ Exe + true + true + + true @@ -9,6 +13,11 @@ + + + + + @@ -18,4 +27,3 @@ - diff --git a/test/StgSharpDebug/TestRegexGenerate.cs b/test/StgSharpDebug/TestRegexGenerate.cs new file mode 100644 index 00000000..6a53a4a1 --- /dev/null +++ b/test/StgSharpDebug/TestRegexGenerate.cs @@ -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 body = new(); + List> 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 function in functions) + { + foreach (string line in function.ToList()) { + Console.WriteLine(line); + } + } + } + } +}