10 Modules and data files

Builds on: Chapter 1 (use), chapter 3 (signatures).

Questions this chapter answers

10.1 Making a module

One file is one module, named after the file without .pnx. What is to be seen outside is marked export.

money.pnx

rem A small module: amounts are cents, kept apart from plain numbers.
export type cents int
word pad2 (n int gives str) n 10 lt if cat ("0", n) else cat (n) end end
export word of_won (won int gives cents) won 100 mul cents end
export word show (c cents gives str) cat (c raw 100 div, ".", c raw 100 mod pad2) end

cents is a declared type over int: represented as an integer, it does not mix with int. raw takes the underlying integer out. Multiplying or dividing cents by an integer gives cents (c 3 mul): scaling keeps the unit. Only the division in show, which needs plain integers, uses raw.

10.2 Using a module

After use money, its words are written as paths such as money.of_won; the exported type cents is written by its name alone. When money is not a registered module, pnx first loads money.pnx from beside the program.

10-use.pnx

use io
use money
word total (a cents, b cents gives cents) a b add end
cat ("total ", 12 money.of_won 3 money.of_won total money.show) io.println

Output

$ pnx 10-use.pnx
total 15.00

10-use-as.pnx

use io
use money as m
cat ("price ", 7 m.of_won m.show) io.println

Output

$ pnx 10-use-as.pnx
price 7.00

pad2, not exported, is not seen outside.

10-private.pnx

use money
5 money.pad2 drop

Refused

$ pnx 10-private.pnx
10-private.pnx:2:3: the module exports nothing of this name

Besides words, a module can export constants, globals, types and records. They are used like this.

ExportedRead or callNote
wordmodule.wordthe signature is the same as inside the module
constant (const)module.name
global (global and collections)module.namestored with value set module.name
type, record, row type of a tablethe name alonewritten without a path, like cents

The module tally below exports a constant, a global and a word. Changing the global is usually the job of the module’s own word (bump), but it can also be stored from outside with set tally.hits.

tally.pnx

rem A module that keeps a count: an exported constant, an exported global and a word.
export const limit 3
export global hits int
export word bump (gives bool)          rem true while hits is still under the limit
  hits limit lt if hits 1 add set hits true else false end
end

10-tally.pnx

use io
use tally
word try_once ()
  tally.bump if "counted" else "limit reached" end io.println
end
try_once try_once try_once try_once                 rem the fourth time the limit is reached
cat ("hits ", tally.hits, " of ", tally.limit) io.println   rem read an exported global and constant
0 set tally.hits                                    rem an exported global is written with set module.name
cat ("hits after reset ", tally.hits) io.println

Output

$ pnx 10-tally.pnx
counted
counted
counted
limit reached
hits 3 of 3
hits after reset 0

A module that use cannot find is refused at load. pnx looks at the standard and host modules first, then for NAME.pnx beside the program file.

10-unknown-module.pnx

use maths                                           rem no maths.pnx beside the program, no such module

Refused

$ pnx 10-unknown-module.pnx
10-unknown-module.pnx:1:5: no module of this name is registered or loaded

Q&A

Q. Who decides a module’s name?

A. Whoever loads it. pnx uses the file name without .pnx, and a C host gives the name to pinax_load. So there is no line in the file that names its module. Two modules of the same name cannot be in one state together.

Common mistake: cents is just an integer, so it can be added to one

Adding cents and int is refused. Only two values of the same declared type are added and subtracted; the types are kept apart to stop numbers of different units from being mixed.

10-mixed.pnx

use money
12 money.of_won 3 add drop

Refused

$ pnx 10-mixed.pnx
10-mixed.pnx:2:19: a declared type is added to and subtracted from its own type, and multiplied, divided or taken modulo by an int; for other arithmetic take raw

10.3 Bare names from modules

A script that calls the same modules all the time, a shell script above all, can name them in a search order. Then their words are written bare.

