Skip to content

Conversation

@kurtschelfthout
Copy link
Contributor

See #96.

This code seems to be going in the right direction in that as far as I can tell it now results in the same IL as the C# compiler generates for DefaultParameterValue (and for optional arguments, which in C# is just syntactic sugar for both Optional and DefaultParameterValue).

However we are not out of the woods.

  1. F# still does not recognise a parameter with both [<Optional; DefaultParameterValue>] as optional only if the method definition is compiled by fsc. In other words:
    Optional value defined in C# can be omitted at F# call site.
    Optional value defined in F# can be omitted at C# call site, and now picks up the default value defined on the F# side.
    Optional value defined in F# can not be omitted at F# call site (both same assembly or two different assemblies).
    So @dsyme to be pedantic, it seem like C#-style optional arguments defined in F# cannot be consumed by F# as optional #1783 is not really a duplicate of [<DefaultParameterValue>] is ignored in F#-authored code #96 in that I think this PR as is pretty much solves [<DefaultParameterValue>] is ignored in F#-authored code #96 but not C#-style optional arguments defined in F# cannot be consumed by F# as optional #1783.

  2. Note that I filtered out DefaultParameterValue from the attributes that are actually written in the IL, as, like Optional it is supposed to be written in the IL attributes directly. As far as I can tell, nothing should have problems with this, but I thought I'd mention it.

  3. I have no idea what GenSlotParam is for, just modified it blindly like GenParamAttribs.

  4. I have little doubt there will be test failures, as well as new tests to add.

Meanwhile any thoughts are appreciated.

…t-param-value

# Conflicts:
#	src/fsharp/IlxGen.fs
#	src/fsharp/TcGlobals.fs
and GenDefaultParameterValue (Attrib (_,_,exprs,_,_,_,_)) =
let (AttribExpr (_,defaultValueExpr)) = List.head exprs
match defaultValueExpr with
| Expr.Const ((Const.String s),_,_) -> Some (ILFieldInit.String s)
Copy link
Contributor

Choose a reason for hiding this comment

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

GenFieldInit?

@dsyme
Copy link
Contributor

dsyme commented Nov 22, 2016

Re GenSlotParam: GenSlotParam generates the data used for parameters at definitions of abstract method slots such as interface methods or override methods

@dsyme
Copy link
Contributor

dsyme commented Nov 22, 2016

This looks very good. I suppose the F#-to-F# case is covered by this todo-like comment in infos.fs

                let isOutArg = HasFSharpAttribute g g.attrib_OutAttribute argInfo.Attribs && isByrefTy g ty
                let isOptArg = HasFSharpAttribute g g.attrib_OptionalArgumentAttribute argInfo.Attribs
                // Note: can't specify caller-side default arguments in F#, by design (default is specified on the callee-side) 
                let optArgInfo = if isOptArg then CalleeSide else NotOptional

I think you want to crack the attributes at that point to produce one of these (as we do for .NET methods)

                CallerSide (Constant v)

It looks like you also have to consider null" default values along the lines of this for C# code

                CallerSide (analyze (ImportILTypeFromMetadata amap m ilScope ilTypeInst [] ilParam.Type))

You can probably share some of the code each way

@kurtschelfthout
Copy link
Contributor Author

Thanks much for the pointers, I'll take a look at that.

Start digression
I was looking at that first snippet before actually, but was confused by the "caller-side" vs "callee-side" terminology. But I now understand what happens: when at a call site the parameter is omitted, the compiler reads the default value from the definition of the method (i.e. the value defined as argument to DefaultParameterValue), and then passes that value explicitly!

In other words these default values are caller side, even though they are optically defined at the callee side. Interestingly, this means that if I change the default value in an assembly, this default value would only be picked up by clients after they recompile their assemblies. I would bet at least one person has lost several hours due to that subtlety...
End digression

@kurtschelfthout
Copy link
Contributor Author

With latest commits it looks like F# to F# optional args work. I left some comments in the code where I did things that may be controversial.

Very simple example works; next step: adding tests!

@dsyme
Copy link
Contributor

dsyme commented Nov 24, 2016

@kurtschelfthout This is looking good

static member FieldInitForDefaultParameterValueAttrib (Attrib (_,_,exprs,_,_,_,_)) =
let (AttribExpr (_,defaultValueExpr)) = List.head exprs
match defaultValueExpr with
| Expr.Const ((Const.String s),_,_) -> Some (ILFieldInit.String s)
Copy link
Contributor

Choose a reason for hiding this comment

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

This is closely related to GenFieldInit in IlxGen.fs. May make sense to share the common core of the code, putting it in TastOps.fs?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, good pointer, will try to reuse...tomorrow evening, time for relaxing now. :)

open System

type Class() =
static member Method1 ([<Optional;DefaultParameterValue(42y)>]i:sbyte) = i
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for being systematic :)

