Skip to content

TM013 Exporting types from modules

sstrickl edited this page Sep 24, 2014 · 12 revisions

Problems

Currently, there is no support for storing compile-time information like types for a given module. Now that Pyret has an (optional) type checker, this means that to compile (and type-check) a given module, we must load the source for all its dependencies. In the current system, this means giving up on separate compilation, and having to reload the source for dependencies, even when unchanged, is an additional burden on systems where the sources are held on a different server like Google Drive.

Furthermore, the types as written within the providing module may include information that should not be exported to clients. For example, a server module may wish to export a given number as type SSN without allowing the client module to inspect the value directly. By exporting positions that correspond to SSNs as the opaque (existential) type SSN, they are restricted to using operations on those values that the server module provides. Similarly, the server module may wish to export a datatype as a type constructor that reveals its element type, but doesn't allow it to be destructed using cases.

This lack of support for storing compile-time information doesn't affect only type-checked programs. Currently there is no official support for transitive module compilation, that is, specifying the root that you are interested in compiling and having all its dependencies compiled (if needed), with the process being repeated for each of those dependencies. While adding transitive module compilation isn't impossible in the current system, that requires loading the entire source for each module to determine the dependencies, at which point we might as well compile it. Also, even if we check the compiled timestamp versus the original file, that doesn't mean that an upstream dependency hasn't changed, so we have to fetch them anyway to be certain. Even worse, on systems like code.pyret.org, fetching the source might be a very heavy-weight operation. If an upstream dependency is recompiled, then we must recompile all dependents to be safe, even though the interface to the module may not have changed.

Solution

To solve these issues, we propose that for a given module, the compiler emit not only the compiled JavaScript equivalent but also an interface file that contains the module dependencies and exports (names, datatypes, and types). In the case that the source file is older than the compiled version, we need not load the source file itself to determine types or dependencies, but can load only the interface file. Furthermore, if the interface file, and thus the interface, does not change after compilation, there is no need to recompile its dependents. In the case of remote storage for source files, the results of compilation can either be stored in local storage or somehow represented in memory. Since these interfaces are (distilled) data about the module, we represent the information in file form using JSON.

Specification

Provide Format

Currently information is provided from a module in two ways: provide, which exports names (of values), and provide-types, which exports types. We propose unifying these into a single provide form with the following clauses:

provide {
  I1 [:: T] [= I2],         # export external id I1 (from internal id I2) (with type T)
  [public] data T,        # public is implicit, and means variants are exposed
  [public] type T1 = T2,  # expose alias for non-datatypes, implicit behaviour
  private type T1 [= T2]  # hide portions or all of T2 as T1
}

NOTE (Ben): I like the idea of unifying these provide blocks, but I don't like the syntax any more. It used to be that we were actually evaluating the module to an object literal, and could (if we wanted to) actually export any value there, including non-object ones. But with this unified syntax, there's no longer much of a pun with object-literal syntax, and we never really use the facility of exporting a non-object value. So I'd prefer the syntax to be

provide:
  I1 [:: T] [= I2],
  [public] data T,
  [public] type T1 = T2,
  private type T1 [= T2]
end

NOTE (Stevie): Agreed. I haven't updated the syntax below to use your suggestion, but I agree it's the right thing and Cody will be implementing that change while working on merging provide and provide-types.

Examples

Untyped exports:

provide {
  insert, combine, map
  data DS
}

Typing and exposing the variants of the data structure:

provide {
  insert :: (<A> A, DS<A> -> DS<A>),
  combine :: (<A> DS<A>, DS<A> -> DS<A>),
  map :: (<A, B> (A -> B), DS<A> -> DS<B>),
  data DS<A>
}

(How do we currently denote the scope of type variables?


)

Hiding the variants of the data structure:

provide {
  insert :: (<A> A, DS<A> -> DS<A>),
  combine :: (<A> DS<A>, DS<A> -> DS<A>),
  map :: (<A, B> (A -> B), DS<A> -> DS<B>),
  private type DS<A>
}

Hiding the name and variants of an internal data structure:

provide {
  insert :: (<A> A, DS<A> -> DS<A>),
  combine :: (<A> DS<A>, DS<A> -> DS<A>),
  map :: (<A, B> (A -> B), DS<A> -> DS<B>),
  private type DS<A> = AVL<A>
}

Hiding the name and variants of both the internal data structure and implemented functions:

provide {
  insert :: (<A> A, DS<A> -> DS<A>) = avl-insert,
  combine :: (<A> DS<A>, DS<A> -> DS<A>) = avl-combine,
  map :: (<A, B> (A -> B), DS<A> -> DS<B>) = avl-map,
  private type DS<A> = AVL<A>
}

Hiding part of a type:

provide {
  private type Maker<A> = ( -> A),
  empty-string-gen :: Maker<String>,
  zero-gen :: Maker<Number>
}

Exporting opaque (existential) types:

provide {
  private type Counter = Number,
  dec :: (Counter -> Counter),
  done? :: (Counter -> Boolean),
}

Same, but without explicit binding to internal type (type checker should make sure Counter used consistently to masking same type within the corresponding internal types):

provide {
  private type Counter,
  dec :: (Counter -> Counter),
  done? :: (Counter -> Boolean),
}

File Format (JSON)

Exported object:

{
  "values": {
    "identifier": { // Type object

    }
  },
  "datatypes": {
    "List": { // Datatype object
      "params": [], // List of parameter objects
      "variants": {
        "link": {
          "type": "variant",
          "fields": [], // List of type members
          "with-fields": [] // List of type members
        },
        "empty": {
          "type": "singleton-variant",
          "with-fields": [] // List of type members
        }
      },
      "fields": [] // List of type members
    },
  },
  "aliases": {
    "aliasName": {
      "params": [],

    }
  }
}

Types:

{ // t-name
  "type": "t-name",
  "module-name": "string",
  "id": { // Name object

  }
}
{ // t-var
  "type": "t-var",
  "id": { // Name object

  }
}
{ // t-arrow
  "type": "t-arrow",
  "args": [] // List of type objects
  "ret": { // Type object

  }
}
{ // t-app
  "type": "t-arrow",
  "onto": { // Type object (should be t-name)

  }
  "args": [] // Non-empty list of type objects
  "ret": { // Type object

  }
}
{ // t-top
  "type": "t-top"
}
{ // t-bot
  "type": "t-bot"
}
{ // t-record
  "type": "t-record",
  "fields": [] // List of type members
}
{ // t-forall
  "type": "t-forall",
  "fields": [], // List of type variables
  "onto": { // Type object

  }
}

Type variables:

{
  "id": { // Name object

  },
  "upperBound": { // Type object

  },
  "variance": "string" // Should be a string representing the variance
}

Type members:

{
  "fieldName": "string",
  "typ": { // Type object

  }
}