9.4. morloc eval
Morloc has three subcommands that turn source into a result, and they serve distinct roles:
-
morloc make— compile a module into an executable (the nexus and its language pools). This is the full language: a module maysourceforeign code, declare types, typeclasses, and instances, import local modules, and export zero, one, or many terms. -
morloc typecheck— type-check a module without compiling or running it. Same full language asmake; it only reports the inferred types of the exported terms. -
morloc eval— compile and run a single expression. An expression composes functions that are already installed on the system; it cannot introduce new ones. An eval expression may import installed modules and uselet/where/do, but it may notsourceforeign code or declare types, typeclasses, instances, or module structure.
The dividing line is module vs expression. make and typecheck
consume a module, which can define and source new functionality and
export any number of terms. eval consumes one expression assembled
purely from already-installed pieces, producing exactly one result. Use
eval for quick experiments, shell pipelines, and for exposing a fixed
set of installed functions to callers who may only compose them — never to introduce new code.
Pass the expression inline with -e, or name a file containing it as
the positional argument. The two are interchangeable: writing an -e
string to a file and running morloc eval file gives the same result — the file is treated as expression text, not as a module.
$ morloc eval -e "import root-py; 1 + 2"
3
$ morloc eval -e 'import root-py; "foo" <> "bar"'
"foobar"
$ printf 'import root-py\n1 + 2\n' > add.loc
$ morloc eval add.loc
3
Because an eval expression can only compose installed functions, eval
is also the safe surface to expose over an API or daemon: it resolves
only installed modules, never local-filesystem modules, so an
untrusted caller cannot source arbitrary foreign code or reach a
module they uploaded. A local import — a bare name that resolves on
the filesystem, or a dot-prefixed name (.utils) — is rejected in
eval mode; build programs that depend on local modules with
morloc make instead. The --allow-local-modules flag re-enables
local resolution for local development only and is insecure for server
use.
9.4.1. Imports in eval strings
Morloc has no implicit prelude: every name an expression refers to must
come from a module the eval string explicitly imports. Operators like +
and <> are typeclass methods sourced from the standard library, so a
typical eval string begins with one or more imports:
$ morloc eval -e "import root-py; import root-cpp; 1 + 2"
3
As described above, only installed modules may be imported (named
bare, like root-py); local imports are rejected in eval mode. See
Importing modules for the full import rules.
If no import brings the required operator or function into scope, the compiler reports an undefined-term error with a hint pointing at the fix:
$ morloc eval -e "1 + 2"
<expr>:1:2: error:
Undefined term: +
hint: an eval expression has no implicit prelude; prefix the expression with 'import root-py;' (or the module that defines +) to bring it into scope
9.4.2. The eval sandbox
The morloc eval CLI is trusted: on your own machine it may import any
installed module and use any intrinsic, exactly like the examples above.
When eval is exposed to untrusted callers — over a daemon or router — it is sandboxed, and two further gates apply on top of the base rules
(installed-only, no source, no type/class/instance declarations):
-
Module allow-list. The expression’s top-level imports are limited to a curated list. An empty list allows no imports, so only pure, module-free expressions run: literals and the pure intrinsics such as
@show/@hash/@lang. Nothing else is in scope — not even+, which is a typeclass method that must be imported. Matching is on the resolved module name, soimport M as Nis checked againstM. -
IO-intrinsic ban. An IO intrinsic (
@open,@save,@write,@stdin, …) may not be written directly in the expression. An IO intrinsic reached through a function exported by an allow-listed module is fine: a server exposes the IO surface it chooses as named functions, never a raw filesystem primitive. Pure intrinsics remain usable.
Together these give the operator term-level control: allow-list a curated
module that re-exports exactly the functions callers may compose (the
standard re-export idiom — a public facade), and eval can reach only
those.
Two flags drive the gates:
| Flag | Effect |
|---|---|
|
Restrict top-level imports to |
|
Enable the gates without granting any modules (an empty allow-list). Mainly for previewing the sandbox from the CLI. |
On the CLI these let you preview how an expression behaves once served:
$ morloc eval --eval-allowed-modules root-py -e 'import root-py; 1 + 2'
3
$ morloc eval --eval-allowed-modules root-py -e 'import root-py; @write "x" 1'
error: IO intrinsics may not be used directly in a sandboxed eval expression ...
$ morloc eval --eval-allowed-modules base -e 'import root-py; 1 + 2'
error: module 'root-py' is not in the eval allow-list
Served eval (over a daemon or router) is always sandboxed — there is no unsandboxed served mode. The operator sets the allow-list once when starting the server. Effects other than IO are not yet gated: a future effect disallow-list distinguishing read, write, and execute is planned; today the module allow-list and the IO-intrinsic ban are the sandbox.
9.4.3. Single-line layout: braces and semicolons
A Morloc source file relies on indentation to delimit blocks. An eval
string is a single shell argument, so block structure must use the
explicit-brace forms that the grammar provides as alternatives to the
indentation-based forms. Two rules apply:
-
Top-level items (imports and the trailing expression) are separated by a literal
;. The eval preprocessor rewrites every top-level;to a newline before handing the string to the parser, which is the same effect as starting a new top-level line in a file. -
Block bindings inside
where,let, anddoare written with literal braces and semicolons:where { a = 1; b = 2 },let { a = 1; b = 2 } in expr,do { stmt1; stmt2; expr }. Semicolons inside{…}are preserved by the preprocessor and consumed by the parser as item separators.
A where clause that would normally span multiple indented lines in a
source file:
result = a + b where
a = 10
b = 20
becomes, on the command line:
$ morloc eval -e 'import root-py; a + b where { a = 10; b = 20 }'
30
Likewise for let:
$ morloc eval -e 'import root-py; let { a = 10; b = 20 } in a + b'
30
A do-block (see Effects and delayed evaluation) uses the same
brace-and-semicolon form:
do { stmt1; stmt2; final_expr }
These explicit-brace forms are not specific to eval — they are part of
the Morloc grammar and may be used in source files too. They are simply
the only practical way to write multi-binding blocks inside a single
shell-quoted string.