eclipse-archived / ceylon

The Ceylon compiler, language module, and command line tools
http://ceylon-lang.org
Apache License 2.0
396 stars 62 forks source link

Dressing of arguments to functions used in dynamic context #6867

Open lucaswerkmeister opened 7 years ago

lucaswerkmeister commented 7 years ago

Motivating example, from the TypeScript loader:

for (sourceFile in program.getSourceFiles()) {
    // ...
    forEachChild(sourceFile, visitDeclaration(packageObject, js, program));
}    

program and thus sourceFile are dynamic, and so is forEachChild. visitDeclaration is a Ceylon function with two parameter lists.

The internals of forEachChild are opaque to Ceylon. All we know is that it receives a Callable. If it calls that Callable, it may do so with undressed values, ones which Ceylon has not yet seen at all. And yet, the body of visitDeclaration, an ordinary Ceylon function, expects them to be dressed, since its parameters are not dynamic. At which point do the arguments get dressed?

  1. The compiler could emit statements at the beginning of visitDeclaration’s body that dress every parameter with its static type. Since visitDeclaration might be in a different module than the code that passes it into a dynamic function, we would have to do this for every Ceylon function. Probably expensive.
  2. Or: the compiler could pass a wrapper function to forEachChild, which first dresses all parameters with their static type before delegating to the real visitDeclaration.

@chochos wdyt?

lucaswerkmeister commented 7 years ago

I’ve realized that the second suggestion can also be interpreted as part of the more general “undressing” operation that should take place whenever a value “passes into the dynamic domain”, so to speak (#6001 and #6308). Just like an Array must be unwrapped, a Callable must be wrapped with something that dresses the arguments.

chochos commented 7 years ago

The first option sounds too invasive. We'd have to emit statements to dress up args in every function. The second option is way better; I don't know if we'd need to emit a custom function at runtime or maybe just call a native one and pass it parameter info (this last option sounds better).

lucaswerkmeister commented 7 years ago

Well, upon further reflection, I think the first option would only have to do this for arguments of dynamic type? Not sure… but I agree, the second one is better.

Having a native function would probably be better, that’s a great idea. I guess it would accept the function itself as well as its callable type (or perhaps just the tuple of its argument types). (I briefly thought it might accept a meta reference, which would be function + meta info, but this needs to work for local and anonymous callables too, which don’t have a metamodel.)

lucaswerkmeister commented 7 years ago

I’ve looked a bit more into this. I think the helper function will accept two arguments: the callable itself, and the Arguments type argument of the Callable (printed with typeNameOrList), e. g. {t:m$1.Empty} or m$1.mtt$([…]). It will then walk the arguments array and dre$$ each argument with the corresponding type from that Arguments type argument. (If there are more actual arguments than the function expects, ignore them – the function won’t access them.)

I’m not sure how to do that last part. We need to get the ith type from something that might be a Tuple, Sequence, Sequential, Empty, or any union or intersection of them. And it seems tuple types aren’t actually linked lists in JS, so stripping off the first type (i. e., consuming one argument) at least isn’t completely trivial.

@chochos does something like this exist already? natc$ dresses an array, but it only uses a single type.

chochos commented 7 years ago

The structure of the function makes sense. As for the arguments:

  1. If args match params, piece of cake.
  2. If args > params and last param is variadic, turn all exceeding args into a Sequence with the proper type (dre$$ each arg)
  3. if args > params and last param is not variadic, then I don't know what to do. Ignore perhaps?
  4. If args < params, check for defaulted params and send undefined, and/or Empty if variadic last arg.
lucaswerkmeister commented 7 years ago
  1. Unless the last param is variadic, in which case the last arg becomes a sequence with one element. (A Singleton, perhaps? Are there any guarantees on what kind of Sequence a sequential argument is?)
  2. Can I use natc$(…).sequence() for that?
  3. Yeah, the Ceylon function won’t access the extra arguments, it’s fine to ignore them IMO.
  4. Actually, it looks like I can just send undefined for a variadic last arg as well, the function header substitutes m$1.empty() in that case. So that should make this simpler – I don’t explicitly need to pass undefined, I can just keep the arguments array shorter.
lucaswerkmeister commented 7 years ago

Am I seeing this correctly – a variadic tail of a tuple element is just the sequential type as normal part of the tuple type’s type list, with the seq member set to 1 for a * tail and to 2 for a + tail?

So I guess the algorithm could be:

lucaswerkmeister commented 7 years ago

Wait, I didn’t take defaulted parameters into account at all here.

lucaswerkmeister commented 7 years ago

Erm. Neither… it seems… does the JS backend…?

lucaswerkmeister commented 7 years ago

Okay, sure, this is a lot easier if I don’t need to worry about union types here :D but how do I decide if a missing argument is an error or acceptable? Just assume every parameter is defaulted?

lucaswerkmeister commented 7 years ago

Am I seeing this correctly…

No, I think I was looking at the wrong kind of type. seq 1 and 2 still seem to be present, but the type is a union of Sequential<T>/Sequence<T> and just T:

Arguments$Callable:m$1.mtt$([{t:'u',l:[{t:m$1.Sequential,a:{Element$Sequential:{t:m$1.$_String}}},{t:m$1.$_String}],seq:1}])

@chochos do you know what that union is doing there? I feel like it might cause some bug (I found #6891 while investigating it, but that seems to be unrelated).

lucaswerkmeister commented 7 years ago

Okay, it looks like I need to use the parameter info (jsc$2’s parms parameter), not the Arguments type argument of Callable (in jsc$2’s targs parameter).

print(identity<String>);
print((String s) => s);

compiles to

m$1.print(m$1.jsc$3(0,m$1.identity,{Value$identity:{t:m$1.$_String}}));
m$1.print(m$1.jsc$2((function($3){return $3;
}),[{nm:'s',mt:'prm',$t:{t:m$1.$_String}}],{Arguments$Callable:m$1.mtt$([{t:m$1.$_String}]),Return$Callable:{t:m$1.$_String}}));

In the first case, there’s no targs referring to Callable (the value present is the type arguments of print!), but I think can still get the parms info via f.$crtmm$().ps.

lucaswerkmeister commented 7 years ago

Alright, I’ve pushed my current stab at this to master-for-typescript. Works for a tiny test module, but runs into problems in the TypeScript converter because of some weirdness related to ceylon-crypto which I haven’t yet been able to fix. (It seems that an argument that’s supposed to only get Asn1Value<Anything>s also gets Option values.)