Skip to content

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

@T-Gro
Copy link
Contributor

T-Gro commented Jun 23, 2025

Here are two general comments. a) I believe the main purpose of an RFC is to specify the new features in detail. This should include in detail the changes that would be necessary in the F# spec. I know this hasn't been done in many RFCs, but I think is is important if we want to call F# a language and not just a compiler. b) The sections on motivation and drawbacks are very one-sided. This should be avoided in RFCs.

I agree especially when it comes to name resolution and type inference.
The algorithm/decision tree for propper resolution is quite complex already.

This RFC would introduce:

  • New ruleset based on ordered prefence lists for each literal kind
  • Further changes to prioritization of overloads
  • A "tie breaker" rule to avoid amiguity errors

The RFC should indicate how the spec will change, and aim to use existing terminology from the spec.

@Happypig375 Happypig375 marked this pull request as draft June 24, 2025 12:09
@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.

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