Context

This article goes over the Context structure in detail

What is the Context?

The context can be viewed as the working image of the Juvix Language. In particular it is the storage/information system of the language. It is also responsible for having the semantics of symbol resolution Due to this, we can view the context as a fancy nested hashtable.

What information is stored in the Context?

The context stores a plethora of information.

  • Currently the Context Contains

    1. Definitions

      • These hold the Standard desugared Juvix definitions.

    2. Types

      • These are the data type declarations in the Juvix language.

      • Every definition also has a type attached as well that one can query.

    3. Constructors

      • These are stored like Definitions, but for type consturctors.

    4. Records/Modules

      • These are recursive name-spaces.

      • Each record/module holds two name-spaces inside of them, a public and private one.

    5. Precedence

      • This is associated with a definition, so we know how to desugar precedence.

    6. Reverse Module use lookup

      • We get to know what modules import what module, and if they are implicit or explicit.

    7. Current Module/Record

      • This is the current module in scope.

In the long run this information will be added to, with each declared item in the image having a general hashtable that any information can be stored on. This will include documentation and the original source code.

How does Symbol resolution work?

Symbol Resolution is handled entirely by the context. When we ask the context what Foo.Bar.zed resolves to, we do a few things.

First we check if Foo is in the current name-space. Specifically if Foo is both in the private name-space and public one, then the private one wins. After this point, we have no means of accessing private name-spaces, so we just access the public Bar and zed inside the Foo module we resolved.

If Foo is not in the locals, then we check the top of the context for Foo. If it's not there, then we are unable to resolve it.

The only other special part of a known symbol is the resolution of names like TopLevel which bypasses any local module checks, and instantly resolves from the top. so Toplevel.Foo.Bar.zed will always look in the toplevel name-space.

Special Rules for dealing with unresolved symbols

Sometimes we wish to quantify over symbols which we may add or create. For our previous example, what if Foo.Bar.zed is undefined? Operations which operate over hypothetical symbols have a way to know if the symbol is defined or not.

The resolution rules are fairly simple, but we will investigate them with three case studies where the symbols may be undefined. zed, Foo.zed, TopLevel.Foo.zed

  1. zed

    • Since zed is not in scope, we only really have two choices. We either mean Toplevel.zed or CurrentNameSpace.zed. Since we find that we typically mean zed in the current module, and not some hypothetical one at a top level, we try to do the latter. So the operation deals with an undefined CurrentNameSpace.zed

  2. Foo.zed

    • Since This case can have a condition where Foo could also be resolved, we break this into another case analysis.

      1. Foo is resolved

        • In this case, we act much like the 1) case, where we explicitly work in an unresolved CurrentNameSpace.Foo.zed. or a Toplevel.Foo.zed depending on which one is in scope (see the scoping section above!).

      2. Foo is not resolved

        • In this case we actually do a different resolution than the zed case. We assume we are talking a toplevel version, namely Toplevel.Foo.zed

  3. Toplevel.Foo.zed

    • This one is rather easy as well, as it falls back to case 2 subsection 2.

Overall some of these rules are arbitrary, namely case 2 subsection 2 may need to conform to be more like case 1, however this requires testing and playing with the system more to determine.

Both this section and the above are implemented in the modify speace impure section.

What are the Functions of the Context?

The context hosts a plethora of useful functions that make traversing it easy. Most of these more useful functions are built upon the more primitive operations.

Many operations that create name-spaces are in IO, to hold the recrusive maps we use to track information.

Primitive Operations

Empty

  empty :: NameSymbol.T -> IO (T term ty sumRep)

empty creates a new context in the name-space you call it with (thus empty "Foo.Bar" makes a context with the nested module Bar in Foo).

The current module is then set to this name.

Lookup

  lookup ::
    NameSymbol.T -> T term ty sumRep -> Maybe (From (Definition term ty sumRep))

  (!?) ::
    T term ty sumRep -> NameSymbol.T -> Maybe (From (Definition term ty sumRep))

looup looks up the given name-symbol according to the rules in the Symbol resolution section. !? is just flipped lookup.

Add

  add ::
    NameSpace.From Symbol ->
    Definition term ty sumRep ->
    T term ty sumRep ->
    T term ty sumRep

add name def context adds name with the definition def to the current name-space of an existing context. The type NameSpace.From can either be Pub or Priv currently, stating how we want to add the definition to the current context.

As can be observed, we are adding a Symbol and not a NameSymbol, and so this primitive can't add name to a separate module. This should be changed in time.

Remove

  remove ::
    NameSpace.From Symbol ->
    T term ty sumRep ->
    T term ty sumRep

remove name context removes the name from the public or private name-space of the current name-space.

InNameSpace

  inNameSpace :: NameSymbol.T -> T term ty sumRep -> Maybe (T term ty sumRep)

inNameSpace name context is the primitive way in which we change the current name-space. like lookup, we use the standard symbol resolution rule to determine which module we resolve to. If the module is not found, then we simply return Nothing. Also if the definition we land on is not a record/module, then likewise we also return Nothing. If all is successful, then we end up with a new context with a new current name-space.

ModifySpaceImp ∧ type Stage ∧ type Return

  data Stage b
    = -- | 'Final' signifies the last symbol that we
      -- pursue in updating a structure
      Final b
    | -- | 'Continue' signifies that there are parts of
      -- the namespace that we can still continue down
      Continue b

  data Return term ty sumRep
    = -- | 'GoOn' signifies that we should continue
      -- going down records
      GoOn (Record term ty sumRep)
    | -- | 'Abort' signifies that we should cancel
      -- the changes on the map and
      Abort
    | -- | 'UpdateNow' signifies that we should
      -- update the context with the current value
      UpdateNow (Definition term ty sumRep)
    | -- | 'RemoveNow' signifies that we show remove
      -- the definition at this level
      RemoveNow

  modifySpaceImp ::
    Monad m =>
    ( Stage (Maybe (Definition term ty sumRep)) ->
      m (Return term ty sumRep)
    ) ->
    NameSymbol.T ->
    T term ty sumRep ->
    m (Maybe (T term ty sumRep))

modifySpaceImp f sym context is a rather important operation. Some forms like Empty even rely on it! modifySpaceImp is responsible for implementing the behavior for known symbols exactly how Symbol Resolution

lays it out, and for unknown symbols how Rules for Unresolved Symbols lays it out.

The Maybe it takes indicates whether the symbol is resolved or not.

We decided to include the Return and Stage type here, as they both talk about something integral to the design of this function. Namely the Return talks about the range of operations we care about. This limits the kinds of operations this modifyNameSpaceImp function can do. The Stage on the other hand denotes to the operation function f whether we are at the final symbol in the resolution or are we still going down to nested structure to find the proper symbol. Many operations will do something special at the very end, and so we often care about what stage we are at.

Finally, our function returns some monad m, this is due to just being general to accept any kind of function, as the name suggests there is a non Impure version of this, however this just runs this more general one in the Identity monad.

Derived Operations

These operations aren't derived from the primitive operations, however are quite important for operation of the context. Some even have special rules one must pay attention to.

SwitchNameSpace

  switchNameSpace ::
    NameSymbol.T -> T term ty sumRep -> IO (Either PathError (T term ty sumRep))

switchNamespace name context is an important operation. This creates and switches to the name-space even if it doesn't exist! The rules for creation are exactly that ruled in the Symbol Resolution section.

The creation of new name-spaces are IO, and so the operation is in IO as well.