ohmjs / ohm

A library and language for building parsers, interpreters, compilers, etc.
MIT License
5.01k stars 217 forks source link

Allow for async semantic actions #362

Closed luizzeroxis closed 2 years ago

luizzeroxis commented 2 years ago

Hello. I am working on a project that uses Ohm, when I realized that this library doesn't really support any kind of asynchronous operations. I request for this feature to be added, or tell me some way to do it.

What I want to do is to have a 'sleep' function inside of a semantic action. In JS, one way to do that is to make a Promise with the await keyword. This only works in async functions, but if I try making my function async, Ohm simply starts it and goes on to the next part of the parse tree (as would be expected if it simply calls the function).

I would like if whatever part of the code calls the function could wait for it to finish before moving on. Of course, this means that every function throughout the call stack would have to be async, including the semantic operation user call. People would have to use await or have the function return a promise, so because of that, it should be an option of the user if they want it to be asynchronous or not.

In the meantime, I'm trying to find a way to make this work in my project. Is there some way I could loop through the parse tree myself? Or somehow modify the underlying code? If not, I might have to move to another library, but I really like Ohm!

pdubroy commented 2 years ago

Hi @luizeldorado! Thanks for the feature request. This is something another user brought up earlier this year in our Discord.

In principle I think it's something that we could support. Can you give me some more details what exactly you are doing inside the async action? Before trying to tackle this, I'd like to better understand what the use cases are.

luizzeroxis commented 2 years ago

Hello @pdubroy, thanks for responding. It's kind of hard to explain without going deep in the code of my project, but I would like to do something like this:

this.semantics.addOperation('interpret', {
    // ...
    async Function (_name, _1, _args, _3) {
        var name = _name.sourceString;
        var args = await _args.asIteration().interpret();

        if (name == "sleep") {

            await new Promise((resolve, reject) => {
                setTimeout(resolve, args[0]);
            });

        } else if (name == "keyboard_wait") {

            await new Promise((resolve, reject) => {
                document.body.addEventListener('keydown', resolve);
            });

        }
    }
    // ...
}

async function executeCode(code) {
    await this.semantics(this.grammar.match(code)).interpret();
}

I'm sure there's other use cases, such as making a fetch request or opening a file, where you want to wait until those things are done before continuing to parse the input.

luizzeroxis commented 2 years ago

Hey, just dropping by to give an update on my situation in case anyone else wants to do something similar.

I've updated my project to use the ohm-extras function toAST(), to convert the MatchResult into a basic object. From there, I manually loop through the nested function, calling the functions to interpret those nodes. Because now it's a function I made, I can control the whole stack and use as many asyncs and awaits as I want!

This makes things more complicated since now I have two objects: one for the mapping, given to toAST() to create the structure properly, and another for all the functions to be executed in each node. I also had to add in a bunch of extra arguments that give me access to the original MatchResult nodes, to use in displaying where exactly in the code a runtime error occurs.

Honestly even if Ohm adds async support, I might not use it, because with the toAST() function, I can do a few optimizations before the code runs (also I would have to rewrite everything again lol).

pdubroy commented 2 years ago

Thanks for the update! Glad to hear you solved your problem.

That's actually what I was going to suggest to you. Writing an interpreter / evaluator directly in your semantic actions works well for simple languages, but once things get more complex, it's probably best to produce an AST and interpret that.

I'm going to go ahead and close this, because I'm not yet convinced we want to allow async actions.