Expressions and values

Jump to: | ‍ ‍ | OMake Home • Guide Home • Guide (single-page) • Contents (short) • Contents (long)
Index: | ‍ ‍ | All • Variables • Functions • Objects • Targets • Options

# Chapter ‍6 Expressions and values

- Dynamic scoping
- Functional evaluation
- Exporting the environment
- Objects
- Field and method calls
- Method override
- Super calls

omake provides a full programming-language including many system and IO functions. The language is object-oriented – everything is an object, including the base values like numbers and strings. The omake language can be characterized as follows:

- Scoping is by default dynamic.
- The language is mostly functional, and side effects on variables are normally passed on from scope to scope via an explicit `export` directive, instead of directly mutating variables
- Evaluation is normally eager – that is, expressions are evaluated as soon as they are encountered.

To illustrate these features, we will use the osh(1) omake program shell. The osh(1) program provides a toploop, where expressions can be entered and the result printed. osh(1) normally interprets input as command text to be executed by the shell, so in many cases we will use the `value` form to evaluate an expression directly.

        osh> 1
        *** omake error: File -: line 1, characters 0-1 command not found: 1
        osh> value 1
        - : "1" : Sequence
        osh> ls -l omake
        -rwxrwxr-x  1 jyh jyh 1662189 Aug 25 10:24 omake*

## 6.1 Dynamic scoping

Dynamic scoping means that the value of a variable is determined by the most recent binding of the variable in scope at runtime. Consider the following program.

        OPTIONS = a b c
        f() =
           println(OPTIONS = $(OPTIONS))
        g() =
           OPTIONS = d e f
           f()

If `f()` is called without redefining the `OPTIONS` variable, the function should print the string `OPTIONS = a b c`.

In contrast, the function `g()` redefines the `OPTIONS` variable and evaluates `f()` in that scope, which now prints the string `OPTIONS = d e f`.

The body of `g` defines a local scope – the redefinition of the `OPTIONS` variable is local to `g` and does not persist after the function terminates.

        osh> g()
        OPTIONS = d e f
        osh> f()
        OPTIONS = a b c

Dynamic scoping can be tremendously helpful for simplifying the code in a project. For example, the OMakeroot file defines a set of functions and rules for building projects using such variables as `CC`, `CFLAGS`, etc. However, different parts of a project may need different values for these variables. For example, we may have a subdirectory called `opt` where we want to use the `-03` option, and a subdirectory called `debug` where we want to use the `-g` option. Dynamic scoping allows us to redefine these variables in the parts of the project without having to redefine the functions that use them.

        section
           CFLAGS = -O3
           .SUBDIRS: opt
        section
           CFLAGS = -g
           .SUBDIRS: debug

However, dynamic scoping also has drawbacks. First, it can become confusing: you might have a variable that is intended to be private, but it is accidentally redefined elsewhere. For example, you might have the following code to construct search paths.

       PATHSEP = :
       make-path(dirs) =
          return $(concat $(PATHSEP), $(dirs))

       make-path(/bin /usr/bin /usr/X11R6/bin)
       - : "/bin:/usr/bin:/usr/X11R6/bin" : String

However, elsewhere in the project, the `PATHSEP` variable is redefined as a directory separator `/`, and your function suddenly returns the string `/bin//usr/bin//usr/X11R6/bin`, obviously not what you want.

The `private` block is used to solve this problem. Variables that are defined in a `private` block use static scoping – that is, the value of the variable is determined by the most recent definition in scope in the source text.

       private
          PATHSEP = :
       make-path(dirs) =
          return $(concat $(PATHSEP), $(dirs))

       PATHSEP = /
       make-path(/bin /usr/bin /usr/X11R6/bin)
       - : "/bin:/usr/bin:/usr/X11R6/bin" : String

## 6.2 Functional evaluation

This has two aspects: First of all, functions are values like other values:

        p(f, x) =
            y = $(f $(x), 1)
            println($"The value is $(y)")
        p($(add), 5)       # prints 6
        p($(sub), 5)       # prints 4

The other aspect is that variables (and thus the whole environment) can exist in several versions: The assignment to a variable creates first a second version in the current block, and is not directly applied to the orignal variable, unless it is finally “exported”.

