This is a master ticket for all the EcmaScript features planned to be used for WebSharper 7 output. This ticket does not deal with overall output signature (modules/imports/exports), but type, member and expression translation changes for WebSharper 7.
Overall goals
JS output signatures should be well-predictable and as easy to interop with from JavaScript/TypeScript as possible.
Fixed output function names and translating to plain objects are possible with attributes.
Keep as close to .NET semantics as possible. For example static constructors must execute lazily so so that DCE (dead code elimination) of code output is not affecting the executing code scope.
DCE of output is best done by WebSharper, do not optimize for outside bundler compatibility when it would conflict with clean code interop. Bundling modes will be documented in another ticket.
WS6 Sitelets (multi-page website projects) were serving full .js output per assembly, just limiting which scripts need to be linked. Prod mode minified these scripts but had no DCE. WebSharper 7 will never to be used in production without DCE, so it is ok to emit helper functions that might or might not be used.
Types
Classes
WS6 uses a Runtime.Class helper function to create JavaScript constructor functions. WS7 is switching to EcmaScript class.
ES classes will be used for all .NET classes that are not one of the following:
static sealed class inheriting from obj without a static constructor - this includes especially F# modules (compiling to separate functions)
annotated with [<Prototype(false)>] (compiling to plain object, all members to separate functions, taking instance object as first param if originally they were an instance method)
F# unions/records which are not annotated with [<JavaScript>] (compiling to plain object as with [<Prototype(false)>])
Lazy class expressions
A runtime helper still might be needed to make the class definition lazily executing if one of these are true:
The class have a static constructor. We might recognize static constructors that are only setting static fields on current class to their initial constant values and let it initialize eagerly as this is quick and side-effect free.
If the class might import any of its subclasses from another JS module. This might not
In the case of multiple output modules for a single .NET assembly (for example one-file-per-class output): if the class has a base class in another module that would directly or indirectly import current module. The important thing to know there is that in JavaScript class A extends B, the value of B is taken when the class expression is evaluating. If it is defined in a module that is not fully initialized yet, it would result in undefined. So the subclass must be wrapped for lazy evaluation.
If the class has a base class that is determined to need a lazy wrapper.
Inheriting from non-WebSharper classes
The System.Object class proxy
WebSharper is designed so that for any plain JavaScript object, equality/comparison/hashing is structural by default so they work on plain JavaScript objects too. Only if any of the argument objects has Equals/Compare/GetHashCode property then it is used. So, to allow for custom equality/comparison/hashing, there is a System.Object proxy that gives a non-structural, reference-based implementation of these methods.
F# unions and records, any exception classes and static classes are not inheriting from this System.Object proxy, neither are classes using a base class that is a JavaScript interop type (Stub/WIG). All other classes inherit from System.Object proxy.
Field initialization order
Field initialization order is relaxed in WebSharper 7, may not follow .NET semantics. This is not a concern for F# as there all fields initializers are implicitly part of the default constructor, you can't have fields initialization on classes without default constructor. But even in C#, as a base class can't access its subclass' instance field values during construction, it is not a big issue. Users should just not rely on side effect order in field initializers, which is already quite an antipattern in .NET C#.
Interfaces
WS6 has no code generated, only metadata support for interfaces. Interface member names are auto-generated from full type name and member name, this can be shortened by the use of Name attributes. Any implementing class will use these JavaScript names for the the translated implementations.
This is still not ideal, in the default case the member names are long (looking like MyNamespace$MyType$MyMember) or a short name can lead to conflicts easier. If for example two different interfaces define a member with [<Name("X")>] then a single class tries to implement both, it's a compilation time error on name conflict.
This can be alleviated by the use of JavaScript Symbols. Name attribute to be expanded to allow defining a Symbol like
[<Name("X", symbol = true)>], and naming with Symbols would be the new default for interfaces.
WS7 also generates an is* function for every interface for TypeScript interop and when a type check against the interface needs to be done by checking against a symbol member or all named members. WS6 did this check every place a type check was needed, and checking only the shortest name, making it only an approximation.
F# unions
WS6 translates union values into an object with a $ property for the tag and $1, $2, etc. properties for union fields. This is compact enough for code size and remoting purposes but hides all union case and field names, making it obscure for interop and debugging.
WS7 would handle unions more like they are in .NET: an abstract base class with an unset Tag property and one subclass for each union cases which define their fields as their original name in .NET (or possibly unnamed F# union field like Item1 can be converted to an indexed field [1]). A constructor is added to each union case.
Example:
let U =
| A of int
| B of x:string
with member this.Value = ...
Translates to:
class U {
Tag;
get Value() { ... }
}
class A extends U {
Tag = 0;
Item1;
constructor (Item1) { this.Item1 = Item1; }
}
class B extends U {
Tag = 1;
x;
constructor (x) { this.x = x; }
}
C# interop concerns
F# unions have .NET methods that are not directly accessible from F# code. These are the Tag property and Is... methods.
Generated ToString, Equals, Compare, GetHashCode are currently not planned to be supported directly.
Null and constant cases
F# allows defining a fieldless union case to be translated to null via attribute. If this happens, WebSharper also translates that case value to null, and transforming all instance members of the type to static (same as in .NET).
Additionally WebSharper's Constant attribute translates fieldless union cases to given constant literals. WebSharper translates null
F# records
Access modifiers on types
WS6 does not care about access modifiers on types. WS7 could make not of them in metadata and whenever possible, not exporting these any non-public types.
Members
Methods
Class methods on types translated to EcmaScript classes become EcmaScript class methods, both instance and static. Bundlers like webpack can't break up classes for DCE, but the intention is that WebSharper can do this in all cases.
Properties
Non-indexed properties now translate to Ecmascript getter/setter. Indexed properties still translate to class methods.
Note: it would be technically possible to translate unary indexed properties with number or string index type in a way that they can be used in JavaScript via the same syntax, e.g. x.IndexedProp[i] and x.IndexedProp[i] = value. The way to do this would be that IndexedProp returns a JavaScript Proxy instance which then handles the get/set accessors. But I don't think the syntax is worth the performance overhead.
Constructors
A single non-inlined constructor for a class can be translated to a JavaScript constructor as is. The challenging problem is overloaded constructors, a concept which JavaScript does not have, so it is necessary to combine constructor implementations into a single constructor.
Take this class for example:
type CtorTest (x: int) =
member this.X = x
new (x, y) = CtorTest(x + y) then Console.Log("CtorTest", x, y)
WebSharper 7 would assign names to constructors by default, in this case New for the default and New_1 for the extra constructor, then the single JS constructor would take this name as first argument, so you can construct the type in JavaScript by new CtorTest("New", 1) or new CtorTest("New_1", 1, 2), implemented e.g. like this:
constructor(i, _1, _2){
let is_New_1;
let x;
let y;
if(i=="New_1"){
is_New_1=true;
x=_1;
y=_2;
i="New";
_1=x+y;
}
if(i=="New"){
const x_1=_1;
super();
this.x=x_1;
}
if(is_New_1){
console.log("CtorTest", x, y);
}
}
Constructor chaining is analyzed and local vars are used to replicate behavior. Sadly WebSharper can't wrap the separate .NET constructors within JS local functions, because super can be called only from top scope.
We might also need to access the original arguments after calling the chained constructors, so they might need local vars to hold the original values as above.
Named constructors
Allow naming constructors for reliable interop, eg:
type CtorTest [<Name "New">] (x: int) =
member this.X = x
[<Name "NewZero">] new (x, y) = CtorTest(x + y)
Then first param of JS constructor should be checked against the given names. It would be good to also have the option to specify [<Name "">] to make a parameterless constructor in .NET to be parameterless in JS too.
Static constructors
JavaScript static blocks in classes execute eagerly when the class definition executes. To not make them eager by default, but also make sure that they have ran even if the class is used from hand-written external JS, it makes sense to still use static block but wrap the whole class initialization with the Runtime.Lazy helper.
F# module functions
F# modules are always static sealed classes in .NET, so WebSharper would translate members of them as straight functions instead of class members.
F# module values
An F# module value is translated by the F# compiler as such:
a property on the .NET class that is the F# module which accesses a static field on a static class created for the F# code file (a class named StartupCode$...)
the static field on the StartupCode which is initialized inside its static constructor
WebSharper translation should follow this pattern exactly so that F# module values are created whenever a value from an F# code file is accessed the first time.
Recursive module values
Recursive module values also have some extra System.Lazy mapping added by the compiler when creating the static field value, but evaluated for its value by the property on the module class. WebSharper should keep this semantics as is.
Access modifiers on members
WebSharper 6 does not care about access modifiers. However, WebSharper 7 should make private members compile to JavaScript private class members (an ES2022 feature). For protected/internal/file access modifiers, there is no good equivalent, so leave them public.
Remoting
WebSharper 6 creates no single access point for RPC functions, but they are like inlines - whenever an RPC is called from the client, an instance of a RemotingProvider object is created and its Async/Task/Send method is called with passing in the RPC method's handle and arguments.
At the minimum, WebSharper 7 should create a static client-side function for each RPC function. This ensures that even if RPC protocol is changed as planned for better module support, the external signature remains the same.
Expressions
let/const vars
WebSharper 7 will drop emitting var variable declarations in all cases. Only let and const will be used, const for F# lets. (C# has no readonly locals yet.)
arrow functions
WebSharper 7 will generate => inside expressions whenever possible. Functions using this value will still be compiled to function expressions, and recursive local functions to function declaration statements.
optional parameters
Methods that have optional parameters in .NET could use JavaScript default parameter syntax: e.g. function FuncWithDefault(x = 1) { ... }
rest parameters
WebSharper 6 translates .NET [<ParamArray>] parameters to be passed in as Array in JavaScript too. WebSharper 7 could make use of JavaScript ... rest parameters to consume these, making calling code's arguments list to look same as in C#/F# source.
This is a master ticket for all the EcmaScript features planned to be used for WebSharper 7 output. This ticket does not deal with overall output signature (modules/imports/exports), but type, member and expression translation changes for WebSharper 7.
Overall goals
Types
Classes
WS6 uses a
Runtime.Class
helper function to create JavaScript constructor functions. WS7 is switching to EcmaScriptclass
.ES classes will be used for all .NET classes that are not one of the following:
obj
without a static constructor - this includes especially F# modules (compiling to separate functions)[<Prototype(false)>]
(compiling to plain object, all members to separate functions, taking instance object as first param if originally they were an instance method)[<JavaScript>]
(compiling to plain object as with[<Prototype(false)>]
)Lazy class expressions
A runtime helper still might be needed to make the class definition lazily executing if one of these are true:
class A extends B
, the value ofB
is taken when theclass
expression is evaluating. If it is defined in a module that is not fully initialized yet, it would result inundefined
. So the subclass must be wrapped for lazy evaluation.Inheriting from non-WebSharper classes
The
System.Object
class proxyWebSharper is designed so that for any plain JavaScript object, equality/comparison/hashing is structural by default so they work on plain JavaScript objects too. Only if any of the argument objects has
Equals
/Compare
/GetHashCode
property then it is used. So, to allow for custom equality/comparison/hashing, there is aSystem.Object
proxy that gives a non-structural, reference-based implementation of these methods.F# unions and records, any exception classes and static classes are not inheriting from this
System.Object
proxy, neither are classes using a base class that is a JavaScript interop type (Stub/WIG). All other classes inherit fromSystem.Object
proxy.Field initialization order
Field initialization order is relaxed in WebSharper 7, may not follow .NET semantics. This is not a concern for F# as there all fields initializers are implicitly part of the default constructor, you can't have fields initialization on classes without default constructor. But even in C#, as a base class can't access its subclass' instance field values during construction, it is not a big issue. Users should just not rely on side effect order in field initializers, which is already quite an antipattern in .NET C#.
Interfaces
WS6 has no code generated, only metadata support for interfaces. Interface member names are auto-generated from full type name and member name, this can be shortened by the use of
Name
attributes. Any implementing class will use these JavaScript names for the the translated implementations.This is still not ideal, in the default case the member names are long (looking like
MyNamespace$MyType$MyMember
) or a short name can lead to conflicts easier. If for example two different interfaces define a member with[<Name("X")>]
then a single class tries to implement both, it's a compilation time error on name conflict.This can be alleviated by the use of JavaScript
Symbol
s.Name
attribute to be expanded to allow defining a Symbol like[<Name("X", symbol = true)>]
, and naming with Symbols would be the new default for interfaces.WS7 also generates an
is*
function for every interface for TypeScript interop and when a type check against the interface needs to be done by checking against a symbol member or all named members. WS6 did this check every place a type check was needed, and checking only the shortest name, making it only an approximation.F# unions
WS6 translates union values into an object with a
$
property for the tag and$1
,$2
, etc. properties for union fields. This is compact enough for code size and remoting purposes but hides all union case and field names, making it obscure for interop and debugging.WS7 would handle unions more like they are in .NET: an abstract base class with an unset
Tag
property and one subclass for each union cases which define their fields as their original name in .NET (or possibly unnamed F# union field likeItem1
can be converted to an indexed field[1]
). A constructor is added to each union case.Example:
Translates to:
C# interop concerns
F# unions have .NET methods that are not directly accessible from F# code. These are the
Tag
property andIs...
methods.Generated
ToString
,Equals
,Compare
,GetHashCode
are currently not planned to be supported directly.Null and constant cases
F# allows defining a fieldless union case to be translated to null via attribute. If this happens, WebSharper also translates that case value to
null
, and transforming all instance members of the type to static (same as in .NET).Additionally WebSharper's
Constant
attribute translates fieldless union cases to given constant literals. WebSharper translatesnull
F# records
Access modifiers on types
WS6 does not care about access modifiers on types. WS7 could make not of them in metadata and whenever possible, not exporting these any non-public types.
Members
Methods
Class methods on types translated to EcmaScript classes become EcmaScript class methods, both instance and static. Bundlers like webpack can't break up classes for DCE, but the intention is that WebSharper can do this in all cases.
Properties
Non-indexed properties now translate to Ecmascript getter/setter. Indexed properties still translate to class methods.
Note: it would be technically possible to translate unary indexed properties with number or string index type in a way that they can be used in JavaScript via the same syntax, e.g.
x.IndexedProp[i]
andx.IndexedProp[i] = value
. The way to do this would be thatIndexedProp
returns a JavaScriptProxy
instance which then handles the get/set accessors. But I don't think the syntax is worth the performance overhead.Constructors
A single non-inlined constructor for a class can be translated to a JavaScript constructor as is. The challenging problem is overloaded constructors, a concept which JavaScript does not have, so it is necessary to combine constructor implementations into a single constructor.
Take this class for example:
WebSharper 7 would assign names to constructors by default, in this case
New
for the default andNew_1
for the extra constructor, then the single JS constructor would take this name as first argument, so you can construct the type in JavaScript bynew CtorTest("New", 1)
ornew CtorTest("New_1", 1, 2)
, implemented e.g. like this:Constructor chaining is analyzed and local vars are used to replicate behavior. Sadly WebSharper can't wrap the separate .NET constructors within JS local functions, because
super
can be called only from top scope. We might also need to access the original arguments after calling the chained constructors, so they might need local vars to hold the original values as above.Named constructors
Allow naming constructors for reliable interop, eg:
Then first param of JS constructor should be checked against the given names. It would be good to also have the option to specify
[<Name "">]
to make a parameterless constructor in .NET to be parameterless in JS too.Static constructors
JavaScript
static
blocks in classes execute eagerly when the class definition executes. To not make them eager by default, but also make sure that they have ran even if the class is used from hand-written external JS, it makes sense to still usestatic
block but wrap the whole class initialization with theRuntime.Lazy
helper.F# module functions
F# modules are always static sealed classes in .NET, so WebSharper would translate members of them as straight functions instead of class members.
F# module values
An F# module value is translated by the F# compiler as such:
StartupCode$...
)WebSharper translation should follow this pattern exactly so that F# module values are created whenever a value from an F# code file is accessed the first time.
Recursive module values
Recursive module values also have some extra
System.Lazy
mapping added by the compiler when creating the static field value, but evaluated for its value by the property on the module class. WebSharper should keep this semantics as is.Access modifiers on members
WebSharper 6 does not care about access modifiers. However, WebSharper 7 should make private members compile to JavaScript private class members (an ES2022 feature). For
protected
/internal
/file
access modifiers, there is no good equivalent, so leave them public.Remoting
WebSharper 6 creates no single access point for RPC functions, but they are like inlines - whenever an RPC is called from the client, an instance of a
RemotingProvider
object is created and itsAsync
/Task
/Send
method is called with passing in the RPC method's handle and arguments.At the minimum, WebSharper 7 should create a static client-side function for each RPC function. This ensures that even if RPC protocol is changed as planned for better module support, the external signature remains the same.
Expressions
let/const vars
WebSharper 7 will drop emitting
var
variable declarations in all cases. Onlylet
andconst
will be used,const
for F#let
s. (C# has no readonly locals yet.)arrow functions
WebSharper 7 will generate
=>
inside expressions whenever possible. Functions usingthis
value will still be compiled tofunction
expressions, and recursive local functions tofunction
declaration statements.optional parameters
Methods that have optional parameters in .NET could use JavaScript default parameter syntax: e.g.
function FuncWithDefault(x = 1) { ... }
rest parameters
WebSharper 6 translates .NET
[<ParamArray>]
parameters to be passed in as Array in JavaScript too. WebSharper 7 could make use of JavaScript...
rest parameters to consume these, making calling code's arguments list to look same as in C#/F# source.