@dsyme
Copy link
Contributor

dsyme commented Nov 25, 2016

@kurtschelfthout Please also check cases where C# and F# arguments have Nullable<T> type, which I understand is allowed for C#-style optional arguments

@kurtschelfthout
Copy link
Contributor Author

With the latest commits, I've taken care of all review comments so far, and all issues I raised in the initial PR description above. So almost there!

The tests revealed one issue though - we should add a check whether the argument of DefaultParameterValue matches the type of the argument. Otherwise invalid IL can be generated (as is currently exposed by one of the tests I added, so I'd expect the test run to fail).

For that, I need to figure out where to add the error, and how to add compile errors :)

static member Method15 ([<Optional;DefaultParameterValue(new DateTime())>]i:DateTime) = i

//Check nullables.
static member MethodNullable1 ([<Optional;DefaultParameterValue(null)>]i:Nullable<int>) = i
Copy link
Contributor

Choose a reason for hiding this comment

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

In C#, can you just declare Optional with `DefaultParameterValue(null)`` implied?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, or whatever the default value of the type is. To support that we could add some type-directed analysis (a simplified version of analyze which you mentioned earlier - wouldn't need to support marshal and ref parameters) to the added code in infos.fs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Or maybe we do need to support byref optional arguments and COM marshalling. Perhaps I'm just assuming things to make my life easier.

@kurtschelfthout
Copy link
Contributor Author

I added a compiler error to deal with the unverifiable code in infos.fs. A few potential issues with that.

  • I'm not sure which type the compiler thinks null is, but this error is triggered if I don't write DefaultParameterValue(null:obj) or whatever the type is. So it seems you need to duplicate the type annotation there.
  • I wanted to show the type of the default parameter and the argument in the error message, but in infos.fs NicePrint is not yet defined...

To mitigate the first issue I'll look into allowing to omit the DefautParameterValue if you just want to specify the default value for the type.

3208,tcCouldNotFindOffsetToStringData,"Could not find method System.Runtime.CompilerServices.OffsetToStringData in references when building 'fixed' expression."
3209,chkNoByrefReturnOfLocal,"The address of the variable '%s' cannot be used at this point. A method or function may not return the address of this local value."
3210,tcNamedActivePattern,"%s is an active pattern and cannot be treated as a discriminated union case with named fields."
3211,DefaultParameterValueNotAppropriateForArgument,"The default value does not have the same type as the argument."
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you please also specify the names of the types.

open System

type Class() =
//check that all known supported primitive types work.
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you please add a space between // and the text. I think we do that everywhere in the code base

// Generate object expressions as ILX "closures"
//--------------------------------------------------------------------------

// generates the data used for parameters at definitions of abstract method slots such as interface methods or override methods.
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you please make it a triple slash comment and uppercase it?

The behavior here matches that of C# as far as I can tell, but it seems
unspecified anywhere. I could not find a reference to it in the C# 5.0
spec or C# 6.0 draft spec. In particular the System.Reflection.Missing
for obj is interesting.
@kurtschelfthout kurtschelfthout changed the title [WIP] DefaultParameterValue shenanigans Calling Optional and DefaultParameterValue actually pass the default value, and can be specified in F# and called from F# Nov 27, 2016
@kurtschelfthout
Copy link
Contributor Author

This is ready for final review.

I do feel like it snowballed a bit on me, so feel free to take as much time as you need.

@dsyme
Copy link
Contributor

dsyme commented Nov 27, 2016

@kurtschelfthout Dude there's a reason why it wasn't fixed earlier. You've done an important job, snowball and all :)

Copy link
Contributor

@dsyme dsyme left a comment

Choose a reason for hiding this comment

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

Superb job. One small change request. Also update the RFC please

Could you take a look if there are optional-argument-functions-with-default-value-attributes in the .NET Framework which we could write some tests against? Also, have you tested it against the original scenarios mentioned in original reports?

let defaultValue = OptionalArgInfo.ValueOfDefaultParameterValueAttrib attr
match defaultValue with
| Some (Expr.Const (_, m, typ)) when not (typeEquiv g typ ty) ->
error(Error(FSComp.SR.DefaultParameterValueNotAppropriateForArgument(), m)); NotOptional
Copy link
Contributor

Choose a reason for hiding this comment

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

I think a warning would be better here

Copy link
Contributor Author

@kurtschelfthout kurtschelfthout Nov 28, 2016

Choose a reason for hiding this comment

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

The problem is that then the compiler can generate unverifiable IL at call sites where the argument is omitted - the default value is put on the stack and the method is called, but the value on the stack will have the type of the default value, which does not correspond to the method argument's type.

