Lunar Programming Language

by David A. Moon
January 2017 - January 2018



Behavior

All behavior in Lunar is implemented by invoking a function with zero or more actual parameters. The function can be a single method or a function bundle that contains several methods and selects one method to execute. A method can be intrinsic or user-defined.

Even the most primitive operations, such as integer addition or equality comparison, are actually methods. The compiler is expected to inline many function invocations for efficiency, when it knows which of the function's methods will be invoked and what that method does.

A function is a member of the abstract class function. It can be invoked with a list of actual parameters. Each actual parameter is a datum.

A function invocation returns a result, which is one datum. To return multiple results, a function returns a sequence of data.

A function invocation might or might not have side-effects as part of its behavior.

Function Bundles

A function bundle is a member of the class function and of the class bundle. A function bundle's behavior is defined by a set of methods. The function uses the actual parameters at run-time to select one method which defines the behavior and result of executing a function invocation. See Method Selection for details.

See Bundles for more about bundles.

Methods

A method is part of a function bundle and is a member of the class method which is a subclass of function. Like a function bundle, a method can be invoked with a list of actual parameters, returns one datum as its result, and can have side-effects. If the method is not applicable to the actual parameters, an error occurs.

A method has a list of formal parameters. Each formal parameter has a name which becomes defined as a constant whose value is the corresponding actual parameter, or a default if no actual parameter was supplied, or a sequence of actual parameters in the case of a rest formal parameter. This definition is visible in the body of the method. Each formal parameter has a type and the method is only applicable when the corresponding actual parameter is a member of that type.

A method has a body which consists of one or more expressions. Invoking the method executes the body expressions. The result of the method is the result of the last expression in the body.

A method can declare a result type. If none is declared it defaults to the static type of the last expression in the body. This can be inferred at compile time. If a result type is declared, the compiler checks that the result is a member of that type.

There are four varieties of formal parameter:

Required and optional parameters are positional. That means the corresponding actual parameter is at the same position in the actual parameter list as the formal parameter's position in the formal parameter list.

Optional and named formal parameters have a default expression. When there is no corresponding actual parameter, the default expression is executed and its result is used instead of the missing actual parameter. Formal parameter names earlier in the formal parameter list are in scope in a default expression. If no default expression is provided, it defaults to false.

Named formal parameters have a selector which is the same as the parameter's name unless specified otherwise. The actual parameters following those that correspond to required or optional formal parameters are considered two at a time as selector and value. The selector actual parameter is simply a member of the class name. When an actual parameter at an appropriate position in the actual parameter list is a name that equals a named formal parameter selector, the following actual parameter is the corresponding actual parameter for the selected named formal parameter. Duplicate selectors in the actual parameters are not an error; the leftmost matching selector is used and the rest are ignored.

Unlike in Python, a named formal parameter can never take its value from a positional actual parameter. Allowing that would break method specificity.

A method can have at most one rest formal parameter. If a rest formal parameter is present, its value is a constant sequence of the actual parameters following those that correspond to required or optional formal parameters. If the method also has any named formal parameters, their corresponding actual parameter selectors and values will be included in that sequence.

Each formal parameter has a type. If a type expression is provided, it is executed once when the method is defined. The type of a rest formal parameter constrains the type of each corresponding actual parameter and is the member type of the sequence. If no type expression is provided, it defaults to everything.

The result of a default expression must be a member of the formal parameter's type. If not, a type_error occurs when the default would be used.

There is a special abbreviated syntax for a formal parameter whose type is a set that contains just one member, a name, integer, or character. The method will only be applicable when the corresponding actual parameter is that name, integer, or character. #foo is short for temp set(#foo) where temp is a unique name that will not be visible in the method body and set(#foo) is the type expression. This would normally only be used as a required formal parameter.

See Formal Parameter List for all syntactic details.

Formal parameter lists also support destructuring. See Destructuring Formal Parameters .

Method Selection

Invoking a function bundle uses the actual parameters to select one method which defines the behavior and result.

The first step identifies the subset of the function's methods which are applicable. A method is applicable when all of the following conditions are true:

If there are no applicable methods, a no_applicable_method_error occurs.