10-search.pnx

search (io, str)
"pinax" upper println
"a,b,c" "," "+" replace println

Output

$ pnx 10-search.pnx
PINAX
a+b+c

The order is only a last resort. A bare name is first a local, then a name of the source, then a core word; only then are the modules of the order asked, left to right. So len stays the core word even with str in the order, and the str word is written str.len. search () empties the order again. A path works with or without it.

10.4 Data files

A host can read a source by data load. A data load accepts declarations and values only, and refuses word definitions, calls and control. So reading an untrusted settings file cannot run any code.

10-data.c

#include "pinax.h"
#include <stdio.h>
#include <string.h>

/* A settings file the host reads as data: declarations and values, no code. */
static const char settings[] =
    "global port int\n"
    "global name str(16)\n"
    "8080 set port\n"
    "\"edge\" set name\n";
static const char hostile[] = "word boom () end\n";

int main(void) {
    static unsigned char block[1 << 20];
    pinax_config config = {.struct_size = sizeof config};
    pinax_state *st;
    if (pinax_open((pinax_mem){block, sizeof block}, &config, &st) != PINAX_OK)
        return 1;
    if (pinax_load(st, "settings", (pinax_str){(const unsigned char *)settings, strlen(settings)}, PINAX_LOAD_DATA) != PINAX_OK)
        return 1;
    pinax_global g;
    pinax_global_find(st, "port", &g);
    printf("port: %lld\n", (long long)*(const int64_t *)g.ptr);
    if (pinax_load(st, "hostile", (pinax_str){(const unsigned char *)hostile, strlen(hostile)}, PINAX_LOAD_DATA) != PINAX_OK) {
        pinax_str why = pinax_error_text(st);
        printf("refused: %.*s\n", (int)why.len, (const char *)why.ptr);
    }
    pinax_close(st);
    return 0;
}

Output

$ ./10-data
port: 8080
refused: a data source defines no words and uses no modules

pnx does not do data loads; they are the host’s business (chapter 14).

10.5 Modules the host lends

io file clock random env are not standard modules but host modules: they are not part of the language, and exist only when the host registers them (chapter 14). pnx registers all five. So the host decides the ways to files, the clock and the environment — a script that is not trusted is given no file.

10-host-modules.pnx

rem run: args alpha beta
use io
use str
use env
use random
use clock
word demo ()
  cat ("arguments: ", env.arg_count) io.println             rem 0 is the program file
  1 env.arg or_else (e) "?" end io.println
  "PINAX_NO_SUCH_VARIABLE" env.getenv or_else (e) cat ("getenv failed: ", e err_text) end io.println
  7 random.seed                                             rem the same seed, the same numbers
  cat (10 random.random_below, " ", 10 random.random_below, " ", 10 random.random_below) io.println
  clock.monotonic_ns to a
  clock.monotonic_ns to b
  cat ("the clock does not go back: ", b a lt not) io.println
end
demo

Output

$ pnx 10-host-modules.pnx
arguments: 1
?
getenv failed: not_found
7 2 8
the clock does not go back: true

env.arg_count and env.arg are the arguments of the command line (0 is the program file); env.getenv reads an environment variable. After random.seed, random.random_below gives the same numbers for the same seed, which suits tests. clock.monotonic_ns never goes back, so it measures how long something took; clock.now_ms is the calendar time, and clock.sleep pauses for that many milliseconds. random.random is a 64-bit random number without a range (not for cryptography).

TokenStack after itWhat happened
11Which argument.
env.arg"alpha"That argument; a number past the last fails.
or_else (e) "?" end"alpha"Skipped on success. On failure "?" takes the place.
io.println—Prints it.

Q&A

Q. May a module file use io.println?

A. Yes, with use io in the module too. But when the host that loads the module has not registered io, the module is refused. A module meant for many hosts is better off returning results than depending on a host module.

Summary