Skip to content
This repository was archived by the owner on Nov 3, 2021. It is now read-only.
This repository was archived by the owner on Nov 3, 2021. It is now read-only.

Text format of function calls with multi-value arguments #18

Description

@fgmccabe

Is there a proposal for extending the notation for function calls to account for the possibility that an argument call is 'supplying' multiple arguments to the call.

E.g., imagine an allocator that returns two values: the base address of an allocated string (say), and the size of the string. And suppose that such an allocated value needs to be passed to a function that needs those two values as different arguments.

The signatures of utf8-count and munge-text might be:

(function $utf8-count (i32) (param i32 i32) ...)
(function $munge-text (i32 i32) (...) ...)

respectively.
The form:

(utf8-count (munge-text ...))

looks confusing because normally each argument of a function call would be denoted by a separate expression.

One suggestion: use the LISP . ' cons operator'; as in:

(utf8-count . (munge-text ...))

although this suffers from the potential issue that . in LISP cannot be used more than once in a form.

Activity

  1. KronicDeth commented on Jul 16, 2019

    @KronicDeth

    Alternatives from other languages:

    • * (prefix) for splat
    • .. (postfix) for expand
  2. rossberg commented on Jul 16, 2019

    @rossberg
    Member

    The following works perfectly fine in the text format:

    (func $f (result i32 i32) ...)
    (func $g (param i32 i32) ...)
    (func $h
      (call $g (call $f))
    )
    
  3. fgmccabe commented on Jul 16, 2019

    @fgmccabe
    Author
  4. KronicDeth commented on Jul 16, 2019

    @KronicDeth

    In my mind I was modeling multi-value results as tuples and therefore they would need to be opted into being splatted into separate values, such as here. If they automatically splat, how do you store the multi-value result in a single variable? Or is that not a thing?

  5. fgmccabe commented on Jul 16, 2019

    @fgmccabe
    Author
  6. tlively commented on Jul 16, 2019

    @tlively
    Member

    @KronicDeth, multivalue results cannot be stored in a single value. Think of it as multiple individual values being separately pushed onto the stack one after another. So they're not really tuples.

  7. rossberg commented on Jul 16, 2019

    @rossberg
    Member

    @KronicDeth, tuples do not exist as first-class values in Wasm. It is a stack machine.

    OTOH, the folded form of the text format seems perfectly in line with how languages with first-class tuples would write the analogous.

    SML:

    fun f() = (1, 2)
    fun g(x, y) = x + y
    val h = g(f())
    

    Haskell:

    f () = (1, 2)
    g (x, y) = x + y
    h = g (f ())
    

    Ocaml:

    let f () = (1, 2)
    let g (x, y) = x + y
    let h = g (f ())
    

    etc.

    The need for splatting in some languages is merely a consequence of an artificial distinction between tuples and multiple arguments. In languages like the above, all functions have exactly one argument, which may be a tuple (though some prefer currying). So for example,

    fun g(x, y) = x + y
    val p = (1, 2)
    val h = g(p)
    

    works perfectly fine in good old SML, similarly in others.

  8. fgmccabe commented on Jul 16, 2019

    @fgmccabe
    Author

    This form of tuple syntax leads to real world confusion. In particular, it conflates

    f(x,y)

    with

    f(u,v,w)

    by binding x->u but y->(v,w)

  9. rossberg commented on Jul 16, 2019

    @rossberg
    Member

    @fgmccabe, not sure what you mean, inconsistent arities are simply a type error in any of these languages.

    Edit: Or are you talking about Wasm? There, the folded form does not imply any check that the nesting aligns with types. But that's already the case, with or without multi values.

  10. fgmccabe commented on Jul 16, 2019

    @fgmccabe
    Author

    It is not actually an 'arity error'. If I define

    f (x,y) = ...

    I can call it with

    f (u,v,w)

    with the above binding.
    This is because in ML et al, tuples are pairs.
    But if you have genuine tuples, then (x,y) != (u,v,w) for any x,y,u,v,w

  11. rossberg commented on Jul 16, 2019

    @rossberg
    Member

    @fgmccabe, no you cannot, n-ary tuples are not pairs in either ML or Haskell. Nor in any other language except Lisp/Scheme, as far as I am aware.

  12. fgmccabe commented on Jul 16, 2019

    @fgmccabe
    Author

    More generally, this is a case of "types driving syntax" (in order to understand the syntax I need to know the types). Ergonomically, it is better to have it the other way around -- e.g., syntax directed type rules.

  13. fgmccabe commented on Jul 16, 2019

    @fgmccabe
    Author

    So, if you are right, the original question stands. How do you invoke a function of three arguments where two of the arguments are the tuple result of another function call?

    Incidentally, the 'pair' interpretation of tuples is exactly how Prolog handles tuples (which is why I am so strong in my ergonomic opinion).
    Furthermore, the type of a tuple in ML takes the cross product notation integer * integer * string; so your assertion leaves me even more confused.

  14. rossberg commented on Jul 16, 2019

    @rossberg
    Member

    If you cannot pass through the tuple then you need to deconstruct it, typically with a pattern-matching let:

    fun f() = (1, 2, 3)
    fun g(x, y) = x + y
    val h = let val (x, y, _) = f() in g(x, y)
    

    Of course, you can also write a function:

    fun drop3rd(x, y, _) = (x, y)
    val h = g(drop3rd(f()))
    
  15. rossberg commented on Jul 16, 2019

    @rossberg
    Member

    The type T * U * V is not the same as T * (U * V). The * is not a binary type constructor but a (family of) mixfix type constructors. (In fact, it SML it is even just shorthand for the record type {1 : T, 2 : U, 3 : V})

  16. 4 remaining items

  17. fgmccabe commented on Jul 16, 2019

    @fgmccabe
    Author

    For bindings, I would like to be able to write

    (local-fun (string-to-linear S))

    where string-to-linear returns a pair of numbers: the address of the string and its length.

    in the binding expression.

    It is quite important to be able to strongly limit the allowed forms in a binding expression; it is also fairly important that the syntax is not overly complex.

    Since we are also constrained to use wasm's s-expression notation, I am looking for a way to write an extended form of s-expression to handle this common case. This is not an issue for the binary form; there we can do what we please.

  18. rossberg commented on Jul 16, 2019

    @rossberg
    Member

    I guess I don't see what's wrong with the very syntax you just gave?

  19. fgmccabe commented on Jul 16, 2019

    @fgmccabe
    Author

    because it's confusing. because of cases like

    (local-fun (string-to-linear S1) offset)

    (local-fun start (string-to-linear S1) end (string-to-linear S2))

    (local-fun (first (string-to-linear S1)) 23 end (string-to-linear S2))

    Understood that 'confusing' is a matter of opinion. But, my earlier comment stands: types should follow syntax, not the other way around.

  20. rossberg commented on Jul 16, 2019

    @rossberg
    Member

    Those all seem reasonable to me. Not sure in what sense syntax "follows" types here, there's simply no relation between syntactic arity and number of values. An expression can produce and consume any number of values (including 0). But that's just natural in a stack machine.

  21. fgmccabe commented on Jul 16, 2019

    @fgmccabe
    Author

    If you look carefully at

    (local-fun (string-to-linear S1) end (string-to-linear S2))

    (local-fun (first (string-to-linear S1)) 23 end (string-to-linear S2))

    you will see the type of the local-fun is actually identical even though the number of arguments supplied is different. So, in order to know that the syntax is valid, (i.e., not an arity error), you have to know the types of the sub-expressions.

    Which is why it is potentially confusing.

    (Note: 'first' returns the first of two arguments)

  22. kripken commented on Jul 16, 2019

    @kripken
    Member

    @rossberg

    The need for splatting in some languages is merely a consequence of an artificial distinction between tuples and multiple arguments

    Some of those languages need such a distinction, though, because they have both tuples and multiple arguments. For example python does, and so it has * to make it clear when a tuple is being turned into separate arguments. Are we sure wasm will never have tuples?

  23. KronicDeth commented on Jul 16, 2019

    @KronicDeth

    I understand the reasoning why pair != tuple when multi-value results are defined in terms of the stack machine @rossberg, so thanks for that clarification.

    I think the main issue we’re having is that the s-expression syntax hides the stackiness of the architecture and that it means that the function call lexical order doesn’t match evaluation order and a lot of us are too used to languages that are single return or only multi-return through tuples.

    Reminding ourselves of the mismatch between s-expressions and the stack is just something we need to do when reading the s-expression form unless we want to have splatting as an human-only syntax to remind us about the equivalent of splatting that happen on the stack.

  24. rossberg commented on Jul 17, 2019

    @rossberg
    Member

    @kripken:

    The need for splatting in some languages is merely a consequence of an artificial distinction between tuples and multiple arguments
    Some of those languages need such a distinction, though, because they have both tuples and multiple arguments.

    Um, didn't you just give a circular reason? They need it because they have it? ;)

    Of course, there are good reasons to make the distinction, esp. in lower-level languages. But notably, they all have to do with implementation concerns, not with semantic necessity.

    Are we sure wasm will never have tuples?

    Well, a stack machine is strictly more expressive than second-class tuples, i.e., already subsumes them. As for first-class tuples, the GC proposal introduces structs, which are essentially just that. In-between the two, what's left?

  25. rossberg commented on Jul 17, 2019

    @rossberg
    Member

    @fgmccabe:

    So, in order to know that the syntax is valid, (i.e., not an arity error), you have to know the types of the sub-expressions.

    "Valid" here means well-typed, so all you're observing is that in order for this to be well-typed, you have to know the types -- but that's always true. What's actually going on is that syntactic arity does not correlate with semantic arity -- neither implies the other. Whether one finds that confusing is, as you said, subjective, or perhaps just a matter of getting used to.

  26. kripken commented on Jul 17, 2019

    @kripken
    Member

    Um, didn't you just give a circular reason?

    I apologize if my wording was unclear, but basically I was saying: Python has tuples and multiple arguments, and I believe it could not manage without splat.

    Well, a stack machine is strictly more expressive than second-class tuples, i.e., already subsumes them. As for first-class tuples, the GC proposal introduces structs, which are essentially just that. In-between the two, what's left?

    Will a GC struct be splat-able into multiple arguments?

  27. rossberg commented on Jul 17, 2019

    @rossberg
    Member

    Python has tuples and multiple arguments, and I believe it could not manage without splat.

    No disagreement. When designed around that choice, a language needs extra constructs to convert between the two.

    Will a GC struct be splat-able into multiple arguments?

    There is no specific support for that, the code will just have to project the fields manually. Is there reason to believe that a built-in primitive would be more efficient?

  28. titzer commented on Jul 17, 2019

    @titzer

    I think we should avoid introducing any notion of tuples, splatting, destructuring, etc to the text format of Wasm. Such concepts won't ever map to the binary format, where everything is flat. As Luke mentioned, we just have to remind ourselves that the S-expression syntax is just shorthand for the stack machine ((func x y) is shorthand x y call[func]). Also I'm not sure why multi-return values is any more confusing than functions that return no result, as they can be syntactically nested but do not produce values.

  29. kripken commented on Jul 17, 2019

    @kripken
    Member

    Thanks @rossberg , now I see what you mean. Makes sense.

    Yeah, as @titzer just said, really the issue is that the s-expression format is just shorthand for the stack machine. I think that has confusing aspects - as s-expressions it doesn't look like a stack machine. I still wish we had a better text format :) but it works.

  30. binji commented on Jul 17, 2019

    @binji
    Member

    Since it's purely syntactic sugar, it can manifest in particularly odd ways too, like

    (i32.const 1)
    (i32.add (i32.const 2))

    or with multi value:

    (func $f (result i32 i32) unreachable)
    (func $g (param i32))
    (func $h (param i32 i32))
    
    (func
      (call $g (call $f) (call $f))
      (call $g (call $h)))

    Both of these are valid, but pretty bizarre as written.

  31. fgmccabe commented on Jul 17, 2019

    @fgmccabe
    Author

    Fun fact: the specification references S-Expressions in its section on Text format:
    https://webassembly.github.io/spec/core/bikeshed/index.html#text-format%E2%91%A0

    The reference it quotes (wikipedia) does NOT conflate multiple arguments and it has a . operator.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions