Update C# grade to .NET 10 - #146
Open
sebgod wants to merge 25 commits into
Open
Conversation
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 <sdk>/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.
Replace the per-module csc invocation in create_exe_or_lib_for_csharp/9
with a generated `<MainModule>.csproj' that the .NET SDK builds in one
shot. MSBuild produces the .dll, the apphost binary (`<exe>.exe' on
Windows, `<exe>' 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
<PublishTrimmed> (and, eventually, <PublishAot>) 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 <OutputPath>./</OutputPath>
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>.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 <IsTrimmable>true</IsTrimmable> 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 <csproj> -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 <TargetFramework>net10.0,
<LangVersion>14, disables nullable annotations, suppresses
EnableDefaultCompileItems, sets <UseAppHost>true on executables,
flags libraries as <IsTrimmable>true and pipes any /define: csharp
flags through to <DefineConstants>.
Add maybe_rename_apphost/5 that renames the bare apphost to
`<base>.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.
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.
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)'.
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 `<!-- Generated by Mercury (mmc --grade csharp). -->'
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.
…kend.
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.
…# 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.
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 <IsTrimmable>true</IsTrimmable> 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 <Private>true</Private> reference copying. Add a "Trimmed
publish" section noting that downstream consumers can opt into
`<PublishTrimmed>true</PublishTrimmed>'. 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.
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-<that whole string>' 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 <Compile Include>
entries relative to the csproj file location. With --use-subdirs
plus --use-grade-subdirs the csproj sits at
Mercury/csharp/<arch>/Mercury/bin/<name>.csproj
so a cwd-relative `Mercury/csharp/<arch>/Mercury/css/foo.cs'
becomes the doubly-nested
...bin/Mercury/csharp/<arch>/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 `<name>.dll',
`<name>.runtimeconfig.json' and `<name>.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: '<cwd>/<name>.dll'.
Companions that the build did not emit (e.g. `.deps.json' for
trivial programs) are skipped silently.
Switch the csharp linker between `dotnet build' (the default) and
`dotnet publish -p:PublishAot=true -r <rid>' 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 `<IsAotCompatible>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 <PublishAot>true</PublishAot>, <SelfContained>true</...>,
<InvariantGlobalization>true</...>, <RuntimeIdentifier>{Rid}</...>
and <PublishDir>./</...> to the property group. aot_library_marker
adds <IsAotCompatible>true</...>. aot_off adds nothing extra.
Branch the linker invocation: aot_publish runs `dotnet publish
<csproj> -c Release -r <rid> -v:quiet --nologo'; aot_off and
aot_library_marker keep the existing `dotnet build <csproj>
-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.
…Term 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<object,object>) 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<object,object> 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.<csharpName>'.
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.<fld> = (<typestr>) f(this.<fld>)' 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.
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 `<IsAotCompatible>true</...>'
property in the library marker form. The other Mercury libraries
(mer_browser, mer_ssdb, mer_mdbcomp) keep their plain
`<IsTrimmable>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
`<IsTrimmable>true</...>' line for AOT-marked libraries, since the
.NET SDK already implies trim-compatibility from
`<IsAotCompatible>'. 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
`<IsAotCompatible>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 `<IsTrimmable>true</...>' line. The
`<IsAotCompatible>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.
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>
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>
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>
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>
C# delegates (MethodPtrN_r0<T1, ..., TN>) are invariant on their type parameters. The compiler generates continuations with specific type instantiations (e.g. MethodPtr2_r0<Exception_result_1, object>) but call sites cast stored continuations to MethodPtr2_r0<object, object>. This throws InvalidCastException at runtime. Fix in two places: 1. library/exception.m: Add invoke_cont helper that uses System.Runtime.CompilerServices.Unsafe.As<T>() 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 ((<DelegateType>) expr) to System.Runtime.CompilerServices.Unsafe.As<<DelegateType>>(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>
The C# backend previously used null to represent empty arrays because the element type was not known at make_empty_array time. Array.Empty<object>() 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<object>() - 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<object>(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The C# backend generates hard casts like (int[]) when the target type is a typed primitive array. Since Array.Empty<object>() 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<T>())` 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>
array_sort: fixed by the typed array cast fix (4621ae8) which generates \\�s T[] ?? System.Array.Empty<T>()\\ 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>
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>
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>
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.
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 `<TargetFrameworks>' instead of the single `<TargetFramework>' line, with per-TFM `<OutputPath>' and `<IntermediateOutputPath>' 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 `<HintPath>' references and Mercury's library lookup keep working) and the net10.0 DLL goes to `./net10.0/'. - Scope `<IsTrimmable>' / `<IsAotCompatible>' 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 `<PackageReference>' for System.Runtime.CompilerServices.Unsafe v4.5.3. The compiler emits Unsafe.As<T> 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<T>(object) which has been in the package since 4.4.0. - Executables retain the single-target shape: `<TargetFramework>', unconditional `<OutputPath>./</...>' / `<AppendTargetFrameworkToOutputPath>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 <TargetFrameworks>net10.0;netstandard2.0</TargetFrameworks>, 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/.
`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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This pull request modernizes and refactors the Mercury C# backend to target .NET 10+ and C# 14, removing legacy support for Mono and the .NET Framework. The build and runtime flow is now fully based on the .NET SDK and MSBuild, simplifying deployment and enabling new features like Native AOT publishing and trimming. Additionally, the C# backend now emits richer type information to support reflection-free runtime type inspection, which is crucial for compatibility with .NET's trimming and AOT toolchains.
Key changes include:
C# Backend Modernization and .NET 10+ Migration
dotnetCLI and Roslyn, and all documentation, configuration, and build logic have been updated to reflect this. [1] [2] [3]csharpgrade now generates a.csprojand invokesdotnet build, producing all necessary assemblies and launchers without wrapper scripts or environment variable hacks. [1] [2]New Features: Trimming and Native AOT
--csharp-aotoption to build fully native, self-contained executables via .NET's Native AOT. The build system and generated.csprojfiles have been updated to support this, with clear documentation of AOT compatibility requirements. [1] [2]Runtime Type Information and Reflection-Free Introspection
MR_DuTerminterface. This allows runtime code to walk type fields and tags without usingSystem.Reflection, ensuring compatibility with .NET trimming and AOT. [1] [2] [3] [4] [5] [6] [7] [8] [9] [10]record classes, which auto-generate equality, hash, and string conversion methods.Documentation and Limitations
Other Improvements
library/io.file.mhas been modernized for .NET 10 compatibility, removing obsolete or now-unnecessary code.These changes collectively bring the Mercury C# backend up to date with modern .NET development practices, simplify the build and deployment process, and lay the groundwork for robust support of trimming and Native AOT scenarios.