(Note that in previous versions of the manual you could read here that there “is no assignment operator”. On the surface this is of course not true, as we provide such an operator. This comment referred to the implementation, which represents environments as functional maps from names to values, and reduces assignment to a functional update of the current environment, yielding to a new version.)

## 6.3 Exporting the environment

The `export` directive can be used to propagate all or part of an inner scope back to its parent. If used without arguments, the entire scope is propagated back to the parent; otherwise the arguments specify which part of the environment to propagate. The most common usage is to export some or all of the definitions in a conditional block. In the following example, the variable `B` is bound to 2 after the conditional. The `A` variable is not redefined.

        if $(test)
           A = 1
           B = $(add $(A), 1)
           export B
        else
           B = 2
           export

If the `export` directive is used without an argument, all of the following is exported:

- The values of all the dynamically scoped variables (as described in Section ‍5.5).
- The current working directory.
- The current Unix environment.
- The current implicit rules and implicit dependencies (see also Section ‍8.11.1).
- The current set of “phony” target declarations (see Sections ‍8.10 and ‍8.11.3).

If the `export` directive is used with an argument, the argument expression is evaluated and the resulting value is interpreted as follows:

- If the value is empty, everything is exported, as described above.
- If the value represents a environment (or a partial environment) captured using the `export` function, then the corresponding environment or partial environment is exported.
- Otherwise, the value must be a sequence of strings specifying which items are to be propagated back. The following strings have special meaning:
  - `.RULE` — implicit rules and implicit dependencies.
  - `.PHONY` — the set of “phony” target declarations.
  All other strings are interpreted as names of the variables that need to be propagated back.

For example, in the following (somewhat artificial) example, the variables `A` and `B` will be exported, and the implicit rule will remain in the environment after the section ends, but the variable `TMP` and the target `tmp_phony` will remain unchanged.

    section
       A = 1
       B = 2
       TMP = $(add $(A), $(B))

       .PHONY: tmp_phony

       tmp_phony:
          prepare_foo

       %.foo: %.bar tmp_phony
          compute_foo $(TMP) $< $@
       export A B .RULE

### 6.3.1 Export regions

This feature was introduced in version 0.9.8.5.

The `export` directive does not need to occur at the end of a block. An export is valid from the point where it is specified to the end of the block in which it is contained. In other words, the export is used in the program that follows it. This can be especially useful for reducing the amount of code you have to write. In the following example, the variable `CFLAGS` is exported from the both branches of the conditional.

        export CFLAGS
        if $(equal $(OSTYPE), Win32)
            CFLAGS += /DWIN32
        else
            CFLAGS += -UWIN32

### 6.3.2 Returning values from exported regions

This feature was introduced in version 0.9.8.5.

The use of export does not affect the value returned by a block. The value is computed as usual, as the value of the last statement in the block, ignoring the export. For example, suppose we wish to implement a table that maps strings to unique integers. Consider the following program.

        # Empty map
        table = $(Map)

        # Add an entry to the table
        intern(s) =
            export
            if $(table.mem $s)
                table.find($s)
            else
                private.i = $(table.length)
                table = $(table.add $s, $i)
                value $i

        intern(foo)
        intern(boo)
        intern(moo)
        # Prints "boo = 1"
        println($"boo = $(intern boo)")

Given a string `s`, the function `intern` returns either the value already associated with `s`, or assigns a new value. In the latter case, the table is updated with the new value. The `export` at the beginning of the function means that the variable `table` is to be exported. The bindings for `s` and `i` are not exported, because they are private.

Evaluation in omake is eager. That is, expressions are evaluated as soon as they are encountered by the evaluator. One effect of this is that the right-hand-side of a variable definition is expanded when the variable is defined.

        osh> A = 1
        - : "1"
        osh> A = $(A)$(A)
        - : "11"

In the second definition, `A = $(A)$(A)`, the right-hand-side is evaluated first, producing the sequence `11`. Then the variable `A` is redefined as the new value. When combined with dynamic scoping, this has many of the same properties as conventional imperative programming.

        osh> A = 1
        - : "1"
        osh> printA() =
            println($"A = $A")
        osh> A = $(A)$(A)
        - : "11"
        osh> printA()
        11

