Skip to content

refactor: split Helper.fs into root modules and declare InternalsVisibleTo in the project file - #40

Merged
xperiandri merged 10 commits into
mainfrom
chore/split-helper
Oct 19, 2026
Merged

xperiandri merged 10 commits into
mainfrom
chore/split-helper

Conversation

@xperiandri

@xperiandri xperiandri commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Proposed Changes

Two fixes from the review of #27 (FlatList.Parallel), made in a pull request of their own so that #27 can be stacked on it. It is based on #42, which lets FAKE write the assembly information as MSBuild properties. Both are comments on Helper.fs: "Move to fsproj with modern syntax <ItemGroup><InternalsVisibleTo..." and "Split to separate files with root modules." The review of this pull request added the small changes that are listed after them.

  • Helper.fs is split into the folder src/FSharp.Collections.Immutable/Helper/, one file per module, listed in the project file in the order of their dependencies: ImmutableCollectionUtil and Comparers (both [<AutoOpen>]), ErrorStrings, Raise, ArrayChecks and Arrays. Each file is a root module with the full name, as the other files of the library are (module internal FSharp.Collections.Immutable.Raise), and keeps the accessibility, the attributes, the nested types and the XML documentation of its module. It is a move: the bodies lose four spaces of indentation, each file opens the namespaces that it uses, and nothing else changes. The full names of the modules and of their members stay, so no other source file changes.
  • The #if INTERACTIVE ... namespace global conditional at the top of Helper.fs is dropped. A root module has no namespace declaration, and nothing loads Helper.fs into F# Interactive: no script or build file mentions it.
  • The library project gives the test assembly access to the internals with <InternalsVisibleTo Include="$(AssemblyBaseName).Tests" />, so that no source file needs an assembly attribute for it. The item works as it is, because the SDK generates the assembly attributes since build: let FAKE write the assembly information as MSBuild properties #42.
  • .github/copilot-instructions.md shows the Helper/ folder in the solution structure.
  • From the review: each constant of ErrorStrings names the resource that it copies, and the one text that is in no resource, ListsHaveDifferentLengths, is removed (88c5acd). readIsDefault is a piped helper (6dc9420). The messages of Raise are interpolated strings (985832d). The InternalsVisibleTo item is where the review asked for it, and the target is gone (8dd45dc). The properties that the item needed are gone too, because the review asked for FAKE to write the assembly information as MSBuild properties, which build: let FAKE write the assembly information as MSBuild properties #42 does (c355652). isDefaultImmutableArray tests IsValueType first, so a class is rejected by one flag (33164c5). The comment of every algorithm in Arrays starts with the summary of the FSharp.Core function that it mirrors and says why the algorithm exists (a8d940c, c14a85c).

Types of changes

What types of changes does your code introduce to FSharp.Collections.Immutable?
Put an x in the boxes that apply

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

None of these: a refactoring and a change of the project file. No public function changes.

Checklist

Put an x in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code.

  • Build and tests pass locally
  • I have added tests that prove my fix is effective or that my feature works (if appropriate)
  • I have added necessary documentation (if appropriate)

Further comments

No behavior changes, and the proof does not rest on the existing tests alone (2447 per target framework, all green):

  • To review the move, let git follow the lines: git diff --color-moved=dimmed-zebra --color-moved-ws=allow-indentation-change 5795c21 61aebe1 -- src/FSharp.Collections.Immutable/Helper.fs src/FSharp.Collections.Immutable/Helper. Of the changed lines, 891 show as moved; the 17 removed and 10 added lines that remain are the dropped conditional, the module headers and the opens. Taking four spaces off every indented line of the old file leaves the same few differences, so nothing else changed in the bodies.
  • Every open of the new files is needed: removing any of them fails to compile.
  • The built assemblies (net8.0 and net10.0) were listed by reflection before and after the move: every type, constructor, method, field, property and event, public and non-public, with attributes, signatures and parameter names, and for 851 method bodies a hash of the IL in which every metadata token is replaced by the name that it stands for. The lists are equal, and so is the generated XML documentation. The assemblies differ in what the compiler emits per source file, an empty <StartupCode$FSharp-Collections-Immutable>.$... class (one for Helper.fs before, one per module and one for the generated attribute file now), and in the new InternalsVisibleTo assembly attribute.
  • The messages of Raise were compared before and after the change to interpolated strings: a script called every function with 31 argument combinations in six cultures (invariant, en-US, de-DE, sv-SE, ar-SA, fa-IR), and the 186 results (exception type, parameter name and message) are identical.
  • Every text of ErrorStrings was looked up in the string resource tables of FSharp.Core 10.1, System.Private.CoreLib 10.0 and System.Collections.Immutable 10.0: 14 are verbatim resources of FSCore.resx, one is ArgumentOutOfRange_NeedNonNegNum of the .NET libraries, and the one that is in none, ListsHaveDifferentLengths, no code uses.

