#98restore top-level statements

  • avatar~clovercommented · 2 years ago
    Author
    Owner

    The following code will error:

    let func = @extern("func");
    
    func(1);
    

    The migration path is to use a top level function. To make this syntax shorter, a parameter list was changed to be optional, meaning all you need is 6 letters, whitespace and braces

    let func = @extern("func");
    
    fn main {
      func(1);
    }
    

    This is because it is ambiguous whether func is a declaration or a local variable. Fully solving this problem likely would need to invoke semantic analysis, but we need this information at the Transform stage, as there are different instructions for loading values depending on what it is. Changing Transform and Sema to remove this restriction would probably hurt the theoretical incremental compilation design.

    Once 0.2 is available, let's see how fn main is, and we can revert this choice if

    • fn main is anoying. initial feedback has shown this, but i bet that people will warm up to it after using it.
    • the above design issues can be solved and documented.

    Milestone: 0.3.0.

  • avatar~clover changed the status to Todo · 2 years ago
  • avatar~clovercommented · 2 years ago
    Author
    Owner

    ok im sorry to the fans of top level code, it's not looking good.

    so much just got simplified. and i've also realized

    • #97 uses syntax that slightly collides with
    • The module keyword from #50 will make this much more complicated if restored. Where does top-level code go in that case?
  • avatar~jeff-hykincommented · 2 years ago

    I've been thinking about this a lot more.

    In particular the curious case of __name__ == "__main__" in python. I'm not fond of __name__ but it hints that its not "main function or no main function"; there are diverse options for limiting side effects (which I think is your main goal @paperdave.net )

    I like to imagine limiting side effects as if it was javascript or python BUT with the requirement that imported file can't have/use side effects while creating their exports.

    ex: from a import foo, where a.py

    def foo():
       return 10
    
    # bad/illegal:
    print("hehe - side effect")
    

    The downside of a main function is that logic is all in one place, rather than close to what it is needed for.

    For example, I recently was making a cold-storage tool, kind of like LocalStorage but as a proxy-object (editing object edits the cold storage). It needed to init itself at runtime with a file before exporting itself as an object.

    class ColdObject /* stuff */;
    
    // has side effect of creating a file
    export const settings = new ColdObject(
        HOME+"/"+libraryName+"/settings.toml"
    )
    

    What you might be interested in is an "auto" main function. A main function that is defined in peices/sequentially.

    export var settings
    // think of "unsafe" in rust
    @sideEffect {
        settings = new ColdObject(
            HOME+"/"+libraryName+"/settings.toml"
        )
    }
    
    export var databaseConfig
    @sideEffect {
        databaseConfig = new ColdObject(
            HOME+"/"+libraryName+"/databaseConfig.toml"
        )
    }
    

    these feel much more scripty to me (I'm curious what @lino-levan thinks) AND at compile time, they can combine into a main function:

    export var settings
    export var databaseConfig
    
    function /* invisible main */ () {
        settings = new ColdObject(
            HOME+"/"+libraryName+"/settings.toml"
        )
        databaseConfig = new ColdObject(
            HOME+"/"+libraryName+"/databaseConfig.toml"
        )
    }
    

    (the import on the other side)

    // normal js-like behavior
    const { settings, databaseConfig } = @import("config", withEffect:true)
    
    // refuses to call the code in the @withEffect blocks
    const { settings, databaseConfig } = @import("config", withEffect:false)
    

    The hard part would be deciding the behavior for something like this:

    export var settings = 10
    // think of "unsafe" in rust
    @sideEffect {
        console.log(settings)
    }
    settings = 20
    

    Should it print 10, like an inline script, or 20 as if the code was "down hoisted". If it is down-hoisted, renaming @sideEffect to something like@afterImport might help devs expect that kind of behavior.

    _

    @ paperdave :

    your hard part is exactly why i gave up solving this problem. the solution is to ban both of the two lines after var decl

    for ColdObject, i think making a function which creates the cold object would be more preferable.

    fn newTestingColdObject(name: string) {
        // there will be no "new" keyword, instead
        // declarations on structure types
        ColdObject.init(std.fs.path.join(HOME, lib_name, name))
    }
    
  • avatar~jeff-hykincommented · 2 years ago

    (to go in a different direction for when/if this is resumed)

    Whats the main downside of the down-hoisting approach? (e.g. it prints 20) is it mainly that devs strongly expect it to print 10?

  • avatar~jeff-hykincommented · 2 years ago

    Related work: ruby's lesser-known BEGIN/END blocks

  • avatar~jeff-hykincommented · 2 years ago

    Seprate but related question, can a main function be called twice? or is main special and it can only be called once.

    In other words does this code print out setting a_lib.a to be: init two times or one time?

    a_lib.wl

    var a = ""
    export fn getA() {
       return a
    }
    export fn setA(val) {
        @external("console").log("setting a_lib.a to be: "+val)
        a = val
    }
    export fn main() {
        setA("init")
    }
    

    location1.wl

    const aLib1 = @import("a_lib.wl")
    export fn main() {
        aLib1.main()
        aLib1.setA("loc1")
    }
    

    location2.wl

    const aLib2 = @import("a_lib.wl")
    export fn main() {
        aLib2.main()
        aLib2.setA("loc2")
    }
    

    main.wl

    const loc1 = @import("location1.wl")
    const loc2 = @import("location2.wl")
    fn main() {
        loc1.main()
        loc2.main()
    }
    
  • avatar~clovercommented · 2 years ago
    Author
    Owner

    another reason to disallow top-level statements is to allow for test declarations to be in an obviously different scope as the main application.

  • avatar~jeff-hykincommented · 2 years ago

    I forgot to mention I now support not sharing globals. Like even via a getter and setter (which IMO is a big deal and might be really hard to enforce)

    I wanted to document this though, because I think previous reason ( "there is no valid usecase" ) wasn't sufficient justification.

    Justification

    There are certain things -- such as the STDIN handle, serial ports, or browser history -- that a program ONLY has 1 of. I consider these "hard" globals, I still hold that those things belong in global variables. The nuance is WHO gets access/control over them.

    Similarly there are "soft" globals, such as one library registering a seralizer with the builtin JSON module. Another example "soft" example is changing the default behavior of errors and warnings, like the python pretty-errors module. A library that exposes a set/get and lets other libraries increment the global

    Allowing libraries unprompted access globals (hard or soft), I agree, is a problem. It creates situations where a library will grab a resource for itself and the dev doesn't know who or where the resource is being grabbed.

    Consequences

    The alternative DOES require the dev to do more work; modules cannot auto-register themselves, they can't automatically claim a cache folder as their own. The dev must explicitly hand each library the resources they want to use.

    Where I'm unsure, is when module A wants to register itself with module B. I think it shouldn't be able to do that on its own, but enforcement could be difficult.

    • Module A can use module B, so long as Module B doesn't keep state.
    • Module A can use module B (with state), so long as Module B's state isn't used by any other module (ex: main)
      • ex: if Module A was given a copy of Module B rather than "the real thing"
    • Module A can use module B, if the dev explicitly gives, for example a lambda, to Module A which can then mutate module B's state