In this example, the print function is defined in the scope of `A`. When it is called on the last line, the dynamic value of `A` is `11`, which is what is printed.

However, dynamic scoping and imperative programming should not be confused. The following example illustrates a difference. The second `printA` is not in the scope of the definition `A = x$(A)$(A)x`, so it prints the original value, `1`.

        osh> A = 1
        - : "1"
        osh> printA() =
            println($"A = $A")
        osh> section
                 A = x$(A)$(A)x
                 printA()
        x11x
        osh> printA()
        1

See also Section ‍7.7 for further ways to control the evaluation order through the use of “lazy” expressions.

## 6.4 Objects

omake is an object-oriented language. Everything is an object, including base values like numbers and strings. In many projects, this may not be so apparent because most evaluation occurs in the default toplevel object, the `Pervasives` object, and few other objects are ever defined.

However, objects provide additional means for data structuring, and in some cases judicious use of objects may simplify your project.

Objects are defined with the following syntax. This defines `name` to be an object with several methods an values.

        name. =                     # += may be used as well
           extends parent-object    # optional
           class class-name         # optional

           # Fields
           X = value
           Y = value

           # Methods
           f(args) =
              body
           g(arg) =
              body

An `extends` directive specifies that this object inherits from the specified `parent-object`. The object may have any number of `extends` directives. If there is more than on `extends` directive, then fields and methods are inherited from all parent objects. If there are name conflicts, the later definitions override the earlier definitions.

The `class` directive is optional. If specified, it defines a name for the object that can be used in `instanceof` operations, as well as `::` scoping directives discussed below.

The body of the object is actually an arbitrary program. The variables defined in the body of the object become its fields, and the functions defined in the body become its methods.

## 6.5 Field and method calls

The fields and methods of an object are named using `object.name` notation. For example, let’s define a one-dimensional point value.

       Point. =
          class Point

          # Default value
          x = $(int 0)

          # Create a new point
          new(x) =
             x = $(int $(x))
             return $(this)

          # Move by one
          move() =
             x = $(add $(x), 1)
             return $(this)

       osh> p1 = $(Point.new 15)
       osh> value $(p1.x)
       - : 15 : Int

       osh> p2 = $(p1.move)
       osh> value $(p2.x)
       - : 16 : Int

The `$(this)` variable always represents the current object. The expression `$(p1.x)` fetches the value of the `x` field in the `p1` object. The expression `$(Point.new 15)` represents a method call to the `new` method of the `Point` object, which returns a new object with 15 as its initial value. The expression `$(p1.move)` is also a method call, which returns a new object at position 16.

Note that objects are functional — it is not possible to modify the fields or methods of an existing object in place. Thus, the `new` and `move` methods return new objects.

## 6.6 Method override

Suppose we wish to create a new object that moves by 2 units, instead of just 1. We can do it by overriding the `move` method.

       Point2. =
          extends $(Point)

          # Override the move method
          move() =
             x = $(add $(x), 2)
             return $(this)

       osh> p2 = $(Point2.new 15)
       osh> p3 = $(p2.move)
       osh> value $(p3.x)
       - : 17 : Int

However, by doing this, we have completely replaced the old `move` method.

## 6.7 Super calls

Suppose we wish to define a new `move` method that just calls the old one twice. We can refer to the old definition of move using a super call, which uses the notation `$(classname::name <args>)`. The `classname` should be the name of the superclass, and `name` the field or method to be referenced. An alternative way of defining the `Point2` object is then as follows.

       Point2. =
          extends $(Point)

          # Call the old method twice
          move() =
             this = $(Point::move)
             return $(Point::move)

Note that the first call to `$(Point::move)` redefines the current object (the `this` variable). This is because the method returns a new object, which is re-used for the second call.

Jump to: | ‍ ‍ | OMake Home • Guide Home • Guide (single-page) • Contents (short) • Contents (long)
Index: | ‍ ‍ | All • Variables • Functions • Objects • Targets • Options