Fable: Php Support for Fable

Created on 3 Jun 2021  路  18Comments  路  Source: fable-compiler/Fable

Description

Currently, the Import node in AST contains only the path and selector of the target function. This is fine for JS and Python that have a similar import system.
In those languages, files are considered as module that contains exported functions, and a user of the module just has to add a directive like this to make use of this module function:

import modulefunction as localfunction from "module";

The module system provides an isolation to avoid name clashes when importing several modules.

This doesn't exist in Php. In php, there is a include/require/require_once directive that just "includes" the content in the scope:

require_once('module.php');

To avoid name clashes, Php has a namespace mechanism. The module.php file for instance can start with a namespace directive to scope all the definitions to the namespace:

namespace MyModule;

function myfunction() { return 0; }

A user of the module has to import it using require_once, but must then know the name of the namespace to use the functions:

require_once('module.php');

$x = \MyModule\myfunction();

To avoid repeating module prefixes, it is possible to define an alias:

require_once('module.php');
use \MyModule\myfunction;

$x = myfunction();

or even

require_once('module.php');
use \MyModule\myfunction as myfunc;

$x = myfunc();

But in both cases, knowning the namespace is mandatory.

Moreover, Php 7.2+ has a decent class construct, and it would be more idiomatic to compile class members as such. Fable today flattens all members as functions in the global scope. The Php transform could promote those functions as static or instance members. But the lack of information in Import make it impossible to generate the call:

module MyModule

type MyType(x: int) =
      member this.Value = x
      static member DefaultValue() = MyType 0

let increment x = x+1

Will generate imports to selectors like "MyType__Value" , "MyType__DefaultValue". But the call in php should look different:

$v = $instance->Value(); // or ->get_Value();
$d = \MyModule\MyType::DefaultValue();
$x = \MyModule\increment(1);

There is currently now way to make the distinction.

Repro code

This can be observed when transpiling fable-library to Php

All 18 comments

It seems this is also request for Python to support classes. @dbrattli

Yes, it would be great to be able to generate more idiomatic Python by having (static) members inside the class definition. I don't think it's a blocker for Python but would make it possible to generate nicer code.

I've found a function in Fsharp2Fable that determines if a class member should be transformed to a plain func or kept as a member. Modifying it for my purpose craeated other problems downstream, but overall it just simplified the transformation.

I had a call with @mathiasverraes this morning and I gave me some excellent insight about the php model. I started some refactoring today and managed to simplify greatly the conversion.
馃憤

@thinkbeforecoding Do you mean you don't need the extra information in the Import expressions now? Anyways, if it can help compilation to Python or other languages shouldn't be harmful to add it.

I'm painfully making tests pass one by one, 馃榿
And Php is far more touchy than JS..
For instance you can call a lambda in a $f variable with $f() and access a field $f in an object with $x->f, but if the field is a lambda, $x->f() will fail because this syntax is reserved for actual methods. You have to write ($x->f) ()...
To resolve this kind of 'subtleties' the transpiler need more info than in js.

I also need to know if parameters are optio al (for the inject stuff) because it has to be explicit in the declaration in php (with = Null for instance) and that info was not available in the parameter info structure.
For now I continue to explore, I add I do I need, and I'll review everything in the end to check that I did not abandon things on the way 馃憤

@thinkbeforecoding I just realized most of this information is in the call expression so adding it to the import would imply some duplication. Would it be ok for you to just use CallInfo or do you still need something else?

open Fable
open Fable.AST.Fable

match expr with
| Call(Import(importInfo,_,_), callInfo, t, r) ->
    match callInfo.CallMemberInfo with
    | Some memberInfo ->
        memberInfo.DeclaringEntity,
        memberInfo.FullName,
        memberInfo.CompiledName,
        memberInfo.IsInstance,
        memberInfo.CurriedParameterGroups

I'll check if I can use it. Is import always in a call?

Import is not always in a call, but if it's called then it's in a call ;)

I've had a look at it, and a lot of calls have no CallMemberInfo (it is None).. so it's not enough for now.

Ah, it can be! Do you have any example? Maybe we are not adding the CallMemberInfo in the Replacements, I've to check 馃

I think I reach the limit for instance with calls to singleton on the AsyncBuilder.
The caller (in async test) try to emit somthing like:

singleton.Delay(...)

The AST is then:
```
Call(Get(Import { Selector = "singleton".. }, FieldGet("Delay", ..) ... )
````
The problem is that singleton is an import, so I have only the selector and filename. And the Call contains no CallMemberInfo (it is None, but it would be about Delay anyway)...

So there is no way to know in what namespace singleton is without adding more info in Import...

I managed to reduce the quantity of information necessary in Import info.

Fable generates Import for different things:

  • Constructors: Found in a Call expr with IsJsConstructor set to true, and the type of this expr is the type constructed. We can get the info from here
  • Method call: Found in a Call epxr with ThisArg set. we don't need namespace as it is called directly on the instance
  • Static Property call: Found as the direct source in a Get(FieldGet), Can only be static, since instance field access are always done either on a Identity or on a call... Until now, the type of the import was Any. I set it to the import target type to be able to have all information.
  • Value: Found standalone for instance as a argument value. Here we need the namespace where the the value is declared
  • Static Function call : Found in a Call(Get(Import, GetField(..))), we find the info from the import type
  • Function Call: Found directly in a Call. We need the namespace where the function is declared

So for now, it seems enough to add the namespace to ImportInfo, and setting the type for Import of types.

@thinkbeforecoding I've added back the ImportKind (which was actually there in Fable 2) to the Import expression. For internal imports, this will include the file full path, which you can use to retrieve the namespace with com.GetRootModule as you already do. I've also included information about whether it is an instance or static member or a class, though I should warn you that trying to attach instance members may take work. Actually the F# compiler already compiles instance members as module functions and to change that you need to change how the arguments are passed, the name mangling, etc.

Ah yes, I think that should help. I'll try to work a bit on this next week 馃憤

@alfonsogarciacaro , is it in a specific branch ?

@thinkbeforecoding Sorry, I hadn't merged it yet 馃槄 It's now in the next branch: https://github.com/fable-compiler/Fable/blob/38dd35e70d4e290048474e5c924a99fc38c4f5ad/src/Fable.AST/Fable.fs#L297-L301

Was this page helpful?
0 / 5 - 0 ratings