Functions in BLISS
Learn how to define and call routines in BLISS - parameters, return values, recursion, and the OWN/LOCAL scope model in this expression-oriented systems language
In BLISS, the unit of reusable code is the routine. As a structured, imperative systems language, BLISS leans heavily on routines to break programs into named, callable pieces - exactly the kind of decomposition that let DEC engineers build the OpenVMS operating system without goto. A routine takes parameters, computes a value, and returns it to the caller.
What makes BLISS routines distinctive is the language’s expression-oriented design. There is no RETURN keyword for the common case - a routine yields the value of its body, just as a block yields the value of its last expression. A one-line routine like ROUTINE square(x) = .x * .x; needs no BEGIN, no END, and no RETURN. The body is the result.
Two rules from earlier tutorials carry straight into routines. First, names evaluate to addresses, so you read a parameter’s value with the dot operator (.x) - formal parameters are locations too. Second, BLISS has no built-in I/O, so these examples call an external print_int routine supplied by a small C wrapper, exactly as the Hello World tutorial did.
This tutorial covers defining and calling routines, parameters and return values, routines that call other routines, recursion, and the difference between OWN (module-wide) and LOCAL (per-call) scope.
Defining and Calling Routines
A routine is introduced with the ROUTINE keyword, an optional parameter list in parentheses, an =, and a body. When the body is a single expression, that expression is the return value. Mark a routine GLOBAL to make it visible to the linker (needed so the C wrapper can call it); plain ROUTINE keeps it private to the module.
Create a file named functions.bli:
MODULE functions =
BEGIN
EXTERNAL ROUTINE
print_int;
! A routine with one parameter. There is no RETURN keyword - the routine
! yields the value of its body, so 'square' returns .x * .x. Formal
! parameters are locations, so we read them with the dot operator.
ROUTINE square(x) = .x * .x;
! Two parameters, read the same way.
ROUTINE add(a, b) = .a + .b;
! Routines call other routines freely. This one builds on square.
ROUTINE sum_of_squares(a, b) = square(.a) + square(.b);
GLOBAL ROUTINE func_demo : NOVALUE =
BEGIN
print_int(square(6)); ! 36
print_int(add(40, 2)); ! 42
print_int(sum_of_squares(3, 4)) ! 9 + 16 = 25
END;
END
ELUDOM
A call looks like square(6) - the routine name followed by parentheses. This is important: a bare routine name in BLISS evaluates to the routine’s address, not a call. The parentheses are what trigger the call, which is why even a no-argument routine is invoked as name(). Arguments are passed by value; square(6) copies 6 into the formal x, and sum_of_squares(3, 4) shows that a routine’s result drops straight into the surrounding expression.
The C wrapper provides the print_int bridge and the main() entry point. As with every BLISS example, blissc uppercases all identifiers, so the C function names must be uppercase, and a BLISS fullword maps to a C int.
Create a file named wrapper.c:
| |
Note that func_demo is declared : NOVALUE. That attribute marks a routine that returns no useful value - the equivalent of C’s void. Use it for routines called purely for their side effects (here, printing). Routines without NOVALUE are expected to produce a value, and BLISS lets you use that value anywhere an expression is allowed.
Recursion
Because a routine’s own name is in scope inside its body, a routine can call itself. Combined with BLISS’s IF expression (which returns the value of the chosen branch), this makes recursion concise. Here is the classic factorial.
Create a file named recursion.bli:
MODULE recursion =
BEGIN
EXTERNAL ROUTINE
print_int;
! factorial calls itself. The IF is an expression, so the whole
! routine body is a single value-producing expression - no RETURN needed.
ROUTINE factorial(n) =
IF .n LEQ 1
THEN 1
ELSE .n * factorial(.n - 1);
GLOBAL ROUTINE rec_demo : NOVALUE =
BEGIN
print_int(factorial(5)); ! 120
print_int(factorial(1)) ! 1
END;
END
ELUDOM
factorial(5) expands as 5 * factorial(4), then 5 * 4 * factorial(3), and so on until .n LEQ 1 returns 1, unwinding to 120. Each recursive call gets its own copy of the parameter n on the stack. Note the keyword comparison operator LEQ (less-or-equal) instead of a symbol - BLISS uses EQL, NEQ, GTR, LSS, GEQ, and LEQ for comparisons.
Scope: OWN vs LOCAL
BLISS distinguishes storage that persists for the life of the program from storage that exists only while a routine runs. An OWN variable is declared at module level, lives in static storage, and is shared by every routine in the module - it keeps its value between calls. A LOCAL variable is declared inside a routine, lives on the stack, and gets a fresh copy on every call.
Create a file named scope.bli:
MODULE scope =
BEGIN
EXTERNAL ROUTINE
print_int;
! OWN: a static, module-level variable shared by all routines.
OWN
counter;
! bump takes no parameters. LOCAL 'next' exists only for this call;
! 'counter' is the shared OWN variable that survives between calls.
ROUTINE bump : NOVALUE =
BEGIN
LOCAL next;
next = .counter + 1; ! read the shared counter into a local
counter = .next ! write the new value back
END;
GLOBAL ROUTINE scope_demo : NOVALUE =
BEGIN
counter = 0; ! initialize the shared variable
bump();
bump();
bump();
print_int(.counter) ! 3 - the OWN counter survived every call
END;
END
ELUDOM
Each bump() reads the shared counter, adds one, and writes it back. Because counter is OWN, the increments accumulate across the three calls and the final value is 3. If counter had been declared LOCAL inside bump, every call would start from an uninitialized fresh cell and the count would never build up. Notice the call site again: bump() with parentheses invokes the routine, while the bare name bump would just be its address. BLISS does not support default parameter values, so every formal must be supplied by the caller (extra arguments are simply ignored, and missing ones are undefined).
Running with Docker
Each example pairs a .bli file with the C wrapper. The wrapper as written runs the first example. To run the others, change the routine name declared and called inside wrapper.c (REC_DEMO for recursion, SCOPE_DEMO for scope) before compiling.
| |
Expected Output
The functions example prints the result of each routine call:
36
42
25
The recursion example prints the two factorials:
120
1
The scope example prints the accumulated counter after three bump() calls:
3
Key Concepts
- Routines are the unit of code reuse - defined with
ROUTINE name(params) = body; mark themGLOBALto expose them to the linker, or leave them plain to keep them private to the module. - No
RETURNfor the common case - a routine yields the value of its body, so single-expression routines likeROUTINE square(x) = .x * .x;need no block and no return statement. - Parameters are locations - formals are passed by value, and you read them with the dot operator (
.x) just like any other variable. - Calls need parentheses -
name(args)invokes a routine; a barenameevaluates to the routine’s address, so even no-argument routines are called asname(). NOVALUEmeans “no result” - use it for routines called only for side effects (the BLISS equivalent ofvoid); other routines can be used anywhere an expression is allowed.- Recursion is natural - a routine’s name is in scope in its own body, and the
IFexpression makes recursive definitions like factorial a single value-producing expression. OWNvsLOCALscope -OWNvariables are module-wide static storage shared across calls;LOCALvariables live on the stack and are fresh on every call.- No default parameters - BLISS has no default argument values, so callers must supply every formal a routine relies on.
Running Today
All examples can be run using Docker:
docker pull codearchaeology/bliss:latest
Comments
Loading comments...
Leave a Comment