The InternalsVisibleTo item needs the SDK to generate the assembly attributes. Before #42, AssemblyInfo.fs came from the build script and Directory.Build.props switched the generation off, so the item alone did nothing, and a test file that used an internal module failed with FS0039. This pull request first carried properties that switched the generation on for the library and switched off the nine attributes that AssemblyInfo.fs provided (8dd45dc). The review asked for FAKE to write AssemblyInfo.Build.props instead, which #42 does, and the last commit of this pull request (c355652) removes the properties. The attributes of both target frameworks are those of #42 plus InternalsVisibleTo("FSharp.Collections.Immutable.Tests"), and the test file that failed builds.

#27 is rebased onto this branch: Helper/ParallelArrays.fs takes the place of the ParallelArrays block that it added to Helper.fs, and its assembly attribute is gone from the source.

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The changes preserve helper implementations and dependency order, accommodate existing assembly metadata generation, and have no identified blocking issues.

No open issues 🎉

What changed in this PR

Prepares the library for #27 by separating shared helpers into root modules and configuring test access to internals without changing public APIs.

Changes:

  • Splits Helper.fs into six files, preserving implementations and module identities.
  • Adds project-level InternalsVisibleTo generation alongside existing assembly metadata.
  • Documents the new helper layout.
File Description
src/​FSharp.Collections.Immutable/​Helper/​Raise.fs Extracts exception helpers.
src/​FSharp.Collections.Immutable/​Helper/​ImmutableCollectionUtil.fs Extracts argument checks and array wrappers.
src/​FSharp.Collections.Immutable/​Helper/​ErrorStrings.fs Extracts exception messages.
src/​FSharp.Collections.Immutable/​Helper/​Comparers.fs Extracts the cached default comparer.
src/​FSharp.Collections.Immutable/​Helper/​Arrays.fs Extracts shared array algorithms.
src/​FSharp.Collections.Immutable/​Helper/​ArrayChecks.fs Extracts array-length checks.
src/​FSharp.Collections.Immutable/​Helper.fs Removes the consolidated helper file.
src/​FSharp.Collections.Immutable/​FSharp.Collections.Immutable.fsproj Registers helpers in dependency order and generates test-access metadata.
.github/​copilot-instructions.md Documents the helper folder structure.

Review effort: Balanced

Comment thread src/FSharp.Collections.Immutable/Helper/ErrorStrings.fs
Comment thread src/FSharp.Collections.Immutable/Helper/ImmutableCollectionUtil.fs
Comment thread src/FSharp.Collections.Immutable/Helper/ImmutableCollectionUtil.fs Outdated
Comment thread src/FSharp.Collections.Immutable/Helper/Raise.fs Outdated
Comment thread src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj Outdated
Comment thread src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj Outdated
Comment thread src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj Outdated
Comment thread src/FSharp.Collections.Immutable/Helper/Arrays.fs Outdated
Comment thread src/FSharp.Collections.Immutable/Helper/Arrays.fs
Comment thread src/FSharp.Collections.Immutable/Helper/Arrays.fs Outdated
Comment thread src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj Outdated
@xperiandri
xperiandri changed the base branch from main to build/assembly-info-props October 19, 2026 14:35
@xperiandri
xperiandri force-pushed the build/assembly-info-props branch from 5795c21 to 758443d Compare October 19, 2026 14:40
@xperiandri
xperiandri force-pushed the build/assembly-info-props branch from 758443d to 4562f5a Compare October 19, 2026 14:46
@xperiandri
xperiandri force-pushed the chore/split-helper branch 2 times, most recently from 2e13163 to 13064b5 Compare October 19, 2026 14:48
Base automatically changed from build/assembly-info-props to main October 19, 2026 15:14
xperiandri and others added 10 commits October 19, 2026 17:14
Each module of Helper.fs moves to its own file in the Helper folder, named after the module and declared as a root
module with its full name, as the other files of the library are: ImmutableCollectionUtil, Comparers, ErrorStrings,
Raise, ArrayChecks and Arrays. A file keeps the doc comment, the attributes and the accessibility of its module
(AutoOpen on ImmutableCollectionUtil and Comparers, internal on all of them) and opens only the namespaces that it uses.

The change is a move: the bodies lose four spaces of indentation and nothing else. The full names of the modules and of
their members stay the same, so no other file changes. The compiled library differs from the previous one only in the
empty startup classes that the compiler emits per file: one for Helper.fs before, one per module now.

The conditional that declared the namespace global for F# Interactive is dropped: a root module has no namespace
declaration, and nothing loads Helper.fs into F# Interactive (no script or build file mentions it).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The tests call internal algorithms directly, where reaching a case through a public function would need arrays too
large for memory. The library project declares that access with an InternalsVisibleTo item, the project-file form of
the InternalsVisibleTo attribute, so that no source file carries an assembly attribute for it.

