LMScript Language & CLI Guide
This reference covers every language construct in LMScript (.lms). For the statically typed
inner bytecode language inside _lmcode_ ... end _lmcode_ blocks, see the
LMCode Tutorial and LMCode Spec.
1. Values, Truthiness, Strict Equality & Block Scope
- Statements: Every simple statement ends with
;. Comments start with#. - Value Kinds:
null,boolean,number,string,array(list[...]),object(map{...}),function,Data(data blocks), andlmcode. - Strict Equality:
==and!=do no type coercion ("1" == 1isfalse); lists and maps compare by reference identity. - Truthiness:
null,false,0, and""are falsey. Empty lists[]and empty maps{}are truthy. - Two Scope Rules:
- A
{ }block owns any variable first assigned inside it (reading it after the block is a static check error, exit 2). - A function never writes the caller's variables (assigning to a name inside
fncreates a function-local variable).
- A
- Configuration Constants: Top-level
const NAME = ...;bindings are immutable, their lists/maps are deeply frozen (useclone(NAME)for a mutable copy), and they can be spliced with${NAME}into_lmcode_blocks.
const REGION = "us-east-1";
name = "lmscript";
items = ["alpha", "beta", "gamma"];
meta = {owner: "platform", retries: 3};
println(type(null), type(true), type(1.5), type(name), type(items), type(meta));
# null boolean number string array object
2. 0-Based Indexing, Negative Indices & Strict Access
Both outer LMScript and inner LMCode use 0-based indexing. Negative indices count from the end
(xs[-1] is the last element). Accessing a missing map key or out-of-bounds index is a located error
(never undefined); use .get(key, default) and .has(key) for optional fields:
langs = ["lmscript", "lmcode", "rust", "cpp"];
println(langs[0]); # lmscript (first element)
println(langs[-1]); # cpp (last element)
println(langs.get(99, "none")); # none
cfg = {server: {port: 8080}};
println(cfg.server.get("port", 80)); # 8080
println(cfg.has("client")); # false
3. Python-Style Strings, Regex & Stateful pos()
| Form | Syntax | Behavior |
|---|---|---|
| Double / Single | "hello\n", 'hello\n' |
Standard escapes (\n, \t, \r, \", \\). |
| Raw String | r"^\d+\.\d+$", r'C:\work\bin' |
Backslashes are preserved verbatim. Ideal for regular expressions. |
| Format String | f"host={host} list={[1, 2]}" |
Evaluates arbitrary expressions inside {...} (lists/maps render as JSON). Use {{ and }} for literal braces. |
| Triple-Quoted | """...""", f"""...""", r"""...""" |
Multi-line strings preserving embedded newlines and quotes. |
| Path Literals | path".", path"report.txt" |
Normalizes to the OS path form (".", "./report.txt" or ".\report.txt") with zero filesystem lookup. |
Pair raw strings (r"...") with LMScript's regex helpers: check containment with regex_test(s, pat),
extract one or all matched substrings with regex_find(s, pat) and regex_matches(s, pat),
extract capture groups with regex_match(s, pat) and regex_find_all(s, pat), or write a
Perl-style stateful lexer using pos() + regex_find_pos(s, pat):
log = "2026-10-17 ERROR disk 91% | 2026-10-18 WARN cpu 72%";
println(regex_test(log, r"ERROR")); # true (contains pattern)
println(regex_find(log, r"\d+%")); # "91%" (first matched substring)
println(regex_matches(log, r"\d+%")); # ["91%", "72%"] (all matched substrings)
# Stateful Perl-like pos(): loop over token rules, advance pos() on first match
const RULES = [["WS", r"\s+"], ["NUM", r"\d+"], ["ID", r"[A-Za-z_]\w*"], ["OP", r"[=+*;-]"]];
src = "x = 42 + y;";
pos(src, 0);
while (pos() < len(src)) {
let matched = false;
for (rule in RULES) {
let tok = regex_find_pos(src, rule[1]);
if (tok != null) { matched = true; break; }
}
assert(matched, f"unexpected input at pos {pos()}");
}
4. Control Flow, Iteration & Functions
fn classify(score) {
if (score >= 90) {
return "A";
} else if (score >= 80) {
return "B";
} else {
return "C";
}
}
# Counted for loop & for-in over list values, map keys, or string code points
for (i = 0; i < 3; i = i + 1) {
println(classify(75 + i * 10));
}
for (v in ["a", "b"]) { println(v); }
for (k in {host: "localhost", port: 8080}) { println(k); }
# Tuple destructuring with let
let (first, second) = ["lmscript", "lmcode"];
label = first == "lmscript" ? "outer" : "inner";
println(f"{first} ({label}) + {second}");
5. Embedded Data Blocks (_json_, _csv_, _toml_, _props_, _graphql_, _cmd_, _lmcode_)
Every embedded block opens with NAME = _KIND_ [options]; and closes with end _KIND_ at column 1.
JSON, CSV, TOML, list, and XML blocks produce opaque Data whose .value() returns a fresh mutable copy;
props, cmd, GraphQL, and LMCode blocks have their own result kinds. For parsed data blocks, ${expr} splices
are substituted per value after parsing so spliced values cannot corrupt structure.
bonus = 5;
users = _csv_;
name,score
Ada,${90 + bonus}
end _csv_
users = users.value();
assert(users[0].score == 95);
settings = _toml_;
[server]
host = "127.0.0.1"
port = 9000
end _toml_
println(settings.value().server.port);
6. Running External Commands (cmd, _cmd_ & system)
LMScript never passes strings through a shell by default: cmd PROGRAM ARGS... and _cmd_ blocks
construct an explicit argv list, and system(argv, timeoutMs_or_opts) returns
{stdout, stderr, code, signal, timed_out, duration_ms, pid}.
argv = cmd lmscript --version;
r = system(argv, 5000);
assert(r.code == 0 && !r.timed_out);
println("Version: " + trim(r.stdout));
7. Imports, Packages, ARGV & PROPS
Include reusable files with import "lib/helpers.lms"; (files starting with package NAME; expose
their symbols as NAME.member). Read CLI arguments from ARGV (alias args), optionally
parse them with args_parse(ARGV, flags), and read configuration properties from PROPS / --pref FILE.
8. Forward Templates (expand) & Reverse Templates (template_extract)
line = expand("[DEPLOY] site=${site} env=${env}", {site: "lmscript.io", env: "prod"});
parsed = template_extract_text("[DEPLOY] site=${site} env=${env}", line);
println(json_stringify(parsed));
9. Recovery (recover / resumepoint) & Concurrency (parallel / parallel_map)
fn task_a() { return 20; }
fn task_b() { return 22; }
# Run tasks concurrently and collect results in input order
results = parallel([task_a(), task_b()]);
assert(sum(results) == 42);
# Bounded fan-out over a runtime list (default concurrency 8, max 64)
fn double(x) { return x * 2; }
doubled = parallel_map([1, 2, 3, 4], double, 4);
println(json_stringify(doubled));