WebAssembly / component-model

Repository for design and specification of the Component Model
Other
914 stars 78 forks source link

Inconsistent wit syntax #254

Open omersadika opened 10 months ago

omersadika commented 10 months ago

I would like to point at some inconsistency I see in wit syntax, It is not clear to me if there is a good reason or I miss something.

When we define a type, interface, world, resource or importing using use we first write the keyword and then the name, for example:

record pair {
    x: u32,
    y: u32,
}
resource blob {
    constructor(init: list<u8>);
    write: func(bytes: list<u8>);
    read: func(n: u32) -> list<u8>;
    merge: static func(lhs: borrow<blob>, rhs: borrow<blob>) -> blob;
}
interface types {
  enum level {
    info,
    debug,
  }
}
interface console {
  log: func(arg: string);
}
world the-world {
  export test: func();
  export run: func();
}
world my-world {
  import host: interface {
    log: func(param: string);
  }

  export run: func();
}

But when it comes to func or inline interface inside world the syntax changes to the name, a colon and then the keyword. I'm not sure why not to keep the same syntax all the time, so for example it will look like this:

resource blob {
    constructor(init: list<u8>);
    func write(bytes: list<u8>);
    func read(n: u32) -> list<u8>;
    static func merge:(lhs: borrow<blob>, rhs: borrow<blob>) -> blob;
}
interface types {
  enum level {
    info,
    debug,
  }
}
interface console {
 func log(arg: string);
}
world the-world {
  export func test();
  export func run();
}
world my-world {
  import interface host {
    func log(param: string);
  }

  export func run();
}
lukewagner commented 10 months ago

That's a good question, because it is subtle. I think the justification for the difference is that

interface foo {
  ...
}

defines a new interface named foo that can be used by other interfaces and worlds as a named type whereas:

world w {
  import foo: interface {
    ...
  }
}

is saying that the world w (and only that world) imports a collection of values of an unnamed type where the collection of values are given the name foo. Thus, the former defines a name for a type whereas the latter is more like a parameter name (analogous to the p in func(p: string)).

omersadika commented 10 months ago

Thanks for the answer, makes sense. I would like to try to take it to the other way than, based on the same logic, I would rather expect that syntax inside an interface for types or resources:

interface types {
  level: enum {
    info,
    debug,
  };
  pair: record {
      x: u32,
      y: u32,
  };
  blob: resource {
      constructor(init: list<u8>);
      write: func(bytes: list<u8>);
      read: func(n: u32) -> list<u8>;
      merge: static func(lhs: borrow<blob>, rhs: borrow<blob>) -> blob;
  };
  log: func(arg: string);
}
lukewagner commented 10 months ago

The subtlety in this direction is that

interface foo {
  level: enum { info, debug }
}

looks like level is a value of an unnamed enum type (e.g., like you can do in C), just like how in:

interface foo {
  bar: func() -> string
}

bar is a (closure) value whose type is an unnamed function type.

Although there are currently bindings-related reasons not to allow unnamed enums, records, variants and most other compound types, in theory we could relax these restrictions in some cases for some types at some point in the future, so it's useful to preemptively put these in separate syntactic categories.

rossberg commented 10 months ago

Drive-by-comment with my 2 cent rant: the underlying problem of so many syntax confusions is that all those languages in the C syntax tradition still decide to omit the = for type definitions. The contrast between the relations = (type equals type) and : (expression has type) would make things much clearer. It's unfortunate that we keep replicating this bad design, even though at least the : bit is more widely adopted these days.