Repository navigation
refactor: split Helper.fs into root modules and declare InternalsVisibleTo in the project file - #40
Merged
Merged
Conversation
Merged
4 of 6 tasks
There was a problem hiding this comment.
🟢 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.fsinto six files, preserving implementations and module identities. - Adds project-level
InternalsVisibleTogeneration 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
xperiandri
commented
Oct 19, 2026
xperiandri
commented
Oct 19, 2026
xperiandri
commented
Oct 19, 2026
xperiandri
commented
Oct 19, 2026
xperiandri
commented
Oct 19, 2026
2 of 6 tasks
xperiandri
force-pushed
the
chore/split-helper
branch
from
October 19, 2026 14:35
1215b89 to
c355652
Compare
xperiandri
force-pushed
the
build/assembly-info-props
branch
from
October 19, 2026 14:40
5795c21 to
758443d
Compare
xperiandri
force-pushed
the
chore/split-helper
branch
from
October 19, 2026 14:40
c355652 to
c2f5021
Compare
xperiandri
force-pushed
the
build/assembly-info-props
branch
from
October 19, 2026 14:46
758443d to
4562f5a
Compare
xperiandri
force-pushed
the
chore/split-helper
branch
2 times, most recently
from
October 19, 2026 14:48
2e13163 to
13064b5
Compare
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
force-pushed
the
chore/split-helper
branch
from
October 19, 2026 15:14
13064b5 to
0685f2b
Compare
xperiandri
enabled auto-merge (squash)
October 19, 2026 15:15
xperiandri
disabled auto-merge
October 19, 2026 15:17
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>
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.
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 onHelper.fs: "Move tofsprojwith 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.fsis split into the foldersrc/FSharp.Collections.Immutable/Helper/, one file per module, listed in the project file in the order of their dependencies:ImmutableCollectionUtilandComparers(both[<AutoOpen>]),ErrorStrings,Raise,ArrayChecksandArrays. 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.#if INTERACTIVE ... namespace globalconditional at the top ofHelper.fsis dropped. A root module has no namespace declaration, and nothing loadsHelper.fsinto F# Interactive: no script or build file mentions it.<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.mdshows theHelper/folder in the solution structure.ErrorStringsnames the resource that it copies, and the one text that is in no resource,ListsHaveDifferentLengths, is removed (88c5acd).readIsDefaultis a piped helper (6dc9420). The messages ofRaiseare interpolated strings (985832d). TheInternalsVisibleToitem 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).isDefaultImmutableArraytestsIsValueTypefirst, so a class is rejected by one flag (33164c5). The comment of every algorithm inArraysstarts 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
xin the boxes that applyNone of these: a refactoring and a change of the project file. No public function changes.
Checklist
Put an
xin 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.Further comments
No behavior changes, and the proof does not rest on the existing tests alone (2447 per target framework, all green):
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.openof the new files is needed: removing any of them fails to compile.<StartupCode$FSharp-Collections-Immutable>.$...class (one forHelper.fsbefore, one per module and one for the generated attribute file now), and in the newInternalsVisibleToassembly attribute.Raisewere 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.ErrorStringswas 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 ofFSCore.resx, one isArgumentOutOfRange_NeedNonNegNumof the .NET libraries, and the one that is in none,ListsHaveDifferentLengths, no code uses.The
InternalsVisibleToitem needs the SDK to generate the assembly attributes. Before #42,AssemblyInfo.fscame from the build script andDirectory.Build.propsswitched 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 thatAssemblyInfo.fsprovided (8dd45dc). The review asked for FAKE to writeAssemblyInfo.Build.propsinstead, 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 plusInternalsVisibleTo("FSharp.Collections.Immutable.Tests"), and the test file that failed builds.#27 is rebased onto this branch:
Helper/ParallelArrays.fstakes the place of theParallelArraysblock that it added toHelper.fs, and its assembly attribute is gone from the source.🤖 Generated with Claude Code