Fable icon indicating copy to clipboard operation
Fable copied to clipboard

Fable 5.0.0-alpha.4 JSX.jsx problem with not recognizing interpolated string

Open halcwb opened this issue 11 months ago • 10 comments

Description

The code from in this repository: 9dee1b64a0f98b41e42242dfa4fa5e60d4961f5a

works with the fable 4 releases but not with the latest 5.0.0-alpha.4 release

client: ./App.fs(270,12): (279,19) error FABLE: Expecting a string literal or interpolation without formatting

Repro code

  • See link to repository.
  • Clone the repository
  • Install the latest alpha version fable tool
  • Start with dotnet run
  • Compare with the current 4.x tool

Expected and actual results

The JSX.jsx templates to be recognized as interpolated strings

Related information

.NET SDK: Version: 9.0.101 Commit: eedb237549 Workload version: 9.0.100-manifests.3068a692 MSBuild version: 17.12.12+1cce77968

Runtime Environment: OS Name: Mac OS X OS Version: 12.7 OS Platform: Darwin RID: osx-x64 Base Path: /usr/local/share/dotnet/sdk/9.0.101/

.NET workloads installed: There are no installed workloads to display. Configured to use loose manifests when installing new manifests.

Host: Version: 9.0.0 Architecture: x64 Commit: 9d5a6a9aa4

.NET SDKs installed: 6.0.302 [/usr/local/share/dotnet/sdk] 6.0.403 [/usr/local/share/dotnet/sdk] 7.0.100 [/usr/local/share/dotnet/sdk] 8.0.100 [/usr/local/share/dotnet/sdk] 9.0.100 [/usr/local/share/dotnet/sdk] 9.0.101 [/usr/local/share/dotnet/sdk]

halcwb avatar Jan 04 '25 14:01 halcwb

Hello @halcwb,

Can you please try to make a smaller reproduction code?

MangelMaxime avatar Jan 04 '25 19:01 MangelMaxime

Hello @halcwb,

Can you please try to make a smaller reproduction code?

Hope this is small enough: https://github.com/halcwb/Fable5Issue.git. I have gutted all code except just this part that seems to be the problem:

module App


open Fable.Core
open Browser
open Fable.React

let inline private toReact (el: JSX.Element) : ReactElement = unbox el


[<JSX.Component>]
let View () =


    let display showProgress (s: string) =

        if showProgress then
            JSX.jsx
                $"""
                import LinearProgress from '@mui/material/LinearProgress';
                <LinearProgress>{s}</LinearProgress>
                """
        else
            JSX.jsx
                $"""
                import Typography from '@mui/material/Typography';

                <React.Fragment>
                    <Typography variant="h6" gutterBottom >
                        {s}
                    </Typography>
                </React.Fragment>
                """

    let content = display true "Testing Fable 5"

    JSX.jsx
        $"""
        import React from 'react';

        <React.StrictMode>
            <React.Fragment>
                {content}
            </React.Fragment>
        </React.StrictMode>
    """


let app = ReactDomClient.createRoot (document.getElementById "app")
app.render (View() |> toReact)

Hope this helps.

halcwb avatar Jan 05 '25 22:01 halcwb

@halcwb Looks related to https://github.com/dotnet/fsharp/pull/16556 which converts some interpolated strings into string concatenation (for performance reasons), which then interferes with the Fable JSX.

Before (Fable 4.24, F# 8.0):

            `
            import Typography from '@mui/material/Typography';

            <React.Fragment>
                <Typography variant="h6" gutterBottom >
                    ${s}
                </Typography>
            </React.Fragment>
            `

After (Fable 5.0.0-x, F# 9.0)

 concat("\n            import LinearProgress from \'@mui/material/LinearProgress\';\n            <LinearProgress>", s, ..."</LinearProgress>\n            ");

I'm not sure if this F# 9.0 behavior can be suppressed with some directive, but as a quick workaround adding a few dummy arguments helps avoid it, e.g.: instead of

        JSX.jsx
            $"""
            {s}
            """

use something like this:

        JSX.jsx
            $"""{""}{""}
            {s}
            """

Not saying this is the best workaround ever, hopefully there is a better way to turn that feature off where needed (LanguageFeature.LowerInterpolatedStringToConcat).

ncave avatar Jan 06 '25 01:01 ncave

@ncave Thanks for your quick reaction! Is there a way to inspect the output of the string interpolation as you show in your examples?

halcwb avatar Jan 06 '25 08:01 halcwb

Is there a way to inspect the output of the string interpolation as you show in your examples?

I am not sure what you mean but that, but if you are speaking about the generated JavaScript, you can look at the generated files on the disk.

MangelMaxime avatar Jan 06 '25 10:01 MangelMaxime

Is there a way to inspect the output of the string interpolation as you show in your examples?

I am not sure what you mean but that, but if you are speaking about the generated JavaScript, you can look at the generated files on the disk.

Thanks. but the compiler crashes, there is no output other than this:

import React from "react";
import * as client from "react-dom/client";

export function View() {
    const display = (showProgress, s) => {
        if (showProgress) {
            return null;
        }
        else {
            return null;
        }
    };
    const content = display(true, "Testing Fable 5");
    return         <React.StrictMode>
            <React.Fragment>
                {content}
            </React.Fragment>
        </React.StrictMode>
    ;
}

export const app = client.createRoot(document.getElementById("app"));

app.render(<View></View>);

//# sourceMappingURL=App.jsx.map

So you just have a `null`` instead of the original jsx string

halcwb avatar Jan 06 '25 10:01 halcwb

Thanks. but the compiler crashes, there is no output other than this:

Ah sorry, didn't know about that.

Then, I think the only way to explore the output/generation is by debugging Fable itself via this repository.

This can be done by running ./build.sh quicktest javascript which use src/quicktest/QuickTest.fs as the source. If you use VSCode, you can also run ./build.sh fable-library --javascript once (to get some file generated) and then debug it using VSCode debugger.

From what I understand from @ncave, we probably would like to be able to make this check return false for JSX cases. But I don't think this is possible out of the box.

https://github.com/dotnet/fsharp/blob/749853e179ad3d10a2c10b95fc2c0631422f3037/src/Compiler/Checking/Expressions/CheckExpressions.fs#L7565-L7568

Or perhaps, we can look at the generated code of concat and find the information needed in order to reverse the transformation for JSX cases.

MangelMaxime avatar Jan 06 '25 13:01 MangelMaxime

@halcwb

Is there a way to inspect the output of the string interpolation?

Yes, if you make a function that returns just the interpolated string (without JSX.jsx), you can see it (after it's transpiled by Fable). Perhaps in a different file.

ncave avatar Jan 06 '25 16:01 ncave

@halcwb Looks related to dotnet/fsharp#16556 which converts some interpolated strings into string concatenation (for performance reasons), which then interferes with the Fable JSX.

Before (Fable 4.24, F# 8.0):

            `
            import Typography from '@mui/material/Typography';

            <React.Fragment>
                <Typography variant="h6" gutterBottom >
                    ${s}
                </Typography>
            </React.Fragment>
            `

After (Fable 5.0.0-x, F# 9.0)

 concat("\n            import LinearProgress from \'@mui/material/LinearProgress\';\n            <LinearProgress>", s, ..."</LinearProgress>\n            ");

I'm not sure if this F# 9.0 behavior can be suppressed with some directive, but as a quick workaround adding a few dummy arguments helps avoid it, e.g.: instead of

        JSX.jsx
            $"""
            {s}
            """

use something like this:

        JSX.jsx
            $"""{""}{""}
            {s}
            """

Not saying this is the best workaround ever, hopefully there is a better way to turn that feature off where needed (LanguageFeature.LowerInterpolatedStringToConcat).

Your suggestion kind of works, but still results in incorrect javascript (see display1). With some experimenting I found that these solutions seems to work:

// doses not work
let display1 showProgress (text: string) =

    if showProgress then
        JSX.jsx
            $"""{""}{""}
            import LinearProgress from '@mui/material/LinearProgress';
            <LinearProgress>{text}</LinearProgress>
            """
    else
        JSX.jsx
            $"""{""}{""}
            import Typography from '@mui/material/Typography';

            <React.Fragment>
                <Typography variant="h6" gutterBottom >
                    {text}
                </Typography>
            </React.Fragment>
            """
// shows corectly
let display2 showProgress (text: string) =

    if showProgress then
        printfn
            $"""
            import LinearProgress from '@mui/material/LinearProgress';
            <LinearProgress>{text}</LinearProgress>
            """
    else
        printfn
            $"""
            import Typography from '@mui/material/Typography';

            <React.Fragment>
                <Typography variant="h6" gutterBottom >
                    {text}
                </Typography>
            </React.Fragment>
            """
// works
let display3 showProgress (text: obj) =
    if showProgress then
        JSX.jsx
            $"""
            import LinearProgress from '@mui/material/LinearProgress';
            <LinearProgress>{text}</LinearProgress>
            """
    else
        JSX.jsx
            $"""
            import Typography from '@mui/material/Typography';

            <React.Fragment>
                <Typography variant="h6" gutterBottom >
                    {text}
                </Typography>
            </React.Fragment>
            """
// works
let display4 showProgress (text: string) =

    if showProgress then
        JSX.jsx
            $"""
            import LinearProgress from '@mui/material/LinearProgress';

            <React.Fragment>
                <LinearProgress>{text :> obj}</LinearProgress>
            </React.Fragment>
            """
    else
        JSX.jsx
            $"""
            import Typography from '@mui/material/Typography';

            <React.Fragment>
                <Typography variant="h6" gutterBottom >
                    {text :> obj}
                </Typography>
            </React.Fragment>
            """

Transpiles to

const display1 = (showProgress, text) => {
    if (showProgress) {
        return {""}{""}
            import LinearProgress from '@mui/material/LinearProgress';
            <LinearProgress>{text}</LinearProgress>
            ;
    }
    else {
        return {""}{""}
            import Typography from '@mui/material/Typography';

            <React.Fragment>
                <Typography variant="h6" gutterBottom >
                    {text}
                </Typography>
            </React.Fragment>
            ;
    }
};
const display2 = (showProgress_1, text_1) => {
    if (showProgress_1) {
        toConsole(`
            import LinearProgress from '@mui/material/LinearProgress';
            <LinearProgress>${text_1}</LinearProgress>
            `);
    }
    else {
        toConsole(`
            import Typography from '@mui/material/Typography';

            <React.Fragment>
                <Typography variant="h6" gutterBottom >
                    ${text_1}
                </Typography>
            </React.Fragment>
            `);
    }
};
const display3 = (showProgress_2, text_2) => {
    if (showProgress_2) {
        return                 <LinearProgress>{text_2}</LinearProgress>
            ;
    }
    else {
        return                 <React.Fragment>
                <Typography variant="h6" gutterBottom >
                    {text_2}
                </Typography>
            </React.Fragment>
            ;
    }
};
const display4 = (showProgress_3, text_3) => {
    if (showProgress_3) {
        return                 <React.Fragment>
                <LinearProgress>{text_3}</LinearProgress>
            </React.Fragment>
            ;
    }
    else {
        return                 <React.Fragment>
                <Typography variant="h6" gutterBottom >
                    {text_3}
                </Typography>
            </React.Fragment>
            ;
    }
};
const content = display3(true, "Testing Fable 5");
return         <React.StrictMode>
        <React.Fragment>
            {content}
        </React.Fragment>
    </React.StrictMode>
;

This is because the concat optimisation only applies when the interpolation is with a string.

So, by just casting the interpolation arguments to objects you can avoid this issue.

halcwb avatar Jan 09 '25 08:01 halcwb

Does this issue still needs the label "needs reproduction"?

halcwb avatar Jan 21 '25 14:01 halcwb