Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
halcwb opened this issue Jan 4, 2025 · 10 comments
Open
Labels

Comments

@halcwb
Copy link

halcwb commented Jan 4, 2025

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]

@MangelMaxime
Copy link
Member

Hello @halcwb,

Can you please try to make a smaller reproduction code?

@halcwb
Copy link
Author

halcwb commented Jan 5, 2025

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.

@ncave
Copy link
Collaborator

ncave commented Jan 6, 2025

@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).

@halcwb
Copy link
Author

halcwb commented Jan 6, 2025

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

@MangelMaxime
Copy link
Member

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.

@halcwb
Copy link
Author

halcwb commented Jan 6, 2025

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

@MangelMaxime
Copy link
Member

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.

@ncave
Copy link
Collaborator

ncave commented Jan 6, 2025

@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.

@halcwb
Copy link
Author

halcwb commented Jan 9, 2025

@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
Copy link
Author

halcwb commented Jan 21, 2025

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants