Skip to content

the ghul command

The ghul command runs a ghūl source file directly, with no project file, and starts an interactive session that compiles and runs code as you type it:

plaintext
$ ghul repl
ghūl repl. :help for commands.
1> let names = ["ada", "grace", "barbara"]
 | names |> map(n => n.length)
[3, 5, 7]

installing

ghul is the ghul.cli .NET tool. It needs the .NET 10 SDK:

sh
dotnet tool install -g ghul.cli

.NET installs global tools in ~/.dotnet/tools, which has to be on your PATH; the first dotnet tool install -g says how to add it if it isn't.

ghul installs its own copy of the compiler the first time it needs one. ghul install-compiler installs it ahead of time, or updates it to the latest release; ghul install-compiler 60.2.0 installs that version instead. ghul version shows both versions:

plaintext
$ ghul version
ghul 0.27.1
ghul.compiler 60.2.0

dotnet tool update -g ghul.cli updates ghul itself.

running a script

A script is one .ghul file of top-level statements. args holds its command-line arguments and env its environment:

ghul
#!/usr/bin/env ghul

let name = if args.count > 0 then args[0] else "world" fi

write_line("hello, {name}")
write_line("your shell is {env["SHELL"] ?? "unknown"}")

ghul followed by the file's name compiles and runs it, and passes it everything after the name:

plaintext
$ ghul greet.ghul ada
*** succeeded ***
hello, ada
your shell is /bin/bash

A script has the compiler's default imports, as the REPL does, so write_line, the pipes and the collections need no use. A script that declares its own namespace chooses its own imports instead. A file with an entry function instead of top-level statements runs too, and an entry returning an int sets the exit status.

On Linux, a script whose first line is #!/usr/bin/env ghul runs like any other executable once it is marked as one:

plaintext
$ chmod +x greet.ghul
$ ./greet.ghul grace
hello, grace
your shell is /bin/bash

Each script is compiled once for each combination of its text and the compiler version, and the result is kept under ~/.cache/ghul-cli/scripts. Running an unchanged script again starts it straight away, without the compiler's *** succeeded *** line. ghul cache clear empties the cache, and ghul --no-cache greet.ghul compiles again regardless.

ghul - reads the script from standard input, and ghul compile greet.ghul compiles a script without running it and prints the path of the result.

ghul runs a file only if it looks like a script: its name ends in .ghul, or it is executable and starts with #!. ghul run notes.txt runs a file whatever it is called.

the REPL

ghul repl starts an interactive session. Each submission is compiled and run as soon as it is complete, and what it defines stays available to every later one. The prompt shows the number the next submission will take, and a line that doesn't end the submission is followed by a | prompt.

A line that finishes an expression or a call ends the submission and shows its value. A line that finishes a let, an assignment or a definition doesn't, since those set something up for what follows: the | prompt stays, and the next line joins the same submission. So a definition and a first use of it can be typed as one submission, and a definition typed on its own is ended with a blank line:

plaintext
1> let names = ["ada", "grace", "barbara"]
 | names |> map(n => n.length)
[3, 5, 7]
2> greet(name: string) -> string =>
 |     "hello, {name}"
 |
3> names |> map(greet)
["hello, ada", "hello, grace", "hello, barbara"]

An if, case, loop or try written over several lines doesn't end the submission when it closes either, and nor does a line that leaves something open, such as an open bracket or a block with no closing keyword. A blank line ends the submission where it stands and shows no value. A line holding only . ends it and shows the value it ends on:

plaintext
4> if names.count > 2 then
 |     "several"
 | else
 |     "few"
 | fi
 | .
several

A line ending in a \ on its own keeps the submission open whatever it holds, and the \ is dropped. Alt-Enter ends the submission from any line and shows its value.

Defining a name again replaces it for later submissions, and the new definition can read the value it replaces:

plaintext
9> let limit = 4
 |
10> let limit = limit * 10
  | limit
40

Types are defined the same way, and a mistake is reported with the line it points at:

plaintext
11> union Shape is
  |     CIRCLE(radius: double)
  |     RECT(width: double, height: double)
  | si
  |
12> area(s: Shape) -> double =>
  |     case s
  |     when c: Shape.CIRCLE then 3.14159 * c.radius * c.radius
  |     when r: Shape.RECT then r.width * r.height
  |     esac
  |
13> [Shape.CIRCLE(1.0), Shape.RECT(3.0, 4.0)] |> map(area)
[3.14159, 12]
14> area(Shape.SQUARE(2.0))
cell-14: 1,12..1,18: error: member SQUARE not found in Shape
 1 | area(Shape.SQUARE(2.0))
   |            ^^^^^^

The compiler's default imports (use default) are in force in every submission, and a use you type stays in force for the rest of the session. ghul repl --no-default-use leaves the default imports out.

editing

At a terminal the whole submission is edited in place, however many lines it has. Enter on the last line ends the submission or waits for more as described above, and Enter on an earlier line starts a new line there. Up and Down move between lines, and past the first or last line bring back earlier submissions, including those of earlier sessions. Each line starts indented after a line that opens a block, and a closing word such as fi steps back out as it is typed.

Tab completes the name being typed from everything the session has defined; where more than one name fits, it lists them. Shift-Tab shows what the name at the cursor is. Home and End, or Ctrl-A and Ctrl-E, go to either end of a line; Ctrl-Left and Ctrl-Right, or Alt-B and Alt-F, move by a word; Ctrl-U and Ctrl-K delete to either end of the line and Ctrl-W the word before the cursor; Ctrl-L clears the screen.

Ctrl-C while a submission is running interrupts it and brings the prompt back with the session intact. At the prompt it does nothing. Ctrl-D in an empty submission leaves.

What you type is coloured as you type it, in the same colours as the examples on this site, and so is the line an error points at. ghul repl --theme dark, --theme light or --theme none chooses the colours, and setting NO_COLOR turns them off.

commands

A line starting with : is a command:

commandwhat it does
:cellslists the submissions so far, each with how it ended
:cells 3shows the whole of submission 3
:rerun 3submits submission 3 again, as a new submission
:rerun 3..submits 3 again, then every later one that ran to the end
:edit 3brings back submission 3 to change and submit as a new one
:type EXPRESSIONshows the type of an expression without running it
:complete TEXTlists what could follow some text
:hover TEXTshows what the end of some text names
:save FILEwrites the submissions that ran to the end to a file
:load FILEsubmits each part of a file :save wrote, in order
:resetstarts a fresh session
:helplists the commands and keys
:quitleaves

:rerun 3.. brings a session up to date after a change to something an earlier submission used. Here names is defined again, and every submission from 3 on runs again with it, including the new definition itself:

plaintext
5> :cells
1  ok          let names = ["ada", "grace", "barbara"]  (2 lines)
2  ok          greet(name: string) -> string =>  (2 lines)
3  ok          names |> map(greet)
4  ok          if names.count > 2 then  (5 lines)
5> let names = ["alan", "edsger"]
 |
6> :rerun 3..
6> names |> map(greet)
["hello, alan", "hello, edsger"]
7> if names.count > 2 then
 |     "several"
 | else
 |     "few"
 | fi
few
8> let names = ["alan", "edsger"]

It stops at the first submission that no longer runs to the end.

how a session differs from a file

Each submission is compiled as a small library of its own. Messages call the third submission cell-3, and code can reach what it defined as cell3, as in cell3.x.

Defining something again replaces it for later submissions rather than editing what has already run: code compiled earlier keeps what it was compiled against. A partial or impl block in a later submission defines the type again, so a function taking the earlier type still takes that one, and the REPL says so:

plaintext
15> trait Named is
  |     name() -> string
  | si
  |
16> impl Named for Shape is
  |     name() -> string =>
  |         if isa Shape.CIRCLE(self) then "circle" else "rectangle" fi
  | si
  |
ghul: area still takes the earlier Shape; redefine it to use the new one
17> Shape.CIRCLE(1.0).name()
circle

A class or trait defined in a session can be extended by a later submission, since that submission is another assembly. The compiler cannot treat a class hierarchy defined in a session as closed, so a case over one needs an else.

A name that begins with _ is visible to later submissions, as it is to the rest of a file:

plaintext
18> let _hidden = 42
  |
19> _hidden + 1
43

.NET cannot stop a running thread from outside, so an interrupted submission is abandoned rather than ended: it keeps its definitions, and can go on running in the background until it finishes or the session ends.

ghul repl --no-server starts the compiler for each submission instead of keeping one running, which takes about a second a submission rather than tens of milliseconds.

notebooks

The ghul.jupyter tool is a Jupyter kernel for ghūl: each notebook cell is a submission to the same kind of session the REPL runs. Install it and register it with Jupyter:

sh
dotnet tool install -g ghul.jupyter
ghul-jupyter install

To use it in VS Code, install the Jupyter extension, then reload the window (Developer: Reload Window), since the Jupyter extension only looks for kernels when it starts. Open or create a .ipynb file, choose Jupyter Kernel... from the kernel picker, and pick ghūl. The ghūl extension highlights the cells. A cell keeps the language it was created with, so cells made before the ghūl kernel was chosen stay in their earlier language until you change it from the language indicator at the cell's bottom right.

The notebook's stop button interrupts a running cell, as Ctrl-C does in the REPL. Hover is not available in a notebook cell.

The kernel compiles cells with the compiler ghul installs, or the ghul-compiler on the path. Installing ghul.cli and running any script once is the simplest way to have one.