The item alone does nothing in this repository. AssemblyInfo.fs comes from the build script, so Directory.Build.props
sets GenerateAssemblyInfo to false, and the SDK turns the items into attributes only in the target that
GenerateAssemblyInfo guards. A test file that used an internal module failed to compile with FS0039. The library
project therefore asks the SDK for the attributes of the items alone: it sets GenerateInternalsVisibleToAttributes,
which the SDK leaves unset while GenerateAssemblyInfo is false, and runs the SDK target CoreGenerateAssemblyInfo
before the compile. The generated file holds that one attribute, and the other assembly attributes stay as they are.

The assemblies of both target frameworks now carry InternalsVisibleTo("FSharp.Collections.Immutable.Tests"), and the
test file that failed to compile builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…he one that has none

Every constant of the module copies one string resource of FSharp.Core (FSCore.resx) or of the .NET libraries, and the
comment of each constant now names the resource: the key in FSCore.resx, or the key in System.Private.CoreLib.

All texts but one were found in the resource tables of the assemblies (FSharp.Core 10.1, System.Private.CoreLib 10.0,
System.Collections.Immutable 10.0) by comparing the strings. `ListsHaveDifferentLengths` ("The lists have different
lengths.") is in none of them: it was the text of the modules written before ADR 0001, and no code uses it any more, so
it is removed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…of the SDK, not with a target

The SDK turns InternalsVisibleTo items into an assembly attribute only when GenerateAssemblyInfo is true, and
Directory.Build.props sets it to false because the build script writes AssemblyInfo.fs. The project now sets it to true
and switches off, with the documented Generate...Attribute properties, every attribute that AssemblyInfo.fs or the
package properties already provide, so that the SDK generates the InternalsVisibleTo attribute alone. That replaces the
target that called the internal SDK target CoreGenerateAssemblyInfo.

The assemblies of both target frameworks carry the same attributes as before plus
InternalsVisibleTo("FSharp.Collections.Immutable.Tests"), as the listing of their custom attributes shows. The properties
are in the first PropertyGroup, and the item is above the item group of the compile items.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The expression that reads the property IsDefault of a boxed immutable array through reflection becomes the inline function
readIsDefault, a pipeline of the lookup of the property, the check that it exists and the read. The function stays inline
like isDefaultImmutableArray, which the public inline members of the computation expressions use.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rings

The exceptions no longer go through String.Format with a format string and an array of boxed values. Each message is an
interpolated string, and the helper that formatted the array is gone. A script called every function with 31 argument
combinations in six cultures (invariant, en-US, de-DE, sv-SE, ar-SA and fa-IR) before and after the change: the 186
messages are identical, the minus sign of sv-SE included, because a hole without a format specifier formats with the
current culture as String.Format does, as the messages of FSharp.Core do.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ltImmutableArray

A boxed immutable array is a value type, so isDefaultImmutableArray tests the IsValueType flag of the type first. A class
(List, an F# list, a HashSet or a sequence expression) is rejected by that one flag, and the generic definition is read
only for a boxed generic structure. In a micro-benchmark of 20 million calls the check for such a class takes 5.9 ns
instead of 7.2 ns. The tests that give a default flat list of strings as a sequence of objects reach the reflection path
through this check: a mutation that negates the flag makes them fail.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The comment of the module and of each function now give the reason: the function of the FSharp.Core Array module returns
or takes reference tuples (or, for the keys of countBy and groupBy, needs a wrapper for null keys and a comparer for keys
that are arrays), where FlatList returns struct tuples and the Tuple namespace returns reference tuples. Each algorithm
has exactly two callers. The numbers come from a measurement of 10 000 elements: zip allocates 80 KB for struct tuples
where Array.zip followed by a conversion allocates 400 KB, and allPairs of 300 by 300 elements allocates 720 KB where the
conversion allocates 3.6 MB.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ore functions

The comment of each function of Arrays now begins with the summary of the function of the FSharp.Core Array module that
it mirrors, word for word (FSharp.Core 10.1, read from its documentation file), and the sentences after it say what
differs and why the function exists. The summary of Array.zip3 says "an array of pairs", which is a slip, so the comment
of zip3 says triples and notes the slip. NullableKey has no original and says so.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… the SDK

The library project switched on the generation of assembly attributes, which
Directory.Build.props had switched off, and then switched off the nine
attributes that AssemblyInfo.fs provided, so that the SDK generated the
InternalsVisibleTo attribute alone.

FAKE writes the assembly information as MSBuild properties now, and the SDK
generates all attributes. The InternalsVisibleTo item works as it is, and the
project needs none of the properties.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@xperiandri
xperiandri enabled auto-merge (squash) October 19, 2026 15:15
@xperiandri
xperiandri disabled auto-merge October 19, 2026 15:17
@xperiandri
xperiandri merged commit d545be5 into main Oct 19, 2026
7 checks passed
@xperiandri
xperiandri deleted the chore/split-helper branch October 19, 2026 15:18
xperiandri added a commit that referenced this pull request Oct 19, 2026
…isibleTo` in the project file (#40)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
xperiandri added a commit that referenced this pull request Oct 19, 2026
…isibleTo` in the project file (#40)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
xperiandri added a commit that referenced this pull request Oct 20, 2026
…isibleTo` in the project file (#40)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants