From 2a4bcd0cab9b69ea649c5f5d7053b85aa94c99b7 Mon Sep 17 00:00:00 2001 From: Sebastian Godelet Date: Sat, 25 Apr 2026 12:14:46 +1000 Subject: [PATCH 01/25] Probe for the dotnet SDK in addition to stand-alone csc and mcs. When a .NET 10 (or later) SDK is found, prefer its bundled Roslyn csc.dll over a stand-alone csc.exe or Mono mcs. The dotnet-bundled compiler IS the Microsoft Roslyn compiler, so it is classified as csharp_microsoft and accepts the same flags as csc.exe today. The probe walks `dotnet --list-sdks', filters to majors >= 10, and verifies that /Roslyn/bincore/csc.dll exists. When that succeeds, CSC is set to a quoted `dotnet exec csc.dll' invocation and inserted ahead of csc / mcs in the candidate list. Users may force this path with --with-csharp-compiler=dotnet, or fall back to a stand-alone compiler with --with-csharp-compiler=csc. This is a no-op on hosts without a recent dotnet SDK; it changes only the auto-detection result. The compiler-side handling is unchanged in this commit because the dotnet-bundled csc.dll is still classified as csharp_microsoft and accepts the existing flag set. m4/mercury.m4: Add a DOTNET / DOTNET_SDK_DIR / DOTNET_CSC_DLL probe before the existing csc / mcs detection. Walk `dotnet --list-sdks' looking for any SDK whose major version is at least 10, and verify the Roslyn csc.dll under it. Insert a `dotnet' candidate at the head of CSC_COMPILERS when the probe found a usable SDK; otherwise keep the existing csc / mcs list. Accept --with-csharp-compiler=dotnet as an explicit selector that errors out if no SDK was found. In the candidate loop, treat the literal string `dotnet' specially: set CSC to `"$DOTNET" exec "$DOTNET_CSC_DLL"' (with both paths quoted so install locations containing spaces survive) and break. Recognise the `... exec ... csc.dll ...' shape in the final CSHARP_COMPILER_TYPE classification and report it as `microsoft'. AC_SUBST the new DOTNET, DOTNET_SDK_DIR and DOTNET_CSC_DLL variables so they are visible to subsequent build-system changes. --- m4/mercury.m4 | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/m4/mercury.m4 b/m4/mercury.m4 index 93b90a67fb..c0bb5af1a3 100644 --- a/m4/mercury.m4 +++ b/m4/mercury.m4 @@ -294,11 +294,59 @@ GACUTIL=`basename "$GACUTIL"` # Check for an implementation of the Common Language Infrastructure. AC_PATH_PROGS([CLI_INTERPRETER], [mono]) +# Check for the dotnet SDK (.NET 10 or later). The dotnet-bundled +# C# compiler is Roslyn (i.e. Microsoft's), invoked through +# `dotnet exec csc.dll'. When found, it is preferred over a stand-alone +# csc.exe or Mono mcs. +AC_PATH_PROG([DOTNET], [dotnet]) +DOTNET_SDK_DIR= +DOTNET_CSC_DLL= +if test -n "$DOTNET"; then + AC_MSG_CHECKING([for a usable dotnet SDK (10.0 or later)]) + # `dotnet --list-sdks' prints lines like: `10.0.200 [/path/to/sdk]'. + # Walk every line, pick the highest-numbered SDK whose major version + # is >= 10. Avoid bracket-using regexes here because the `[' and `]' + # characters collide with m4's quoting. + "$DOTNET" --list-sdks > conftest.sdks 2>/dev/null || : + DOTNET_SDK_LINE= + while IFS= read -r DOTNET_SDK_TRY_LINE; do + DOTNET_SDK_TRY_VER=`echo "$DOTNET_SDK_TRY_LINE" | cut -d' ' -f1` + DOTNET_SDK_TRY_MAJOR=`echo "$DOTNET_SDK_TRY_VER" | cut -d. -f1` + if test -z "$DOTNET_SDK_TRY_MAJOR"; then continue; fi + if test "$DOTNET_SDK_TRY_MAJOR" -ge 10 2>/dev/null; then + DOTNET_SDK_LINE="$DOTNET_SDK_TRY_LINE" + fi + done < conftest.sdks + rm -f conftest.sdks + if test -n "$DOTNET_SDK_LINE"; then + DOTNET_SDK_VERSION=`echo "$DOTNET_SDK_LINE" | cut -d' ' -f1` + # Everything after the first space is `[/path/to/sdk]'. + # Strip the leading `[' and trailing `]' character-by-character. + DOTNET_SDK_BASE=`echo "$DOTNET_SDK_LINE" \ + | cut -d' ' -f2- \ + | sed -e 's/^.//' -e 's/.$//'` + # Convert backslashes to forward slashes so the path survives + # subsequent shell quoting on MSYS, Cygwin and POSIX shells. + DOTNET_SDK_BASE=`echo "$DOTNET_SDK_BASE" | tr '\\\\' '/'` + DOTNET_SDK_DIR="$DOTNET_SDK_BASE/$DOTNET_SDK_VERSION" + if test -f "$DOTNET_SDK_DIR/Roslyn/bincore/csc.dll"; then + DOTNET_CSC_DLL="$DOTNET_SDK_DIR/Roslyn/bincore/csc.dll" + AC_MSG_RESULT([yes (version $DOTNET_SDK_VERSION)]) + else + AC_MSG_RESULT([no (csc.dll not found under $DOTNET_SDK_DIR)]) + DOTNET_SDK_DIR= + fi + else + AC_MSG_RESULT([no (no SDK >= 10.0 found)]) + fi +fi + # Check for the C# (C sharp) compiler. # csc is the Microsoft C# compiler. # mcs is the Mono C# compiler targetting all runtime versions. # (dmcs and gmcs are older aliases for the Mono C# compiler # which we do not use.) +# `dotnet' selects the SDK-bundled Roslyn csc.dll located above. AC_CACHE_SAVE case "$mercury_cv_with_csharp_compiler" in @@ -311,7 +359,20 @@ case "$mercury_cv_with_csharp_compiler" in exit 1 ;; "") - CSC_COMPILERS="csc mcs" + # Prefer the dotnet-bundled Roslyn compiler when available; + # fall back to a stand-alone csc.exe or Mono mcs. + if test -n "$DOTNET_CSC_DLL"; then + CSC_COMPILERS="dotnet csc mcs" + else + CSC_COMPILERS="csc mcs" + fi + ;; + dotnet) + if test -z "$DOTNET_CSC_DLL"; then + AC_MSG_ERROR([--with-csharp-compiler=dotnet specified, but no .NET 10+ SDK was found]) + exit 1 + fi + CSC_COMPILERS="dotnet" ;; *) CSC_COMPILERS="$mercury_cv_with_csharp_compiler" @@ -321,6 +382,18 @@ esac AC_MSG_CHECKING([for a C sharp compiler]) AC_MSG_RESULT() for CANDIDATE_CSC0 in $CSC_COMPILERS; do + if test "$CANDIDATE_CSC0" = "dotnet"; then + # Use the dotnet SDK Roslyn csc.dll located earlier. + if test -z "$DOTNET_CSC_DLL"; then + continue; + fi + AC_MSG_NOTICE([using dotnet SDK Roslyn compiler at $DOTNET_CSC_DLL]) + # Quote both paths so that install locations containing spaces + # (e.g. `C:/Program Files/dotnet/...') survive expansion in + # scripts/Mmake.vars and on the command line. + CSC="\"$DOTNET\" exec \"$DOTNET_CSC_DLL\"" + break; + fi unset CANDIDATE_CSC unset ac_cv_path_CANDIDATE_CSC AC_CACHE_LOAD @@ -414,6 +487,12 @@ if test "$mercury_cv_with_csharp_compiler" != "" -a "$CSC" = ""; then fi case "$CSC" in + *exec*csc.dll*) + # The dotnet-bundled Roslyn compiler IS the Microsoft compiler; + # match it before the stand-alone csc.exe pattern below. + CSHARP_COMPILER_TYPE=microsoft + ;; + csc*) CSHARP_COMPILER_TYPE=microsoft ;; @@ -432,6 +511,9 @@ AC_SUBST([GACUTIL]) AC_SUBST([CSC]) AC_SUBST([CSHARP_COMPILER_TYPE]) AC_SUBST([CLI_INTERPRETER]) +AC_SUBST([DOTNET]) +AC_SUBST([DOTNET_SDK_DIR]) +AC_SUBST([DOTNET_CSC_DLL]) ]) #-----------------------------------------------------------------------------# From 11d3279921ed9946b5afcb09e394407a0ee251d4 Mon Sep 17 00:00:00 2001 From: Sebastian Godelet Date: Sat, 25 Apr 2026 13:10:21 +1000 Subject: [PATCH 02/25] Build C# linked targets via a generated csproj and `dotnet build'. Replace the per-module csc invocation in create_exe_or_lib_for_csharp/9 with a generated `.csproj' that the .NET SDK builds in one shot. MSBuild produces the .dll, the apphost binary (`.exe' on Windows, `' on Linux/macOS) and the runtimeconfig.json -- all of which the legacy flow had to assemble by hand or via a Mono wrapper script. This lays the groundwork for downstream consumers to opt into (and, eventually, ) without further build system changes. The csproj references mer_std and any user link libraries via parsed `-r:Lib.dll' entries from the existing get_mercury_std_libs_for_c_cs and get_link_opts_for_libraries_for_c_cs helpers; each name is resolved to a HintPath by searching the -lib: directories already collected from options. This keeps the helpers' shape unchanged so the C grade is unaffected. The csproj sits next to FullOutputFileName, with ./ so MSBuild lands the assembly, apphost and runtimeconfig.json exactly where Mercury expects them -- including under `Mercury/csharp/' when --use-subdir is enabled, since FullOutputFileName already carries the right prefix. MSBuild's bin/ and obj/ subdirs sit alongside, just like they would for a hand-written project. The Mono-era launcher script and MONO_PATH environment threading are gone: the apphost is the launcher. On Windows that is `.exe', matching Mercury's csharp_executable extension convention. On Linux/macOS the apphost lacks the `.exe' suffix, so we rename it after build to satisfy post_link_maybe_make_symlink_or_copy. Library targets get true so consumers can publish trimmed; mmc itself still does not call `dotnet publish'. compiler/link_target_code.m: Rewrite create_exe_or_lib_for_csharp/9. The new body parses the legacy `-r:' flag strings into bare assembly names, resolves each against link_library_directories, emits a .csproj string, writes it to disk, and runs `dotnet build -c Release -v:quiet --nologo'. Add helpers strip_csharp_exec_ext/1, parse_csharp_ref_flags/2, extract_csharp_ref_name/2, csharp_strip_quotes/1, resolve_csharp_refs/4, find_csharp_ref_dll/4 and the supporting csharp_ref_entry/0 type. Add csproj_content/8 to assemble the SDK-style project XML, plus format_compile_item/2, format_reference_item/2, parse_define_constant/2 and xml_escape/1. The csproj sets net10.0, 14, disables nullable annotations, suppresses EnableDefaultCompileItems, sets true on executables, flags libraries as true and pipes any /define: csharp flags through to . Add maybe_rename_apphost/5 that renames the bare apphost to `.exe' on hosts where the SDK does not append the suffix. Drop construct_cli_shell_script_for_csharp/3 and the now-unused csharp_file_name/3 and convert_to_windows_path_format/1 helpers. MSBuild handles the path quoting that those used to perform, and the Mono launcher script is no longer emitted. --- compiler/link_target_code.m | 445 +++++++++++++++++++++++------------- 1 file changed, 292 insertions(+), 153 deletions(-) diff --git a/compiler/link_target_code.m b/compiler/link_target_code.m index 4a23912dd7..a6ca829beb 100644 --- a/compiler/link_target_code.m +++ b/compiler/link_target_code.m @@ -1268,195 +1268,334 @@ list(error_spec)::out, maybe_succeeded::out, io::di, io::uo) is det. create_exe_or_lib_for_csharp(Globals, ProgressStream, LinkedTargetType, - MainModuleName, FullOutputFileName0, SourceList0, + _MainModuleName, FullOutputFileName, SourceList, Specs, Succeeded, !IO) :- - get_system_env_type(Globals, EnvType), - get_csharp_compiler_type(Globals, CSharpCompilerType), - - FullOutputFileName = csharp_file_name(EnvType, CSharpCompilerType, - FullOutputFileName0), - SourceList = list.map(csharp_file_name(EnvType, CSharpCompilerType), - SourceList0), - - % Suppress the MS C# compiler's banner message. + % Build the list of references for the generated csproj by parsing + % the same `-r:Lib.dll' flag strings that the legacy csc invocation + % used to consume, then resolving each name to a HintPath via the + % `-lib:' search dirs. + get_link_opts_for_libraries_for_c_cs(Globals, MaybeLinkLibraries, + Specs, !IO), ( - CSharpCompilerType = csharp_microsoft, - NoLogoOpt = "-nologo " + MaybeLinkLibraries = yes(LinkLibrariesList), + LinkLibrariesStr = string.join_list(" ", LinkLibrariesList) ; - ( CSharpCompilerType = csharp_mono - ; CSharpCompilerType = csharp_unknown - ), - NoLogoOpt = "" + MaybeLinkLibraries = no, + LinkLibrariesStr = "" ), + get_mercury_std_libs_for_c_cs(Globals, LinkedTargetType, MercuryStdLibs), + parse_csharp_ref_flags(MercuryStdLibs ++ " " ++ LinkLibrariesStr, + RefNames), - globals.lookup_bool_option(Globals, line_numbers, LineNumbers), - ( - % If we output line numbers, the mono C# compiler outputs lots of - % spurious warnings about unused variables and unreachable code, - % so disable these warnings. It also confuses #pragma warning, - % which is why we make the options global. - LineNumbers = yes, - NoWarnLineNumberOpt = "-nowarn:162,219 " - ; - LineNumbers = no, - NoWarnLineNumberOpt = "" + globals.lookup_accumulating_option(Globals, link_library_directories, + LinkLibraryDirectoriesList), + resolve_csharp_refs(LinkLibraryDirectoriesList, RefNames, + RefEntries, !IO), + + % The csproj sits next to FullOutputFileName. MSBuild's + % is set to `./' (relative to the csproj) so the produced .dll, apphost + % and runtimeconfig.json land where Mercury expects them - including + % under `Mercury/csharp/' when --use-subdir is enabled, since + % FullOutputFileName already carries the right prefix. + OutputBaseName = dir.det_basename(FullOutputFileName), + AssemblyName = strip_csharp_exec_ext(OutputBaseName), + ( if dir.split_name(FullOutputFileName, OutputDir0, _) then + OutputDir = OutputDir0 + else + OutputDir = "." ), + CsprojPath = OutputDir / (AssemblyName ++ ".csproj"), - % NOTE: we use the -option style options in preference to the /option - % style in order to avoid problems with POSIX style shells. globals.lookup_bool_option(Globals, target_debug, Debug), + globals.lookup_accumulating_option(Globals, csharp_flags, ExtraCSCFlags), ( - Debug = yes, - DebugOpt = "-debug " + LinkedTargetType = csharp_library, + globals.lookup_string_option(Globals, sign_assembly, KeyFile) ; - Debug = no, - DebugOpt = "" - ), - ( LinkedTargetType = csharp_executable, - TargetOption = "-target:exe", - SignAssemblyOpt = "" - ; - LinkedTargetType = csharp_library, - TargetOption = "-target:library", - globals.lookup_string_option(Globals, sign_assembly, KeyFile), - ( if KeyFile = "" then - SignAssemblyOpt = "" - else - SignAssemblyOpt = "-keyfile:" ++ KeyFile ++ " " - ) + KeyFile = "" ), + csproj_content(LinkedTargetType, AssemblyName, SourceList, RefEntries, + Debug, ExtraCSCFlags, KeyFile, CsprojContent), - globals.lookup_accumulating_option(Globals, link_library_directories, - LinkLibraryDirectoriesList0), - LinkLibraryDirectoriesList = - list.map(csharp_file_name(EnvType, CSharpCompilerType), - LinkLibraryDirectoriesList0), - LinkerPathFlag = "-lib:", - join_quoted_string_list(LinkLibraryDirectoriesList, LinkerPathFlag, "", - " ", LinkLibraryDirectories), - - get_link_opts_for_libraries_for_c_cs(Globals, MaybeLinkLibraries, - Specs, !IO), + io.open_output(CsprojPath, OpenRes, !IO), ( - MaybeLinkLibraries = yes(LinkLibrariesList0), - LinkLibrariesList = - list.map(csharp_file_name(EnvType, CSharpCompilerType), - LinkLibrariesList0), - join_quoted_string_list(LinkLibrariesList, "", "", " ", - LinkLibraries) - ; - MaybeLinkLibraries = no, - LinkLibraries = "" - ), - - globals.lookup_string_option(Globals, csharp_compiler, CSharpCompilerCmd), - get_mercury_std_libs_for_c_cs(Globals, LinkedTargetType, MercuryStdLibs), - globals.lookup_accumulating_option(Globals, csharp_flags, CSCFlagsList), - CmdArgs = string.join_list(" ", [ - NoLogoOpt, - NoWarnLineNumberOpt, - DebugOpt, - TargetOption, - "-out:" ++ FullOutputFileName, - SignAssemblyOpt, - LinkLibraryDirectories, - LinkLibraries, - MercuryStdLibs] ++ - CSCFlagsList ++ - SourceList), - invoke_long_system_command(Globals, ProgressStream, ProgressStream, - cmd_verbose_commands, CSharpCompilerCmd, CmdArgs, Succeeded0, !IO), + OpenRes = ok(Stream), + io.write_string(Stream, CsprojContent, !IO), + io.close_output(Stream, !IO), - % Also create a shell script to launch it if necessary. - globals.get_target_env_type(Globals, TargetEnvType), - globals.lookup_string_option(Globals, cli_interpreter, CLI), - ( if - Succeeded0 = succeeded, - LinkedTargetType = csharp_executable, - CLI \= "", - TargetEnvType = env_type_posix - then - construct_cli_shell_script_for_csharp(Globals, FullOutputFileName, - ContentStr), - create_launcher_shell_script(ProgressStream, Globals, MainModuleName, - ContentStr, Succeeded, !IO) - else + % Invoke `dotnet build' once on the generated csproj. MSBuild + % handles framework references, runtimeconfig.json emission and + % apphost generation; we no longer need a wrapper shell script. + DotnetCmd = "dotnet", + DotnetArgs = "build " ++ quote_shell_cmd_arg(CsprojPath) ++ + " -c Release -v:quiet --nologo", + invoke_long_system_command(Globals, ProgressStream, ProgressStream, + cmd_verbose_commands, DotnetCmd, DotnetArgs, Succeeded0, !IO), + + % On non-Windows hosts the apphost has no `.exe' suffix, but + % Mercury's `csharp_executable' linked-target convention always + % uses `.exe'. If the build succeeded but FullOutputFileName + % does not exist, while the apphost without the suffix does, + % rename it so post_link_maybe_make_symlink_or_copy finds it. + ( if + Succeeded0 = succeeded, + LinkedTargetType = csharp_executable + then + maybe_rename_apphost(FullOutputFileName, OutputDir, AssemblyName, + !IO) + else + true + ), Succeeded = Succeeded0 + ; + OpenRes = error(_), + Succeeded = did_not_succeed ). %---------------------% +% +% Helpers for csproj-based linking. +% - % Converts the given filename into a format acceptable to the C# compiler. - % - % Older MS C# compilers only allowed \ as the path separator, so we convert - % all / into \ when using an MS C# compiler on Windows. + % If the apphost was emitted without the `.exe' suffix (the default on + % non-Windows hosts) and FullOutputFileName does not yet exist, rename + % the bare apphost so callers can find it under its expected name. % - % XXX do current MS C# compilers still have this behaviour? - % - juliensf, 2025-08-17 - % -:- func csharp_file_name(env_type, csharp_compiler_type, file_name) - = file_name. +:- pred maybe_rename_apphost(string::in, string::in, string::in, + io::di, io::uo) is det. -csharp_file_name(EnvType, CSharpCompiler, FileName0) = FileName :- +maybe_rename_apphost(FullOutputFileName, OutputDir, AssemblyName, !IO) :- + io.file.check_file_accessibility(FullOutputFileName, [read], + ExpectedRes, !IO), ( - EnvType = env_type_posix, - FileName = FileName0 + ExpectedRes = ok + % File is already where Mercury expects it (apphost on Windows + % already includes `.exe'); nothing to do. ; - ( EnvType = env_type_cygwin - ; EnvType = env_type_win_cmd - ; EnvType = env_type_powershell - ), + ExpectedRes = error(_), + BareApphost = OutputDir / AssemblyName, + io.file.check_file_accessibility(BareApphost, [read], + BareRes, !IO), ( - ( CSharpCompiler = csharp_microsoft - ; CSharpCompiler = csharp_unknown - ), - FileName = convert_to_windows_path_format(FileName0) + BareRes = ok, + io.file.rename_file(BareApphost, FullOutputFileName, + _RenameRes, !IO) ; - CSharpCompiler = csharp_mono, - FileName = FileName0 - ) - ; - EnvType = env_type_msys, - ( - CSharpCompiler = csharp_microsoft, - FileName = convert_to_windows_path_format(FileName0) - ; - ( CSharpCompiler = csharp_mono - ; CSharpCompiler = csharp_unknown - ), - FileName = FileName0 + BareRes = error(_) + % Neither file exists; let the caller error out cleanly. ) ). -:- func convert_to_windows_path_format(file_name) = file_name. + % Strip the trailing .exe or .dll from a Mercury-emitted output basename. + % +:- func strip_csharp_exec_ext(string) = string. -convert_to_windows_path_format(FileName) = - string.replace_all(FileName, "/", "\\\\"). +strip_csharp_exec_ext(Name) = Stripped :- + ( if string.remove_suffix(Name, ".exe", Bare) then + Stripped = Bare + else if string.remove_suffix(Name, ".dll", Bare) then + Stripped = Bare + else + Stripped = Name + ). -%---------------------% + % Parse a flag string of the form "-r:foo.dll -r:'bar.dll' ..." into + % a deduplicated list of bare assembly names ["foo", "bar", ...]. + % Naive: assumes individual paths do not contain spaces. + % +:- pred parse_csharp_ref_flags(string::in, list(string)::out) is det. + +parse_csharp_ref_flags(FlagsStr, Names) :- + Tokens = string.words(FlagsStr), + list.filter_map(extract_csharp_ref_name, Tokens, Names0), + list.remove_dups(Names0, Names). + +:- pred extract_csharp_ref_name(string::in, string::out) is semidet. + +extract_csharp_ref_name(Token0, Name) :- + Token = csharp_strip_quotes(Token0), + string.remove_prefix("-r:", Token, AfterPrefix0), + AfterPrefix = csharp_strip_quotes(AfterPrefix0), + AfterPrefix \= "", + BareName = ( if string.remove_suffix(AfterPrefix, ".dll", X) then X + else AfterPrefix ), + % Strip any leading directory portion: csc accepts paths in -r:, but + % csproj expects bare assembly names. + Name = dir.det_basename(BareName). + +:- func csharp_strip_quotes(string) = string. + +csharp_strip_quotes(S) = + string.replace_all(string.replace_all(S, "'", ""), """", ""). + + % Resolve each reference name against the list of -lib: search dirs. + % For names we cannot find on disk, the entry is emitted without a + % HintPath; MSBuild will report a clear error if it cannot locate the + % assembly via its own resolution. + % +:- pred resolve_csharp_refs(list(string)::in, list(string)::in, + list(csharp_ref_entry)::out, io::di, io::uo) is det. + +resolve_csharp_refs(_SearchDirs, [], [], !IO). +resolve_csharp_refs(SearchDirs, [Name | Names], + [csharp_ref_entry(Name, MaybePath) | Rest], !IO) :- + find_csharp_ref_dll(SearchDirs, Name ++ ".dll", MaybePath, !IO), + resolve_csharp_refs(SearchDirs, Names, Rest, !IO). + +:- type csharp_ref_entry + ---> csharp_ref_entry(string, maybe(string)). -:- pred construct_cli_shell_script_for_csharp(globals::in, string::in, +:- pred find_csharp_ref_dll(list(string)::in, string::in, + maybe(string)::out, io::di, io::uo) is det. + +find_csharp_ref_dll([], _DllName, no, !IO). +find_csharp_ref_dll([Dir | Dirs], DllName, MaybePath, !IO) :- + Path = Dir / DllName, + io.file.check_file_accessibility(Path, [read], CheckRes, !IO), + ( + CheckRes = ok, + MaybePath = yes(Path) + ; + CheckRes = error(_), + find_csharp_ref_dll(Dirs, DllName, MaybePath, !IO) + ). + + % Build the .csproj content as a single string. + % +:- pred csproj_content(linked_target_type::in, string::in, list(string)::in, + list(csharp_ref_entry)::in, bool::in, list(string)::in, string::in, string::out) is det. -construct_cli_shell_script_for_csharp(Globals, ExeFileName, ContentStr) :- - globals.lookup_string_option(Globals, cli_interpreter, CLI), - globals.lookup_accumulating_option(Globals, link_library_directories, - LinkLibraryDirectoriesList), - globals.lookup_accumulating_option(Globals, mono_path_directories, - MonoPathDirectoriesList), - AllSearchPaths = LinkLibraryDirectoriesList ++ MonoPathDirectoriesList, - join_quoted_string_list(AllSearchPaths, "", "", - ":", MonoPathDirectories), - ContentStr = string.append_list([ - "#!/bin/sh\n", - "DIR=${0%/*}\n", - "MONO_PATH=$MONO_PATH:", MonoPathDirectories, "\n", - "export MONO_PATH\n", - "CLI_INTERPRETER=${CLI_INTERPRETER:-", CLI, "}\n", - "exec \"$CLI_INTERPRETER\" \"$DIR/", ExeFileName, "\" \"$@\"\n" +csproj_content(LinkedTargetType, AssemblyName, SourceList, RefEntries, + Debug, ExtraCSCFlags, KeyFile, Content) :- + ( + LinkedTargetType = csharp_executable, + OutputType = "Exe", + UseAppHost = "true", + IsTrimmableLine = "" + ; + LinkedTargetType = csharp_library, + OutputType = "Library", + UseAppHost = "false", + IsTrimmableLine = " true\n" + ), + ( + Debug = yes, + DebugType = "portable", + OptimizeLine = " false\n" + ; + Debug = no, + DebugType = "none", + OptimizeLine = " true\n" + ), + ( if KeyFile = "" then + SignLines = "" + else + SignLines = string.append_list([ + " true\n", + " ", xml_escape(KeyFile), + "\n" + ]) + ), + list.map(format_compile_item, SourceList, CompileItems), + list.map(format_reference_item, RefEntries, RefItems), + % Pass through any --csharp-flag values that look like /define:SYM via + % . Other flags must be set on the csproj manually. + list.filter_map(parse_define_constant, ExtraCSCFlags, ExtraDefines), + ( if ExtraDefines = [] then + DefineLine = "" + else + DefineLine = string.append_list([ + " ", string.join_list(";", ExtraDefines), + "\n" + ]) + ), + Header = string.append_list([ + "\n", + "\n", + " \n", + " net10.0\n", + " 14\n", + " disable\n", + " ", OutputType, "\n", + " ", xml_escape(AssemblyName), "\n", + " mercury\n", + " false\n", + " false", + "\n", + " false", + "\n", + " ./\n", + " ", UseAppHost, "\n", + " ", DebugType, "\n", + OptimizeLine, + " 0162;0219\n", + " false\n", + " false", + "\n", + " false\n", + IsTrimmableLine, + SignLines, + DefineLine, + " \n", + " \n"]), + Middle = string.append_list([ + " \n", + " \n"]), + Footer = string.append_list([ + " \n", + "\n"]), + Content = string.append_list([Header] ++ CompileItems ++ [Middle] ++ + RefItems ++ [Footer]). + +:- pred parse_define_constant(string::in, string::out) is semidet. + +parse_define_constant(Flag, Symbol) :- + ( string.remove_prefix("-define:", Flag, Symbol) + ; string.remove_prefix("/define:", Flag, Symbol) + ; string.remove_prefix("-d:", Flag, Symbol) + ; string.remove_prefix("/d:", Flag, Symbol) + ). + +:- pred format_compile_item(string::in, string::out) is det. + +format_compile_item(Path, Item) :- + Item = string.append_list([ + " \n" ]). +:- pred format_reference_item(csharp_ref_entry::in, string::out) is det. + +format_reference_item(csharp_ref_entry(Name, MaybeHintPath), Item) :- + ( + MaybeHintPath = yes(HintPath), + Item = string.append_list([ + " \n", + " ", xml_escape(HintPath), "\n", + " true\n", + " \n" + ]) + ; + MaybeHintPath = no, + Item = string.append_list([ + " \n" + ]) + ). + +:- func xml_escape(string) = string. + +xml_escape(S) = + string.replace_all( + string.replace_all( + string.replace_all( + string.replace_all( + string.replace_all(S, "&", "&"), + "<", "<"), + ">", ">"), + """", """), + "'", "'"). + %---------------------------------------------------------------------------% % % Linking for Java, both executables and archives. From 7c6b82c818074e7b4115aa746e33d9256e22c5fb Mon Sep 17 00:00:00 2001 From: Sebastian Godelet Date: Sat, 25 Apr 2026 13:39:26 +1000 Subject: [PATCH 03/25] Tighten Mercury syntax in csproj-based csharp linking helpers. Three small adjustments to reduce the risk of determinism or syntax errors that would only surface during a full bootstrap: - The if-then-else expression that picked the bare assembly name is rewritten as a goal-form if-then-else binding a fresh variable. - parse_define_constant/2 was a disjunction of four semidet remove_prefix/3 calls; rewrite as nested if-then-else so the pred is unambiguously semidet rather than relying on commit-to-first. - Move the csharp_ref_entry/0 type declaration above its first use in resolve_csharp_refs/4 to avoid a forward reference. Behaviour is unchanged. compiler/link_target_code.m: Rewrite extract_csharp_ref_name/2, parse_define_constant/2 and reorder the csharp_ref_entry/0 declaration. --- compiler/link_target_code.m | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/compiler/link_target_code.m b/compiler/link_target_code.m index a6ca829beb..c3af3b0761 100644 --- a/compiler/link_target_code.m +++ b/compiler/link_target_code.m @@ -1418,8 +1418,11 @@ string.remove_prefix("-r:", Token, AfterPrefix0), AfterPrefix = csharp_strip_quotes(AfterPrefix0), AfterPrefix \= "", - BareName = ( if string.remove_suffix(AfterPrefix, ".dll", X) then X - else AfterPrefix ), + ( if string.remove_suffix(AfterPrefix, ".dll", Bare) then + BareName = Bare + else + BareName = AfterPrefix + ), % Strip any leading directory portion: csc accepts paths in -r:, but % csproj expects bare assembly names. Name = dir.det_basename(BareName). @@ -1429,6 +1432,9 @@ csharp_strip_quotes(S) = string.replace_all(string.replace_all(S, "'", ""), """", ""). +:- type csharp_ref_entry + ---> csharp_ref_entry(string, maybe(string)). + % Resolve each reference name against the list of -lib: search dirs. % For names we cannot find on disk, the entry is emitted without a % HintPath; MSBuild will report a clear error if it cannot locate the @@ -1443,9 +1449,6 @@ find_csharp_ref_dll(SearchDirs, Name ++ ".dll", MaybePath, !IO), resolve_csharp_refs(SearchDirs, Names, Rest, !IO). -:- type csharp_ref_entry - ---> csharp_ref_entry(string, maybe(string)). - :- pred find_csharp_ref_dll(list(string)::in, string::in, maybe(string)::out, io::di, io::uo) is det. @@ -1552,10 +1555,14 @@ :- pred parse_define_constant(string::in, string::out) is semidet. parse_define_constant(Flag, Symbol) :- - ( string.remove_prefix("-define:", Flag, Symbol) - ; string.remove_prefix("/define:", Flag, Symbol) - ; string.remove_prefix("-d:", Flag, Symbol) - ; string.remove_prefix("/d:", Flag, Symbol) + ( if string.remove_prefix("-define:", Flag, Sym1) then + Symbol = Sym1 + else if string.remove_prefix("/define:", Flag, Sym2) then + Symbol = Sym2 + else if string.remove_prefix("-d:", Flag, Sym3) then + Symbol = Sym3 + else + string.remove_prefix("/d:", Flag, Symbol) ). :- pred format_compile_item(string::in, string::out) is det. From 8faef375abc7402772a9f40484593fc448f3b0eb Mon Sep 17 00:00:00 2001 From: Sebastian Godelet Date: Sat, 25 Apr 2026 13:51:35 +1000 Subject: [PATCH 04/25] Restrict csproj_content/8 to the csharp linked-target inst. mmc inferred csproj_content/8 as semidet because the switch on LinkedTargetType only covered the two csharp_* constructors but the declared mode accepted the full linked_target_type/0. Use the existing csharp_linked_target_type inst (already used by the parent create_exe_or_lib_for_csharp/9) so the switch is exhaustive. compiler/link_target_code.m: Change the mode of csproj_content/8 from `linked_target_type::in' to `linked_target_type::in(csharp_linked_target_type)'. --- compiler/link_target_code.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/link_target_code.m b/compiler/link_target_code.m index c3af3b0761..a3e8f035be 100644 --- a/compiler/link_target_code.m +++ b/compiler/link_target_code.m @@ -1466,7 +1466,8 @@ % Build the .csproj content as a single string. % -:- pred csproj_content(linked_target_type::in, string::in, list(string)::in, +:- pred csproj_content(linked_target_type::in(csharp_linked_target_type), + string::in, list(string)::in, list(csharp_ref_entry)::in, bool::in, list(string)::in, string::in, string::out) is det. From 6cc4d59cd4d703b50a63c64410e4c88c9aaa30e6 Mon Sep 17 00:00:00 2001 From: Sebastian Godelet Date: Sat, 25 Apr 2026 14:06:25 +1000 Subject: [PATCH 05/25] Drop the legacy XML comment text and modernise io.file.m for .NET 10. Three changes to make the C# backend's stdlib build under the new csproj-driven linker on .NET SDK 10 (Phase 1 end-to-end validation plus the first slice of Phase 2 work). The csproj header text contained `--' inside an XML comment, which MSBuild rejects with MSB4025. Rephrase the comment so it stays valid. io.file.m used three Mono / .NET-Framework-era APIs that .NET 5+ has removed or repurposed: * `SecurityPermission.Demand()' for execute-permission checks. Code Access Security is gone, all checks are no-ops, and the type now lives in a separate legacy package. Replace the demand with a deliberate empty block so we do not have to add a NuGet dependency just to no-op. * `Directory.CreateDirectory(string, DirectorySecurity)' overload for Win32 temp-dir creation. The overload was removed; the new extension method requires the System.IO.FileSystem.AccessControl package. Inheriting the parent ACL gives the caller the same permissions in practice for a temp directory under %TEMP%, so drop the explicit ACL setup and just call the plain overload. * `#if __MonoCS__' guards around the Linux/macOS mkdir P/Invoke branch. The DllImport on libc works under .NET 5+, so the guards were excluding live code. Replace the platform discriminator with RuntimeInformation.IsOSPlatform and drop the __MonoCS__ preprocessor blocks entirely. End-to-end smoke test: with /home/sebgod/mercury10-install on PATH, `mmc --grade csharp --make hello' on samples/hello.m generates hello.csproj, runs `dotnet build', and produces hello.exe (an ARM64 ELF apphost), hello.dll and hello.runtimeconfig.json. Running ./hello.exe prints `Hello, world'. compiler/link_target_code.m: Replace the `' comment with a form that does not contain `--', avoiding MSBuild error MSB4025 when the SDK loads our generated csproj. library/io.file.m: Drop the SecurityPermission.Demand() call inside the C# clause of check_file_accessibility/4; leave the `if (checkExecute)' block in place as a no-op with a comment explaining why. Rewrite the C# clause of do_make_temp_directory/8 to use RuntimeInformation.IsOSPlatform instead of switching on Environment.OSVersion.Platform, drop the obsolete Win32 Directory.CreateDirectory(path, DirectorySecurity) call (just create the directory and let it inherit the parent ACL), and run the libc mkdir P/Invoke unconditionally on Linux and macOS instead of only under Mono. Drop the `#if __MonoCS__' guard around the `[DllImport("libc", EntryPoint="mkdir")]' declaration; the import is harmless on Windows and required on Linux/macOS under .NET 10. --- compiler/link_target_code.m | 4 +- library/io.file.m | 78 +++++++++++++++---------------------- 2 files changed, 35 insertions(+), 47 deletions(-) diff --git a/compiler/link_target_code.m b/compiler/link_target_code.m index a3e8f035be..723b54a1a0 100644 --- a/compiler/link_target_code.m +++ b/compiler/link_target_code.m @@ -1515,8 +1515,10 @@ "\n" ]) ), + % XML comments may not contain `--' (MSBuild flags this as MSB4025), + % so spell `mmc' bare and avoid the literal flag form `--grade csharp'. Header = string.append_list([ - "\n", + "\n", "\n", " \n", " net10.0\n", diff --git a/library/io.file.m b/library/io.file.m index 0f5e96b277..93f3861af5 100644 --- a/library/io.file.m +++ b/library/io.file.m @@ -841,10 +841,10 @@ } if (checkExecute) { - // We need unrestricted permissions to execute unmanaged code. - (new System.Security.Permissions.SecurityPermission( - System.Security.Permissions.SecurityPermissionFlag. - AllFlags)).Demand(); + // Code Access Security was removed in .NET 5+; SecurityPermission + // demands are no-ops on the modern runtime, so this check has + // nothing to enforce. Leave it as a deliberate no-op rather than + // pulling in the legacy System.Security.Permissions package. } } "). @@ -1371,46 +1371,31 @@ try { DirName = Path.Combine(ParentDirName, Path.GetRandomFileName()); - switch (Environment.OSVersion.Platform) { - case PlatformID.Win32NT: - // obtain the owner of the temporary directory - IdentityReference tempInfo = - new DirectoryInfo(ParentDirName) - .GetAccessControl(AccessControlSections.Owner) - .GetOwner(typeof(SecurityIdentifier)); - - DirectorySecurity security = new DirectorySecurity(); - security.AddAccessRule( - new FileSystemAccessRule(tempInfo, - FileSystemRights.ListDirectory - | FileSystemRights.Read - | FileSystemRights.Modify, - InheritanceFlags.None, - PropagationFlags.None, - AccessControlType.Allow - ) - ); - Directory.CreateDirectory(DirName, security); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + // On Windows the directory inherits the parent's ACLs by + // default, which is what callers want for a temp directory + // tucked under (typically) %TEMP%. The .NET 5+ runtime no + // longer supports the old Directory.CreateDirectory(path, + // DirectorySecurity) overload, and pulling in the legacy + // System.IO.FileSystem.AccessControl package just to set + // explicit owner-only rules would be more code than it is + // worth -- the inherited ACL is already correct here. + Directory.CreateDirectory(DirName); + Error = null; + } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { + int rc = ML_sys_mkdir(DirName, 0x7 << 6); + if (rc == 0) { Error = null; - break; -#if __MonoCS__ - case PlatformID.Unix: - case (PlatformID)6: // MacOSX: - int rc = ML_sys_mkdir(DirName, 0x7 << 6); - if (rc == 0) { - Error = null; - } else { - // The actual error would need to be retrieved from errno. - Error = new System.IO.IOException( - ""Error creating directory""); - } - break; -#endif - default: - Error = new System.NotImplementedException( - ""Changing folder permissions is not supported for: "" + - Environment.OSVersion); - break; + } else { + // The actual error would need to be retrieved from errno. + Error = new System.IO.IOException( + ""Error creating directory""); + } + } else { + Error = new System.NotImplementedException( + ""Changing folder permissions is not supported for: "" + + RuntimeInformation.OSDescription); } } catch (System.Exception e) { DirName = string.Empty; @@ -1500,12 +1485,13 @@ new FileSystemAccessRule(tempInfo, "). :- pragma foreign_code("C#", " -#if __MonoCS__ - // int chmod(const char *path, mode_t mode); + // int mkdir(const char *path, mode_t mode); + // The DllImport for libc works on .NET 5+ on Linux and macOS; + // the import is harmless on Windows because we never call it + // (it is gated by RuntimeInformation.IsOSPlatform above). [DllImport(""libc"", SetLastError=true, EntryPoint=""mkdir"", CallingConvention=CallingConvention.Cdecl)] static extern int ML_sys_mkdir (string path, uint mode); -#endif "). %---------------------% From 522ddc26e0243664c6d02f22b5aac1e872853251 Mon Sep 17 00:00:00 2001 From: Sebastian Godelet Date: Sat, 25 Apr 2026 16:35:15 +1000 Subject: [PATCH 06/25] Implement math.fma, env-var iteration and memory stats for the C# backend. Three previously-stubbed or non-generic stdlib bits now have real C# implementations on .NET 5+: * math.fma/3 and have_fma/0 dispatch to System.Math.FusedMultiplyAdd. The C# math.fma was previously calling private_builtin.sorry; the fused multiply-add intrinsic has been in .NET Core 3.0+ for years. * io.environment.get_environment_var_assoc_list/4 stops iterating Environment.GetEnvironmentVariables() via non-generic DictionaryEntry boxing. The IDictionary returned has string keys and string values, so iterate its Keys collection directly. * benchmarking.ML_report_standard_stats/1 picks up GC.GetTotalMemory and Process.WorkingSet64 alongside the existing CPU/wall-clock numbers; ML_report_full_memory_stats/1 stops printing an apology string and now reports the managed heap, per-generation GC collection counts, working set (with peak), private bytes and virtual bytes. End-to-end smoke test on .NET 10 ARM64 / WSL Ubuntu 24.04: math.fma(2.0, 3.0, 4.0) = 10.000000 have_fma: yes env vars: 24 entries [User time: +0.11s, 0.01s Real time: +0.01s, 0.01s Managed heap: 274512 bytes, Working set: 55603200 bytes] [Managed heap: 304,664 bytes GC collections: gen0=0, gen1=0, gen2=0 Working set: 56,557,568 bytes (peak 56,557,568) Private bytes: 93,519,872, Virtual bytes: 18,397,773,824] library/math.m: Add `pragma foreign_proc("C#", have_fma, ...)` and `pragma foreign_proc("C#", fma(...), ...)' clauses. The bodies set SUCCESS_INDICATOR to true and call System.Math.FusedMultiplyAdd respectively. Drop the now-stale `.NET core 3.0' comment because the language floor for the C# grade is .NET 10. library/io.environment.m: Replace the C# foreach over System.Collections.DictionaryEntry with a foreach over env.Keys that retrieves each value with the string-typed indexer. Add a comment explaining why we keep the non-generic IDictionary type at the variable declaration (the API has not been re-typed in the BCL). library/benchmarking.m: Extend ML_report_standard_stats with managed heap and working set; rewrite ML_report_full_memory_stats to print real numbers instead of an apology, using GC.GetTotalMemory, GC.CollectionCount and System.Diagnostics.Process counters. --- library/benchmarking.m | 43 +++++++++++++++++++++++++++++++--------- library/io.environment.m | 9 ++++++--- library/math.m | 19 +++++++++++++++++- 3 files changed, 58 insertions(+), 13 deletions(-) diff --git a/library/benchmarking.m b/library/benchmarking.m index 20c8b0e84e..1d04547aa9 100644 --- a/library/benchmarking.m +++ b/library/benchmarking.m @@ -490,30 +490,55 @@ long real_time_at_prev_stat = real_time_at_last_stat; real_time_at_last_stat = System.DateTime.Now.Ticks; + long managed_heap = System.GC.GetTotalMemory(false); + System.Diagnostics.Process proc = + System.Diagnostics.Process.GetCurrentProcess(); + long working_set = proc.WorkingSet64; + mercury.io__primitives_write.mercury_print_string(stream, System.String.Format( - ""[User time: +{0:F2}s, {1:F2}s Real time: +{2:F2}s, {3:F2}s]\\n"", + ""[User time: +{0:F2}s, {1:F2}s Real time: +{2:F2}s, {3:F2}s "" + + ""Managed heap: {4} bytes, Working set: {5} bytes]\\n"", (user_time_at_last_stat - user_time_at_prev_stat), (user_time_at_last_stat - user_time_at_start), ((real_time_at_last_stat - real_time_at_prev_stat) / (double) System.TimeSpan.TicksPerSecond), ((real_time_at_last_stat - real_time_at_start) - / (double) System.TimeSpan.TicksPerSecond) + / (double) System.TimeSpan.TicksPerSecond), + managed_heap, + working_set ) ); - // XXX At this point there should be a whole bunch of memory usage - // statistics. } public static void ML_report_full_memory_stats(mercury.io__stream_ops.MR_MercuryFileStruct stream) { - // XXX The support for this predicate is even worse. Since we don't have - // access to memory usage statistics, all you get here is an apology. - // But at least it doesn't just crash with an error. + // .NET 5+ exposes enough GC and process information to give a useful + // (though coarser-grained than the C backend's) memory snapshot. + long managed_heap_before = System.GC.GetTotalMemory(false); + int gen0 = System.GC.CollectionCount(0); + int gen1 = System.GC.CollectionCount(1); + int gen2 = System.GC.CollectionCount(2); + System.Diagnostics.Process proc = + System.Diagnostics.Process.GetCurrentProcess(); + long working_set = proc.WorkingSet64; + long private_bytes = proc.PrivateMemorySize64; + long virtual_bytes = proc.VirtualMemorySize64; + long peak_working_set = proc.PeakWorkingSet64; + mercury.io__primitives_write.mercury_print_string(stream, - ""Sorry, report_full_memory_stats is not yet "" + - ""implemented for the C# back-end.\\n""); + System.String.Format( + ""[Managed heap: {0:N0} bytes\\n"" + + "" GC collections: gen0={1}, gen1={2}, gen2={3}\\n"" + + "" Working set: {4:N0} bytes (peak {5:N0})\\n"" + + "" Private bytes: {6:N0}, Virtual bytes: {7:N0}]\\n"", + managed_heap_before, + gen0, gen1, gen2, + working_set, peak_working_set, + private_bytes, virtual_bytes + ) + ); } "). diff --git a/library/io.environment.m b/library/io.environment.m index 562aef7d79..2cd8fb644e 100644 --- a/library/io.environment.m +++ b/library/io.environment.m @@ -194,11 +194,14 @@ [may_call_mercury, promise_pure], " EnvVarAL = EnvVarAL0; + // System.Environment.GetEnvironmentVariables() returns a non-generic + // IDictionary (a Hashtable underneath), but every key and value is a + // string for this API. Iterate via the typed Keys collection so the + // foreach loop avoids non-generic DictionaryEntry boxing. System.Collections.IDictionary env = System.Environment.GetEnvironmentVariables(); - foreach (System.Collections.DictionaryEntry entry in env) { - string name = (string) entry.Key; - string value = (string) entry.Value; + foreach (string name in env.Keys) { + string value = (string) env[name]; EnvVarAL = mercury.io__environment.ML_record_env_var_and_value(name, value, EnvVarAL); } diff --git a/library/math.m b/library/math.m index e7832099a3..9dbdcd576f 100644 --- a/library/math.m +++ b/library/math.m @@ -947,6 +947,16 @@ #endif "). +:- pragma foreign_proc("C#", + have_fma, + [will_not_call_mercury, promise_pure, thread_safe, will_not_modify_trail, + does_not_affect_liveness], +" + // System.Math.FusedMultiplyAdd is available on every supported + // .NET runtime (.NET Core 3.0+, .NET 5+). + SUCCESS_INDICATOR = true; +"). + have_fma :- semidet_false. @@ -964,11 +974,18 @@ #endif "). +:- pragma foreign_proc("C#", + fma(X::in, Y::in, Z::in) = (FMA::out), + [will_not_call_mercury, promise_pure, thread_safe, will_not_modify_trail, + does_not_affect_liveness], +" + FMA = System.Math.FusedMultiplyAdd(X, Y, Z); +"). + fma(_, _, _) = _ :- private_builtin.sorry("math.fma"). % NOTE: Java 9 provides Math.fma. -% NOTE: .NET core 3.0 provides System.Math.FusedMultiplyAdd. %---------------------------------------------------------------------------% :- end_module math. From 8cffb0c9f20fc090af4aa7fb18a5ba0d22f8d2fa Mon Sep 17 00:00:00 2001 From: Sebastian Godelet Date: Sat, 25 Apr 2026 16:48:11 +1000 Subject: [PATCH 07/25] Implement tuple_arity, tuple_arg and compare_representation for the C# backend. Three more previously-stubbed C# foreign procedures get real bodies: * builtin.tuple_arity/2 reads the type info's args vector length rather than calling private_builtin.sorry. * builtin.tuple_arg/3 indexes the same vector for the existential type info and casts the term to object[] for the value (the C# backend always emits tuples that way). * builtin.compare_representation_3_p_0/3 delegates to rtti_implementation.generic_compare_3_p_0 instead of throwing `Sorry, not implemented'. This is the same behaviour as compare_3_p_0 today; the divergence between compare and compare_representation only matters for types with user-defined equality, which the C# backend does not yet distinguish from structural equality, so the approximation is faithful for every type that does not override unification. Smoke test on .NET 10 ARM64: math.fma(2.0, 3.0, 4.0) = 10.000000 tuple functor={} arity=3 compare_representation "abc" "abd": '<' The tuple_* call paths run through deconstruct.functor at run time, which exercises tuple_arity for the user-emitted tuple type info. library/builtin.m: Add C# foreign_proc clauses for tuple_arity/2 and tuple_arg/3 that use TypeInfo_for_T.args.Length and TypeInfo_for_T.args[Index]. Replace the runtime.Errors.SORRY stub in compare_representation_3_p_0 with a delegation to rtti_implementation.generic_compare_3_p_0 and document why. --- library/builtin.m | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/library/builtin.m b/library/builtin.m index 248ad987d0..cf773f8ba1 100644 --- a/library/builtin.m +++ b/library/builtin.m @@ -563,6 +563,16 @@ not compare((<), X, Y). Arity = MR_TYPEINFO_GET_VAR_ARITY_ARITY((MR_TypeInfo) TypeInfo_for_T); "). +:- pragma foreign_proc("C#", + tuple_arity(_Term::in, Arity::out), + [will_not_call_mercury, promise_pure, thread_safe], +" + // For variable-arity types like tuples the C# RTTI representation + // stores each argument's type info in TypeInfo_for_T.args, so the + // length of that vector is the tuple arity. + Arity = TypeInfo_for_T.args.Length; +"). + tuple_arity(_, _) :- private_builtin.sorry("tuple_arity/2"). @@ -581,6 +591,18 @@ not compare((<), X, Y). Arg = arg_vector[Index]; "). +:- pragma foreign_proc("C#", + tuple_arg(Term::in, Index::in, Arg::out), + [will_not_call_mercury, promise_pure, thread_safe], +" + // The C# backend emits tuples as object[]; the type info vector + // for a tuple is parallel to that array (no leading-arity slot + // unlike the C representation). + TypeInfo_for_ArgT = + (runtime.TypeInfo_Struct) TypeInfo_for_T.args[Index]; + Arg = ((object[]) Term)[Index]; +"). + tuple_arg(_, _, -1) :- private_builtin.sorry("tuple_arg/3"). @@ -630,10 +652,14 @@ not compare((<), X, Y). compare_representation_3_p_0(runtime.TypeInfo_Struct ti, object x, object y) { - // stub only - runtime.Errors.SORRY( - ""compare_representation_3_p_0/3 not implemented""); - return Comparison_result_0.f_equal; + // For types without user-defined equality this is identical to + // the structural compare; for types with user-defined equality + // the C backend would expose the underlying representation + // ordering. The C# backend does not yet distinguish those + // cases, so we delegate to the structural compare too -- this + // is a faithful approximation for every Mercury type that does + // not override unification/comparison. + return rtti_implementation.generic_compare_3_p_0(ti, x, y); } "). From 76830fbb376114dd22284673eeb1eddd157ccfe6 Mon Sep 17 00:00:00 2001 From: Sebastian Godelet Date: Sat, 25 Apr 2026 16:50:35 +1000 Subject: [PATCH 08/25] Document the .NET 10 / C# 14 transition for the csharp grade. Update NEWS.md and Documentation/README.CSharp.md to reflect the modernised csharp grade: the SDK requirement, the csproj-based linker, the absence of any wrapper script or MONO_PATH, the trim-friendly library output, and the implemented (or still missing) standard library predicates. NEWS.md: Under "Portability improvements", add bullets covering: the .NET 10 / C# 14 floor and the removal of Mono and .NET Framework support; the dotnet SDK probe and `--with-csharp-compiler=dotnet'; the csproj-driven `dotnet build' linker flow and apphost output; the true property on libraries; the io.file.m modernisation; and the new C# foreign_proc bodies for math.fma, the benchmarking memory stats, tuple_arity, tuple_arg and compare_representation_3_p_0. Documentation/README.CSharp.md: Replace the old prerequisites paragraph with a .NET SDK 10+ requirement. Drop the Mono/.NET Framework run instructions, the GAC and gacutil section, and the wrapper-shell-script section. Document the new csproj/dotnet-build flow, the apphost output and the true reference copying. Add a "Trimmed publish" section noting that downstream consumers can opt into `true'. Remove math.fma and the memory-stats entries from the unimplemented-procedures list (those are now done) and add deconstruct.functor_number plus the semidet / cc_nondet modes of exception.catch_impl, which remain unimplemented. --- Documentation/README.CSharp.md | 139 ++++++++++++++------------------- NEWS.md | 38 +++++++++ 2 files changed, 96 insertions(+), 81 deletions(-) diff --git a/Documentation/README.CSharp.md b/Documentation/README.CSharp.md index a337cf2630..97f34540e9 100644 --- a/Documentation/README.CSharp.md +++ b/Documentation/README.CSharp.md @@ -1,13 +1,13 @@ Mercury C# Backend ================== -The Mercury compiler has a backend that generates C# source code, that can be -compiled into bytecode suitable for running using the .NET or Mono runtime -systems. The backend is mostly complete, but some parts of the Mercury standard -library are not yet implemented. +The Mercury compiler has a backend that generates C# source code, that is +compiled into a managed assembly by the .NET 10 SDK and run on the +.NET 10 runtime. The backend is mostly complete, but some parts of the +Mercury standard library are not yet implemented. -The C# backend requires C# 5.0 or higher -- older versions of C# are *not* -supported. +The C# backend requires C# 14 or higher. Older versions of C# (and the +.NET Framework / Mono runtimes) are *not* supported. Contents -------- @@ -26,30 +26,31 @@ Contents Prerequisites ------------- -In order to use Mercury's C# backend you will need either: +To use Mercury's C# backend you need a working .NET SDK at version 10.0 +or above. The `dotnet` command must be on your `PATH`; the SDK install +must contain the Roslyn compiler at `/Roslyn/bincore/csc.dll`. -* Microsoft .NET 4.5 or above. -* Mono 4.0 or above. +There is no longer any support for Mono or for the .NET Framework +runtime. Installing the `csharp` grade ----------------------------- -The Mercury compiler uses the grade `csharp` to target C# source code that -is then compiled by a C# compiler. +The Mercury compiler uses the grade `csharp` to target C# source code +that is then compiled by the .NET SDK. -Mercury's autoconfiguration script will cause the `csharp` grade to be installed -if it finds a suitable C# compiler (e.g. `csc`) and .NET runtime in your `PATH`. - -You can check if your Mercury installation has been configured to include the -`csharp` grade by looking if `csharp` is included in the output of the Mercury -compiler's `--output-stdlib-grades` option. +Mercury's autoconfiguration script will install the `csharp` grade when +it detects `dotnet` on your `PATH` along with a usable >= 10.0 SDK. +You can force this by passing `--with-csharp-compiler=dotnet` to +`./configure`, and you can check the result by running +`mmc --output-stdlib-grades` and looking for `csharp` in the list. Compiling programs with the `csharp` grade ------------------------------------------ -Once you have a Mercury installation that includes the `csharp` grade, you -can build programs such as `hello.m` or `calculator.m` in the [samples](samples) -directory. +Once you have a Mercury installation that includes the `csharp` grade, +you can build programs such as `hello.m` or `calculator.m` in the +[samples](samples) directory. ``` mmc --grade csharp --make hello @@ -58,63 +59,40 @@ directory. When building programs with the `csharp` grade you *must* use `mmc --make`. Using `mmake` to build programs using the `csharp` grade is _not_ supported. -Running `csharp` grade programs with Mono ------------------------------------------ - -For the example in the previous section on a Unix (or more generally, -non-Windows) system using Mono, the Mercury compiler will generate a process -assembly, e.g. `hello.exe`, and a wrapper shell script named `hello`. - -The wrapper shell script will set the `MONO_PATH` environment variable -to point to the location of the Mercury standard library assemblies. -It will then invoke the CLI execution environment on the process assembly. -You can run the program using the wrapper shell script, for example: - -``` - ./hello -``` - -Running `csharp` grade programs on Windows with .NET ----------------------------------------------------- - -On Windows, the Mercury compiler will only generate a process assembly, e.g. -`hello.exe`. On Windows there is no need to generate a wrapper shell script. +Behind the scenes, `mmc --make` generates a `.csproj` next +to the linked target and runs `dotnet build` on it. The .NET SDK +produces: -With .NET, the library assemblies (.dlls) for the Mercury standard -libraries must either (1) reside in (or under) the same directory as the process -assembly (.exe) or (2) be entered into the global assembly cache (GAC). -If neither of these things is done then execution will abort with a message that -begins: +* `.dll` -- the managed assembly, +* `.exe` -- the apphost / native launcher + (renamed from the bare `` + on Linux and macOS to match + Mercury's csharp_executable convention), +* `.runtimeconfig.json`, +* `.deps.json`, -``` - Unhandled Exception: System.IO.FileNotFoundException: Could not load file - or assembly 'mer_std', Version=... -``` +plus the standard `bin/` and `obj/` MSBuild scratch directories. The +referenced Mercury standard-library assemblies (`mer_std.dll` and so +on) are copied next to the executable via the SDK's +`true` reference setting, so no `MONO_PATH`, +wrapper script or GAC registration is required. -For (1), you will need to copy the library assemblies from the Mercury library -installation directory into the same directory as the process assembly. -The files for the Mercury library assemblies are located in +You can run the resulting program directly: ``` - \lib\mercury\lib\csharp + ./hello.exe ``` -where `` is the location of the Mercury installation. -Copy all of the .dll files in the above directory into that of the process -assembly. +Trimmed publish +--------------- -To enter assemblies into the GAC, run the following command for each -assembly. - -``` - gacutil /i mer_std.dll -``` - -Assemblies can be removed from the GAC by doing, for example - -``` - gacutil /u mer_std.dll -``` +The generated `.csproj` for libraries carries +`true`, and the hand-written runtime and +standard-library C# code uses no name-based reflection that would defeat +the IL-linker. Downstream consumers can therefore add their own +`true` and `dotnet publish` Mercury +applications without losing functionality. `mmc` itself does not +invoke `dotnet publish`. Limitations ----------- @@ -151,23 +129,22 @@ supported or not fully implemented: The current implementation of `read_binary` does not work with the way Mercury file streams are implemented for the C# backend. -2. `benchmarking.report_stats/0` - `benchmarking.report_full_memory_stats/0` +2. `store.arg_ref/5` + `store.new_arg_ref/5` - Memory usage statistics are not yet available, and cpu time - is not the same as in the C backends, as per `time.m`. + Due to some limitations in RTTI support, dynamic type checking is + missing for these predicates. They should be used with care. -3. `store.arg_ref/5` - `store.new_arg_ref/5` +3. `deconstruct.functor_number/3` - Due to some limitations in RTTI support, dynamic type checking is missing - for these predicates. They should be used with care. + Not implemented; the C backend implements this through a header + inclusion that is not portable to C#, and the C# RTTI layer does + not yet expose an equivalent functor-number lookup. -4. `math.fma/3` +4. `exception.catch_impl/3` for the `semidet` and `cc_nondet` modes. - This function is not available because it is not supported by C# 5.0. - (It will be supported once the minimum version of C# required by - Mercury increases.) + Currently throws `Sorry, not implemented'. The `det`, `cc_multi` + and `multi` modes are implemented. Interfacing with C# ------------------- diff --git a/NEWS.md b/NEWS.md index 379b7b61df..49a3d01088 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1963,6 +1963,44 @@ Portability improvements * We have updated the script `tools/configure_cross` to support cross-compiling using clang. +* The `csharp` grade now targets .NET 10 (or later) and C# 14. + The previous Mono and .NET Framework code paths have been removed. + When configure detects a `dotnet` command and a .NET 10+ SDK with + Roslyn (`/Roslyn/bincore/csc.dll`), it is preferred over a + stand-alone `csc` or `mcs`; users may force this selection with + `--with-csharp-compiler=dotnet`. + +* `mmc --grade csharp --make` now produces its output by generating + a `.csproj` next to the linked target and invoking + `dotnet build` once. MSBuild emits the managed `.dll`, + the apphost binary (`.exe` on Windows; renamed to + `.exe` on Linux and macOS to match Mercury's existing + csharp_executable file-name convention), and the runtimeconfig.json. + As a result, the wrapper shell script and the `MONO_PATH` + environment-variable threading have been removed. + +* Library targets in `csharp` grade now carry + `true` so consumers may publish trimmed + binaries with `true` from their + own `.csproj`. + +* The C# implementation of `library/io.file.m` has been modernised: + the obsolete `Directory.CreateDirectory(string, DirectorySecurity)` + overload (removed in .NET 5) is gone, the Code Access Security + `SecurityPermission.Demand()` calls have been replaced with + no-ops (CAS is no longer enforced under modern .NET), and the + `__MonoCS__` preprocessor guards around the libc `mkdir` P/Invoke + have been removed in favour of `RuntimeInformation.IsOSPlatform`. + +* The C# backend has gained real implementations for several + previously-stubbed standard library predicates: `math.fma/3` and + `math.have_fma/0` now use `System.Math.FusedMultiplyAdd`, + `benchmarking.report_stats/3` and `benchmarking.report_full_memory_stats/2` + now report real GC and process memory information, and + `builtin.tuple_arity/2`, `builtin.tuple_arg/3`, and + `builtin.compare_representation_3_p_0/3` no longer throw a + `Sorry, not implemented' exception. + Changes to the extras distribution ---------------------------------- From f2038b915bb4c936725728254d5ce8876ccaba04 Mon Sep 17 00:00:00 2001 From: Sebastian Godelet Date: Tue, 28 Apr 2026 13:10:54 +1000 Subject: [PATCH 09/25] Fix the csproj-based csharp linker on native Windows. The csproj/dotnet-build flow added on this branch was only exercised under WSL, where (a) the dotnet executable lives at a path with no spaces (`/usr/share/dotnet/dotnet'), (b) every install prefix is a true POSIX absolute path, and (c) the system has stand-alone `csc' or `mcs' on PATH. None of those hold for a stock MSYS2/CLANGARM64 shell on Windows, where the build dies in three different ways: at mmc startup, in the `dotnet build' invocation itself, and at run time when the user-visible apphost cannot find its companion files. Address each in turn. m4/mercury.m4: Drop the `dotnet' branch of CSC selection. The probe used to set CSC=`"\$DOTNET" exec "\$DOTNET_CSC_DLL"', a multi-word string with embedded double quotes. After @CSC@ substitution that lands in scripts/Mercury.config as the value of MERCURY_CSHARP_COMPILER, which the option file parser then word-splits at the embedded quotes; the leftover fragments (`exec', `C:/Program', ...) are re-injected as positional module names and abort mmc startup with `cannot find source for module ...'. Since the csproj/dotnet- build flow no longer invokes csc per module, --csharp-compiler is purely vestigial in csharp grade; revert CSC_COMPILERS to `csc mcs' and let users with only the SDK installed keep using `--with-csharp-compiler=' to point at csc.exe. Keep the dotnet probe itself: link_target_code.m relies on `dotnet' being on PATH at link time, and the SDK version is also useful for diagnostics. Refresh the comment on the probe and on the `--list-sdks' walk so it accurately describes what the loop does (keep the last >= 10 entry, which by --list-sdks's documented ascending order is the newest installed SDK >= 10) rather than claiming a max-version compare it never performs. compiler/link_target_code.m: Switch the `dotnet build' call from invoke_long_system_command/8 to invoke_system_command/7. The long-cmd helper writes args to a temp file and invokes `dotnet @tmpfile', but the dotnet CLI does not tokenize @file response files like csc/msbuild do -- it reads the entire file as a single argument and then hunts for a matching `dotnet-' tool, so we get a `does not exist' error. The dotnet command line stays well under the Windows length limit, so the @file machinery is not needed here anyway. In create_exe_or_lib_for_csharp/9, anchor every entry of SourceList to an absolute path (using dir.current_directory/3 plus the new make_csproj_source_path_absolute/2 helper) before handing it to csproj_content/8. SourceList paths are relative to the compiler's cwd, but MSBuild resolves entries relative to the csproj file location. With --use-subdirs plus --use-grade-subdirs the csproj sits at Mercury/csharp//Mercury/bin/.csproj so a cwd-relative `Mercury/csharp//Mercury/css/foo.cs' becomes the doubly-nested ...bin/Mercury/csharp//Mercury/css/foo.cs and csc reports CS2001. Absolute paths sidestep csproj-relative resolution entirely. Fall back to the unmodified list when dir.current_directory/3 fails, which still works in the common no-subdirs case. Extend post_link_maybe_make_symlink_or_copy/8 with a new copy_csharp_apphost_companions/7 helper that copies `.dll', `.runtimeconfig.json' and `.deps.json' alongside the user-visible apphost. When --use-subdirs sends the build into a deep subdir, the existing logic copied just the apphost up to cwd; the apphost then aborted at run time with The application to execute does not exist: '/.dll'. Companions that the build did not emit (e.g. `.deps.json' for trivial programs) are skipped silently. --- compiler/link_target_code.m | 122 +++++++++++++++++++++++++++++++++--- m4/mercury.m4 | 53 ++++------------ 2 files changed, 127 insertions(+), 48 deletions(-) diff --git a/compiler/link_target_code.m b/compiler/link_target_code.m index 723b54a1a0..ef18387025 100644 --- a/compiler/link_target_code.m +++ b/compiler/link_target_code.m @@ -1315,7 +1315,24 @@ LinkedTargetType = csharp_executable, KeyFile = "" ), - csproj_content(LinkedTargetType, AssemblyName, SourceList, RefEntries, + % SourceList paths are relative to the compiler's cwd, but MSBuild + % resolves entries relative to the csproj + % file location. With --use-subdirs/--use-grade-subdirs the csproj + % sits several directories deep, so a cwd-relative source path would + % be doubly-nested. Anchor each entry to an absolute path before + % handing it to the csproj generator. If we cannot determine the + % cwd we fall back to the unmodified list, which still works in the + % common no-subdirs case. + dir.current_directory(CwdResult, !IO), + ( + CwdResult = ok(Cwd), + AbsSourceList = list.map(make_csproj_source_path_absolute(Cwd), + SourceList) + ; + CwdResult = error(_), + AbsSourceList = SourceList + ), + csproj_content(LinkedTargetType, AssemblyName, AbsSourceList, RefEntries, Debug, ExtraCSCFlags, KeyFile, CsprojContent), io.open_output(CsprojPath, OpenRes, !IO), @@ -1327,11 +1344,16 @@ % Invoke `dotnet build' once on the generated csproj. MSBuild % handles framework references, runtimeconfig.json emission and % apphost generation; we no longer need a wrapper shell script. - DotnetCmd = "dotnet", - DotnetArgs = "build " ++ quote_shell_cmd_arg(CsprojPath) ++ + % Use invoke_system_command rather than invoke_long_system_command: + % the dotnet CLI does not tokenize @file response files like csc + % and msbuild do (it reads the entire file as a single argument), + % so the @file machinery would route us to a non-existent + % `dotnet-build path/to/csproj ...' tool. The dotnet build + % command line stays well under the Windows length limit anyway. + DotnetCmd = "dotnet build " ++ quote_shell_cmd_arg(CsprojPath) ++ " -c Release -v:quiet --nologo", - invoke_long_system_command(Globals, ProgressStream, ProgressStream, - cmd_verbose_commands, DotnetCmd, DotnetArgs, Succeeded0, !IO), + invoke_system_command(Globals, ProgressStream, ProgressStream, + cmd_verbose_commands, DotnetCmd, Succeeded0, !IO), % On non-Windows hosts the apphost has no `.exe' suffix, but % Mercury's `csharp_executable' linked-target convention always @@ -1358,6 +1380,19 @@ % Helpers for csproj-based linking. % + % Return Path unchanged if it is already absolute; otherwise prepend Cwd + % so the result is an absolute path safe to embed in + % entries regardless of where the csproj file ends up. + % +:- func make_csproj_source_path_absolute(string, string) = string. + +make_csproj_source_path_absolute(Cwd, Path) = + ( if dir.path_name_is_absolute(Path) then + Path + else + Cwd / Path + ). + % If the apphost was emitted without the `.exe' suffix (the default on % non-Windows hosts) and FullOutputFileName does not yet exist, rename % the bare apphost so callers can find it under its expected name. @@ -1704,10 +1739,31 @@ MadeSymlinkOrCopy = yes ), + % C# executables on .NET are self-contained apphosts that + % delegate to a managed assembly co-located with the apphost + % (`.dll' plus `.runtimeconfig.json' and optionally + % `.deps.json'). When FullFileName lives several + % directories deep -- e.g. under + % `Mercury/csharp//Mercury/bin/' with --use-subdirs and + % --use-grade-subdirs -- the apphost copied to CurDirFileName + % has none of those companions next to it and aborts at run + % time with `The application to execute does not exist'. Copy + % each companion alongside the user-visible apphost too. + ( if + Succeeded0 = succeeded, + LinkedTargetType = csharp_executable, + MadeSymlinkOrCopy = yes + then + copy_csharp_apphost_companions(Globals, ProgressStream, + FullFileName, CurDirFileName, Succeeded0, Succeeded1, !IO) + else + Succeeded1 = Succeeded0 + ), + % For the Java and C# grades we also need to symlink or copy the % launcher scripts or batch files. ( if - Succeeded0 = succeeded, + Succeeded1 = succeeded, ( LinkedTargetType = csharp_executable, % NOTE: we don't generate a launcher script for C# executables @@ -1739,10 +1795,62 @@ FullLauncherName, CurDirLauncherName, Succeeded, !IO) ) else - Succeeded = Succeeded0 + Succeeded = Succeeded1 ) ). +%---------------------% + + % Copy the .dll, .runtimeconfig.json and .deps.json that an apphost + % needs from the build dir of FullFileName to the directory that + % contains CurDirFileName, preserving the assembly base name. Files + % that the build did not emit are skipped silently. + % +:- pred copy_csharp_apphost_companions(globals::in, io.text_output_stream::in, + file_name::in, file_name::in, + maybe_succeeded::in, maybe_succeeded::out, io::di, io::uo) is det. + +copy_csharp_apphost_companions(Globals, ProgressStream, + FullFileName, CurDirFileName, !Succeeded, !IO) :- + AssemblyBase = strip_csharp_exec_ext(dir.det_basename(FullFileName)), + ( if dir.split_name(FullFileName, FullDir0, _) then + FullDir = FullDir0 + else + FullDir = "." + ), + ( if dir.split_name(CurDirFileName, CurDir0, _) then + CurDir = CurDir0 + else + CurDir = "." + ), + Companions = ["dll", "runtimeconfig.json", "deps.json"], + list.foldl2( + copy_one_apphost_companion(Globals, ProgressStream, + FullDir, CurDir, AssemblyBase), + Companions, !Succeeded, !IO). + +:- pred copy_one_apphost_companion(globals::in, io.text_output_stream::in, + string::in, string::in, string::in, string::in, + maybe_succeeded::in, maybe_succeeded::out, io::di, io::uo) is det. + +copy_one_apphost_companion(Globals, ProgressStream, + FullDir, CurDir, AssemblyBase, Ext, !Succeeded, !IO) :- + Filename = AssemblyBase ++ "." ++ Ext, + Src = FullDir / Filename, + Dst = CurDir / Filename, + io.file.check_file_accessibility(Src, [read], CheckRes, !IO), + ( + CheckRes = ok, + io.file.remove_file_recursively(Dst, _, !IO), + make_symlink_or_copy_file(Globals, ProgressStream, Src, Dst, + CopyOk, !IO), + !:Succeeded = !.Succeeded `and` CopyOk + ; + CheckRes = error(_) + % Companion was not emitted (e.g. .deps.json may be omitted for + % trivial builds); skip silently. + ). + %---------------------% :- pred get_launcher_script_extension(globals::in, ext::out) is det. diff --git a/m4/mercury.m4 b/m4/mercury.m4 index c0bb5af1a3..f3bd6218b3 100644 --- a/m4/mercury.m4 +++ b/m4/mercury.m4 @@ -294,19 +294,22 @@ GACUTIL=`basename "$GACUTIL"` # Check for an implementation of the Common Language Infrastructure. AC_PATH_PROGS([CLI_INTERPRETER], [mono]) -# Check for the dotnet SDK (.NET 10 or later). The dotnet-bundled -# C# compiler is Roslyn (i.e. Microsoft's), invoked through -# `dotnet exec csc.dll'. When found, it is preferred over a stand-alone -# csc.exe or Mono mcs. +# Check for the dotnet SDK (.NET 10 or later). The dotnet binary +# itself is required at link time: link_target_code.m drives the +# csharp grade by generating a csproj and invoking `dotnet build'. +# The Roslyn csc.dll path (DOTNET_CSC_DLL) is recorded for diagnostics +# and possible future use; the per-module C# compiler is still chosen +# below from stand-alone csc.exe or Mono mcs. AC_PATH_PROG([DOTNET], [dotnet]) DOTNET_SDK_DIR= DOTNET_CSC_DLL= if test -n "$DOTNET"; then AC_MSG_CHECKING([for a usable dotnet SDK (10.0 or later)]) - # `dotnet --list-sdks' prints lines like: `10.0.200 [/path/to/sdk]'. - # Walk every line, pick the highest-numbered SDK whose major version - # is >= 10. Avoid bracket-using regexes here because the `[' and `]' - # characters collide with m4's quoting. + # `dotnet --list-sdks' prints lines like: `10.0.200 [/path/to/sdk]', + # in ascending version order. Walk every line, keeping the last one + # whose major version is >= 10 -- which, given the documented order, + # is the newest installed SDK >= 10. Avoid bracket-using regexes + # here because the `[' and `]' characters collide with m4's quoting. "$DOTNET" --list-sdks > conftest.sdks 2>/dev/null || : DOTNET_SDK_LINE= while IFS= read -r DOTNET_SDK_TRY_LINE; do @@ -346,7 +349,6 @@ fi # mcs is the Mono C# compiler targetting all runtime versions. # (dmcs and gmcs are older aliases for the Mono C# compiler # which we do not use.) -# `dotnet' selects the SDK-bundled Roslyn csc.dll located above. AC_CACHE_SAVE case "$mercury_cv_with_csharp_compiler" in @@ -359,20 +361,7 @@ case "$mercury_cv_with_csharp_compiler" in exit 1 ;; "") - # Prefer the dotnet-bundled Roslyn compiler when available; - # fall back to a stand-alone csc.exe or Mono mcs. - if test -n "$DOTNET_CSC_DLL"; then - CSC_COMPILERS="dotnet csc mcs" - else - CSC_COMPILERS="csc mcs" - fi - ;; - dotnet) - if test -z "$DOTNET_CSC_DLL"; then - AC_MSG_ERROR([--with-csharp-compiler=dotnet specified, but no .NET 10+ SDK was found]) - exit 1 - fi - CSC_COMPILERS="dotnet" + CSC_COMPILERS="csc mcs" ;; *) CSC_COMPILERS="$mercury_cv_with_csharp_compiler" @@ -382,18 +371,6 @@ esac AC_MSG_CHECKING([for a C sharp compiler]) AC_MSG_RESULT() for CANDIDATE_CSC0 in $CSC_COMPILERS; do - if test "$CANDIDATE_CSC0" = "dotnet"; then - # Use the dotnet SDK Roslyn csc.dll located earlier. - if test -z "$DOTNET_CSC_DLL"; then - continue; - fi - AC_MSG_NOTICE([using dotnet SDK Roslyn compiler at $DOTNET_CSC_DLL]) - # Quote both paths so that install locations containing spaces - # (e.g. `C:/Program Files/dotnet/...') survive expansion in - # scripts/Mmake.vars and on the command line. - CSC="\"$DOTNET\" exec \"$DOTNET_CSC_DLL\"" - break; - fi unset CANDIDATE_CSC unset ac_cv_path_CANDIDATE_CSC AC_CACHE_LOAD @@ -487,12 +464,6 @@ if test "$mercury_cv_with_csharp_compiler" != "" -a "$CSC" = ""; then fi case "$CSC" in - *exec*csc.dll*) - # The dotnet-bundled Roslyn compiler IS the Microsoft compiler; - # match it before the stand-alone csc.exe pattern below. - CSHARP_COMPILER_TYPE=microsoft - ;; - csc*) CSHARP_COMPILER_TYPE=microsoft ;; From 5778d00ee864844bbb034f069219809880cdd13d Mon Sep 17 00:00:00 2001 From: Sebastian Godelet Date: Tue, 28 Apr 2026 14:07:25 +1000 Subject: [PATCH 10/25] Add `--csharp-aot' to opt csharp executables into Native AOT publishing. Switch the csharp linker between `dotnet build' (the default) and `dotnet publish -p:PublishAot=true -r ' based on a new opt-in option. When enabled on a csharp_executable, the build produces a single self-contained native binary -- no managed `.dll', `runtimeconfig.json' or `deps.json' companions -- by routing the generated csproj through MSBuild's PublishAot toolchain. When enabled on a csharp_library, only an `true' csproj marker is added; the build flow itself is unchanged so users opting into AOT in their own consumer programs can see the marker and trust the library. The option is opt-in because the AOT-cleanliness contract cannot be verified from the linker stage. The user is responsible for ensuring no module reachable from main consumes dynamic RTTI (type_desc, construct, deconstruct, term_to_xml, generic io.write or compare_representation) and that every linked Mercury library was itself built AOT-compatible. Trim or AOT warnings emitted by `dotnet publish' surface as a non-zero exit and abort the link step exactly like a normal C# compilation error. A separate follow-up will mark `mer_std' itself AOT-compatible so a program built with `--csharp-aot' against the standard library does not see IL2104 trim warnings. Until then `mer_std' produces such warnings, but they are non-fatal: the apphost still runs. compiler/options.m: Add the `csharp_aot' boolean option in the `oc_target_csharp' block alongside `csharp_compiler' and friends, defaulting to `bool(no)'. Document the AOT-cleanliness contract and the library-marker behaviour in the help text. compiler/link_target_code.m: Add a new `csharp_aot_request' enum (aot_off / aot_publish(Rid) / aot_library_marker) computed once in create_exe_or_lib_for_csharp/9 from the option, the linked-target type and a derived RID. Add compute_dotnet_rid/2 mapping `target_arch' (typically a GNU triple like `aarch64-w64-mingw32') to a .NET runtime identifier (`win-arm64', `linux-x64', `osx-arm64', ...). Recognised architectures are aarch64/arm64, x86_64/amd64 and i686/i386; recognised host strings are darwin/apple, mingw/windows/msvc and linux. When derivation fails, `--csharp-aot' falls back to `dotnet build' and writes a notice to the progress stream so the opt-in is not silently denied. Thread the request through csproj_content/8 (now /9): aot_publish adds true, true, true, {Rid} and ./ to the property group. aot_library_marker adds true. aot_off adds nothing extra. Branch the linker invocation: aot_publish runs `dotnet publish -c Release -r -v:quiet --nologo'; aot_off and aot_library_marker keep the existing `dotnet build -c Release -v:quiet --nologo'. invoke_system_command/7 is used for both, since the @file path mishandled by the dotnet CLI is still avoided. Gate copy_csharp_apphost_companions/7 on `--csharp-aot=no': AOT publish produces a single self-contained native binary, so there are no `.dll', `.runtimeconfig.json' or `.deps.json' companion files to copy alongside the user-visible apphost. NEWS.md: Add a bullet under "Portability improvements" announcing the new `--csharp-aot' option, the published apphost shape, the AOT- cleanliness contract and the library-marker behaviour. Documentation/README.CSharp.md: Add a "Native AOT publishing" section after "Trimmed publish" covering the same material in user-facing prose: which csproj properties get added under AOT, how the RID is derived, the two user-side preconditions and the fallback behaviour when the RID cannot be derived. --- Documentation/README.CSharp.md | 35 +++++++ NEWS.md | 15 +++ compiler/link_target_code.m | 174 ++++++++++++++++++++++++++++++--- compiler/options.m | 15 +++ 4 files changed, 226 insertions(+), 13 deletions(-) diff --git a/Documentation/README.CSharp.md b/Documentation/README.CSharp.md index 97f34540e9..64c636c410 100644 --- a/Documentation/README.CSharp.md +++ b/Documentation/README.CSharp.md @@ -94,6 +94,41 @@ the IL-linker. Downstream consumers can therefore add their own applications without losing functionality. `mmc` itself does not invoke `dotnet publish`. +Native AOT publishing +--------------------- + +The `--csharp-aot` option flips a `csharp_executable` build from +`dotnet build` to `dotnet publish -p:PublishAot=true -r `. The +generated csproj adds `true`, +`true`, `true +` and a `` derived from +Mercury's target architecture (e.g. `aarch64-w64-mingw32` -> `win-arm64`, +`x86_64-pc-linux-gnu` -> `linux-x64`). `` is forced to `./` +so the produced native binary lands next to the csproj where Mercury +expects it, identical to the regular build flow. No `.dll`, +`runtimeconfig.json` or `deps.json` companions are emitted; the apphost +is the entire program. + +The option is opt-in and the user owns the AOT-cleanliness contract: + +* No module reachable from the program's `main/2` may import + `type_desc`, `construct`, `deconstruct` or `term_to_xml`, nor call + the generic forms of `io.write/3` or `compare_representation/3`. + Such uses require runtime reflection, which the AOT compiler trims. +* Every linked Mercury library (the standard library and any `-l` + reference) must have been built AOT-compatible. + +If the target architecture cannot be mapped to a .NET RID, the build +falls back to a regular `dotnet build` and prints a notice to the +progress stream. Trim or AOT warnings from `dotnet publish` surface +as a non-zero exit and abort the link step exactly like a normal C# +compilation error. + +For `csharp_library` targets, `--csharp-aot` only adds +`true` to the generated csproj as +a marker for downstream consumers; the build itself is still a regular +`dotnet build`, and `mmc` does not run `dotnet publish` on libraries. + Limitations ----------- diff --git a/NEWS.md b/NEWS.md index 49a3d01088..389e4bf077 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1984,6 +1984,21 @@ Portability improvements binaries with `true` from their own `.csproj`. +* The new `--csharp-aot` option opts a `csharp_executable` build into + Native AOT publishing: `mmc` switches the link step from + `dotnet build` to `dotnet publish -p:PublishAot=true -r `, and + the generated csproj sets ``, ``, + `` and a `` derived from + the target architecture. The result is a single self-contained + native binary with no managed `.dll`, `runtimeconfig.json` or + `deps.json` companions. The user is responsible for ensuring no + module reachable from main consumes dynamic RTTI (`type_desc`, + `construct`, `deconstruct`, `term_to_xml`, generic `io.write` or + `compare_representation`) and that every linked Mercury library was + built AOT-compatible. For library targets the option only adds an + `true` marker to the csproj; + the build flow is unchanged. + * The C# implementation of `library/io.file.m` has been modernised: the obsolete `Directory.CreateDirectory(string, DirectorySecurity)` overload (removed in .NET 5) is gone, the Code Access Security diff --git a/compiler/link_target_code.m b/compiler/link_target_code.m index ef18387025..1b361d71c1 100644 --- a/compiler/link_target_code.m +++ b/compiler/link_target_code.m @@ -1275,7 +1275,7 @@ % used to consume, then resolving each name to a HintPath via the % `-lib:' search dirs. get_link_opts_for_libraries_for_c_cs(Globals, MaybeLinkLibraries, - Specs, !IO), + LinkSpecs, !IO), ( MaybeLinkLibraries = yes(LinkLibrariesList), LinkLibrariesStr = string.join_list(" ", LinkLibrariesList) @@ -1332,8 +1332,41 @@ CwdResult = error(_), AbsSourceList = SourceList ), + % Decide whether `--csharp-aot' applies. For executables, derive a + % .NET RID from the target triple so we can switch `dotnet build' to + % `dotnet publish -p:PublishAot=true -r '. For libraries, only + % emit an `true' marker; the build + % flow itself is unchanged. If RID derivation fails, write a notice + % to the progress stream and fall back to a regular `dotnet build' + % so the user is not silently denied their opt-in. + globals.lookup_bool_option(Globals, csharp_aot, AotOption), + ( + AotOption = no, + AotRequest = aot_off + ; + AotOption = yes, + ( + LinkedTargetType = csharp_executable, + compute_dotnet_rid(Globals, RidResult), + ( + RidResult = ok(Rid), + AotRequest = aot_publish(Rid) + ; + RidResult = error(RidMsg), + AotRequest = aot_off, + io.format(ProgressStream, + "%% --csharp-aot: %s. Falling back to `dotnet build'.\n", + [s(RidMsg)], !IO) + ) + ; + LinkedTargetType = csharp_library, + AotRequest = aot_library_marker + ) + ), + Specs = LinkSpecs, + csproj_content(LinkedTargetType, AssemblyName, AbsSourceList, RefEntries, - Debug, ExtraCSCFlags, KeyFile, CsprojContent), + Debug, ExtraCSCFlags, KeyFile, AotRequest, CsprojContent), io.open_output(CsprojPath, OpenRes, !IO), ( @@ -1341,17 +1374,29 @@ io.write_string(Stream, CsprojContent, !IO), io.close_output(Stream, !IO), - % Invoke `dotnet build' once on the generated csproj. MSBuild - % handles framework references, runtimeconfig.json emission and - % apphost generation; we no longer need a wrapper shell script. + % Invoke `dotnet build' (or `dotnet publish' under --csharp-aot) + % once on the generated csproj. MSBuild handles framework + % references, runtimeconfig.json emission and apphost generation; + % we no longer need a wrapper shell script. % Use invoke_system_command rather than invoke_long_system_command: % the dotnet CLI does not tokenize @file response files like csc % and msbuild do (it reads the entire file as a single argument), % so the @file machinery would route us to a non-existent - % `dotnet-build path/to/csproj ...' tool. The dotnet build - % command line stays well under the Windows length limit anyway. - DotnetCmd = "dotnet build " ++ quote_shell_cmd_arg(CsprojPath) ++ - " -c Release -v:quiet --nologo", + % `dotnet-build path/to/csproj ...' tool. The dotnet command + % line stays well under the Windows length limit anyway. + ( + AotRequest = aot_publish(PublishRid), + DotnetCmd = "dotnet publish " ++ + quote_shell_cmd_arg(CsprojPath) ++ + " -c Release -r " ++ PublishRid ++ " -v:quiet --nologo" + ; + ( AotRequest = aot_off + ; AotRequest = aot_library_marker + ), + DotnetCmd = "dotnet build " ++ + quote_shell_cmd_arg(CsprojPath) ++ + " -c Release -v:quiet --nologo" + ), invoke_system_command(Globals, ProgressStream, ProgressStream, cmd_verbose_commands, DotnetCmd, Succeeded0, !IO), @@ -1499,15 +1544,90 @@ find_csharp_ref_dll(Dirs, DllName, MaybePath, !IO) ). + % How `--csharp-aot' should affect this csproj invocation, computed + % once in create_exe_or_lib_for_csharp/9 and threaded through. + % +:- type csharp_aot_request + ---> aot_off + % `--csharp-aot' was not requested (or could not be honoured). + ; aot_publish(string) + % `--csharp-aot' on a csharp_executable target. The string is + % the .NET runtime identifier (RID) we will pass to + % `dotnet publish -r ...'. + ; aot_library_marker. + % `--csharp-aot' on a csharp_library target: emit + % `true' as a marker for + % downstream consumers but keep the regular `dotnet build' + % flow. + + % Map Mercury's target architecture (typically a GNU triple such as + % `aarch64-w64-mingw32') to a .NET runtime identifier such as + % `win-arm64'. Returns error/1 with a short reason when the host + % cannot be expressed as a RID; callers should treat that as + % "fall back to a regular build and warn". + % +:- pred compute_dotnet_rid(globals::in, maybe_error(string)::out) is det. + +compute_dotnet_rid(Globals, MaybeRid) :- + globals.lookup_string_option(Globals, target_arch, TargetArch), + ( if + ( string.sub_string_search(TargetArch, "aarch64", _) + ; string.sub_string_search(TargetArch, "arm64", _) + ) + then + Arch = "arm64" + else if + ( string.sub_string_search(TargetArch, "x86_64", _) + ; string.sub_string_search(TargetArch, "amd64", _) + ) + then + Arch = "x64" + else if + ( string.sub_string_search(TargetArch, "i686", _) + ; string.sub_string_search(TargetArch, "i386", _) + ) + then + Arch = "x86" + else + Arch = "" + ), + ( if + ( string.sub_string_search(TargetArch, "darwin", _) + ; string.sub_string_search(TargetArch, "apple", _) + ) + then + Os = "osx" + else if + ( string.sub_string_search(TargetArch, "mingw", _) + ; string.sub_string_search(TargetArch, "windows", _) + ; string.sub_string_search(TargetArch, "msvc", _) + ) + then + Os = "win" + else if string.sub_string_search(TargetArch, "linux", _) then + Os = "linux" + else + Os = "" + ), + ( if Arch = "" then + MaybeRid = error("could not derive a .NET RID architecture " ++ + "from target_arch `" ++ TargetArch ++ "'") + else if Os = "" then + MaybeRid = error("could not derive a .NET RID OS " ++ + "from target_arch `" ++ TargetArch ++ "'") + else + MaybeRid = ok(Os ++ "-" ++ Arch) + ). + % Build the .csproj content as a single string. % :- pred csproj_content(linked_target_type::in(csharp_linked_target_type), string::in, list(string)::in, list(csharp_ref_entry)::in, bool::in, list(string)::in, string::in, - string::out) is det. + csharp_aot_request::in, string::out) is det. csproj_content(LinkedTargetType, AssemblyName, SourceList, RefEntries, - Debug, ExtraCSCFlags, KeyFile, Content) :- + Debug, ExtraCSCFlags, KeyFile, AotRequest, Content) :- ( LinkedTargetType = csharp_executable, OutputType = "Exe", @@ -1519,6 +1639,29 @@ UseAppHost = "false", IsTrimmableLine = " true\n" ), + % Native AOT properties. For executables, switch on PublishAot, + % nail the runtime identifier, force globalization-invariant data + % (the only mode AOT supports without a sidecar ICU package), and + % redirect the publish output to the csproj directory so the binary + % lands where Mercury expects it. For libraries, just announce + % AOT compatibility; consumers may then opt into AOT publishing. + ( + AotRequest = aot_off, + AotPropertyLines = "" + ; + AotRequest = aot_publish(Rid), + AotPropertyLines = string.append_list([ + " true\n", + " true\n", + " true\n", + " ", xml_escape(Rid), + "\n", + " ./\n" + ]) + ; + AotRequest = aot_library_marker, + AotPropertyLines = " true\n" + ), ( Debug = yes, DebugType = "portable", @@ -1577,6 +1720,7 @@ "\n", " false\n", IsTrimmableLine, + AotPropertyLines, SignLines, DefineLine, " \n", @@ -1748,11 +1892,15 @@ % --use-grade-subdirs -- the apphost copied to CurDirFileName % has none of those companions next to it and aborts at run % time with `The application to execute does not exist'. Copy - % each companion alongside the user-visible apphost too. + % each companion alongside the user-visible apphost too. Skip + % under --csharp-aot: native AOT publish produces a single + % self-contained binary, so there are no companions to find. + globals.lookup_bool_option(Globals, csharp_aot, AotEnabled), ( if Succeeded0 = succeeded, LinkedTargetType = csharp_executable, - MadeSymlinkOrCopy = yes + MadeSymlinkOrCopy = yes, + AotEnabled = no then copy_csharp_apphost_companions(Globals, ProgressStream, FullFileName, CurDirFileName, Succeeded0, Succeeded1, !IO) diff --git a/compiler/options.m b/compiler/options.m index 17c8b07919..212baa1b59 100644 --- a/compiler/options.m +++ b/compiler/options.m @@ -892,6 +892,7 @@ ; csharp_compiler_type ; csharp_flags ; quoted_csharp_flag + ; csharp_aot ; mono_path_directories % Link options. @@ -4719,6 +4720,20 @@ alt_arg_help("quoted-csharp-flag", ["csharp-flag"], "option", [ w("Specify a single word option to be passed to the C# compiler."), w("The word will be quoted when passed to the shell.")])). +optdb(oc_target_csharp, csharp_aot, bool(no), + help("csharp-aot", [ + cindex("Native AOT (C# backend)"), + w("Build C# executables as native AOT binaries via"), + samp("dotnet publish -p:PublishAot=true"), w("instead of the default"), + samp("dotnet build", "."), + w("This is opt-in: the user is responsible for ensuring no module"), + w("reachable from main consumes dynamic RTTI"), + w("(such as type_desc, construct, deconstruct, term_to_xml,"), + w("generic io.write or compare_representation),"), + w("and that every linked Mercury library was built AOT-compatible."), + w("For library targets, this option only adds an"), + samp("true"), + w("marker to the generated csproj; the build flow is unchanged.")])). optdb(oc_target_csharp, mono_path_directories, accumulating([]), alt_arg_help("mono-path-directory", ["mono-path-dir"], "directory", [ From 07c99124f0a9d9141a2c5af6d5fe8bc3a181be01 Mon Sep 17 00:00:00 2001 From: Sebastian Godelet Date: Tue, 28 Apr 2026 19:44:37 +1000 Subject: [PATCH 11/25] Replace System.Reflection-based RTTI in csharp DU walks with an MR_DuTerm interface. Every C# class generated from a Mercury discriminated-union type ctor now implements a new mercury.runtime.MR_DuTerm interface with four methods: MR_GetField(int) for indexed positional-field access, MR_GetFieldCount(), MR_GetSecondaryTag() for the data_tag value (-1 if absent) and MR_DeepCopy(System.Func) for a per-class override that clones the term and recurses on every positional field. This lets library/rtti_implementation.m and library/builtin.m walk a term's fields through plain virtual dispatch instead of Type.GetField / GetFields / InvokeMember, removing the IL2075 trim warnings that the .NET trim/AOT toolchain emits on those reflection paths today. Reflective access to fields disappears from mer_std, so dynamic RTTI consumers in mer_std become AOT-correct, not just AOT-compatible-by-luck. builtin.deep_copy's fallback for non-DU reference types (closures, typeinfos, typeclass-infos, runtime structures) becomes a shallow alias rather than the previous reflection-based field walk. Those values are immutable from Mercury's point of view, so the previous walk was redundant in practice; dropping it is what lets mer_std finally compile clean under PublishAot. The interface route was picked over the originally-planned per-type-ctor __Rtti static helper plus delegate fields on TypeCtorInfo_Struct because it avoids touching TypeCtorInfo_Struct's positional `init(...)' call convention, lets the trimmer drop the entire MR_DuTerm machinery in programs that never reach an interface cast, and gives the right inheritance semantics for sectag-using subclasses for free. C and Java grades are unaffected: ml_target_csharp is the only branch that adds MR_DuTerm to Implements, the rewrites all live inside the existing `pragma foreign_proc("C#", ...)' / `foreign_code("C#", ...)' clauses, and Java still walks fields by name via java.lang.reflect on its own separate trim/AOT story. runtime/mercury_dotnet.cs.in: Declare the public MR_DuTerm interface in namespace mercury.runtime. Place it just before PseudoTypeInfo, with a doc comment pointing at csharp-aot-rtti-plan.md. The interface declares MR_GetField, MR_GetFieldCount, MR_GetSecondaryTag and MR_DeepCopy(System.Func deepCopyFn). The doc comment for MR_DeepCopy spells out that the fn is the recursive deep-copy entry point so nested DU terms, tuples, arrays and primitives all flow through the same dispatch. compiler/ml_code_util.m: Add ml_csharp_mr_du_term_interface, an mlds_interface_id that resolves to mercury.runtime.MR_DuTerm. Modelled on the existing ml_java_mercury_type_interface helper for the Java backend's MercuryType. compiler/ml_type_gen.m: In the C# arm of the three mlds_class_defn construction sites for DU types -- the base class in ml_gen_hld_du_type, the secondary tag class in ml_gen_hld_secondary_tag_class and the per-functor subclass in ml_gen_hld_du_ctor_member -- replace the empty Implements list with [ml_csharp_mr_du_term_interface]. The ml_target_c branches stay empty; ml_target_java keeps its existing MercuryType wiring. compiler/mlds_to_cs_class.m: Recognise "MR_DuTerm" in interface_is_special_for_csharp so that interface_to_string_for_csharp emits the unqualified `mercury.runtime.MR_DuTerm' (no arity suffix), matching the existing convention for "MercuryType". After the constructors are emitted in output_class_defn_for_csharp, if Implements contains ml_csharp_mr_du_term_interface emit the four MR_DuTerm method bodies via a new output_mr_du_term_methods_for_csharp predicate. The bodies are derived from the class's own MemberFields list: classify_mr_du_term_fields walks it once, peeling off fvn_du_ctor_field_hld entries (the F1, F2, ... positional fields) into a list of cs_du_field/2 (each pairing the C# field name with its C# type string) and noting whether fvn_data_tag is present. output_mr_get_field_cases then emits one switch case per positional field returning `this.'. Pick `virtual ' on classes that inherit nothing and `override ' on classes that inherit another class -- in csharp DU emission the only inheritance edge is a sectag-using subclass extending its secondary-tag (or the single-functor base) class, so an inherits_class(_) class always has a reachable data_tag whether or not its own MemberFields list one; treat HasDataTag as yes in that case so MR_GetSecondaryTag returns this.data_tag instead of -1. For MR_DeepCopy, when the class has no positional fields the body is `return this.MemberwiseClone();'; otherwise it casts the clone to the class's own type, assigns `n. = () f(this.)' for each positional field via a new output_mr_deep_copy_assignments helper, and returns n. The cast is emitted unconditionally; for `object'-typed fields C# treats `(object) ...' as a no-op. Add `:- import_module ml_backend.ml_code_util.' to access the ml_csharp_mr_du_term_interface helper, and `:- import_module int.' for the case-index counter. library/rtti_implementation.m: Rewrite the four C# reflection sites: - get_remote_secondary_tag now reads the secondary tag via `((MR_DuTerm) X).MR_GetSecondaryTag()'. - get_type_info_from_term and get_typeclass_info_from_term keep their object[] fast path and route the else branch through `((MR_DuTerm) Term).MR_GetField(Index)' instead of building a field-name string and looking it up by reflection. - ML_get_subterm_non_array collapses to a one-line `((MR_DuTerm) term).MR_GetField(num_extra_args + index)'. The previous helpers ML_get_functor_desc_by_tags, ML_get_field_name_by_index, ML_get_subterm_by_field_name and the type_ctor_base subtype-redirection branch are dropped -- subtypes share the same C# class (and hence the same MR_GetField switch) as their base type, so no redirection is needed. The Java pragma foreign_code block at the bottom is left alone. library/builtin.m: Rewrite deep_copy. The null / value-type / string / array branches are unchanged. Reference-typed objects that implement MR_DuTerm dispatch through `du.MR_DeepCopy(deep_copy)', which clones the term and recurses on every positional field through this same deep_copy fn. Other reference types (closures, typeinfos, ...) are returned as a shallow alias instead of the previous reflective walk; they are immutable from Mercury's point of view, so the alias is sufficient and the lost walk was already a no-op in practice. Drop the deep_copy_fields helper entirely, since the per-class MR_DeepCopy method now does the field-by-field assignment. --- compiler/ml_code_util.m | 15 +++ compiler/ml_type_gen.m | 31 +++++- compiler/mlds_to_cs_class.m | 193 ++++++++++++++++++++++++++++++++++ library/builtin.m | 65 ++++-------- library/rtti_implementation.m | 91 +++------------- runtime/mercury_dotnet.cs.in | 29 +++++ 6 files changed, 298 insertions(+), 126 deletions(-) diff --git a/compiler/ml_code_util.m b/compiler/ml_code_util.m index 89f00b2b6d..cc71fed9c0 100644 --- a/compiler/ml_code_util.m +++ b/compiler/ml_code_util.m @@ -128,6 +128,16 @@ % :- func ml_java_mercury_enum_class = mlds_class_id. + % Return the interface id corresponding to the + % `mercury.runtime.MR_DuTerm' interface. Implemented by every + % MLDS-generated C# class for a Mercury type ctor, so that + % library/rtti_implementation.m and library/builtin.m can walk + % a term's positional fields and read its secondary tag without + % using System.Reflection (which the .NET trim/AOT toolchain + % cannot statically prove safe). + % +:- func ml_csharp_mr_du_term_interface = mlds_interface_id. + %---------------------------------------------------------------------------% % % Routines for generating labels and entity names. @@ -726,6 +736,11 @@ qual_class_name(InterfaceModuleName, module_qual, "MercuryEnum"), EnumClassId = mlds_class_id(EnumClass, 0). +ml_csharp_mr_du_term_interface = TypeInterfaceDefn :- + InterfaceModuleName = + mercury_module_name_to_mlds(csharp_mercury_runtime_package_name), + TypeInterfaceDefn = mlds_interface_id(InterfaceModuleName, "MR_DuTerm"). + %---------------------------------------------------------------------------% % % Code for generating mlds_function_names. diff --git a/compiler/ml_type_gen.m b/compiler/ml_type_gen.m index b99fb66090..1826e15e7e 100644 --- a/compiler/ml_type_gen.m +++ b/compiler/ml_type_gen.m @@ -471,9 +471,14 @@ % interface. Implements = [ml_java_mercury_type_interface] ; - ( Target = ml_target_c - ; Target = ml_target_csharp - ), + Target = ml_target_csharp, + % All C# classes corresponding to discriminated-union types + % implement the MR_DuTerm interface, so library/rtti_implementation.m + % can walk their fields without System.Reflection (mer_std must + % stay AOT/trim compatible). + Implements = [ml_csharp_mr_du_term_interface] + ; + Target = ml_target_c, Implements = [] ), @@ -522,7 +527,15 @@ EmptyBaseClasses = no, Inherits = inherits_nothing ), - Implements = [], + ( + Target = ml_target_csharp, + Implements = [ml_csharp_mr_du_term_interface] + ; + ( Target = ml_target_c + ; Target = ml_target_java + ), + Implements = [] + ), Ctors = [], % Type parameters are only used by the Java backend, which doesn't use @@ -692,7 +705,15 @@ ) ), Imports = [], - Implements = [], + ( + Target = ml_target_csharp, + Implements = [ml_csharp_mr_du_term_interface] + ; + ( Target = ml_target_c + ; Target = ml_target_java + ), + Implements = [] + ), get_type_defn_tparams(TypeDefn, TypeParams), % Put it all together. diff --git a/compiler/mlds_to_cs_class.m b/compiler/mlds_to_cs_class.m index 9021206c78..e1f75131a0 100644 --- a/compiler/mlds_to_cs_class.m +++ b/compiler/mlds_to_cs_class.m @@ -49,6 +49,7 @@ :- import_module libs.globals. :- import_module mdbcomp. :- import_module mdbcomp.sym_name. +:- import_module ml_backend.ml_code_util. :- import_module ml_backend.mlds_to_cs_data. :- import_module ml_backend.mlds_to_cs_func. :- import_module ml_backend.mlds_to_cs_name. @@ -59,6 +60,7 @@ :- import_module parse_tree.prog_data. :- import_module bool. +:- import_module int. :- import_module list. :- import_module maybe. :- import_module require. @@ -119,6 +121,19 @@ list.foldl( output_function_defn_for_csharp(Info, Stream, Indent1, CtorsAux), Ctors, !IO), + + % If this class implements MR_DuTerm (which we attach to every + % csharp DU representation class in ml_type_gen.m), emit method + % bodies for the four interface methods. Done by walking the + % MemberFields list -- positional fvn_du_ctor_field_hld fields + % become the cases in MR_GetField; the optional fvn_data_tag field + % drives MR_GetSecondaryTag. + ( if list.member(ml_csharp_mr_du_term_interface, Implements) then + output_mr_du_term_methods_for_csharp(Info, Stream, Indent1, ClassName, + ClassArity, Inherits, MemberFields, !IO) + else + true + ), io.format(Stream, "%s}\n\n", [s(IndentStr)], !IO). output_enum_class_defn_for_csharp(Info0, Stream, Indent, EnumDefn, !IO) :- @@ -249,6 +264,7 @@ :- pred interface_is_special_for_csharp(string::in) is semidet. interface_is_special_for_csharp("MercuryType"). +interface_is_special_for_csharp("MR_DuTerm"). %---------------------------------------------------------------------------% @@ -343,6 +359,183 @@ ConstnessPrefix = "" ). +%---------------------------------------------------------------------------% +% +% MR_DuTerm interface emission. +% +% Emit the four methods that satisfy mercury.runtime.MR_DuTerm on every +% C# class generated from a Mercury discriminated-union type ctor. +% This lets library/rtti_implementation.m walk a term's positional +% fields and read its secondary tag without using System.Reflection, +% so that mer_std remains compatible with the .NET trim/AOT toolchain. +% + + % A positional field on a DU representation class: the C# field + % name and the C# type string for the field. Used by + % output_mr_du_term_methods_for_csharp to emit MR_GetField cases + % and MR_DeepCopy assignments. + % +:- type cs_du_field + ---> cs_du_field(string, string). + +:- pred output_mr_du_term_methods_for_csharp(csharp_out_info::in, + io.text_output_stream::in, indent::in, mlds_class_name::in, arity::in, + mlds_class_inherits::in, list(mlds_field_var_defn)::in, + io::di, io::uo) is det. + +output_mr_du_term_methods_for_csharp(Info, Stream, Indent, ClassName, + ClassArity, Inherits, MemberFields, !IO) :- + classify_mr_du_term_fields(Info, MemberFields, PositionalFields, + OwnHasDataTag), + list.length(PositionalFields, NumPositional), + % In csharp DU emission, the only classes that inherit are + % sectag-using subclasses inheriting their secondary-tag class + % (or, in the single-functor case, the base class). Both ancestors + % carry a data_tag field, so an inherits_class(_) class always has + % a reachable data_tag whether or not its own MemberFields list one. + ( + Inherits = inherits_nothing, + Modifier = "virtual ", + HasDataTag = OwnHasDataTag + ; + Inherits = inherits_class(_), + Modifier = "override ", + HasDataTag = yes + ), + IndentStr = indent2_string(Indent), + Indent1Str = indent2_string(Indent + 1u), + Indent2Str = indent2_string(Indent + 2u), + ClassNameStr = + unqual_class_name_to_ll_string_for_csharp(ClassName, ClassArity), + + % MR_GetField. + io.format(Stream, "%spublic %sobject MR_GetField(int index)\n", + [s(IndentStr), s(Modifier)], !IO), + io.format(Stream, "%s{\n", [s(IndentStr)], !IO), + ( + PositionalFields = [], + io.format(Stream, + "%sthrow new System.IndexOutOfRangeException(" ++ + "\"MR_GetField: no positional fields\");\n", + [s(Indent1Str)], !IO) + ; + PositionalFields = [_ | _], + io.format(Stream, "%sswitch (index) {\n", [s(Indent1Str)], !IO), + output_mr_get_field_cases(Stream, Indent2Str, 0, + PositionalFields, !IO), + io.format(Stream, "%sdefault:\n", [s(Indent2Str)], !IO), + io.format(Stream, + "%s throw new System.IndexOutOfRangeException(" ++ + "\"MR_GetField: index out of range\");\n", + [s(Indent2Str)], !IO), + io.format(Stream, "%s}\n", [s(Indent1Str)], !IO) + ), + io.format(Stream, "%s}\n", [s(IndentStr)], !IO), + + % MR_GetFieldCount. + io.format(Stream, + "%spublic %sint MR_GetFieldCount() { return %d; }\n", + [s(IndentStr), s(Modifier), i(NumPositional)], !IO), + + % MR_GetSecondaryTag. + ( + HasDataTag = yes, + io.format(Stream, + "%spublic %sint MR_GetSecondaryTag()" ++ + " { return this.data_tag; }\n", + [s(IndentStr), s(Modifier)], !IO) + ; + HasDataTag = no, + io.format(Stream, + "%spublic %sint MR_GetSecondaryTag() { return -1; }\n", + [s(IndentStr), s(Modifier)], !IO) + ), + + % MR_DeepCopy. Clone the instance via MemberwiseClone (which + % preserves the runtime type of `this'), then overwrite each + % positional field with the result of recursing through the + % caller-supplied deep-copy fn. Fields of `object' type accept + % the boxed result directly; fields of more specific types + % require an explicit cast back from `object'. + io.format(Stream, + "%spublic %sobject MR_DeepCopy(System.Func f)\n", + [s(IndentStr), s(Modifier)], !IO), + io.format(Stream, "%s{\n", [s(IndentStr)], !IO), + ( + PositionalFields = [], + io.format(Stream, "%sreturn this.MemberwiseClone();\n", + [s(Indent1Str)], !IO) + ; + PositionalFields = [_ | _], + io.format(Stream, "%s%s n = (%s) this.MemberwiseClone();\n", + [s(Indent1Str), s(ClassNameStr), s(ClassNameStr)], !IO), + output_mr_deep_copy_assignments(Stream, Indent1Str, PositionalFields, + !IO), + io.format(Stream, "%sreturn n;\n", [s(Indent1Str)], !IO) + ), + io.format(Stream, "%s}\n", [s(IndentStr)], !IO). + + % Walk the field list, splitting it into the ordered list of + % positional-field {name, typestr} pairs (used by MR_GetField and + % MR_DeepCopy) and a flag for whether the class carries a data_tag + % field (used by MR_GetSecondaryTag). Other field kinds (e.g. + % fvn_mr_value on enums, fvn_global_data_field on global data) are + % ignored: they should not appear on classes implementing MR_DuTerm. + % +:- pred classify_mr_du_term_fields(csharp_out_info::in, + list(mlds_field_var_defn)::in, list(cs_du_field)::out, bool::out) is det. + +classify_mr_du_term_fields(_, [], [], no). +classify_mr_du_term_fields(Info, [FieldDefn | FieldDefns], + PositionalFields, HasDataTag) :- + classify_mr_du_term_fields(Info, FieldDefns, PositionalFields0, + HasDataTag0), + FieldDefn = mlds_field_var_defn(FieldVarName, _, _, FieldType, _, _), + ( + FieldVarName = fvn_du_ctor_field_hld(_), + FieldNameStr = field_var_name_to_ll_string_for_csharp(FieldVarName), + FieldTypeStr = type_to_string_for_csharp(Info, FieldType), + PositionalFields = + [cs_du_field(FieldNameStr, FieldTypeStr) | PositionalFields0], + HasDataTag = HasDataTag0 + ; + FieldVarName = fvn_data_tag, + PositionalFields = PositionalFields0, + HasDataTag = yes + ; + ( FieldVarName = fvn_global_data_field(_, _) + ; FieldVarName = fvn_mr_value + ; FieldVarName = fvn_enum_const(_) + ; FieldVarName = fvn_base_class(_) + ; FieldVarName = fvn_ptr_num + ; FieldVarName = fvn_env_field_from_local_var(_) + ; FieldVarName = fvn_prev + ; FieldVarName = fvn_trace + ), + PositionalFields = PositionalFields0, + HasDataTag = HasDataTag0 + ). + +:- pred output_mr_get_field_cases(io.text_output_stream::in, string::in, + int::in, list(cs_du_field)::in, io::di, io::uo) is det. + +output_mr_get_field_cases(_, _, _, [], !IO). +output_mr_get_field_cases(Stream, IndentStr, Idx, + [cs_du_field(FieldName, _) | Fields], !IO) :- + io.format(Stream, "%scase %d: return this.%s;\n", + [s(IndentStr), i(Idx), s(FieldName)], !IO), + output_mr_get_field_cases(Stream, IndentStr, Idx + 1, Fields, !IO). + +:- pred output_mr_deep_copy_assignments(io.text_output_stream::in, + string::in, list(cs_du_field)::in, io::di, io::uo) is det. + +output_mr_deep_copy_assignments(_, _, [], !IO). +output_mr_deep_copy_assignments(Stream, IndentStr, + [cs_du_field(FieldName, FieldTypeStr) | Fields], !IO) :- + io.format(Stream, "%sn.%s = (%s) f(this.%s);\n", + [s(IndentStr), s(FieldName), s(FieldTypeStr), s(FieldName)], !IO), + output_mr_deep_copy_assignments(Stream, IndentStr, Fields, !IO). + %---------------------------------------------------------------------------% :- end_module ml_backend.mlds_to_cs_class. %---------------------------------------------------------------------------% diff --git a/library/builtin.m b/library/builtin.m index cf773f8ba1..0f961efc86 100644 --- a/library/builtin.m +++ b/library/builtin.m @@ -671,57 +671,32 @@ public static object deep_copy(object o) } System.Type t = o.GetType(); - System.Array arr; if (t.IsValueType) { + // Primitive value types (int, char, bool, ...) are immutable. return o; } else if (t == typeof(string)) { - // XXX For some reason we need to handle strings specially. - // It is probably something to do with the fact that they - // are a builtin type. - string s; - s = (string) o; - return s; - } else if ((arr = o as System.Array) != null) { + // Strings are immutable in .NET. + return (string) o; + } else if (o is System.Array arr) { + // Mercury arrays (object[] tuples and array.array values). + // Element references are shared; this matches the behaviour + // of the original reflection-based deep_copy. return arr.Clone(); + } else if (o is mercury.runtime.MR_DuTerm du) { + // Every C# class generated from a Mercury DU type ctor + // implements MR_DuTerm. The MR_DeepCopy method calls + // MemberwiseClone, then recurses on each positional field + // through this very deep_copy fn. + return du.MR_DeepCopy(deep_copy); } else { - object n; - - // This will do a bitwise shallow copy of the object. - n = t.InvokeMember(""MemberwiseClone"", - System.Reflection.BindingFlags.Instance | - System.Reflection.BindingFlags.NonPublic | - System.Reflection.BindingFlags.InvokeMethod, - null, o, new object[] {}); - - // Set each of the fields to point to a deep copy of the - // field. - deep_copy_fields(t.GetFields( - System.Reflection.BindingFlags.Public | - System.Reflection.BindingFlags.Instance), - n, o); - - // XXX This requires that mercury.dll have - // System.Security.Permissions.ReflectionPermission - // so that the non-public fields are accessible. - deep_copy_fields(t.GetFields( - System.Reflection.BindingFlags.NonPublic | - System.Reflection.BindingFlags.Instance), - n, o); - - return n; - } -} - -public static void deep_copy_fields(System.Reflection.FieldInfo[] fields, - object dest, object src) -{ - // XXX We don't handle init-only fields, but I can't think of a way. - foreach (System.Reflection.FieldInfo f in fields) - { - if (!f.IsNotSerialized) { - f.SetValue(dest, deep_copy(f.GetValue(src))); - } + // Closures, type infos, type class infos and other runtime + // structures are treated as immutable from Mercury's point + // of view, so a shallow alias is sufficient. (The previous + // reflection-based path walked their fields too, but doing + // so was redundant in practice and required full + // System.Reflection access incompatible with .NET trim/AOT.) + return o; } } "). diff --git a/library/rtti_implementation.m b/library/rtti_implementation.m index f4bcce92a4..8d001f4ab9 100644 --- a/library/rtti_implementation.m +++ b/library/rtti_implementation.m @@ -3786,67 +3786,16 @@ throw new Error( DuFunctorDesc functor_desc, int index, int num_extra_args) { - TypeCtorInfo_Struct base_tc = type_info.type_ctor.type_ctor_base; - string field_name; - - if (base_tc != null) { - // For subtypes, we need to get the corresponding DuFunctorDesc - // from the base type ctor. - DuFunctorDesc base_functor_desc = - ML_get_functor_desc_by_tags(base_tc, - functor_desc.du_functor_primary, - functor_desc.du_functor_secondary); - field_name = - ML_get_field_name_by_index(base_functor_desc, - index, num_extra_args); - } else { - field_name = - ML_get_field_name_by_index(functor_desc, - index, num_extra_args); - } - - return ML_get_subterm_by_field_name(term, field_name); - } - - private static DuFunctorDesc - ML_get_functor_desc_by_tags(TypeCtorInfo_Struct base_tc, - byte ptag, int sectag) - { - DuPtagLayout ptag_layout = base_tc.index_or_search_ptag_layout(ptag); - if (sectag == -1) { - sectag = 0; - } - return ptag_layout.index_or_search_sectag_functor(sectag); - } - - private static string - ML_get_field_name_by_index(DuFunctorDesc functor_desc, - int index, int num_extra_args) - { - // Look up the field name if it exists, otherwise recreate the field - // name that would have been used. - string field_name = null; - if (functor_desc.du_functor_arg_names != null) { - field_name = functor_desc.du_functor_arg_names[index]; - } - if (field_name != null) { - field_name = ML_name_mangle(field_name); - } else { - // The F field variables are numbered from 1. - int i = 1 + index + num_extra_args; - field_name = ""F"" + i; - } - return field_name; - } - - private static object - ML_get_subterm_by_field_name(object term, string field_name) - { - System.Reflection.FieldInfo f = term.GetType().GetField(field_name); - if (f == null) { - throw new System.Exception(""no such field: "" + field_name); - } - return f.GetValue(term); + // The MR_DuTerm interface (implemented by every C# class + // generated from a Mercury DU type ctor) gives us indexed + // positional-field access without System.Reflection. The + // term's runtime type carries its own MR_GetField switch, + // so we no longer need to reconstruct the field name from + // the functor descriptor (and so the subtype redirection + // through type_ctor_base is also unnecessary -- subtypes + // share the same C# class as their base type). + return ((mercury.runtime.MR_DuTerm) term).MR_GetField( + num_extra_args + index); } "). @@ -4136,7 +4085,7 @@ throw new Error( get_remote_secondary_tag(X::in) = (Tag::out), [will_not_call_mercury, promise_pure, thread_safe], " - Tag = (int) X.GetType().GetField(""data_tag"").GetValue(X); + Tag = ((mercury.runtime.MR_DuTerm) X).MR_GetSecondaryTag(); "). :- pragma foreign_proc("Java", get_remote_secondary_tag(X::in) = (Tag::out), @@ -4440,13 +4389,8 @@ throw new Error( if (Term is object[]) { TypeInfo = (runtime.TypeInfo_Struct) ((object[]) Term)[Index]; } else { - // The F field variables are numbered from 1. - string fieldName = ""F"" + (1 + Index); - System.Reflection.FieldInfo f = Term.GetType().GetField(fieldName); - if (f == null) { - throw new System.Exception(""no such field: "" + fieldName); - } - TypeInfo = (runtime.TypeInfo_Struct) f.GetValue(Term); + TypeInfo = (runtime.TypeInfo_Struct) + ((mercury.runtime.MR_DuTerm) Term).MR_GetField(Index); } "). @@ -4485,13 +4429,8 @@ throw new Error( if (Term is object[]) { TypeClassInfo = /*typeclass_info*/ (object[]) ((object[]) Term)[Index]; } else { - // The F field variables are numbered from 1. - string fieldName = ""F"" + (1 + Index); - System.Reflection.FieldInfo f = Term.GetType().GetField(fieldName); - if (f == null) { - throw new System.Exception(""no such field: "" + fieldName); - } - TypeClassInfo = /*typeclass_info*/ (object[]) f.GetValue(Term); + TypeClassInfo = /*typeclass_info*/ (object[]) + ((mercury.runtime.MR_DuTerm) Term).MR_GetField(Index); } "). diff --git a/runtime/mercury_dotnet.cs.in b/runtime/mercury_dotnet.cs.in index cf3207804e..eb5d2372a9 100644 --- a/runtime/mercury_dotnet.cs.in +++ b/runtime/mercury_dotnet.cs.in @@ -351,6 +351,35 @@ public enum TypeCtorRep { MR_TYPECTOR_REP_MAX = 56 } +// Interface implemented by every C# class generated from a Mercury +// discriminated-union type ctor (and its functor sub-classes). It lets +// the standard library walk a term's positional fields and read its +// secondary tag without resorting to System.Reflection, so mer_std +// stays compatible with the .NET trim/AOT toolchain (no IL2075). +// +// See csharp-aot-rtti-plan.md at the repo root for the design. +public interface MR_DuTerm { + // Get the i-th positional field of this term. + // F1 is index 0, F2 is index 1, ... + // Boxed for primitive-typed fields. Out-of-range index throws. + object MR_GetField(int index); + + // Number of positional F1..Fn fields on this term, *including* any + // leading typeinfo / typeclassinfo poly-arg fields. + int MR_GetFieldCount(); + + // Secondary tag value on this term, or -1 if the term carries + // no secondary tag. + int MR_GetSecondaryTag(); + + // Deep-copy this term: clone the object (preserving its runtime + // type) and replace every positional field with the result of + // applying deepCopyFn to it. The fn is the recursive deep-copy + // entry point in library/builtin.m so that nested DU terms, + // tuples, arrays, and primitives are all copied consistently. + object MR_DeepCopy(System.Func deepCopyFn); +} + public class PseudoTypeInfo { public readonly int variable_number; From 0d15a185cccfc119c3a46307cc20465de495470f Mon Sep 17 00:00:00 2001 From: Sebastian Godelet Date: Tue, 28 Apr 2026 19:36:44 +1000 Subject: [PATCH 12/25] Mark mer_std as Native-AOT-compatible. Now that the dynamic-RTTI helpers in rtti_implementation.m and builtin.deep_copy route every type-introspection call through the new MR_DuTerm interface (no System.Reflection.Type.GetField, no Type.GetMember, no Activator), the standard library's csproj can advertise itself as AOT-compatible to consumers built with `--csharp-aot'. Without this marker, `dotnet publish' raises IL2104 ("Assembly mer_std produced trim warnings") on every program that opts into Native AOT, even though no actual reflective access remains in mer_std. The change is mechanical: pass `--csharp-aot' when compiling mer_std so that csproj_content/8 emits an `true' property in the library marker form. The other Mercury libraries (mer_browser, mer_ssdb, mer_mdbcomp) keep their plain `true' marker for now -- they have not been audited for AOT-compatibility, so they would attract trim warnings that should not be hidden behind the stronger marker. A small follow-up cleanup in csproj_content/8 drops the redundant `true' line for AOT-marked libraries, since the .NET SDK already implies trim-compatibility from `'. The compiled assembly still surfaces both metadata strings (the SDK injects `IsTrimmable' as an assembly attribute under the AOT contract), but the on-disk csproj is no longer self-redundant. library/Mmakefile: Add a csharp-grade conditional that appends `--csharp-aot' to MCFLAGS, so the generated mer_std.csproj receives the `true' marker. The existing `--allow-stubs --no-warn-stubs' block above is left unchanged. compiler/link_target_code.m: In csproj_content/8, when the linked target is a `csharp_library' and the AotRequest is `aot_library_marker', suppress the `true' line. The `true' line emitted from the AOT property block already implies the trim contract, so the IsTrimmable line is redundant in that mode. All other AOT modes (aot_off, aot_publish/1) keep the IsTrimmable line as before. --- compiler/link_target_code.m | 10 +++++++++- library/Mmakefile | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/compiler/link_target_code.m b/compiler/link_target_code.m index 1b361d71c1..73ee29e776 100644 --- a/compiler/link_target_code.m +++ b/compiler/link_target_code.m @@ -1637,7 +1637,15 @@ LinkedTargetType = csharp_library, OutputType = "Library", UseAppHost = "false", - IsTrimmableLine = " true\n" + % `true' implies `true', + % so when AotRequest = aot_library_marker we skip the redundant + % IsTrimmable line below; the AotPropertyLines block emits the + % stronger marker instead. + ( if AotRequest = aot_library_marker then + IsTrimmableLine = "" + else + IsTrimmableLine = " true\n" + ) ), % Native AOT properties. For executables, switch on PublishAot, % nail the runtime identifier, force globalization-invariant data diff --git a/library/Mmakefile b/library/Mmakefile index f098fd8500..7516e7b16b 100644 --- a/library/Mmakefile +++ b/library/Mmakefile @@ -109,6 +109,20 @@ ifneq ("$(filter csharp% java%,$(GRADE))","") MCFLAGS += --allow-stubs --no-warn-stubs endif +# In csharp grade, mark mer_std as Native-AOT-compatible. The +# rtti_implementation.m / builtin.deep_copy paths route through the +# MR_DuTerm interface (no System.Reflection), so the .NET trim/AOT +# toolchain can statically prove the dynamic-RTTI machinery without +# IL2075 warnings. For a `csharp_library' target `--csharp-aot' only +# adds an `true' csproj marker; +# the build flow itself is unchanged. The other Mercury libraries +# (mer_browser, mer_ssdb, mer_mdbcomp) keep their plain +# `true' marker for now because they have not yet +# been audited for AOT-compatibility. +ifneq ("$(filter csharp%,$(GRADE))","") +MCFLAGS += --csharp-aot +endif + #-----------------------------------------------------------------------------# CFLAGS += $(DLL_CFLAGS) From df9e14d17d1baa45d1fb987cf41e034516efb26d Mon Sep 17 00:00:00 2001 From: Sebastian Godelet Date: Fri, 1 May 2026 21:52:41 +1000 Subject: [PATCH 13/25] Fix C# binary stream dual-buffering bug in io module. The C# backend's binary I/O was broken because mercury_open created both a BufferedStream and a StreamReader/StreamWriter wrapping the same underlying FileStream. The two independent buffers caused position desync: reading a byte via BufferedStream.ReadByte() would advance the FileStream past what StreamReader expected, making subsequent text reads (used by read_binary/write_binary) see premature EOF. Fix by splitting binary and text modes in mercury_open: - Binary modes (rb/wb/ab) no longer create StreamReader/StreamWriter. - mercury_stdin_binary/mercury_stdout_binary use null reader/writer. - mercury_getc: when reader is null, decode UTF-8 directly from the stream using new mercury_read_utf8_codepoint helper. - mercury_print_string/do_write_char: when writer is null, encode to UTF-8 bytes using new mercury_write_codepoint_to_stream helper (uses System.Text.Rune.EncodeToUtf8 from .NET 10). Also implement seek_binary_2 and binary_stream_offset_2 for C#, which were previously unimplemented (threw sorry). These use Stream.Seek and Stream.Position respectively, with putback-aware offset adjustment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- library/io.primitives_read.m | 79 +++++++++++++++++++++++++---- library/io.primitives_write.m | 65 ++++++++++++++++++------ library/io.stream_ops.m | 94 +++++++++++++++++++++++++++++++---- 3 files changed, 205 insertions(+), 33 deletions(-) diff --git a/library/io.primitives_read.m b/library/io.primitives_read.m index 4f58e2107d..452c2ec7f8 100644 --- a/library/io.primitives_read.m +++ b/library/io.primitives_read.m @@ -874,6 +874,36 @@ private static readonly string NewLine = System.Environment.NewLine; +// Read a single UTF-8 encoded code point directly from a binary stream. +// Returns -1 on EOF; throws on truncated or invalid UTF-8. +private static int +mercury_read_utf8_codepoint(System.IO.Stream s) +{ + int b0 = s.ReadByte(); + if (b0 == -1) return -1; + if (b0 <= 0x7F) return b0; + + int seqLen; + int acc; + if (b0 < 0xE0) { seqLen = 2; acc = b0 & 0x1F; } + else if (b0 < 0xF0) { seqLen = 3; acc = b0 & 0x0F; } + else { seqLen = 4; acc = b0 & 0x07; } + + for (int i = 1; i < seqLen; i++) { + int bn = s.ReadByte(); + if (bn == -1) { + throw new System.IO.IOException( + ""truncated UTF-8 sequence in binary stream""); + } + if ((bn & 0xC0) != 0x80) { + throw new System.IO.IOException( + ""invalid UTF-8 continuation byte in binary stream""); + } + acc = (acc << 6) | (bn & 0x3F); + } + return acc; +} + public static int mercury_getc(mercury.io__stream_ops.MR_MercuryFileStruct mf) { @@ -888,7 +918,15 @@ return c; } - c = mf.reader.Read(); + if (mf.reader != null) { + c = mf.reader.Read(); + } else { + // Binary stream: decode UTF-8 directly from mf.stream. + // This avoids dual-buffering between StreamReader and + // BufferedStream which causes position desync. + c = mercury_read_utf8_codepoint(mf.stream); + } + switch (mf.line_ending) { case mercury.io__stream_ops.ML_line_ending_kind.ML_raw_binary: case mercury.io__stream_ops.ML_line_ending_kind.ML_Unix_line_ending: @@ -908,10 +946,13 @@ // If not, we still need to treat this as a newline, and thus // increment the line counter. mf.line_number++; - } else if (System.Char.IsSurrogate((char) c)) { + } else if (mf.reader != null && System.Char.IsSurrogate((char) c)) { int c2 = mf.reader.Read(); c = System.Char.ConvertToUtf32((char) c, (char) c2); } + // Note: surrogate pairs cannot arise when reading UTF-8 + // byte-by-byte (the binary stream path), since UTF-8 decodes + // directly to full code points. } else /* c == NewLine[0] */ { switch (mercury.io__primitives_read.NewLine.Length) { case 1: @@ -919,13 +960,33 @@ c = '\\n'; break; case 2: - if (mf.reader.Peek() == - mercury.io__primitives_read.NewLine[1]) - { - mf.reader.Read(); - mf.line_number++; - c = '\\n'; - } else if (c == '\\n') { + if (mf.reader != null) { + if (mf.reader.Peek() == + mercury.io__primitives_read.NewLine[1]) + { + mf.reader.Read(); + mf.line_number++; + c = '\\n'; + } + } else { + // Binary stream: peek at next byte. + int nb = mf.stream.ReadByte(); + if (nb == mercury.io__primitives_read.NewLine[1]) { + mf.line_number++; + c = '\\n'; + } else if (nb != -1) { + // Put the byte back by seeking. + if (mf.stream.CanSeek) { + mf.stream.Seek(-1, + System.IO.SeekOrigin.Current); + } else { + // Non-seekable stream (e.g. pipe): + // store in putback. + mf.putback = nb; + } + } + } + if (c == '\\n') { // the input file was ill-formed, e.g. it contained only // raw CRs rather than CR-LF. Perhaps we should throw an // exception? If not, we still need to treat this diff --git a/library/io.primitives_write.m b/library/io.primitives_write.m index e9d8d6f907..7a63713f2a 100644 --- a/library/io.primitives_write.m +++ b/library/io.primitives_write.m @@ -1035,22 +1035,35 @@ " mercury.io__stream_ops.MR_MercuryFileStruct mf = Stream; try { - // See mercury_print_string(). - System.IO.TextWriter w = mf.writer; - if (Character == '\\n') { - switch (mf.line_ending) { - case mercury.io__stream_ops.ML_line_ending_kind.ML_raw_binary: - case mercury.io__stream_ops.ML_line_ending_kind.ML_Unix_line_ending: + if (mf.writer == null) { + // Binary stream: write UTF-8 bytes directly. + if (Character == '\\n') { + mf.stream.WriteByte(0x0A); + mf.line_number++; + } else { + mercury.io__primitives_write + .mercury_write_codepoint_to_stream( + mf.stream, Character); + } + } else { + // See mercury_print_string(). + System.IO.TextWriter w = mf.writer; + if (Character == '\\n') { + switch (mf.line_ending) { + case mercury.io__stream_ops.ML_line_ending_kind.ML_raw_binary: + case mercury.io__stream_ops.ML_line_ending_kind.ML_Unix_line_ending: + mercury.io__primitives_write.mercury_write_codepoint(w, + Character); + break; + case mercury.io__stream_ops.ML_line_ending_kind.ML_OS_line_ending: + w.WriteLine(""""); + break; + } + mf.line_number++; + } else { mercury.io__primitives_write.mercury_write_codepoint(w, Character); - break; - case mercury.io__stream_ops.ML_line_ending_kind.ML_OS_line_ending: - w.WriteLine(""""); - break; } - mf.line_number++; - } else { - mercury.io__primitives_write.mercury_write_codepoint(w, Character); } Error = null; } catch (System.SystemException e) { @@ -1201,16 +1214,40 @@ if (c <= 0xffff) { w.Write((char) c); } else { - w.Write(System.Char.ConvertFromUtf32(c)); + w.Write(new System.Text.Rune(c).ToString()); } } +// Write a single Unicode code point as UTF-8 bytes to a binary stream. +public static void +mercury_write_codepoint_to_stream(System.IO.Stream s, int c) +{ + System.Text.Rune rune = new System.Text.Rune(c); + System.Span buf = stackalloc byte[4]; + int written = rune.EncodeToUtf8(buf); + s.Write(buf.Slice(0, written)); +} + // Any changes here should also be reflected in the code for io.write_char, // which (for efficiency) uses its own inline code, rather than calling // this function. public static void mercury_print_string(mercury.io__stream_ops.MR_MercuryFileStruct mf, string s) { + if (mf.writer == null) { + // Binary stream: encode to UTF-8 and write directly to mf.stream. + // This avoids dual-buffering between StreamWriter and + // BufferedStream which causes position desync. + byte[] bytes = mercury.io__stream_ops.text_encoding.GetBytes(s); + mf.stream.Write(bytes, 0, bytes.Length); + for (int i = 0; i < s.Length; i++) { + if (s[i] == '\\n') { + mf.line_number++; + } + } + return; + } + switch (mf.line_ending) { case mercury.io__stream_ops.ML_line_ending_kind.ML_raw_binary: case mercury.io__stream_ops.ML_line_ending_kind.ML_Unix_line_ending: diff --git a/library/io.stream_ops.m b/library/io.stream_ops.m index 4ad1ec41b9..3d82d71809 100644 --- a/library/io.stream_ops.m +++ b/library/io.stream_ops.m @@ -294,7 +294,38 @@ throw new RuntimeException(""Invalid file opening mode: "" + Error = EINVAL; } "). -% MISSING C# seek_binary_2 +:- pragma foreign_proc("C#", + seek_binary_2(Stream::in, Flag::in, Off::in, Error::out, + _IO0::di, _IO::uo), + [will_not_call_mercury, promise_pure, thread_safe], +" + try { + System.IO.SeekOrigin origin; + switch (Flag) { + case 0: origin = System.IO.SeekOrigin.Begin; break; + case 1: origin = System.IO.SeekOrigin.Current; break; + case 2: origin = System.IO.SeekOrigin.End; break; + default: + throw new System.ArgumentException( + ""invalid seek flag: "" + Flag); + } + if (Stream.putback != -1) { + // A putback byte is buffered. If seeking from current position, + // the real stream position is one ahead of the logical position, + // so adjust. + if (Flag == 1) { + // Seeking relative: the logical position is one behind + // the stream position because of the putback. + Off--; + } + Stream.putback = -1; + } + Stream.stream.Seek(Off, origin); + Error = null; + } catch (System.Exception e) { + Error = e; + } +"). :- pragma foreign_proc("Java", seek_binary_2(Stream::in, Flag::in, Off::in, Error::out, _IO0::di, _IO::uo), @@ -329,7 +360,24 @@ throw new RuntimeException(""Invalid file opening mode: "" + Error = EINVAL; } "). -% MISSING C# binary_stream_offset_2 +:- pragma foreign_proc("C#", + binary_stream_offset_2(Stream::in, Offset::out, Error::out, + _IO0::di, _IO::uo), + [will_not_call_mercury, promise_pure, thread_safe], +" + try { + Offset = Stream.stream.Position; + if (Stream.putback != -1) { + // A byte has been put back but not yet consumed; the logical + // position is one behind the physical stream position. + Offset--; + } + Error = null; + } catch (System.Exception e) { + Offset = -1; + Error = e; + } +"). :- pragma foreign_proc("Java", binary_stream_offset_2(Stream::in, Offset::out, Error::out, _IO0::di, _IO::uo), @@ -1794,33 +1842,59 @@ protected MR_BinaryOutputFile initialValue() { // by multiple processes simultaneously. XXX Is this a good idea? share = System.IO.FileShare.ReadWrite; - if (openmode == ""r"" || openmode == ""rb"") { + if (openmode == ""r"") { // Like '<' in Bourne shell. - // Read a file. The file must exist already. + // Read a text file. The file must exist already. mode = System.IO.FileMode.Open; access = System.IO.FileAccess.Read; stream = System.IO.File.Open(filename, mode, access, share); reader = new System.IO.StreamReader(stream, mercury.io__stream_ops.text_encoding); writer = null; - } else if (openmode == ""w"" || openmode == ""wb"") { + } else if (openmode == ""rb"") { + // Read a binary file. The file must exist already. + // No StreamReader — binary streams use mf.stream directly + // for both byte-level and text (via UTF-8) operations, avoiding + // dual-buffering issues that break io.read_binary. + mode = System.IO.FileMode.Open; + access = System.IO.FileAccess.Read; + stream = System.IO.File.Open(filename, mode, access, share); + reader = null; + writer = null; + } else if (openmode == ""w"") { // Like '>' in Bourne shell. - // Overwrite an existing file, or create a new file. + // Overwrite an existing text file, or create a new file. mode = System.IO.FileMode.Create; access = System.IO.FileAccess.Write; stream = System.IO.File.Open(filename, mode, access, share); reader = null; writer = new System.IO.StreamWriter(stream, mercury.io__stream_ops.text_encoding); - } else if (openmode == ""a"" || openmode == ""ab"") { + } else if (openmode == ""wb"") { + // Overwrite an existing binary file, or create a new file. + // No StreamWriter — see ""rb"" comment above. + mode = System.IO.FileMode.Create; + access = System.IO.FileAccess.Write; + stream = System.IO.File.Open(filename, mode, access, share); + reader = null; + writer = null; + } else if (openmode == ""a"") { // Like '>>' in Bourne shell. - // Append to an existing file, or create a new file. + // Append to an existing text file, or create a new file. mode = System.IO.FileMode.Append; access = System.IO.FileAccess.Write; stream = System.IO.File.Open(filename, mode, access, share); reader = null; writer = new System.IO.StreamWriter(stream, mercury.io__stream_ops.text_encoding); + } else if (openmode == ""ab"") { + // Append to an existing binary file, or create a new file. + // No StreamWriter — see ""rb"" comment above. + mode = System.IO.FileMode.Append; + access = System.IO.FileAccess.Write; + stream = System.IO.File.Open(filename, mode, access, share); + reader = null; + writer = null; } else { runtime.Errors.SORRY(System.String.Concat( ""foreign code for this function, open mode:"", @@ -1871,10 +1945,10 @@ protected MR_BinaryOutputFile initialValue() { // XXX should we use BufferedStreams here? public static MR_MercuryFileStruct mercury_stdin_binary = mercury_file_init(System.Console.OpenStandardInput(), - System.Console.In, null, ML_line_ending_kind.ML_raw_binary); + null, null, ML_line_ending_kind.ML_raw_binary); public static MR_MercuryFileStruct mercury_stdout_binary = mercury_file_init(System.Console.OpenStandardOutput(), - null, System.Console.Out, ML_line_ending_kind.ML_raw_binary); + null, null, ML_line_ending_kind.ML_raw_binary); // Note: these are set again in io.init_state. public static MR_MercuryFileStruct mercury_current_text_input = From deab72bb35161bbfa93442710429edec35562b4e Mon Sep 17 00:00:00 2001 From: Sebastian Godelet Date: Fri, 1 May 2026 21:53:59 +1000 Subject: [PATCH 14/25] Use lowercase ""r"" round-trip format for C# float_to_string. The ""R"" format specifier in .NET produces uppercase ""E"" in scientific notation (e.g. 1.23E-21), but Mercury and the C backend use lowercase ""e"". Switch to ""r"" which produces lowercase directly, avoiding any post-processing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- library/string.format.m | 2 +- library/string.m | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library/string.format.m b/library/string.format.m index 7d728000a0..d4f4f4f8d9 100644 --- a/library/string.format.m +++ b/library/string.format.m @@ -1752,7 +1752,7 @@ // XXX According to the documentation it tries 15 digits of precision, // then 17 digits skipping 16 digits of precision, unlike what we do // for the C backend. - FloatString = FloatVal.ToString(""R""); + FloatString = FloatVal.ToString(""r""); "). :- pragma foreign_proc("Java", float_to_string_first_pass(FloatVal::in, FloatString::uo), diff --git a/library/string.m b/library/string.m index b2909f2a9a..17e59c6497 100644 --- a/library/string.m +++ b/library/string.m @@ -6884,12 +6884,12 @@ not is_surrogate(First0), } else if (System.Double.IsNegativeInfinity(Flt)) { Str = ""-infinity""; } else { - Str = Flt.ToString(""R""); + Str = Flt.ToString(""r""); // Append '.0' if there is no 'e' or '.' in the string. bool contains = false; foreach (char c in Str) { - if (c == 'e' || c == 'E' || c == '.') { + if (c == 'e' || c == '.') { contains = true; break; } From 159bf940f8bcea1c447c19e6788f65733309a1e3 Mon Sep 17 00:00:00 2001 From: Sebastian Godelet Date: Fri, 1 May 2026 21:54:27 +1000 Subject: [PATCH 15/25] Replace exception-based surrogate handling with guards in C# string ops. Three functions in string.m used try/catch around Char.ConvertToUtf32 to handle unpaired surrogates: unsafe_index, unsafe_index_next_repl_2, and unsafe_prev_index_repl_2. Replace with explicit IsHighSurrogate/ IsLowSurrogate checks which are cheaper and clearer. Also replace all Char.ConvertFromUtf32 calls with new System.Text.Rune(cp).ToString(), using the .NET 10 Rune type for consistent Unicode handling across the C# backend. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- library/string.m | 69 ++++++++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 38 deletions(-) diff --git a/library/string.m b/library/string.m index 17e59c6497..6d6a27ee06 100644 --- a/library/string.m +++ b/library/string.m @@ -2418,7 +2418,7 @@ } else if (cp <= 0xffff) { sb.Append((char) cp); } else { - sb.Append(System.Char.ConvertFromUtf32(cp)); + sb.Append(new System.Text.Rune(cp).ToString()); } CharList = list.det_tail(CharList); } @@ -2561,7 +2561,7 @@ } else if (c <= 0xffff) { arr[--size] = (char) c; } else { - string s = System.Char.ConvertFromUtf32(c); + string s = new System.Text.Rune(c).ToString(); arr[--size] = s[1]; arr[--size] = s[0]; } @@ -2989,16 +2989,17 @@ [will_not_call_mercury, promise_pure, thread_safe], " char c1 = Str[Index]; - Ch = c1; - if (System.Char.IsSurrogate(c1)) { - try { - char c2 = Str[Index + 1]; + if (System.Char.IsHighSurrogate(c1) && Index + 1 < Str.Length) { + char c2 = Str[Index + 1]; + if (System.Char.IsLowSurrogate(c2)) { Ch = System.Char.ConvertToUtf32(c1, c2); - } catch (System.ArgumentOutOfRangeException) { - // Return unpaired surrogate code point. - } catch (System.IndexOutOfRangeException) { - // Return unpaired surrogate code point. + } else { + // Unpaired high surrogate. + Ch = c1; } + } else { + // BMP character or unpaired low surrogate. + Ch = c1; } "). :- pragma foreign_proc("Java", @@ -3070,23 +3071,23 @@ does_not_affect_liveness, no_sharing], " ReplacedCodeUnit = -1; - try { - Ch = System.Char.ConvertToUtf32(Str, Index); - if (Ch <= 0xffff) { - NextIndex = Index + 1; - } else { + if (Index >= 0 && Index < Str.Length) { + char c1 = Str[Index]; + if (System.Char.IsHighSurrogate(c1) && Index + 1 < Str.Length + && System.Char.IsLowSurrogate(Str[Index + 1])) + { + Ch = System.Char.ConvertToUtf32(c1, Str[Index + 1]); NextIndex = Index + 2; + } else { + // BMP character or unpaired surrogate. + Ch = c1; + NextIndex = Index + 1; } SUCCESS_INDICATOR = true; - } catch (System.ArgumentOutOfRangeException) { + } else { Ch = 0; NextIndex = Index; SUCCESS_INDICATOR = false; - } catch (System.ArgumentException) { - // Return unpaired surrogate code point. - Ch = Str[Index]; - NextIndex = Index + 1; - SUCCESS_INDICATOR = true; } "). :- pragma foreign_proc("Java", @@ -3176,21 +3177,13 @@ SUCCESS_INDICATOR = false; } else { char c2 = Str[Index - 1]; - if (System.Char.IsLowSurrogate(c2)) { - try { - char c1 = Str[Index - 2]; - Ch = System.Char.ConvertToUtf32(c1, c2); - PrevIndex = Index - 2; - } catch (System.ArgumentOutOfRangeException) { - // Return unpaired surrogate code point. - Ch = (int) c2; - PrevIndex = Index - 1; - } catch (System.IndexOutOfRangeException) { - // Return unpaired surrogate code point. - Ch = (int) c2; - PrevIndex = Index - 1; - } + if (System.Char.IsLowSurrogate(c2) && Index >= 2 + && System.Char.IsHighSurrogate(Str[Index - 2])) + { + Ch = System.Char.ConvertToUtf32(Str[Index - 2], c2); + PrevIndex = Index - 2; } else { + // BMP character or unpaired surrogate. Ch = (int) c2; PrevIndex = Index - 1; } @@ -3353,7 +3346,7 @@ oldwidth = 1; } Str = Str0.Substring(0, Index) - + System.Char.ConvertFromUtf32(Ch) + + new System.Text.Rune(Ch).ToString() + Str0.Substring(Index + oldwidth); "). :- pragma foreign_proc("Java", @@ -4183,7 +4176,7 @@ if (Ch <= 0xffff) { Index = Str.IndexOf((char) Ch, BeginAt); } else { - string s = System.Char.ConvertFromUtf32(Ch); + string s = new System.Text.Rune(Ch).ToString(); Index = Str.IndexOf(s, BeginAt, System.StringComparison.Ordinal); } SUCCESS_INDICATOR = (Index >= 0); @@ -4239,7 +4232,7 @@ if (Ch <= 0xffff) { Index = Str.LastIndexOf((char) Ch); } else { - string s = System.Char.ConvertFromUtf32(Ch); + string s = new System.Text.Rune(Ch).ToString(); Index = Str.LastIndexOf(s, System.StringComparison.Ordinal); } SUCCESS_INDICATOR = (Index >= 0); From 460404182118b2322b212d15cd9d5b8dfc1d610a Mon Sep 17 00:00:00 2001 From: Sebastian Godelet Date: Fri, 1 May 2026 21:54:49 +1000 Subject: [PATCH 16/25] Remove 5 now-passing tests from EXPECT_FAIL_TESTS.csharp. With the binary stream fix, float formatting fix, and other improvements, the following tests now pass in the csharp grade: - hard_coded/write_binary (binary I/O fix) - hard_coded/foreign_name_mutable - hard_coded/intermod_foreign_type - hard_coded/print_stream - hard_coded/stdlib_init Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/EXPECT_FAIL_TESTS.csharp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/EXPECT_FAIL_TESTS.csharp b/tests/EXPECT_FAIL_TESTS.csharp index b02fc9bd28..74977336aa 100644 --- a/tests/EXPECT_FAIL_TESTS.csharp +++ b/tests/EXPECT_FAIL_TESTS.csharp @@ -3,15 +3,10 @@ hard_coded/bug383 hard_coded/construct_mangle hard_coded/dst_test hard_coded/final_excp -hard_coded/foreign_name_mutable hard_coded/functor_ho_inst_excp_1 hard_coded/functor_ho_inst_excp_2 hard_coded/init_excp -hard_coded/intermod_foreign_type hard_coded/mutable_excp -hard_coded/print_stream hard_coded/seek_test -hard_coded/stdlib_init hard_coded/stream_putback_binary -hard_coded/write_binary hard_coded/write_xml From b03762214640cf7cd6a467648623b6bca25ba908 Mon Sep 17 00:00:00 2001 From: Sebastian Godelet Date: Fri, 1 May 2026 22:37:15 +1000 Subject: [PATCH 17/25] Fix C# InvalidCastException for multi/nondet delegate casts C# delegates (MethodPtrN_r0) are invariant on their type parameters. The compiler generates continuations with specific type instantiations (e.g. MethodPtr2_r0) but call sites cast stored continuations to MethodPtr2_r0. This throws InvalidCastException at runtime. Fix in two places: 1. library/exception.m: Add invoke_cont helper that uses System.Runtime.CompilerServices.Unsafe.As() to reinterpret the delegate reference without a type check. This is sound because all Mercury type parameters are boxed and the calling convention is identical. Unsafe.As is a JIT intrinsic (zero overhead, AOT-safe). 2. compiler/mlds_to_cs_stmt.m: Change method pointer call codegen from (() expr) to System.Runtime.CompilerServices.Unsafe.As<>(expr). This fixes the pattern globally for all newly compiled code. Fixes test_try_all and any other test exercising multi/nondet continuations with specific output types. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- compiler/mlds_to_cs_stmt.m | 14 +++++++++++++- library/exception.m | 24 ++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/compiler/mlds_to_cs_stmt.m b/compiler/mlds_to_cs_stmt.m index 0b3a59abe2..98c9cd6026 100644 --- a/compiler/mlds_to_cs_stmt.m +++ b/compiler/mlds_to_cs_stmt.m @@ -537,8 +537,20 @@ output_call_rval_for_csharp(Info, FuncRval, Stream, !IO) else % This is a call using a method pointer. + % We use Unsafe.As(object) rather than a C# cast because the + % compiler-generated continuations may have a more specific generic + % type instantiation than the call site expects (e.g. + % MethodPtr2_r0 vs + % MethodPtr2_r0). C# delegates are invariant on + % their type parameters, so a direct cast would throw + % InvalidCastException. Unsafe.As is a JIT intrinsic (zero + % overhead, AOT-safe) that reinterprets the reference. This is + % sound because all Mercury type parameters are boxed as object + % at runtime and the calling convention is identical. PtrTypeName = method_ptr_type_to_string(Info, ArgTypes, RetTypes), - io.format(Stream, "((%s) ", [s(PtrTypeName)], !IO), + io.format(Stream, + "System.Runtime.CompilerServices.Unsafe.As<%s>(", + [s(PtrTypeName)], !IO), output_call_rval_for_csharp(Info, FuncRval, Stream, !IO), io.write_string(Stream, ")", !IO) ), diff --git a/library/exception.m b/library/exception.m index 8a3248a3ed..88c51ad8a9 100644 --- a/library/exception.m +++ b/library/exception.m @@ -814,7 +814,7 @@ pred catch_impl(pred(T), handler(T), T). exception.ssdb_hooks.on_catch_impl_exception(CSN); object T = exception.ML_call_handler_det(TypeInfo_for_T, Handler, (univ.Univ_0) ex.exception); - ((runtime.MethodPtr2_r0) cont)(T, cont_env_ptr); + exception.invoke_cont(cont, T, cont_env_ptr); } // Not really used. @@ -835,7 +835,7 @@ pred catch_impl(pred(T), handler(T), T). exception.ssdb_hooks.on_catch_impl_exception(CSN); object T = exception.ML_call_handler_det(TypeInfo_for_T, Handler, (univ.Univ_0) ex.exception); - ((runtime.MethodPtr2_r0) cont)(T, cont_env_ptr); + exception.invoke_cont(cont, T, cont_env_ptr); } // Not really used. @@ -1448,6 +1448,26 @@ public virtual void on_catch_impl_exception(int CSN) {} } public static SsdbHooks ssdb_hooks = new SsdbHooks(); + +// Invoke a multi/nondet continuation without a generic-delegate cast. +// +// The compiler generates continuations as MethodPtr2_r0 +// where ConcreteT is the instantiated output type (e.g. Exception_result_1). +// The catch_impl foreign_proc only knows the continuation as 'object', and +// MethodPtr2_r0 delegates are invariant, so a direct cast to +// MethodPtr2_r0 throws InvalidCastException. +// +// Unsafe.As(object) is a JIT intrinsic (zero-overhead, AOT-safe) that +// reinterprets the reference without a runtime type check. This is sound +// because (a) all Mercury type parameters are boxed reference types, so the +// calling convention is identical, and (b) the value passed as the first +// argument always has the correct runtime type. +[System.Runtime.CompilerServices.MethodImpl( + System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] +private static void invoke_cont(object cont, object arg, object env) { + System.Runtime.CompilerServices.Unsafe + .As>(cont)(arg, env); +} "). %---------------------------------------------------------------------------% From ff265fe8b1c86af804c03d831d68a85370c3fa6a Mon Sep 17 00:00:00 2001 From: Sebastian Godelet Date: Fri, 1 May 2026 23:18:40 +1000 Subject: [PATCH 18/25] Replace null with Array.Empty() for empty C# arrays The C# backend previously used null to represent empty arrays because the element type was not known at make_empty_array time. Array.Empty() provides a zero-length array that eliminates NullReferenceException in Clone(), Length, and other method calls on empty arrays. - ML_new_array, ML_array_resize, ML_shrink_array: return Array.Empty instead of null for size-0 arrays - make_empty_array: replace null with Array.Empty() - max, size: remove null guards (Length works on empty arrays) - ML_array_resize, ML_shrink_array: also check Length == 0 alongside null for backward compat with any pre-existing null arrays The compiler still generates (int[]) casts on from_list results, which will need a follow-up fix to handle the object[] returned by Array.Empty(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- library/array.m | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/library/array.m b/library/array.m index 4289ec88bb..ff4b54881d 100644 --- a/library/array.m +++ b/library/array.m @@ -1175,7 +1175,7 @@ { System.Array arr; if (Size == 0) { - return null; + return System.Array.Empty(); } if ( Item is int || Item is uint || Item is sbyte || Item is byte || @@ -1214,9 +1214,9 @@ ML_array_resize(System.Array arr0, int Size, object Item) { if (Size == 0) { - return null; + return System.Array.Empty(); } - if (arr0 == null) { + if (arr0 == null || arr0.Length == 0) { return ML_new_array(Size, Item); } if (arr0.Length == Size) { @@ -1283,8 +1283,8 @@ public static System.Array ML_shrink_array(System.Array arr, int Size) { - if (arr == null) { - return null; + if (arr == null || arr.Length == 0) { + return System.Array.Empty(); } // We need to use Item here to determine the type instead of arr itself @@ -1707,14 +1707,7 @@ make_empty_array(Array::array_uo), [will_not_call_mercury, promise_pure, thread_safe], " - // XXX A better solution than using the null pointer to represent - // the empty array would be to create an array of size 0. However, - // we need to determine the element type of the array before we can - // do that. This could be done by examining the RTTI of the array - // type and then using System.Type.GetType("""") to - // determine it. However constructing the string is - // a non-trivial amount of work. - Array = null; + Array = System.Array.Empty(); "). :- pragma foreign_proc("Java", make_empty_array(Array::array_uo), @@ -2295,11 +2288,7 @@ max(Array::in, Max::out), [will_not_call_mercury, promise_pure, thread_safe], " - if (Array != null) { - Max = Array.Length - 1; - } else { - Max = -1; - } + Max = Array.Length - 1; "). :- pragma foreign_proc("Java", max(Array::in, Max::out), @@ -2380,11 +2369,7 @@ size(Array::in, Max::out), [will_not_call_mercury, promise_pure, thread_safe], " - if (Array != null) { - Max = Array.Length; - } else { - Max = 0; - } + Max = Array.Length; "). :- pragma foreign_proc("Java", size(Array::in, Max::out), From c8ca9a967c0dcf69a8dfafb83eec8cc9b75f8602 Mon Sep 17 00:00:00 2001 From: Sebastian Godelet Date: Sat, 2 May 2026 08:53:26 +1000 Subject: [PATCH 19/25] Fix C# InvalidCastException for typed arrays from Array.Empty() The C# backend generates hard casts like (int[]) when the target type is a typed primitive array. Since Array.Empty() returns object[], this cast throws InvalidCastException at runtime for empty arrays. Fix: when casting to a typed primitive array (int[], double[], bool[], etc.), generate `(expr as T[] ?? System.Array.Empty())` instead of `(T[]) expr`. This safely handles both the correctly-typed case (non-empty arrays are already int[]) and the empty object[] case (as T[] returns null, ?? provides a properly-typed empty array). Unblocks hard_coded/array_sort and other tests using typed arrays from empty lists in the csharp grade. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- compiler/mlds_to_cs_data.m | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/compiler/mlds_to_cs_data.m b/compiler/mlds_to_cs_data.m index 8a15ef04ac..3200358d2d 100644 --- a/compiler/mlds_to_cs_data.m +++ b/compiler/mlds_to_cs_data.m @@ -322,6 +322,20 @@ io.write_string(Stream, "runtime.TypeInfo_Struct.maybe_new(", !IO), output_rval_for_csharp(Info, Expr, Stream, !IO), io.write_string(Stream, ")", !IO) + else if + Type = mlds_mercury_array_type(ElementType), + csharp_builtin_type(ElementType, ElementTypeStr) + then + % When casting to a typed primitive array (e.g. int[]), the source + % may be an object[] returned by Array.Empty(). Use + % `as T[] ?? System.Array.Empty()` to handle both the correctly + % typed case and the empty object[] case without throwing + % InvalidCastException. + TypeStr = type_to_string_for_csharp(Info, Type), + io.write_string(Stream, "(", !IO), + output_rval_for_csharp(Info, Expr, Stream, !IO), + io.format(Stream, " as %s ?? System.Array.Empty<%s>())", + [s(TypeStr), s(ElementTypeStr)], !IO) else % While the Java backend represents Mercury enums as Java classes % with a value field, the C# backend represents them as C# enums. From 3c1088d47be0d329d0a8e5300694a0378b71b82e Mon Sep 17 00:00:00 2001 From: Sebastian Godelet Date: Sat, 2 May 2026 09:39:21 +1000 Subject: [PATCH 20/25] Remove 3 now-passing tests from EXPECT_FAIL_TESTS.csharp array_sort: fixed by the typed array cast fix (4621ae8d8) which generates \\s T[] ?? System.Array.Empty()\\ instead of \\(T[]) expr\\. bug383, seek_test: were already working in the csharp grade after the earlier fixes on this branch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/EXPECT_FAIL_TESTS.csharp | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/EXPECT_FAIL_TESTS.csharp b/tests/EXPECT_FAIL_TESTS.csharp index 74977336aa..2c846ca190 100644 --- a/tests/EXPECT_FAIL_TESTS.csharp +++ b/tests/EXPECT_FAIL_TESTS.csharp @@ -1,5 +1,3 @@ -hard_coded/array_sort -hard_coded/bug383 hard_coded/construct_mangle hard_coded/dst_test hard_coded/final_excp @@ -7,6 +5,5 @@ hard_coded/functor_ho_inst_excp_1 hard_coded/functor_ho_inst_excp_2 hard_coded/init_excp hard_coded/mutable_excp -hard_coded/seek_test hard_coded/stream_putback_binary hard_coded/write_xml From cbea8ba113d68a34e0dc85518ba8b67558395a2e Mon Sep 17 00:00:00 2001 From: Sebastian Godelet Date: Sat, 2 May 2026 10:54:13 +1000 Subject: [PATCH 21/25] Fix C# DST handling and Errors.SORRY/fatal_error output library/time.m: Replace IsAmbiguousTime-based DST disambiguation with the same algorithm used by the Java backend: convert first, then check whether the result is in DST and adjust by the savings amount if it does not match the caller's DST indicator. This fixes dst_test for the csharp grade. runtime/mercury_dotnet.cs.in: Change Errors.SORRY and Errors.fatal_error to print to stderr and exit, matching C's MR_fatal_error() behaviour. Use explicit \n to avoid Windows \r\n line endings. This fixes functor_ho_inst_excp_1 and functor_ho_inst_excp_2 for csharp. tests/EXPECT_FAIL_TESTS.csharp: Remove dst_test, functor_ho_inst_excp_1, functor_ho_inst_excp_2, and stream_putback_binary (now passing in csharp grade). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- library/time.m | 43 +++++++++++++++++++++++++++------- runtime/mercury_dotnet.cs.in | 18 +++++++++----- tests/EXPECT_FAIL_TESTS.csharp | 4 ---- 3 files changed, 46 insertions(+), 19 deletions(-) diff --git a/library/time.m b/library/time.m index 66fb0b77d6..bf066a88d7 100644 --- a/library/time.m +++ b/library/time.m @@ -936,21 +936,46 @@ "). :- pragma foreign_proc("C#", target_mktime(Yr::in, Mnt::in, MD::in, Hrs::in, Min::in, Sec::in, - _YD::in, _WD::in, _N::in, IsOk::out, Time::out, ErrorMsg::out, + _YD::in, _WD::in, N::in, IsOk::out, Time::out, ErrorMsg::out, _IO0::di, _IO::uo), [will_not_call_mercury, promise_pure], " - // We don't use YD, WD and N. - // XXX Ignoring N, the daylight savings time indicator, is bad. - // On the day when you switch back to standard time from daylight - // savings time, the time '2:30am' occurs twice, once during daylight - // savings time (N = 1), and then again an hour later, during standard - // time (N = 0). The .NET API does not seem to provide any way - // to get the right answer in both cases. try { System.DateTime local_time = new System.DateTime(Yr + 1900, Mnt + 1, MD, Hrs, Min, Sec); - Time = local_time.ToUniversalTime(); + + // For ambiguous local times, ToUniversalTime assumes standard time. + System.DateTime utcTime = local_time.ToUniversalTime(); + + if (N != -1) { + // Correct for DST, following the same algorithm as the Java + // implementation: convert first, then check whether the result + // is in DST, and adjust by the savings amount if it does not + // match what the caller requested. + System.TimeZoneInfo tz = System.TimeZoneInfo.Local; + System.DateTimeOffset utcDto = + new System.DateTimeOffset(utcTime, System.TimeSpan.Zero); + bool isDst = tz.IsDaylightSavingTime(utcDto); + + if ((N == 1 && !isDst) || (N == 0 && isDst)) { + System.TimeSpan savings = System.TimeSpan.Zero; + foreach (var rule in tz.GetAdjustmentRules()) { + if (rule.DateStart <= local_time.Date && + local_time.Date <= rule.DateEnd) + { + savings = rule.DaylightDelta; + break; + } + } + if (N == 1) { + utcTime = utcTime.Subtract(savings); + } else { + utcTime = utcTime.Add(savings); + } + } + } + + Time = utcTime; IsOk = mr_bool.YES; ErrorMsg = \"\"; } catch (System.ArgumentOutOfRangeException e) { diff --git a/runtime/mercury_dotnet.cs.in b/runtime/mercury_dotnet.cs.in index eb5d2372a9..418932da4e 100644 --- a/runtime/mercury_dotnet.cs.in +++ b/runtime/mercury_dotnet.cs.in @@ -1058,18 +1058,24 @@ public class SystemException : System.Exception public class Errors { + // SORRY and fatal_error match the behaviour of C's MR_fatal_error(): + // flush stdout, print the message on stderr, then exit. This avoids + // the stack-trace noise of an unhandled exception and keeps test + // output consistent across backends. + public static void SORRY(string s) { - string msg; - msg = System.String.Concat("Sorry, unimplemented: ", s); - throw new mercury.runtime.SystemException(msg); + System.Console.Out.Flush(); + System.Console.Error.Write( + "Mercury runtime: not yet implemented: " + s + "\n"); + System.Environment.Exit(1); } public static void fatal_error(string s) { - string msg; - msg = System.String.Concat("Fatal error: ", s); - throw new mercury.runtime.SystemException(msg); + System.Console.Out.Flush(); + System.Console.Error.Write("Mercury runtime: " + s + "\n"); + System.Environment.Exit(1); } } diff --git a/tests/EXPECT_FAIL_TESTS.csharp b/tests/EXPECT_FAIL_TESTS.csharp index 2c846ca190..cfbf43c2b7 100644 --- a/tests/EXPECT_FAIL_TESTS.csharp +++ b/tests/EXPECT_FAIL_TESTS.csharp @@ -1,9 +1,5 @@ hard_coded/construct_mangle -hard_coded/dst_test hard_coded/final_excp -hard_coded/functor_ho_inst_excp_1 -hard_coded/functor_ho_inst_excp_2 hard_coded/init_excp hard_coded/mutable_excp -hard_coded/stream_putback_binary hard_coded/write_xml From 34bcfdc52cb44c53b569d799a3e68c4d81d1b095 Mon Sep 17 00:00:00 2001 From: Sebastian Godelet Date: Sat, 2 May 2026 13:43:45 +1000 Subject: [PATCH 22/25] Emit C# record class for Mercury DU types DU representation classes (those implementing MR_DuTerm) are now emitted as 'record class' instead of plain 'class'. This gives us auto-generated structural Equals, GetHashCode, and ToString from C# 9+. The change is in mlds_to_cs_class.m: when the class implements the MR_DuTerm interface, we use 'record class' as the class kind. All three DU hierarchy levels (base class, secondary tag class, functor class) implement MR_DuTerm, so the entire DU hierarchy is consistently records. record class supports inheritance, generics, and [System.Serializable], so the existing codegen patterns are preserved unchanged. Tested: 27+ csharp-grade tests pass with no regressions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- compiler/mlds_to_cs_class.m | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/compiler/mlds_to_cs_class.m b/compiler/mlds_to_cs_class.m index e1f75131a0..ae2c4a66ea 100644 --- a/compiler/mlds_to_cs_class.m +++ b/compiler/mlds_to_cs_class.m @@ -93,10 +93,18 @@ GenericTypeParamsStr = "" ), io.format(Stream, "%s[System.Serializable]\n", [s(IndentStr)], !IO), - io.format(Stream, "%s%s%s%sclass %s%s\n", + % DU representation classes (those implementing MR_DuTerm) are emitted + % as record classes so that C# auto-generates structural Equals, + % GetHashCode, and ToString for them. + ( if list.member(ml_csharp_mr_du_term_interface, Implements) then + ClassKind = "record class" + else + ClassKind = "class" + ), + io.format(Stream, "%s%s%s%s%s %s%s\n", [s(IndentStr), s(AccessPrefix), s(OverridePrefix), s(ConstnessPrefix), - s(ClassNameStr), s(GenericTypeParamsStr)], !IO), + s(ClassKind), s(ClassNameStr), s(GenericTypeParamsStr)], !IO), SuperClassNames = get_superclass_names(Info, Inherits, Implements), ( SuperClassNames = [] From 0247cd3a41cce71d6edb65a813907f4540a48563 Mon Sep 17 00:00:00 2001 From: Sebastian Godelet Date: Sun, 3 May 2026 19:13:54 +1000 Subject: [PATCH 23/25] Remove --csharp-compiler / --csc plumbing. The csharp grade now uses `mmc --make' exclusively, which drives `dotnet build' on a generated csproj. The old `--csharp-compiler' / `--csharp-compiler-type' options and the `csc'/`mcs' configure probe were the last remnants of an mmake-driven csharp build path that no longer exists, so they only ever caused confusion when users tried to point them at a compiler that mmc would never invoke. This commit removes: - the user-facing `--csharp-compiler', `--csharp-compiler-type', `--output-csharp-compiler' and `--output-csharp-compiler-type' options; - the `csharp_compiler_type' ADT and the `g_csharp_compiler_type' slot on the globals record; - the `--with-csharp-compiler' configure switch and the `csc'/`mcs' probe in `m4/mercury.m4' (the existing `MERCURY_CHECK_DOTNET' macro at lines 277-341 already gates the csharp grade on a usable .NET 10 SDK); - the `@CSC@' / `@CSHARP_COMPILER_TYPE@' / `@CSHARP_DELAYSIGN_FLAG@' AC_SUBST hooks and every place that injects them into generated config (`scripts/Mercury.config.in', the four `*_FLAGS.in' files); - the dead `CSC = @CSC@' Mmake variable. The LIBGRADES test that decides whether to add `csharp' to the auto-detected default-grade list now keys off `$DOTNET' / `$DOTNET_SDK_DIR' instead of the dropped `$CSC' -- explicit `--enable-libgrades=...,csharp' already worked, but the auto- detection branch was broken until this fix. NEWS.md: Note the change under "Changes to the Mercury compiler". Documentation/README.CSharp.md: Drop the brief mention of an external csc/mcs compiler in the prerequisites section. compiler/options.m: Remove the four removed option entries from the `option' ADT, the `optdef'/`optdb' tables, the `long_option' table, and the optimisation/special-handling switch. compiler/op_mode.m: Drop the `opmq_output_csharp_compiler[_type]' query op modes. compiler/handle_options.m: Drop the corresponding cases from `convert_op_mode' and the callers of the removed query modes; thread the smaller arity of `globals_init' and `check_option_values' through the call sites. compiler/check_options.m: Remove the `csharp_compiler_type' argument from `check_option_values' and drop the `convert_csharp_compiler_type' block in `check_system_env_options'. compiler/globals.m: Remove `csharp_compiler_type', the `g_csharp_compiler_type' field, the matching getter, and the `convert_csharp_compiler_type' helper. compiler/mercury_compile_main.m: Drop the two `opmq_output_csharp_compiler{_type}' arms from the op-mode dispatcher. m4/mercury.m4: Remove `MERCURY_CHECK_CSHARP_COMPILER' and its callers. configure.ac: Remove `--with-csharp-compiler' and the call to `MERCURY_CHECK_CSHARP_COMPILER'. Switch the LIBGRADES csharp test from `$CSC' to `$DOTNET' / `$DOTNET_SDK_DIR'. scripts/Mercury.config.in: Drop `MERCURY_CSHARP_COMPILER', `MERCURY_CSHARP_COMPILER_TYPE', and the `--csharp-compiler[ -type]' flags they fed into `DEFAULT_MCFLAGS'. scripts/Mmake.vars.in: Drop the dead `CSC = @CSC@' line. Keep the `CSCFLAGS' / `ALL_CSCFLAGS' machinery, since `options_file.m' still forwards Mmake-set `CSCFLAGS' to `mmc' as `--csharp-flag' values that end up in the generated csproj. browser/MDB_FLAGS.in: mdbcomp/MDBCOMP_FLAGS.in: library/LIB_FLAGS.in: ssdb/SSDB_FLAGS.in: Drop the `@CSHARP_DELAYSIGN_FLAG@' substitution; the dotnet SDK csproj path does not honour delay-signing through this hook. tests/hard_coded/pretty_printer_stress_test.data: tests/warnings/help_text.err_exp: Update golden output to drop references to the removed options. --- Documentation/README.CSharp.md | 8 +- NEWS.md | 7 + browser/MDB_FLAGS.in | 1 - compiler/check_options.m | 28 +--- compiler/globals.m | 24 +-- compiler/handle_options.m | 12 +- compiler/mercury_compile_main.m | 8 - compiler/op_mode.m | 14 -- compiler/options.m | 20 --- configure.ac | 30 +--- library/LIB_FLAGS.in | 1 - m4/mercury.m4 | 148 +----------------- mdbcomp/MDBCOMP_FLAGS.in | 1 - scripts/Mercury.config.in | 5 - scripts/Mmake.vars.in | 5 +- ssdb/SSDB_FLAGS.in | 1 - .../pretty_printer_stress_test.data | 13 -- tests/warnings/help_text.err_exp | 13 -- 18 files changed, 31 insertions(+), 308 deletions(-) diff --git a/Documentation/README.CSharp.md b/Documentation/README.CSharp.md index 64c636c410..e288579f47 100644 --- a/Documentation/README.CSharp.md +++ b/Documentation/README.CSharp.md @@ -27,8 +27,7 @@ Prerequisites ------------- To use Mercury's C# backend you need a working .NET SDK at version 10.0 -or above. The `dotnet` command must be on your `PATH`; the SDK install -must contain the Roslyn compiler at `/Roslyn/bincore/csc.dll`. +or above. The `dotnet` command must be on your `PATH`. There is no longer any support for Mono or for the .NET Framework runtime. @@ -41,9 +40,8 @@ that is then compiled by the .NET SDK. Mercury's autoconfiguration script will install the `csharp` grade when it detects `dotnet` on your `PATH` along with a usable >= 10.0 SDK. -You can force this by passing `--with-csharp-compiler=dotnet` to -`./configure`, and you can check the result by running -`mmc --output-stdlib-grades` and looking for `csharp` in the list. +You can check the result by running `mmc --output-stdlib-grades` and +looking for `csharp` in the list. Compiling programs with the `csharp` grade ------------------------------------------ diff --git a/NEWS.md b/NEWS.md index 389e4bf077..32b864eff3 100644 --- a/NEWS.md +++ b/NEWS.md @@ -82,6 +82,13 @@ Changes that may break compatibility * We have dropped support for versions of MSVC before version 19.3 (Visual Studio 2022). +* The `csharp` grade now requires the .NET 10 SDK and uses `dotnet build` + to link assemblies. The compiler no longer invokes `csc` or `mcs` + directly. The following options and configure flags have been removed: + `--csharp-compiler`, `--csharp-compiler-type`, `--output-csharp-compiler`, + `--output-csharp-compiler-type`, and `--with-csharp-compiler`. Mono and + the .NET Framework runtime are no longer supported. + * The `--use-subdirs` and `--use-grade-subdirs` options now cause `.mh` files to be placed in a `Mercury/mhs` subdirectory instead of the current directory. This reduces clutter in the current directory, but may require diff --git a/browser/MDB_FLAGS.in b/browser/MDB_FLAGS.in index 2bee7ea6da..5d05890fa9 100644 --- a/browser/MDB_FLAGS.in +++ b/browser/MDB_FLAGS.in @@ -17,7 +17,6 @@ --c-include-directory ../mdbcomp/Mercury/mhs --c-include-directory ../mdbcomp/Mercury/mihs --csharp-flag -keyfile:../mercury.snk -@CSHARP_DELAYSIGN_FLAG@ -L../boehm_gc -L../runtime -L../library diff --git a/compiler/check_options.m b/compiler/check_options.m index 66e0b4faf2..0d66b3db24 100644 --- a/compiler/check_options.m +++ b/compiler/check_options.m @@ -34,7 +34,7 @@ compilation_target::out, word_size::out, gc_method::out, termination_norm::out, termination_norm::out, trace_level::out, trace_suppress_items::out, ssdb_trace_level::out, may_be_thread_safe::out, - c_compiler_type::out, csharp_compiler_type::out, + c_compiler_type::out, reuse_strategy::out, maybe(feedback_info)::out, env_type::out, env_type::out, env_type::out, limit_error_contexts_map::out, linked_target_ext_info_map::out, @@ -73,7 +73,7 @@ check_option_values(!OptionTable, Target, WordSize, GC_Method, TermNorm, Term2Norm, TraceLevel, TraceSuppress, SSTraceLevel, - MaybeThreadSafe, C_CompilerType, CSharp_CompilerType, + MaybeThreadSafe, C_CompilerType, ReuseStrategy, MaybeFeedbackInfo, HostEnvType, SystemEnvType, TargetEnvType, LimitErrorContextsMap, LinkExtMap, !:Specs, !IO) :- @@ -85,7 +85,7 @@ check_debug_options(!.OptionTable, TraceLevel, TraceSuppress, SSTraceLevel, !Specs), check_system_env_options(!.OptionTable, - C_CompilerType, CSharp_CompilerType, + C_CompilerType, HostEnvType, SystemEnvType, TargetEnvType, !Specs), check_hlds_dump_options(!OptionTable, !Specs), check_diagnostics_options(!.OptionTable, LimitErrorContextsMap, !Specs), @@ -335,11 +335,11 @@ ). :- pred check_system_env_options(option_table::in, - c_compiler_type::out, csharp_compiler_type::out, + c_compiler_type::out, env_type::out, env_type::out, env_type::out, list(error_spec)::in, list(error_spec)::out) is det. -check_system_env_options(OptionTable, C_CompilerType, CSharp_CompilerType, +check_system_env_options(OptionTable, C_CompilerType, HostEnvType, SystemEnvType, TargetEnvType, !Specs) :- lookup_string_option(OptionTable, c_compiler_type, C_CompilerTypeStr), ( if convert_c_compiler_type(C_CompilerTypeStr, C_CompilerTypePrime) then @@ -363,24 +363,6 @@ add_error(phase_options, CCTpec, !Specs) ), - lookup_string_option(OptionTable, csharp_compiler_type, - CSharp_CompilerTypeStr), - ( if - convert_csharp_compiler_type(CSharp_CompilerTypeStr, - CSharp_CompilerTypePrime) - then - CSharp_CompilerType = CSharp_CompilerTypePrime - else - CSharp_CompilerType = csharp_unknown, % dummy - CSCSpec = - [words("Invalid argument"), quote(CSharp_CompilerTypeStr), - words("to the"), quote("--csharp-compiler-type"), words("option;"), - words("must be")] ++ - quote_list_to_pieces("or", ["microsoft", "mono", "unknown"]) ++ - [suffix("."), nl], - add_error(phase_options, CSCSpec, !Specs) - ), - lookup_string_option(OptionTable, host_env_type, HostEnvTypeStr), ( if convert_env_type(HostEnvTypeStr, HostEnvTypePrime) then HostEnvType = HostEnvTypePrime diff --git a/compiler/globals.m b/compiler/globals.m index 9b793ceb65..4dda2c965d 100644 --- a/compiler/globals.m +++ b/compiler/globals.m @@ -175,13 +175,6 @@ :- type clang_version ---> clang_version(int, int, int). - % For the csharp backend, which csharp compiler are we using? - % -:- type csharp_compiler_type - ---> csharp_microsoft - ; csharp_mono - ; csharp_unknown. - :- type static_or_shared ---> sos_static ; sos_shared. @@ -359,8 +352,6 @@ is semidet. :- pred convert_c_compiler_type(string::in, c_compiler_type::out) is semidet. -:- pred convert_csharp_compiler_type(string::in, csharp_compiler_type::out) - is semidet. :- pred convert_static_or_shared(string::in, static_or_shared::out) is semidet. :- pred convert_reuse_strategy(string::in, int::in, reuse_strategy::out) @@ -404,7 +395,7 @@ op_mode::in, maybe(feedback_info)::in, file_install_cmd::in, trace_suppress_items::in, reuse_strategy::in, limit_error_contexts_map::in, linked_target_ext_info_map::in, - c_compiler_type::in, csharp_compiler_type::in, + c_compiler_type::in, static_or_shared::in, static_or_shared::in, set(static_or_shared)::in, maybe_stdlib_grades::in, compilation_target::in, subdir_setting::in, word_size::in, gc_method::in, termination_norm::in, termination_norm::in, @@ -429,8 +420,6 @@ linked_target_ext_info_map::out) is det. :- pred get_grade_dir(globals::in, string::out) is det. :- pred get_c_compiler_type(globals::in, c_compiler_type::out) is det. -:- pred get_csharp_compiler_type(globals::in, csharp_compiler_type::out) - is det. :- pred get_linkage(globals::in, static_or_shared::out) is det. :- pred get_mercury_linkage(globals::in, static_or_shared::out) is det. :- pred get_library_install_linkages(globals::in, set(static_or_shared)::out) @@ -809,10 +798,6 @@ Version > 0, C_CompilerType = cc_cl_arm64(yes(Version)). -convert_csharp_compiler_type("microsoft", csharp_microsoft). -convert_csharp_compiler_type("mono", csharp_mono). -convert_csharp_compiler_type("unknown", csharp_unknown). - convert_static_or_shared("static", sos_static). convert_static_or_shared("shared", sos_shared). @@ -934,7 +919,6 @@ % The sub-word-sized arguments, clustered together % to allow them to be packed together. - g_csharp_compiler_type :: csharp_compiler_type, % g_linkage holds the value of the --linkage option, while % g_mercury_linkage does the same for --mercury-linkage. g_linkage :: static_or_shared, @@ -958,7 +942,7 @@ globals_init(DefaultOptions, Options, OptTuple, OpMode, MaybeFeedback, FileInstallCmd, TraceSuppress, ReuseStrategy, LimitErrorContextsMap, LinkedTargetExtInfoMap, - C_CompilerType, CSharp_CompilerType, + C_CompilerType, Linkage, MercuryLinkage, LibLinkages, MaybeStdLibGradeSet, Target, SubdirSetting, WordSize, GC_Method, TerminationNorm, Termination2Norm, @@ -970,7 +954,7 @@ LimitErrorContextsMap, LinkedTargetExtInfoMap, "", C_CompilerType), Globals0 = globals(DefaultOptions, Options, OptTuple, OpMode, MaybeFeedback, FileInstallCmd, ExtDirsMaps0, MaybeStdLibGradeSet, - ReadOnlyGlobals0, CSharp_CompilerType, + ReadOnlyGlobals0, Linkage, MercuryLinkage, LibLinkages, Target, SubdirSetting, WordSize, GC_Method, TerminationNorm, Termination2Norm, @@ -1011,8 +995,6 @@ X = Globals ^ g_read_only ^ rog_grade_dir. get_c_compiler_type(Globals, X) :- X = Globals ^ g_read_only ^ rog_c_compiler_type. -get_csharp_compiler_type(Globals, X) :- - X = Globals ^ g_csharp_compiler_type. get_linkage(Globals, X) :- X = Globals ^ g_linkage. get_mercury_linkage(Globals, X) :- diff --git a/compiler/handle_options.m b/compiler/handle_options.m index e4235ee409..5fa08fe728 100644 --- a/compiler/handle_options.m +++ b/compiler/handle_options.m @@ -189,7 +189,7 @@ check_option_values(OptionTable0, OptionTable, Target, WordSize, GC_Method, TermNorm, Term2Norm, TraceLevel, TraceSuppress, SSTraceLevel, - MaybeThreadSafe, C_CompilerType, CSharp_CompilerType, + MaybeThreadSafe, C_CompilerType, ReuseStrategy, MaybeFeedbackInfo, HostEnvType, SystemEnvType, TargetEnvType, LimitErrorContextsMap, LinkExtMap, !:Specs, !IO), @@ -260,7 +260,7 @@ MaybeStdLibGrades, MaybeEnvOptFileMerStdLibDir, OptTuple, OpMode, Target, WordSize, GC_Method, TermNorm, Term2Norm, TraceLevel, TraceSuppress, SSTraceLevel, - MaybeThreadSafe, C_CompilerType, CSharp_CompilerType, + MaybeThreadSafe, C_CompilerType, Linkage, MercuryLinkage, LibraryInstallLinkages, ReuseStrategy, MaybeFeedbackInfo, HostEnvType, SystemEnvType, TargetEnvType, @@ -313,7 +313,7 @@ opt_tuple::in, op_mode::in, compilation_target::in, word_size::in, gc_method::in, termination_norm::in, termination_norm::in, trace_level::in, trace_suppress_items::in, ssdb_trace_level::in, - may_be_thread_safe::in, c_compiler_type::in, csharp_compiler_type::in, + may_be_thread_safe::in, c_compiler_type::in, static_or_shared::in, static_or_shared::in, set(static_or_shared)::in, reuse_strategy::in, maybe(feedback_info)::in, env_type::in, env_type::in, env_type::in, limit_error_contexts_map::in, @@ -325,7 +325,7 @@ MaybeStdLibGrades, MaybeEnvOptFileMerStdLibDir, !.OptTuple, OpMode, Target, WordSize, GC_Method, TermNorm, Term2Norm, TraceLevel, TraceSuppress, SSTraceLevel, - MaybeThreadSafe, C_CompilerType, CSharp_CompilerType, + MaybeThreadSafe, C_CompilerType, Linkage, MercuryLinkage, LibLinkages, ReuseStrategy, MaybeFeedbackInfo, HostEnvType, SystemEnvType, TargetEnvType, LimitErrorContextsMap, LinkExtMap, !Specs, !:Globals, !IO) :- @@ -394,7 +394,7 @@ % the options from which the subdir setting is computed. globals_init(DefaultOptionTable, OptionTable0, !.OptTuple, OpMode, MaybeFeedbackInfo, FileInstallCmd, TraceSuppress, ReuseStrategy, - LimitErrorContextsMap, LinkExtMap, C_CompilerType, CSharp_CompilerType, + LimitErrorContextsMap, LinkExtMap, C_CompilerType, Linkage, MercuryLinkage, LibLinkages, MaybeStdLibGrades, Target, use_cur_dir, WordSize, GC_Method, TermNorm, Term2Norm, TraceLevel, SSTraceLevel, MaybeThreadSafe, @@ -1904,8 +1904,6 @@ ; OpModeQuery = opmq_output_cflags ; OpModeQuery = opmq_output_c_include_directory_flags ; OpModeQuery = opmq_output_grade_defines - ; OpModeQuery = opmq_output_csharp_compiler - ; OpModeQuery = opmq_output_csharp_compiler_type ; OpModeQuery = opmq_output_java_class_dir ; OpModeQuery = opmq_output_link_command ; OpModeQuery = opmq_output_shared_lib_link_command diff --git a/compiler/mercury_compile_main.m b/compiler/mercury_compile_main.m index 733b904ee8..d827dd9dc7 100644 --- a/compiler/mercury_compile_main.m +++ b/compiler/mercury_compile_main.m @@ -299,14 +299,6 @@ OpModeQuery = opmq_output_c_include_directory_flags, get_c_include_dir_flags(Globals, CInclFlags), io.print_line(StdOutStream, CInclFlags, !IO) - ; - OpModeQuery = opmq_output_csharp_compiler, - globals.lookup_string_option(Globals, csharp_compiler, CSC), - io.print_line(StdOutStream, CSC, !IO) - ; - OpModeQuery = opmq_output_csharp_compiler_type, - globals.lookup_string_option(Globals, csharp_compiler_type, CSC_Type), - io.print_line(StdOutStream, CSC_Type, !IO) ; OpModeQuery = opmq_output_java_class_dir, % XXX LEGACY diff --git a/compiler/op_mode.m b/compiler/op_mode.m index 3ef0995bf8..9daf44f462 100644 --- a/compiler/op_mode.m +++ b/compiler/op_mode.m @@ -53,9 +53,6 @@ ; opmq_output_c_include_directory_flags ; opmq_output_grade_defines - ; opmq_output_csharp_compiler % C# compiler properties. - ; opmq_output_csharp_compiler_type - ; opmq_output_java_class_dir % Java properties. ; opmq_output_link_command % Linker properties. @@ -342,11 +339,6 @@ only_opmode_output_grade_defines - opm_top_query(opmq_output_grade_defines), - only_opmode_output_csharp_compiler - - opm_top_query(opmq_output_csharp_compiler), - only_opmode_output_csharp_compiler_type - - opm_top_query(opmq_output_csharp_compiler_type), - only_opmode_output_link_command - opm_top_query(opmq_output_link_command), only_opmode_output_shared_lib_link_command - @@ -458,12 +450,6 @@ ; MOPQ = opmq_output_grade_defines, Str = "--output-grade-defines" - ; - MOPQ = opmq_output_csharp_compiler, - Str = "--output-csharp-compiler" - ; - MOPQ = opmq_output_csharp_compiler_type, - Str = "--output-csharp-compiler-type" ; MOPQ = opmq_output_link_command, Str = "--output-link-command" diff --git a/compiler/options.m b/compiler/options.m index 212baa1b59..4d0fe2294a 100644 --- a/compiler/options.m +++ b/compiler/options.m @@ -240,8 +240,6 @@ ; only_opmode_output_link_command ; only_opmode_output_shared_lib_link_command ; only_opmode_output_library_link_flags - ; only_opmode_output_csharp_compiler - ; only_opmode_output_csharp_compiler_type ; only_opmode_output_java_class_dir ; only_opmode_output_optimization_options @@ -887,9 +885,7 @@ ; quoted_java_runtime_flag % C# - ; csharp_compiler ; cli_interpreter - ; csharp_compiler_type ; csharp_flags ; quoted_csharp_flag ; csharp_aot @@ -1526,13 +1522,6 @@ w("This includes the Mercury standard library, as well as any other"), w("libraries specified via either the"), opt("--ml"), w("or"), opt("-l"), w("option.")])). -optdb(oc_opmode, only_opmode_output_csharp_compiler, bool(no), - help("output-csharp-compiler", [ - w("Print to standard output the command for invoking"), - w("the C# compiler.")])). -optdb(oc_opmode, only_opmode_output_csharp_compiler_type, bool(no), - help("output-csharp-compiler-type", [ - w("Print to standard output the C# compiler's type.")])). optdb(oc_opmode, only_opmode_output_java_class_dir, bool(no), alt_help("output-java-class-directory", ["output-class-directory", "output-java-class-dir", @@ -4696,21 +4685,12 @@ % C# -optdb(oc_target_csharp, csharp_compiler, string("csc"), - arg_help("csharp-compiler", "csc", [ - cindex("C# compiler"), - w("Specify the name of the C# Compiler. The default is"), - samp("csc", ".")])). optdb(oc_target_csharp, cli_interpreter, string(""), arg_help("cli-interpreter", "prog", [ cindex("CIL interpreter"), w("Specify the program that implements the Common Language"), w("Infrastructure (CLI) execution environment, e.g."), samp("mono", ".")])). -optdb(oc_target_csharp, csharp_compiler_type, string("mono"), - % The `mmc' script will override the default with a value - % determined at configuration time for the above two options. - priv_arg_help("csharp-compiler-type", "{microsoft,mono,unknown}", [])). optdb(oc_target_csharp, csharp_flags, accumulating([]), arg_help("csharp-flags", "options", [ cindex("C# compiler options"), diff --git a/configure.ac b/configure.ac index 5551b80a84..495bf14aa3 100644 --- a/configure.ac +++ b/configure.ac @@ -1381,33 +1381,8 @@ AC_SUBST(MATH_LIB) # Microsoft.NET configuration # -AC_ARG_WITH(csharp-compiler, - AS_HELP_STRING([--with-csharp-compiler=], - [Specify which C Sharp compiler to use (default: - autodetect)]), - [mercury_cv_with_csharp_compiler="$withval"], - [mercury_cv_with_csharp_compiler=""]) MERCURY_CHECK_DOTNET -# The Roslyn (i.e. Microsoft) C# compiler does not support signing assemblies -# with a strong name on non-Windows platforms. If we are using that compiler -# on a non-Windows platform then only delay sign the assembly. - -CSHARP_DELAYSIGN_FLAG= -if test "$CSHARP_COMPILER_TYPE" = "microsoft"; then - case "$host" in - *cygwin* | *mingw*) - CSHARP_DELAYSIGN_FLAG= - ;; - - *) - CSHARP_DELAYSIGN_FLAG="--csharp-flag -delaysign" - ;; - esac -fi - -AC_SUBST([CSHARP_DELAYSIGN_FLAG]) - #-----------------------------------------------------------------------------# # Java configuration # @@ -3824,8 +3799,9 @@ then fi fi -# Add C# back-end grade, if a C# compiler is installed. -if test "$CSC" != "" -a "$enable_csharp_grade" = yes +# Add C# back-end grade, if a usable .NET SDK is installed. +if test "$DOTNET" != "" -a -n "$DOTNET_SDK_DIR" -a \ + "$enable_csharp_grade" = yes then LIBGRADES="$LIBGRADES csharp" fi diff --git a/library/LIB_FLAGS.in b/library/LIB_FLAGS.in index 5bd850809b..bf42583d3f 100644 --- a/library/LIB_FLAGS.in +++ b/library/LIB_FLAGS.in @@ -14,7 +14,6 @@ --c-include-directory ../runtime --c-include-directory ../robdd --csharp-flag -keyfile:../mercury.snk -@CSHARP_DELAYSIGN_FLAG@ -L../boehm_gc -L../runtime -L../library diff --git a/m4/mercury.m4 b/m4/mercury.m4 index f3bd6218b3..b758f9a6c6 100644 --- a/m4/mercury.m4 +++ b/m4/mercury.m4 @@ -297,12 +297,8 @@ AC_PATH_PROGS([CLI_INTERPRETER], [mono]) # Check for the dotnet SDK (.NET 10 or later). The dotnet binary # itself is required at link time: link_target_code.m drives the # csharp grade by generating a csproj and invoking `dotnet build'. -# The Roslyn csc.dll path (DOTNET_CSC_DLL) is recorded for diagnostics -# and possible future use; the per-module C# compiler is still chosen -# below from stand-alone csc.exe or Mono mcs. AC_PATH_PROG([DOTNET], [dotnet]) DOTNET_SDK_DIR= -DOTNET_CSC_DLL= if test -n "$DOTNET"; then AC_MSG_CHECKING([for a usable dotnet SDK (10.0 or later)]) # `dotnet --list-sdks' prints lines like: `10.0.200 [/path/to/sdk]', @@ -332,159 +328,17 @@ if test -n "$DOTNET"; then # subsequent shell quoting on MSYS, Cygwin and POSIX shells. DOTNET_SDK_BASE=`echo "$DOTNET_SDK_BASE" | tr '\\\\' '/'` DOTNET_SDK_DIR="$DOTNET_SDK_BASE/$DOTNET_SDK_VERSION" - if test -f "$DOTNET_SDK_DIR/Roslyn/bincore/csc.dll"; then - DOTNET_CSC_DLL="$DOTNET_SDK_DIR/Roslyn/bincore/csc.dll" - AC_MSG_RESULT([yes (version $DOTNET_SDK_VERSION)]) - else - AC_MSG_RESULT([no (csc.dll not found under $DOTNET_SDK_DIR)]) - DOTNET_SDK_DIR= - fi + AC_MSG_RESULT([yes (version $DOTNET_SDK_VERSION)]) else AC_MSG_RESULT([no (no SDK >= 10.0 found)]) fi fi -# Check for the C# (C sharp) compiler. -# csc is the Microsoft C# compiler. -# mcs is the Mono C# compiler targetting all runtime versions. -# (dmcs and gmcs are older aliases for the Mono C# compiler -# which we do not use.) - -AC_CACHE_SAVE -case "$mercury_cv_with_csharp_compiler" in - no) - AC_MSG_ERROR(invalid option --without-csharp-compiler) - exit 1 - ;; - yes) - AC_MSG_ERROR(missing argument to --with-csharp-compiler=... option) - exit 1 - ;; - "") - CSC_COMPILERS="csc mcs" - ;; - *) - CSC_COMPILERS="$mercury_cv_with_csharp_compiler" - ;; -esac - -AC_MSG_CHECKING([for a C sharp compiler]) -AC_MSG_RESULT() -for CANDIDATE_CSC0 in $CSC_COMPILERS; do - unset CANDIDATE_CSC - unset ac_cv_path_CANDIDATE_CSC - AC_CACHE_LOAD - AC_PATH_PROG([CANDIDATE_CSC], [$CANDIDATE_CSC0]) - - if test -z "$CANDIDATE_CSC"; then - continue; - fi - CANDIDATE_CSC=`basename "$CANDIDATE_CSC"` - -# Check that the compiler is suitable. - case "$CANDIDATE_CSC" in - csc*) - # The Microsoft C# compiler and the Chicken Scheme compiler share - # the same executable name, so if we find an executable named csc - # above, check that it is actually the Microsoft C# compiler, - # and if it is not, then try to use one of the other instead. - $CANDIDATE_CSC 2>&1 | grep "^Microsoft" >/dev/null - if test $? -ne 0 - then - AC_MSG_WARN([$CANDIDATE_CSC is not the Microsoft C sharp compiler]) - continue; - else - CSC="$CANDIDATE_CSC" - break; - fi - ;; - - *mcs) - # We want to check that the 'mcs' compiler supports generics. - # We test all the mono C sharp compilers in order to be more - # defensive. - AC_MSG_CHECKING([whether $CANDIDATE_CSC supports C sharp generics]) - - cat > conftest.cs << EOF - using System; - using System.Collections.Generic; - - class Hello - { - private class ExampleClass { } - - static void Main() - { - Console.WriteLine("Hello world!"); - - // Declare a list of type int. - List list1 = new List(); - - // Declare a list of type string. - List list2 = new List(); - - // Declare a list of type ExampleClass. - List list3 = new List(); - } - } -EOF - echo $CANDIDATE_CSC conftest.cs >&AS_MESSAGE_LOG_FD 2>&1 - OUTPUT=$($CANDIDATE_CSC conftest.cs 2>&1) - RESULT=$? - rm -f conftest.cs conftest.exe - echo $OUTPUT >&AS_MESSAGE_LOG_FD - echo returned $RESULT >&AS_MESSAGE_LOG_FD - if echo $OUTPUT | grep CS1644 > /dev/null; then - # This compiler does not support generics. - AC_MSG_RESULT(no) - continue; - elif test $RESULT -ne 0; then - AC_MSG_RESULT(no) - AC_MSG_WARN([$CANDIDATE_CSC returned exit code $RESULT]) - continue; - else - AC_MSG_RESULT(yes) - CSC="$CANDIDATE_CSC" - break; - fi - ;; - - *) - CSC="$CANDIDATE_CSC" - break; - ;; - esac -done - -# If the user specified one or more compilers and we couldn't find any, -# then abort configuration. -if test "$mercury_cv_with_csharp_compiler" != "" -a "$CSC" = ""; then - AC_MSG_ERROR([No suitable C sharp compiler could be found.]) - exit 1 -fi - -case "$CSC" in - csc*) - CSHARP_COMPILER_TYPE=microsoft - ;; - - mcs*) - CSHARP_COMPILER_TYPE=mono - ;; - - *) - CSHARP_COMPILER_TYPE=unknown - ;; -esac - AC_SUBST([ILASM]) AC_SUBST([GACUTIL]) -AC_SUBST([CSC]) -AC_SUBST([CSHARP_COMPILER_TYPE]) AC_SUBST([CLI_INTERPRETER]) AC_SUBST([DOTNET]) AC_SUBST([DOTNET_SDK_DIR]) -AC_SUBST([DOTNET_CSC_DLL]) ]) #-----------------------------------------------------------------------------# diff --git a/mdbcomp/MDBCOMP_FLAGS.in b/mdbcomp/MDBCOMP_FLAGS.in index e0b57974c0..03f5d61580 100644 --- a/mdbcomp/MDBCOMP_FLAGS.in +++ b/mdbcomp/MDBCOMP_FLAGS.in @@ -12,7 +12,6 @@ --c-include-directory ../library/Mercury/mhs --c-include-directory ../library/Mercury/mihs --csharp-flag -keyfile:../mercury.snk -@CSHARP_DELAYSIGN_FLAG@ -L../boehm_gc -L../runtime -L../library diff --git a/scripts/Mercury.config.in b/scripts/Mercury.config.in index 98f5c70819..b048e6a375 100644 --- a/scripts/Mercury.config.in +++ b/scripts/Mercury.config.in @@ -34,7 +34,6 @@ # # Environment variables: # -# MERCURY_CSHARP_COMPILER # MERCURY_C_COMPILER # MERCURY_DEFAULT_GRADE # MERCURY_DEFAULT_OPT_LEVEL @@ -51,8 +50,6 @@ MERCURY_C_COMPILER_TYPE=@C_COMPILER_TYPE@ MERCURY_MATH_LIB=@MATH_LIB@ MERCURY_JAVA_COMPILER=@JAVAC@ MERCURY_JAVA_INTERPRETER=@JAVA_INTERPRETER@ -MERCURY_CSHARP_COMPILER=@CSC@ -MERCURY_CSHARP_COMPILER_TYPE=@CSHARP_COMPILER_TYPE@ MERCURY_CLI_INTERPRETER=@CLI_INTERPRETER@ MERCURY_TARGET_ARCH=@FULLARCH@ MERCURY_HOST_ENV_TYPE=@HOST_ENV_TYPE@ @@ -88,8 +85,6 @@ DEFAULT_MCFLAGS=\ --c-compiler-type "$(MERCURY_C_COMPILER_TYPE)" \ --java-compiler "$(MERCURY_JAVA_COMPILER)" \ --java-interpreter "$(MERCURY_JAVA_INTERPRETER)" \ - --csharp-compiler "$(MERCURY_CSHARP_COMPILER)" \ - --csharp-compiler-type "$(MERCURY_CSHARP_COMPILER_TYPE)" \ --cli-interpreter "$(MERCURY_CLI_INTERPRETER)" \ --java-flags "@JAVAC_FLAGS_FOR_HEAP_SIZE_CONFIG@" \ --cflags-for-optimization "@CFLAGS_FOR_OPT@" \ diff --git a/scripts/Mmake.vars.in b/scripts/Mmake.vars.in index 182284a4d9..931abc31ad 100644 --- a/scripts/Mmake.vars.in +++ b/scripts/Mmake.vars.in @@ -265,7 +265,10 @@ LIB_CFLAGS = $(patsubst %,-I %,$(EXTRA_C_INCL_DIRS)) # Stuff for the C# back-end. # -CSC = @CSC@ +# mmc --make drives `dotnet build' directly for the csharp grade, +# so there is no Mmake-side C# compiler invocation. CSCFLAGS values +# set in an Mmake file are still forwarded to mmc as --csharp-flag +# arguments by options_file.m. ALL_CSCFLAGS = $(CSCFLAGS) $(EXTRA_CSCFLAGS) $(TARGET_CSCFLAGS) \ $(LIB_CSCFLAGS) CSCFLAGS = diff --git a/ssdb/SSDB_FLAGS.in b/ssdb/SSDB_FLAGS.in index 08d5bc7147..c049de30ba 100644 --- a/ssdb/SSDB_FLAGS.in +++ b/ssdb/SSDB_FLAGS.in @@ -21,7 +21,6 @@ --c-include-directory ../browser/Mercury/mhs --c-include-directory ../browser/Mercury/mihs --csharp-flag -keyfile:../mercury.snk -@CSHARP_DELAYSIGN_FLAG@ -L../boehm_gc -L../runtime -L../library diff --git a/tests/hard_coded/pretty_printer_stress_test.data b/tests/hard_coded/pretty_printer_stress_test.data index 77a8d30125..ba7422ad18 100644 --- a/tests/hard_coded/pretty_printer_stress_test.data +++ b/tests/hard_coded/pretty_printer_stress_test.data @@ -178,8 +178,6 @@ type option ; only_opmode_output_libgrades ; only_opmode_output_cc ; only_opmode_output_c_compiler_type - ; only_opmode_output_csharp_compiler - ; only_opmode_output_csharp_compiler_type ; only_opmode_output_cflags ; only_opmode_output_library_link_flags ; only_opmode_output_grade_defines @@ -666,7 +664,6 @@ type option ; object_file_extension ; pic_object_file_extension ; c_compiler_type - ; csharp_compiler_type ; java_compiler ; java_interpreter @@ -677,7 +674,6 @@ type option ; java_runtime_flags ; quoted_java_runtime_flag - ; csharp_compiler ; csharp_flags ; quoted_csharp_flag ; cli_interpreter @@ -1035,8 +1031,6 @@ optdef(oc_opmode, only_opmode_output_stdlib_grades, bool(no)), optdef(oc_opmode, only_opmode_output_libgrades, bool(no)), optdef(oc_opmode, only_opmode_output_cc, bool(no)), optdef(oc_opmode, only_opmode_output_c_compiler_type, bool(no)), -optdef(oc_opmode, only_opmode_output_csharp_compiler, bool(no)), -optdef(oc_opmode, only_opmode_output_csharp_compiler_type, bool(no)), optdef(oc_opmode, only_opmode_output_cflags, bool(no)), optdef(oc_opmode, only_opmode_output_library_link_flags, bool(no)), optdef(oc_opmode, only_opmode_output_grade_defines, bool(no)), @@ -1493,7 +1487,6 @@ optdef(oc_target_comp, c_flag_to_name_object_file, string("-o ")), optdef(oc_target_comp, object_file_extension, string(".o")), optdef(oc_target_comp, pic_object_file_extension, string(".o")), optdef(oc_target_comp, c_compiler_type, string("gcc")), -optdef(oc_target_comp, csharp_compiler_type, string("mono")), optdef(oc_target_comp, java_compiler, string("javac")), optdef(oc_target_comp, java_interpreter, string("java")), @@ -1504,7 +1497,6 @@ optdef(oc_target_comp, java_object_file_extension, string(".class")), optdef(oc_target_comp, java_runtime_flags, accumulating([])), optdef(oc_target_comp, quoted_java_runtime_flag, string_special), -optdef(oc_target_comp, csharp_compiler, string("csc")), optdef(oc_target_comp, csharp_flags, accumulating([])), optdef(oc_target_comp, quoted_csharp_flag, string_special), optdef(oc_target_comp, cli_interpreter, string("")), @@ -1896,9 +1888,6 @@ long_option("output-libgrades", only_opmode_output_libgrades), long_option("output-cc", only_opmode_output_cc), long_option("output-cc-type", only_opmode_output_c_compiler_type), long_option("output-c-compiler-type", only_opmode_output_c_compiler_type), -long_option("output-csharp-compiler", only_opmode_output_csharp_compiler), -long_option("output-csharp-compiler-type", - only_opmode_output_csharp_compiler_type), long_option("output-cflags", only_opmode_output_cflags), long_option("output-library-link-flags", only_opmode_output_library_link_flags), @@ -2496,7 +2485,6 @@ long_option("c-flag-to-name-object-file", c_flag_to_name_object_file), long_option("object-file-extension", object_file_extension), long_option("pic-object-file-extension", pic_object_file_extension), long_option("c-compiler-type", c_compiler_type), -long_option("csharp-compiler-type", csharp_compiler_type), long_option("java-compiler", java_compiler), long_option("javac", java_compiler), @@ -2512,7 +2500,6 @@ long_option("java-object-file-extension", java_object_file_extension), long_option("java-runtime-flags", java_runtime_flags), long_option("java-runtime-flag", quoted_java_runtime_flag), -long_option("csharp-compiler", csharp_compiler), long_option("csharp-flags", csharp_flags), long_option("csharp-flag", quoted_csharp_flag), long_option("cli-interpreter", cli_interpreter), diff --git a/tests/warnings/help_text.err_exp b/tests/warnings/help_text.err_exp index b55e7d7dc8..55aa1b1cb7 100644 --- a/tests/warnings/help_text.err_exp +++ b/tests/warnings/help_text.err_exp @@ -236,12 +236,6 @@ Options that give the compiler its overall task Mercury standard library, as well as any other libraries specified via either the `--ml' or `-l' option. - --output-csharp-compiler - Print to standard output the command for invoking the C# compiler. - - --output-csharp-compiler-type - Print to standard output the C# compiler's type. - --output-java-class-directory --output-class-directory --output-java-class-dir @@ -2793,17 +2787,10 @@ Options for target language compilation Options for compiling C# code - --csharp-compiler - Specify the name of the C# Compiler. The default is `csc'. - --cli-interpreter Specify the program that implements the Common Language Infrastructure (CLI) execution environment, e.g. `mono'. - PRIVATE OPTION - --csharp-compiler-type {microsoft,mono,unknown} - There is no help text available. - --csharp-flags --no-csharp-flags Specify options to be passed to the C# compiler. These options will not From ca0d4ec28c8bcc534b1329e6b4f03bf02c8e9f96 Mon Sep 17 00:00:00 2001 From: Sebastian Godelet Date: Mon, 4 May 2026 08:48:10 +1000 Subject: [PATCH 24/25] Multi-target the csharp libgrade against net10.0 + netstandard2.0. The csharp libgrade now ships every Mercury library as a multi-targeted assembly (net10.0 and netstandard2.0). Downstream consumers stuck on netstandard2.0 hosts (.NET Framework 4.6.1+, Unity, Mono, etc.) can now reference Mercury libraries directly; net10.0 consumers continue to get the AOT-/trim-friendly variant, and Mercury's own internal references keep using the netstandard2.0 DLL at the existing path. Executables stay single-target net10.0: an apphost ships against one runtime, and the multi-target machinery only buys complexity there. compiler/link_target_code.m: - For `csharp_library' targets, emit `' instead of the single `' line, with per-TFM `' and `' so the two builds do not clobber each other in `obj/Release/'. The netstandard2.0 DLL still lands at the csproj root (preserving the prior single-target convention so existing `' references and Mercury's library lookup keep working) and the net10.0 DLL goes to `./net10.0/'. - Scope `' / `' to non-netstandard2.0 TFMs via Condition= attributes, since both properties are net5.0+ only and warn under netstandard2.0. - Add a netstandard2.0-only `' for System.Runtime.CompilerServices.Unsafe v4.5.3. The compiler emits Unsafe.As in delegate-pointer call sites (mlds_to_cs_stmt.m), and the type is intrinsic on net5.0+ but lives in a NuGet package on netstandard2.0. Pinned to 4.5.3, the version paired with netstandard2.0 itself; we only use Unsafe.As(object) which has been in the package since 4.4.0. - Executables retain the single-target shape: `', unconditional `./' / `false' in the main PropertyGroup, no extra TFM-conditional groups. runtime/mercury_dotnet.cs.in: - Add an internal IsExternalInit polyfill guarded by `#if !NET5_0_OR_GREATER'. C# 9's `record class' (which mlds_to_cs_class.m emits for Mercury DU types) references this type via the synthesised members of records and via init-only accessors in any future use; the framework provides it inbox on net5.0+ but not on netstandard2.0. library/string.m: - Replace five uses of `new System.Text.Rune(cp).ToString()' with `System.Char.ConvertFromUtf32(cp)'. System.Text.Rune is net5.0+ only; ConvertFromUtf32 has identical semantics for valid scalar values and exists in netstandard2.0. library/io.primitives_write.m: - Replace `System.Text.Rune' uses in `mercury_write_codepoint' and `mercury_write_codepoint_to_stream' with the portable `System.Char.ConvertFromUtf32' / `System.Text.Encoding.UTF8.GetBytes' pair. This drops the previous Span/stackalloc/EncodeToUtf8 fast path, which was net5.0+ only, in favour of a string materialisation that works on both TFMs. The single-codepoint path is not performance-critical. library/math.m: - TFM-gate `fma' and `have_fma' for the C# backend. On net5.0+ both call System.Math.FusedMultiplyAdd. On netstandard2.0 there is no single-rounded FMA primitive, so `have_fma' returns false and `fma' throws System.NotSupportedException -- mirroring the C backend's fatal_error path when MR_HAVE_FMA is undefined. The previous unconditional `SUCCESS_INDICATOR = true' for `have_fma' was a lie on netstandard2.0 even before this change, since FusedMultiplyAdd was unresolvable. Verified: `make install' on Windows arm64 (clang-arm64 / .NET SDK 10.0.107) builds every standard library DLL clean for both TFMs, the generated mer_std.csproj contains the expected net10.0;netstandard2.0, hello.m links and runs (Hello, world / EXIT=0), and a multi-target user library produces both ./greeter.dll (netstandard2.0) and ./net10.0/greeter.dll without touching any other Mercury machinery. Follow-ups (not in this commit): only the netstandard2.0 DLL is copied into the install prefix; the net10.0 DLLs are produced in the build tree but not propagated. A consumer on a net10.0 host still works, because netstandard2.0 assemblies load on net5.0+, but they miss the AOT/trim-friendly variant. Installing both flavours requires Mmakefile changes to library/, mdbcomp/, browser/, ssdb/. --- compiler/link_target_code.m | 99 ++++++++++++++++++++++++++++++++--- library/io.primitives_write.m | 11 ++-- library/math.m | 20 ++++++- library/string.m | 10 ++-- runtime/mercury_dotnet.cs.in | 16 ++++++ 5 files changed, 136 insertions(+), 20 deletions(-) diff --git a/compiler/link_target_code.m b/compiler/link_target_code.m index 73ee29e776..cb048317ce 100644 --- a/compiler/link_target_code.m +++ b/compiler/link_target_code.m @@ -1628,23 +1628,100 @@ csproj_content(LinkedTargetType, AssemblyName, SourceList, RefEntries, Debug, ExtraCSCFlags, KeyFile, AotRequest, Content) :- + % Executables target a single runtime (apphost ships against one TFM); + % libraries multi-target net10.0 + netstandard2.0 so downstream + % consumers can pick either modern .NET or .NET Framework / Mono / + % Unity hosts that only understand netstandard2.0. ( LinkedTargetType = csharp_executable, OutputType = "Exe", UseAppHost = "true", - IsTrimmableLine = "" + TargetFrameworkLine = + " net10.0\n", + % Single-target: `` lives in the common PropertyGroup + % so the DLL/apphost lands beside the csproj. + MainOutputPathLines = string.append_list([ + " false", + "\n", + " ./\n" + ]), + ExtraPropertyGroups = "", + ExtraItemGroups = "", + % `` / `` are net5.0+ only, but the + % single-target executable case never picks netstandard2.0, so an + % unconditional emission stays safe. + IsTrimmableLine = "", + IsTrimmableTfmCondition = "" ; LinkedTargetType = csharp_library, OutputType = "Library", UseAppHost = "false", + TargetFrameworkLine = " " ++ + "net10.0;netstandard2.0\n", + MainOutputPathLines = "", + % For multi-targeted libraries, lay out the netstandard2.0 DLL at + % the csproj root (preserving the prior single-target convention + % so existing `` references and Mercury's internal + % library lookups keep working) and route the net10.0 DLL into a + % per-TFM subdirectory. Both `` and + % `false` need the per-TFM + % condition, because `` flips the latter's + % default to `true`. + % `` must also be per-TFM, otherwise the + % two builds share `obj//` and the second silently + % overwrites or skips the first. + ExtraPropertyGroups = string.append_list([ + " \n", + " ./\n", + " false", + "\n", + " ", + "obj/$(Configuration)/netstandard2.0/", + "\n", + " \n", + " \n", + " ./net10.0/\n", + " false", + "\n", + " ", + "obj/$(Configuration)/net10.0/", + "\n", + " \n" + ]), + % `System.Runtime.CompilerServices.Unsafe` is intrinsic on net5.0+ + % but lives in a standalone NuGet package on netstandard2.0. The + % compiler's MLDS-to-C# backend emits `Unsafe.As(...)` in + % delegate-pointer call sites (mlds_to_cs_stmt.m), so the package + % reference is mandatory for the netstandard2.0 build to compile. + % Pinned to 4.5.3 -- the version paired with netstandard2.0 + % itself. We only use Unsafe.As(object) which has been in the + % package since 4.4.0; bumping to 6.0.0 only changes signing + % metadata, not behaviour. + ExtraItemGroups = string.append_list([ + " \n", + " \n", + " \n" + ]), % `true' implies `true', % so when AotRequest = aot_library_marker we skip the redundant % IsTrimmable line below; the AotPropertyLines block emits the - % stronger marker instead. + % stronger marker instead. Both properties are net5.0+ only, so + % they must be scoped to non-netstandard2.0 TFMs to avoid MSBuild + % warnings on the netstandard2.0 half of the multi-target build. + IsTrimmableTfmCondition = + " Condition=""'$(TargetFramework)'!='netstandard2.0'""", ( if AotRequest = aot_library_marker then IsTrimmableLine = "" else - IsTrimmableLine = " true\n" + IsTrimmableLine = string.append_list([ + " true\n" + ]) ) ), % Native AOT properties. For executables, switch on PublishAot, @@ -1668,7 +1745,13 @@ ]) ; AotRequest = aot_library_marker, - AotPropertyLines = " true\n" + % `` is net5.0+ only. In the multi-target + % library case, scope it to non-netstandard2.0 TFMs so MSBuild + % does not warn during the netstandard2.0 build. + AotPropertyLines = string.append_list([ + " true\n" + ]) ), ( Debug = yes, @@ -1707,18 +1790,16 @@ "\n", "\n", " \n", - " net10.0\n", + TargetFrameworkLine, " 14\n", " disable\n", " ", OutputType, "\n", " ", xml_escape(AssemblyName), "\n", " mercury\n", " false\n", - " false", - "\n", + MainOutputPathLines, " false", "\n", - " ./\n", " ", UseAppHost, "\n", " ", DebugType, "\n", OptimizeLine, @@ -1732,12 +1813,14 @@ SignLines, DefineLine, " \n", + ExtraPropertyGroups, " \n"]), Middle = string.append_list([ " \n", " \n"]), Footer = string.append_list([ " \n", + ExtraItemGroups, "\n"]), Content = string.append_list([Header] ++ CompileItems ++ [Middle] ++ RefItems ++ [Footer]). diff --git a/library/io.primitives_write.m b/library/io.primitives_write.m index 7a63713f2a..b17fb4f934 100644 --- a/library/io.primitives_write.m +++ b/library/io.primitives_write.m @@ -1214,18 +1214,19 @@ if (c <= 0xffff) { w.Write((char) c); } else { - w.Write(new System.Text.Rune(c).ToString()); + w.Write(System.Char.ConvertFromUtf32(c)); } } // Write a single Unicode code point as UTF-8 bytes to a binary stream. +// We materialize the codepoint as a string so the same path works on +// every TFM we target (System.Text.Rune.EncodeToUtf8 is net5.0+ only). public static void mercury_write_codepoint_to_stream(System.IO.Stream s, int c) { - System.Text.Rune rune = new System.Text.Rune(c); - System.Span buf = stackalloc byte[4]; - int written = rune.EncodeToUtf8(buf); - s.Write(buf.Slice(0, written)); + byte[] bytes = System.Text.Encoding.UTF8.GetBytes( + System.Char.ConvertFromUtf32(c)); + s.Write(bytes, 0, bytes.Length); } // Any changes here should also be reflected in the code for io.write_char, diff --git a/library/math.m b/library/math.m index 9dbdcd576f..40247ce7f9 100644 --- a/library/math.m +++ b/library/math.m @@ -952,9 +952,15 @@ [will_not_call_mercury, promise_pure, thread_safe, will_not_modify_trail, does_not_affect_liveness], " - // System.Math.FusedMultiplyAdd is available on every supported - // .NET runtime (.NET Core 3.0+, .NET 5+). +#if NET5_0_OR_GREATER + // System.Math.FusedMultiplyAdd is intrinsic on net5.0+. SUCCESS_INDICATOR = true; +#else + // netstandard2.0 has no single-rounded FMA primitive; signal absence + // so callers can choose a different algorithm rather than silently + // accepting a double-rounded `X*Y + Z`. + SUCCESS_INDICATOR = false; +#endif "). have_fma :- @@ -979,7 +985,17 @@ [will_not_call_mercury, promise_pure, thread_safe, will_not_modify_trail, does_not_affect_liveness], " +#if NET5_0_OR_GREATER FMA = System.Math.FusedMultiplyAdd(X, Y, Z); +#else + // netstandard2.0 has no FMA primitive; mirror the C backend, which + // calls MR_fatal_error when MR_HAVE_FMA is undefined. Callers must + // gate this with `have_fma/0`. + FMA = 0.0; + throw new System.NotSupportedException( + ""math.fma not supported on this .NET target;"" + + "" check have_fma/0 before calling.""); +#endif "). fma(_, _, _) = _ :- diff --git a/library/string.m b/library/string.m index 6d6a27ee06..2177a9727a 100644 --- a/library/string.m +++ b/library/string.m @@ -2418,7 +2418,7 @@ } else if (cp <= 0xffff) { sb.Append((char) cp); } else { - sb.Append(new System.Text.Rune(cp).ToString()); + sb.Append(System.Char.ConvertFromUtf32(cp)); } CharList = list.det_tail(CharList); } @@ -2561,7 +2561,7 @@ } else if (c <= 0xffff) { arr[--size] = (char) c; } else { - string s = new System.Text.Rune(c).ToString(); + string s = System.Char.ConvertFromUtf32(c); arr[--size] = s[1]; arr[--size] = s[0]; } @@ -3346,7 +3346,7 @@ oldwidth = 1; } Str = Str0.Substring(0, Index) - + new System.Text.Rune(Ch).ToString() + + System.Char.ConvertFromUtf32(Ch) + Str0.Substring(Index + oldwidth); "). :- pragma foreign_proc("Java", @@ -4176,7 +4176,7 @@ if (Ch <= 0xffff) { Index = Str.IndexOf((char) Ch, BeginAt); } else { - string s = new System.Text.Rune(Ch).ToString(); + string s = System.Char.ConvertFromUtf32(Ch); Index = Str.IndexOf(s, BeginAt, System.StringComparison.Ordinal); } SUCCESS_INDICATOR = (Index >= 0); @@ -4232,7 +4232,7 @@ if (Ch <= 0xffff) { Index = Str.LastIndexOf((char) Ch); } else { - string s = new System.Text.Rune(Ch).ToString(); + string s = System.Char.ConvertFromUtf32(Ch); Index = Str.LastIndexOf(s, System.StringComparison.Ordinal); } SUCCESS_INDICATOR = (Index >= 0); diff --git a/runtime/mercury_dotnet.cs.in b/runtime/mercury_dotnet.cs.in index 418932da4e..897edab415 100644 --- a/runtime/mercury_dotnet.cs.in +++ b/runtime/mercury_dotnet.cs.in @@ -1287,3 +1287,19 @@ public class MercuryBitmap { } } + +// Polyfill for `init` accessors and the synthesised members of `record +// class` types when targeting netstandard2.0. The C# compiler emits +// references to System.Runtime.CompilerServices.IsExternalInit even for +// records that do not expose init-only properties, so the polyfill must +// be present for any TFM older than net5.0. The type is guarded by the +// netstandard-only branch so we do not collide with the framework +// definition on net5.0+. +#if !NET5_0_OR_GREATER +namespace System.Runtime.CompilerServices +{ + [System.ComponentModel.EditorBrowsable( + System.ComponentModel.EditorBrowsableState.Never)] + internal static class IsExternalInit { } +} +#endif From 77b67c87e7656540482dfec1e58c3c454901afa3 Mon Sep 17 00:00:00 2001 From: Sebastian Godelet Date: Mon, 4 May 2026 14:58:57 +1000 Subject: [PATCH 25/25] Use forward slashes when emitting Mercury subdirectory paths. `glue_dir_names_base_name' delegated to `dir.relative_path_name_from_components', which joins with the platform's native separator -- backslash on Windows. The strings this predicate returns flow into the `.d', `.dep' and `.dv' files that `mmake' reads, so on Windows we emitted rules and prerequisites like `Mercury\os\foo.o' that did not match the `Mercury/os/%.o' pattern rules in `Mmake.rules'. `make' silently treated those backslashed names as targets it could not build, and the bootstrap either skipped recompilation entirely or failed at the link step with `No rule to make target Mercury\os\mercury_compile_init.o'. Forward slash is universally accepted: Win32 file APIs normalise `/' to `\', the Mercury runtime's own I/O code already handles both, and `dir.is_directory_separator' treats `/' as the alternate separator on Windows. Hard-coding `"/"' in this one helper makes the makefile output consume cleanly under MSYS2 mmake without changing behaviour on Linux or macOS, where `/' was already the native separator. compiler/file_names.m: Replace the call to `dir.relative_path_name_from_components' in `glue_dir_names_base_name' with `string.join_list("/", Components)'. Add a comment explaining why the platform separator is the wrong choice here. --- compiler/file_names.m | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/compiler/file_names.m b/compiler/file_names.m index f84d8eb11e..6f2515e210 100644 --- a/compiler/file_names.m +++ b/compiler/file_names.m @@ -2058,7 +2058,12 @@ ; DirComponents = [_ | _], Components = DirComponents ++ [CurDirFileName], - FullFileName = dir.relative_path_name_from_components(Components) + % Always use "/" to glue the components together. The result is + % consumed by makefiles (which require "/" on every platform) and + % by the filesystem (Windows accepts "/" as a path separator). + % Using dir.relative_path_name_from_components/1 would emit "\" + % on Windows, which mmake's pattern rules fail to match. + FullFileName = string.join_list("/", Components) ). %---------------------------------------------------------------------------%