Skip to content

Create FS-1150-Literal-inferred-types.md#800

Draft
Happypig375 wants to merge 57 commits intofsharp:mainfrom
Happypig375:patch-28
Draft

Create FS-1150-Literal-inferred-types.md#800
Happypig375 wants to merge 57 commits intofsharp:mainfrom
Happypig375:patch-28

Conversation

@Happypig375
Copy link
Contributor

Click “Files changed” → “⋯” → “View file” for the rendered RFC.

@Happypig375 Happypig375 marked this pull request as draft June 3, 2025 14:45
@Happypig375
Copy link
Contributor Author

pending changes from fsharp/fslang-suggestions#1427 (comment)

@Happypig375
Copy link
Contributor Author

@T-Gro Should there be new static constraints associated with these inferences?

Copy link
Contributor

@T-Gro T-Gro left a comment

Choose a reason for hiding this comment

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

I think it would be good to emphasize that the biggest advantage is not with explicit type annotation for standalone values (seen here in examples but also typical for C# not using var).

The motivating examples should come from passing arguments to existing methods/functions. The benefits are then multi-fold:

  • Easier code with fewer boilerplate
  • Fewer runtime conversions
  • Possibilities for "improve perf by recompiling" scenarios, e.g. if a library changes it's arguments to support stack allocated ROS.

Copy link
Contributor

@T-Gro T-Gro left a comment

Choose a reason for hiding this comment

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

One aspect I would like to see in bigger detail, e.g. as a diff to existing spec, is how this plays with method resolution in case of multiple ambiguous overloads - non-generic as well as generic. Since this RFC touches literals, it does not have much overlap with function parameter inference - even though scenarios combining literals and symbols like

let myFunc x =
    let _ = [ 1; x ; 3]

mean it has to be considered as well.

@T-Gro
Copy link
Contributor

T-Gro commented Jun 6, 2025

@T-Gro Should there be new static constraints associated with these inferences?

Could you elaborate please? I am not sure I follow what exactly you mean.

@Happypig375
Copy link
Contributor Author

Happypig375 commented Jun 6, 2025

@T-Gro

let inline f a b = a + b
let a: int64 = f 1L 2L
let b: byte = f 1uy 2uy
// val inline f: ^a -> ^b -> ^c when (^a or ^b): (static member (+): ^a * ^b -> ^c)
// works similarly to
let inline g() = 1
let c: int64 = g()
let d: byte = g()
// val inline g: unit -> ^a when ^a: 1

then this inline function needs a new static constraint, yes?

let inline myFunc x = [1; x; 3]
// val inline myFunc: ^a -> ^b when ^a: 1 and ^a: 3 and ^b: [^a]
let x: int list = myFunc 2 // yes
let y: byte array = myFunc 2 // yes
let z: ResizeArray<uint> = myFunc -1 // error: uint cannot express the integer -1

This also means a potential breaking change

let f a b = a + b
let a = f 1uy 2uy // f: byte -> byte -> byte
let b = f 1 2 // error
// similarly...
let x = 1
let y: int64 = x // infers x as int64, but currently uses int32->int64 implicit conversion
let z: int32 = x // error?

Tooling can provide a codefix to specify a type for x in this case.

@T-Gro
Copy link
Contributor

T-Gro commented Jun 6, 2025

let inline myFunc x = [1; x; 3]
// val inline myFunc: ^a -> ^b when ^a: 1 and ^a: 3 and ^b: [^a]

So the type constraint would be with the semantics 'T when 'T can be used to express literal "1" ?

I haven't thought about such deep and invasive impact on type inference - this would likely change any piece of code working with literals in basic arithmetic ways. This probably mean that the feature would have to be rejected due to the impact on existing code.

@Happypig375
Copy link
Contributor Author

Happypig375 commented Jun 6, 2025

@T-Gro Yes, the implementation will probably introduce additional static constraints like the ability to admit specific literals like 1. But it would also enable more beginner friendliness like

1 + 1.5 + 2

such invasive impacts on type inference is restricted to inline code only. inline is already an advanced feature, type defaulting will occur without inline. For any code that wishes to fix types at e.g. API boundaries, just use type annotations.

This may have some impact on existing code, but this also enables a lot of succinctness (no one has to understand what the uy in 1uy means) while retaining robustness (hover over the literal to see its inferred type) and performance (optimizations possible while constructing other collections).

@Happypig375
Copy link
Contributor Author

Happypig375 commented Jun 7, 2025

open System.Numerics
type Vector3 with
    static member op_Implicit struct(x, y, z) = Vector3(x, y, z)
let inline f() = [
    1, 2, 3
    4, 5, 6
]
// val inline f: unit -> ^a when ^a: [^b] and ^b: 1 and ^b: 2 and ^b: 3 and ^b: 4 and ^b: 5 and ^b: 6
let x: Vector3 array = f() // works
let y: Vector3 array = [
    1, 2, 3
    4, 5, 6
] // works

An alternative to these static constraints is to disallow literals with no target type, such that f() will type-default without retaining type constraints.

@T-Gro
Copy link
Contributor

T-Gro commented Jun 9, 2025

@T-Gro Yes, the implementation will probably introduce additional static constraints like the ability to admit specific literals like 1. But it would also enable more beginner friendliness like

1 + 1.5 + 2

What algebra do you anticipate for expressing these constraints?
The ability to carry arbitrary, but fixed and constant values - e.g. any number of https://github.com/dotnet/fsharp/blob/17f5065ba43892e1e020de5b671b858f182bdacd/src/Compiler/SyntaxTree/SyntaxTree.fs#L131 ?

And when materialized after inlining/type-direction, every member would be checked for compile-time translation or fail otherwise?

let intArrayFunc (x: int array) = Array.sum x
let floatArrayFunc (x: float32 array) = Array.sum x

let _ = intArrayFunc [1.0;2.0] //OK
let _ = floatArrayFunc [5;-1;nan,infinity] // OK

let _ = intArrayFunc [1.0;2.5] // FSxxx Literal '2.5' cannot be safely used as 'int'
let _ = floatArrayFunc [0.3] // FSxxx Literal '0.3' cannot be safely used as 'float32'
//-----------------------^^  Would this report at compile-time? Or think of big int64 literals used for float32, where loss of precision also occurs

@Happypig375
Copy link
Contributor Author

I realized that this would also change

let a = ["a"; "b"; "c"]
let b = a :> obj // Should this now produce an error after inferring as ReadOnlySpan?
System.String.Concat(",", a)

@T-Gro
Copy link
Contributor

T-Gro commented Aug 5, 2025

I realized that this would also change

let a = ["a"; "b"; "c"]
let b = a :> obj // Should this now produce an error after inferring as ReadOnlySpan?
System.String.Concat(",", a)

In the general case, this is hard - type inference works top to bottom, and doing a decision system which can effected previously decided bindings will be a massive change.

In this specific case, where a warning comes from PostInferenceChecks (post-inference), like the case with byref safety, this could likely be achieved like you write - infer based on Concat call, and checks would then complain about the :> obj cast.

This however introduces inconsistency, or rather leaks the implementation details of checks (during inference or after it) to the end user.

@T-Gro
Copy link
Contributor

T-Gro commented Aug 5, 2025

@Happypig375 :

I will do another review round during August and will come back to the suggestion with my current opinions ( approve in principle | generally agree | undecided | probably not) on each sub proposal listed in the RFC.

Now that we have the details written out (thanks 👍 again for doing that, I think it does bring clarity into the breadth of the proposal and the different phases that can be decided and implemented separately), we can refer to them and achieve a more focused discussion.

@Happypig375
Copy link
Contributor Author

Happypig375 commented Aug 5, 2025

@T-Gro 👍 My attention is currently diverted to fold loops so this can be on hold for now.

@Happypig375
Copy link
Contributor Author

Are only non-draft PRs reviewed?

@T-Gro
Copy link
Contributor

T-Gro commented Feb 4, 2026

I have reverted some other open RFCs back to draft status so that I do not need to revisit them again - it is an indicator that it awaits either further discussion, reaction to feedback, or some final touches.

For this PR in particular, I do not want to rush the decision - many of the subitems carry significant change with them, and I particularly careful with hard-to-revert decisions. Compiler perf for now-common-idioms is an aspect of it, especially for new mechanics using SRTP-like resolution under the cover.

I apologize for taking a longer time and I know I wrote I will do that sooner.

@T-Gro
Copy link
Contributor

T-Gro commented Feb 5, 2026

Once again, thank you @Happypig375 for the work on this RFC.
The level of detail and consideration for edge cases and gradual improvement of the feature trough specified layers is evident

After considering the proposal in depth, here is the recommendation:


Approved separately

The following sections are approved and should proceed as separate, focused RFCs:

Section Feature Suggestion link
a (Levels 1-2 only) Type-directed literals at method arguments and explicit annotations Part of fsharp/fslang-suggestions#1421
e float64 type abbreviation and d literal suffix New (consistency with int32/int64)
h Deconstruct pattern support Was approved fsharp/fslang-suggestions#751
r UTF-8 B-suffix string literals Part of fsharp/fslang-suggestions#1421
t Record update syntax for C# records Approving in principle now fsharp/fslang-suggestions#1138

Not Approved for F# that exists today

The following 3 categories would require significant changes to type inference and the constraint system, and right now I do not see the direct benefits outweigh the cost coming with added complexity, potential bugs and tooling+ecosystem changes.
F# exists in an ecosystem with variety of tools and libraries and full set of proposals would change how components interact for pretty much any non-trivial code.

I do like having them written out and described in detail, and they might serve very well for an experimental greenfield prototype demonstrating another possibility for F# or its descendants. Such prototype/language would need to make different assumptions about SRTP constraints and put them into the very core of the language and the ecosystem.

If any student looking for thesis material reads this, such prototype on bringing duck typing in a statically type .NET language could serve very well 👍 .

Category 1: Backwards Type Inference for Literals (a3-a7)

F# already supports deferred type resolution for generic constructs like let add x y = x + y. However, literals like 1 and [1;2;3] have historically resolved to fixed default types (int, list). Extending backwards inference to literals introduces a new category of action-at-distance where the meaning of simple expressions changes based on code added later. This is a larger shift in expected behavior than existing generic inference patterns.

Category 2: New SRTP Constraint Forms (b, f, g, k, p, q)

The proposed numeric value constraints (^a: 1), range constraints (^a: 1..10), tuple constraints (^a: (^b, ^c)), list constraints (^a: [^b]), and measurable constraints represent substantial additions to the constraint system. The implementation cost and impact on error messages, tooling, and developer experience outweigh the syntactic convenience.

Category 3 Semantic Redefinition (c, q)

Type-directed numeric literals (c) and string literals (q) would change the meaning of 1 and "abc" based on downstream context. Unlike generic functions where polymorphism is expected, literals have traditionally been concrete. Changing this creates debugging and comprehension challenges that outweigh the benefits.

Need more time / deferred

Deferred Pending Dependencies (d, l)

Section d requires SRTP field constraints which are not yet implemented. Section l requires FS-1023 (type providers generating from types). These should be revisited when their dependencies are available.


Recommended Next Steps

  1. Split section r (UTF-8 B-strings) into its own RFC — it is backward-compatible and uncontroversial
  2. Proceed with a1/a2 scoped strictly to method call sites and explicit type annotations, matching C# collection expression semantics
  3. Submit e, h, and t as independent RFCs referencing their respective suggestions

The core value of fsharp/fslang-suggestions#1421 — reducing friction when calling .NET APIs expecting non-default numeric/collection types — is achieved through a1 and a2 without the broader inference changes.

@Happypig375
Copy link
Contributor Author

Happypig375 commented Feb 5, 2026

@T-Gro I understand that backward inference may be unintuitive and definitely costs compiler performance. But does this mean having 1 and "abc" also work similarly to a2? i.e. forward inference is approved?

let a: byte = 1 // we know the target type is byte. Interpret 1 as a byte literal.
let b = 1 // type information for b doesn't exist yet. Infer as int.
let c: byte = b // type error.
let a: Span<char> = "abc" // we know the target type is char span. Interpret "abc" as char span.
let b = "abc" // type information for b doesn't exist yet. Infer as string.
let c: Span<char> = b // type error.

@T-Gro
Copy link
Contributor

T-Gro commented Feb 5, 2026

Yes, for known target types this is approved.
What is not approved and was an aspect of the "q" item is the existence of ^a when ^a: [^b] and ^b: (byte|char), since such change would affect type checking of all string literals across all codebases.

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.

4 participants