The second step sorts the applicable methods by specificity. If exactly one applicable method is more specific than every other applicable method, that method is selected. Otherwise, an ambiguous_method_error occurs.

Method specificity is the ≤ relation defined as follows:

Method M1 ≤ M2 if and only if for all possible parameter positions, M1's formal parameter type at that position ≤ M2's formal parameter type at that position. Type T1 ≤ T2 if they are the same type or T1 is a subtype of T2.

A parameter position is either a non-negative integer or a name.

A method's formal parameter type at an integer position is the type of the required or optional formal parameter at that position, if there is one, otherwise it is the type of the rest formal parameter if there is one.

A method's formal parameter type at a named position is the type of the named formal parameter whose selector equals the position.

A method's formal parameter type at a position is nothing if the position is numeric and the method has no required or optional formal parameter at that position and has no rest formal parameter. Thus if a method would not be applicable if there were an actual parameter at that position, its formal parameter type nothing is ≤ all types.

When the position is a name and the method has no named formal parameter whose selector equals the position, the method's formal parameter type at that position is the method's rest formal parameter's type if it has one, otherwise nothing.

The number of parameter positions is infinite, but the specificity computation remains finite because for any given method only a finite number of positions can have different types.

Note that method specificity is a partial ordering and is independent of the actual parameters. A variety of optimizations are possible to do some or all of the method selection work at compile time or link time, or to cache the work's result at run time.

Here is a useful trick to simplify method selection:

Methods M1 and M2 are disjoint if they cannot both be applicable to the same actual parameters. This is true if at any possible parameter position, M1's formal parameter type at that position is disjoint with M2's formal parameter type at that position, assuming nothing is not considered disjoint with itself.

Consider all pairs of methods M1 and M2 in a function bundle's set of methods. If neither M1 ≤ M2, nor M2 ≤ M1, nor disjoint?(M1, M2), these two methods are potentially ambiguous. Create a dummy method M3 whose formal parameter type at every possible parameter position is the intersection of M1's type and M2's type at that position. Obviously M3 ≤ M1 and M3 ≤ M2. If the dummy method is invoked, it reports an ambiguous_method_error.

Store in the function bundle a list containing all real and dummy methods, sorted by ≤. Method selection can simply choose the first applicable method in this list. It is guaranteed to be the most specific applicable method or to report an ambiguous_method_error.

Casting and Delegation

It is sometimes useful for a method to delegate to a less specific method in the same function bundle. This is generalized to casting (actually up-casting).

An actual parameter can be the expression value as type. In that case method selection uses the result of type rather than the actual type of the result of value to determine the set of applicable methods.

If the result of type is not a type or the result of value is not a member of the value of type, a type_error occurs.

Here is a sketch of an example:

defclass window ..when necessary.
when necessary.
..

defclass outlined_window () window

def render(w outlined_window, d display)
  draw_outline(d, w.x, w.y, w.width, w.height)
  render(w as window, d)  ; call superclass method

Sealed Methods

A method can be sealed, which means that if the method is applicable then it is necessarily the most specific method. This might make the program easier to understand for humans, and is necessary for the compiler to understand the program well enough to perform inlining, because new methods can be added at run-time so long as they do not violate sealing.

A sealing_violation_error occurs if there is an attempt to define a method that is more specific than a sealed method for the same function bundle.

Unlike sealed classes, sealed methods do not admit an exception for definitions in the same source file.

Dominant Methods

A method can be dominant, which asserts that if the method is applicable then it has the same effects and result as any other applicable method that is not less specific than the dominant method. This is a weaker condition than sealed which still allows inlining. It can be convenient for avoiding ambiguous_method_error.

Dominant method assertions are not verified by the compiler nor by the runtime, so they should be used sparingly.

Intrinsic Methods

A method can be intrinsic, which means that the compiler has special knowledge of the method. The body of the method might be replaced by special compiler-generated code.


Previous page   Table of Contents   Next page



Creative Commons License
Lunar by David A. Moon is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
Please inform me if you find this useful, or use any of the ideas embedded in it.
Comments and criticisms to dave underscore moon atsign alum dot mit dot edu.