Authoritative source: Your first program

Your first program

We will begin with one source file and no project manifest. The program prints a line, so a successful run is unmistakable.

Create a file named hello.tl in the tulam repository root:

action main() : Eff { console:Console } Unit = {
    putStrLn("Hello from tulam!")
};

Check it:

stack exec tulam -- check hello.tl

Then compile and run it:

stack exec tulam -- run hello.tl

The final program output is:

Hello from tulam!

Compiler progress messages may appear around it. Those messages describe the front end, target choice, and native artifact; the greeting itself comes from your program.

Reading the program

The complete declaration is:

action main() : Eff { console:Console } Unit = {
    putStrLn("Hello from tulam!")
};

Let us read it from left to right.

action main()

action declares a named computation that sequences effectful operations. main is the entry-point name used by the compiler unless you select another one. The empty parentheses say that main accepts no arguments.

Most pure computations are declared with function. We use action here because printing interacts with the outside world.

Eff { console:Console } Unit

This is the result type.

Eff says that the computation may perform effects. Between braces is an effect row:

{ console:Console }

It says that the computation requires one installed capability, labeled console, satisfying the Console effect contract. The label becomes useful when more than one effect exposes an operation with the same name.

Unit is the value produced when the action finishes. It carries no interesting payload; the visible purpose of this action is its console output.

Effect types are not informal comments. The checker verifies that the body uses only effects permitted by its type. Later chapters show effect inference, multiple effects, and handlers. For now, reading this type as “a console action that finishes with no interesting value” is enough.

= { ... }

The equals sign introduces the action body. Braces contain its ordered statements. This example has only one:

putStrLn("Hello from tulam!")

putStrLn is the standard Console operation that writes a string followed by a newline.

The final semicolon

The semicolon after } separates this top-level declaration from the next one. Trailing semicolons are accepted in declaration blocks as well.

tulam uses separators consistently:

There is no semicolon after the only statement inside this action because the last expression is the action’s result. Writing a trailing semicolon there is also allowed.

What check does

stack exec tulam -- check hello.tl

does considerably more than recognize the punctuation. It loads the standard library, then performs the required front-end stages, including:

check does not produce or execute a native artifact. It is the quickest normal command for answering “Is this a valid program according to the current compiler?”

Parser acceptance alone is not success. A program must pass all required static checks before tulam considers it valid.

What run adds

stack exec tulam -- run hello.tl

repeats the strict front end, then:

  1. closes the reachable program around main;
  2. specializes typed functions and evidence;
  3. produces an immutable RuntimeCore artifact;
  4. lowers effects through the shared pipeline;
  5. verifies, optimizes, and verifies that artifact again;
  6. asks the LLVM native provider to emit LLVM;
  7. invokes the compiler-shipped Clang and LLD; and
  8. runs the resulting executable.

The default temporary executable path for a single-file build is /tmp/tulam_native. You can choose a different path:

stack exec tulam -- build hello.tl --output ./hello
./hello

build stops after writing the artifact, whereas run immediately executes it.

Add a pure function

Now make the greeting a little more interesting:

function greeting(name:String) : String =
    concat("Hello, ", concat(name, "!"));

action main() : Eff { console:Console } Unit = {
    putStrLn(greeting("Ada"))
};

The first declaration is pure:

function greeting(name:String) : String = ...;

It takes one parameter named name whose type is String, and it returns a String. concat joins two strings. Nested calls build the complete greeting.

The action calls the pure function and sends its result to putStrLn. Pure code does not need a special wrapper to be called from effectful code.

Run it again:

stack exec tulam -- run hello.tl

The program prints:

Hello, Ada!

Make a deliberate mistake

Change the argument from a string to an integer:

putStrLn(greeting(42))

Then run:

stack exec tulam -- check hello.tl

The checker rejects the call because greeting requires a String, while 42 has type Int. This error is detected before native code generation.

Restore "Ada" before continuing.

Trying small, intentional mistakes is useful when learning a typed language. The diagnostic tells you not only that a program failed, but also which facts the compiler knows at that point.

Entry points and displayed results

An entry point can also be a pure function returning Int:

function main() : Int = 0;

The current native entry wrapper prints supported scalar results. This program therefore prints 0. The wrapper itself returns a successful process status unless compilation, linking, or execution fails; an Int result is not currently forwarded as the operating-system exit code.

To compile a differently named declaration as the entry point, use --entry:

function answer() : Int = 0;
stack exec tulam -- run hello.tl --entry answer

This prints 0.

Projects record their entry module and definition in tulam.project.tl, so the option is mainly useful for the single-file workflow and compiler development.

A note about modules

Our hello.tl file has no module declaration. That is convenient for a single-file experiment. Larger programs use explicit modules:

module Hello.Main;

The module name, source roots, and entry point then belong to a project. Chapter 4 introduces that layout without changing the program’s basic meaning.

Recap

You have now used the complete native path:

Next: Check, run, build, and test.