So our options are:

  • Keep it as an error
  • Make it a warning, but ignore the default value (i.e. infer the default value from the type)
  • Make it a warning, ignore the default value and the optional argument (i.e. won't be able to omit this argument) - because inferring the default value from the type can have some surprising consequences.
  • Just thought of a fourth option - generate a warning at the definition site, and an error at the call site when the parameter is omitted.

Also note C# generates an error in this case:

image

@kurtschelfthout
Copy link
Contributor Author

@dsyme other review items:

@kurtschelfthout kurtschelfthout changed the title Calling Optional and DefaultParameterValue actually pass the default value, and can be specified in F# and called from F# Omitting Optional; DefaultParameterValue arguments passes the default value and works for F# to F# calls Nov 28, 2016
@dsyme
Copy link
Contributor

dsyme commented Nov 28, 2016

I think we do this: "Make it a warning, ignore the default value and the optionality of the argument"

With "update the RFC" did you mean write it :) I can't find an existing RFC.... In any case I'll be happy to do that.

Yes :) thanks!

@kurtschelfthout
Copy link
Contributor Author

So now the error is a warning as requested, but there is a problem I'm not sure how to address. Every time the method GetParamAttribs is called the same warning gets emitted again. By the way, this is also the case for the CallerInfo related warning a few lines down (CallerMemberNameIsOverriden). I've noticed this method gets called once per definition, and three times per call. So in other words define a method and a method call to it, you'll get 4x the same warning(s).

I have tried to move the checks to PostInferenceChecks but it's awkward, because the CallerSide union type carries the ILFieldInit directly, and then I'd have to back out the type again from that in PostInferenceChecks.

Perhaps the OptionalArgInfo should be changed to carry either TType * Expr or ILFieldInit depending on where the method is from (same assembly or different assembly). We don't need to do any checks on the latter.

@dsyme
Copy link
Contributor

dsyme commented Nov 29, 2016

Every time the method GetParamAttribs is called the same warning gets emitted again

I don't think this is such a problem in practice.

I'm happy with things as they are, I think we can now accept this?

@dsyme dsyme merged commit 9f407d5 into dotnet:master Nov 29, 2016
@dsyme
Copy link
Contributor

dsyme commented Nov 29, 2016

@kurtschelfthout Thank you so much for addressing this, it's a major improvement

@kurtschelfthout
Copy link
Contributor Author

@dsyme thanks for the many pointers and support. The only reason I could even get started on this is because you put such a nice analysis on the bug report.

@haf-lynx
Copy link

haf-lynx commented Dec 1, 2016

Well done =). Can I use this commit of the compiler with Visual Studio on Windows straight away somehow?

@forki
Copy link
Contributor

forki commented Dec 1, 2016

@haf-lynx sorry no. I mean yes you can build your own compiler, but it's not exactly recommended to use that one in production.

@goswinr
Copy link
Contributor

goswinr commented Apr 18, 2018

I am still confused. Now that this is merged, What is the correct way to declare optional parameters that can also be used via C# or Ironpython?
is it [<Optional; DefaultParameterValue>] ?
for example: [<Optional; DefaultParameterValue(true)>] myParameter:bool ?

@cartermp
Copy link
Contributor

@goswinr The latter:

type Class1() =
    static member Right([<Optional;DefaultParameterValue(12)>] i: int) = i + 1

type Class2() =
    // Compiler warning
    static member Wrong([<Optional;DefaultParameterValue("I am a string")>] i: int) = i + 1

@haf
Copy link

haf commented May 14, 2018

What's the default of CancellationToken in this case?

screen shot 2018-05-14 at 23 11 54

@cartermp
Copy link
Contributor

@dsyme Any thoughts here? It appears that any unchecked value is not accepted.

open System.Runtime.InteropServices

[<Struct>]
type Egads = { Yee: int; Doot: string }

type C() =
    member __.M([<Optional; DefaultParameterValue(Unchecked.defaultof<Egads>)>] ?e: Egads) = ()

It does not appear to be covered in the RFC either.

@dsyme
Copy link
Contributor

dsyme commented May 16, 2018

Yes there is an incompleteness here.

The relevant C# equivalent is this:

using System.Runtime.InteropServices;

class C {
    static void M(System.DateTime e = new System.DateTime()) { System.Console.WriteLine ("e = {0}", e); }
    static void Main() { M(); }
}

Or this

    static void M(System.DateTime e = default(System.DateTime)) ...

The IL is this:

.method private hidebysig static void  M([opt] valuetype [mscorlib]System.DateTime e) cil managed
{
  .param [1] = nullref

I somehow hadn't realised that default(DateTime) or new System.DateTime() counted as a literal constant in C#. I'm surprised this hasn't come up before, but there you go.

We think we can make it that Unchecked.defaultof<_> and/or System.DateTime() are accepted. Will take a look.

@xperiandri
Copy link
Contributor

What about constructors? Does this PR cover them too?

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.

9 participants