Largo Language Reference

This document covers the language as currently implemented. Features marked ⚠ TODO
are planned but not yet implemented (attempts to compile them raise a compile-time
error). Features marked
⚠ TBD are planned but syntax is not yet decided.
Features marked
⚠ PARTIAL are partially implemented.

Telegram channel here


Getting Largo

See pypi/largolang for installation instructions.

Overview

Largo is a scripting language with a python-ish syntax whose runtime
is a persistent, transactional, garbage-collected object database.
Every value, every compiled procedure, every live task, parameter,
stack frame, lives on disk at all times. Pull the plug mid-execution
and the system resumes exactly where it left off.

This means your working memory is as big as your disk, and you don't
have to worry at all about persistence or saving and restoring
anything. It also means you can write a "while true" loop and expect
it to run forever--across reboots, power failures, even moving to a
new machine. No crontabs.

All tasks run non-preemptive async style, uninterrupted until they
wait. Tasks can wait on time, IO, and changes. The following code will
print the sum every time it changes--forever:

for v in <foo.x + foo.y>:
    print("The sum of foo.x and foo.y is {v}.")

Or equivalently (and no less efficiently):

for v in <"The sum of foo.x and foo.y is: {foo.x + foo.y}">:
    print(v)

In the meantime this task just waits in a queue, on disk, taking
no additional CPU or memory.

You can also define object fields which are computed by a function
that will only be re-invoked if its dependencies have changed since
the last time you accessed it (and those compose efficiently).

Common objects like Maps, Lists, Sets, and weak variations of those,
are implemented in terms of Tables under the hood, but you can also
create your own tables any time. Tables are just Objects like
anything else, and garbage collect away if abandoned. Furthermore,
when you create a table you specify the garbage collection roles of
the columns, which determines what is a weak vs strong reference
within the table. Weak references quietly revert to unknown when
GC'd.

There are lots of other niceties like clean currying with both
positional and keyword args; lambdas (the angle brackets in the
example above); syntax for value, key:value, and zipping iteration;
string dedenting; auto-indenting of interpolated multi-line strings;
in-line default values (x\y means x if known else y); labeled breaks
and continues; constant, variable, monotonic, and weak fields on
objects; clean multiple inheritance of types; multiple dispatch;
nested comments; explicit doc strings; naturally styled shell
commands; and so on.

It is currently in pre-alpha stage, meaning it's usable as far as it
goes but there are gaping holes--some things just haven't been
implemented yet because I haven't needed them yet, including some
very basic things. Some of the things mentioned in this overview are
not yet fully implemented, but solid foundations exist for all of
them. It's not ready to use in production, but it's a great time
to get involved if you want to help shape a new language, either
directly with design suggestions or coding help, or indirectly by
trying to use it and telling me where largo's failing you so I know
what needs fixing or prioritizing. Join the Telegram channel
linked at the top of this document.

It is my intention to add a permissions layer to largo so that
modules can be safely imported and run without worrying about them
mucking up your other tasks, or snooping or deleting your files. And
by this I mean down to the level of who is allowed to read or write
what field on what object, let alone access a particular file or
directory. Between this and the long-running nature of it, largo is
more like an operating system than just a programming language.
Permissions are still on my todo list until it becomes a higher
priority, but the foundations are there (write permissions are
already gated at the property level--e.g., var vs val fields--just
not yet by "user").

Largo runs atop python and for now most of the primitives and
libraries just delegate to python, which has helped to keep the
code base relatively small.


Lexical Conventions

Whitespace and Comments

-- this is a line comment

(| this is a block comment |)

(| block comments (| can be nested |) freely |)

Statements end at a newline. Continuation across lines is
implicit inside any bracketed context ((), [], {}, <>).

Identifiers

Identifiers follow the pattern [a-zA-Z_][a-zA-Z0-9_]*. There are no
reserved keywords. (Though the vim syntax highlighting isn't as smart as
the actual compiler, so some keywords highlight where they shouldn't.)


Literals

Numbers

42          -- integer
3.14        -- float
1.5e10      -- scientific notation

SQLite numeric note: Largo is backed by SQLite, which does not distinguish integers
from whole-number floats. Any float whose value is mathematically whole (e.g. 2.0,
1e3) is silently coerced to an integer. For all practical purposes, there is only
one numeric type
in Largo.

Strings

Largo has two string delimiters and two quoting modes, giving four combinations.

Plain strings '...'

No interpolation. Standard backslash escapes apply: \n, \t, \\, \', \uXXXX, etc.

'hello world'
'line one\nline two'

Format strings "..."

Expressions are embedded with {}. Use {{ and }} to produce literal braces.

"hello {name}"
"result = {x + y}"
"escaped brace: {{ not interpolated }}"

Guarded strings $'...'$ and $"..."$

Used when the string content itself contains quotes of the same kind. Backslash sequences
are passed through literally (no escape processing). {} interpolation still works
in $"..."$. Any number of $ signs may be used as the guard; opening and closing
counts must match.

$'it's fine to have apostrophes'$
$"embed {expr} and "quotes" freely"$
$$$'contains $$"nested"$$ guard sequences'$$$

Multi-line strings

Any string delimiter may span multiple lines. The content is auto-dedented: the
longest common leading whitespace among all non-empty lines after the first newline
is stripped. If the closing delimiter sits on its own line whose indentation is
less-than-or-equal to the common indent, the string ends with \n.

text = '
    line one
    line two
    '
-- text == "line one\nline two\n"

text2 = '
    line one
    line two'
-- text2 == "line one\nline two"   (no trailing newline)

Indented interpolation in format strings

When a {} expression in a format string is preceded by whitespace on its line, and the
expression's value spans multiple lines, every line of that value is indented to match
the column of the {. This makes embedding indented sub-blocks natural.

result = "Header:
              {body}
          "
-- If body == "line 1\nline 2", result is:
--   "Header:\n    line 1\n    line 2\n"

Collections

[]                      -- empty List
[1, 2, 3]               -- List (mutable, ordered)
[:]                     -- empty Map
[a=1, b=2]              -- Map with field-name keys (shorthand for string keys)
["key": val]            -- Map with expression key
[a=1, "k": v, x+1: w]  -- field-name and expression keys freely mixed in same []
#[]                     -- empty Tuple
#[1, 2, 3]              -- Tuple (immutable, memoized — same contents = same identity)
{}                      -- empty Set
{1, 2, 3}               -- Set (mutable)

Tuples are the primary bridge between content-based and identity-based equality.
#[a, b, c] with the same contents always yields the same object. A plain List can be
memoized with tuple(list).

⚠ TODO: Immutable memoized maps #[a=1, b=2] and memoized sets #{...}.

Comprehensions

List, map, and set comprehensions use the same bracketing as their literal forms.

[expr for x in seq]                    -- List comprehension
[key: val for x in seq]                -- Map comprehension
{expr for x in seq}                    -- Set comprehension
[expr for x in seq if cond]            -- filtered (works for all three types)

The loop head supports the same iteration forms as for, including index/key capture and zip-style multiple iterators:

[i:x*2 for i:x in seq]                -- i = index/key, x = value
[x+y for x in a, y in b]              -- zip two iterables (sum two lists)
[x:y for x in a, y in b if y>0]       -- zip two iterables into a map, with filter

Nested (cartesian) for clauses in a single comprehension are not currently supported; use nested comprehensions or an explicit loop instead.


Variables and Assignment

x = 42                  -- assign to local variable
x = y = 0               -- chained assignment (right-to-left evaluation)
(x = expensive())       -- assignment embedded in expression (must be parenthesized)

Assignment is right-associative. The left-hand side may be:

x = val             -- local variable
obj.field = val     -- field assignment
obj[key] = val      -- index assignment (single key only for now)

⚠ TODO: := means in-place mutation.


Operators

Operator Precedence (high to low)

Level Operator(s) Notes
1 (tightest) f() f{} f[] .field x? x! call, curry, index, field, known, once known
2 a \ b default: a if known, else b
3 a ^ b exponentiation (right-associative)
4 -x unary negation
5 * / // % multiply, divide, integer-divide, modulo
6 + - add, subtract
7 a..b a... ...b range (exclusive end)
8 in membership / instance test ⚠ TODO
9 < <= > >= == != comparison — chainable ⚠ PARTIAL
10 not logical not
11 and logical and (short-circuits) ⚠ TODO
12 or xor logical or / xor ⚠ TODO
13 if…then…else ternary (right-associative)
14 (loosest) = := assignment (parenthesize when used as sub-expression)

Operator Notes

Default \ — returns the left operand if it is known (non-None), otherwise
evaluates and returns the right operand:

display_name = user_alias \ user_name \ "Anonymous"

Known test x? — always returns a bool. false? is true — false is a perfectly
known value:

x?                  -- True if x is not None
false?              -- True
config["key"]?      -- True only if that key has been set

Once Known: x! — waits until x is known, then returns it. Especially
useful for one-mode fields, but can also pause on intermittently missing
values.

spawn print("foo.x = {foo.x!}") -- background task prints foo.x once set
val = bar(x, y)!                -- Returns bar(x, y) once bar(x, y) returns a known value.

⚠ TODO: Need to decide if bar()! should catch exceptions or pass
them through. Right now they pass through, so you cannot wait for a
a function to "succeed" per se. Possibly bar()!! should also catch
exceptions?

Accessing unknown values — field access and map indexing return unknown (None)
rather than raising an error in most cases, including when the base object is itself
unknown. The exception is accessing a field that does not exist on a known object's
type — that is a semantic error.

obj.unset_field     -- unknown (field not yet set)
unknown_var.field   -- unknown (base is unknown)
map["absent"]       -- unknown

Map indexing by field — For Maps only, field access is a shorthand for indexing
by that string (for destination as well):

m.foo == m['foo']

Chained comparisons — chains must be uniformly ascending (<, <=) or uniformly
descending (>, >=); == and != may appear in either:

x < y               -- ok
x < y <= z          -- ⚠ PARTIAL: parsed, not yet compiled

Range .. and ... — exclusive end (Python-style: a..brange(a, b)):

a..b                -- [a, b)
a...                -- [a, end)
...b                -- [0, b)

Unary negation -x — standard arithmetic negation. Delegates to Python's negation, so behaviour on non-numeric types is undefined.

Exponentiation x ^ y — right-associative. Delegates to Python's pow, so 2^10 == 1024. Behaviour on non-numeric types is undefined.

Note on operator dispatch: Currently all built-in operators map directly to their Python equivalents. The intention is to replace this with a Julia-style multiple-dispatch system, where operators like ^ de-sugar to generic functions (e.g. pow) that can be overloaded for user-defined types. For now, passing the wrong types will fail at runtime with a Python error.

and, or, xor ⚠ TODO — parsed but not yet compiled.

Ternary if … then … else — right-associative. The if branch is the condition, then is the value when true, else is the value when false:

x = if c then a else b
label = if score > 90 then "A" else if score > 80 then "B" else "C"

Control Flow

If / Else if / Else

if condition:
    body

if condition:
    body
else if other_condition:
    other_body
else:
    fallback

The condition must evaluate to a Boolean. A non-Boolean raises a runtime error.

⚠ TODO: while … then: and for … then: — a then: block that runs on normal
loop exit (bypassed by break), mirroring Python's loop else:.

While

while condition:
    body

while condition as loop_name:
    body

For

The for statement calls iter() on its source and advances through the results. Any
iterable works — sequences, maps, sets, reactive functions (see below).

for x in seq:
    body

for i:x in seq:             -- i receives the index/key, x receives the value
    body

for x in seq, y in other:   -- multiple iterators advance together (zip-like)
    body

for x in seq as loop_name:  -- named loop
    body

Break and Continue

break and continue always require the loop label, even when using the default name:

break for               -- break the innermost for loop
continue while          -- continue the innermost while loop
break my_loop           -- break a named loop
continue my_loop

Return

return          -- return None
return expr     -- return a value

Functions

Defining Functions

def add(x, y):
    return x + y

def greet(name):
    return "Hello, {name}!"

Functions are first-class objects and can be passed, returned, and stored like any value.

Closures and Snapshot Semantics

Both def and lambdas use snapshot semantics: at definition time, any variables from
enclosing function scopes that are referenced by the body are captured in a shallow copy
of those frames. Snapshotting stops at the module boundary — module-level variables are
not captured
. Consequences:

  • Functions defined directly at module level capture nothing; they reference module
    variables live (by name) each time they run.
  • Functions defined inside other functions capture their parent function's locals at the
    moment of definition.
  • Nested functions cannot modify a caller's local variables.
  • Mutable objects (Maps, Lists) are still shared — only the frame bindings are snapshotted.
  • Each closure captures its environment independently, so a loop creating functions
    produces distinct closures:
funcs = [<x: x + n> for n in 0..10] -- Each lambda captures a different value of n
-- funcs[3](10) == 13

Doc Strings

Doc strings appear before the def line and use a scroll-like
{| … |} format. Each interior line starts with |; this prefix
is stripped. The first non-blank line is the summary; lines from
the first non-blank subsequent line form the details. Both are
available at runtime on the function object.

{|
 | The great Foo function.
 |
 | Does some really neat things.
 | Very Fooey things.
 |}
def foo(x, y):
    return x + y

foo.summary   -- "The great Foo function."
foo.details   -- "Does some really neat things.\nVery Fooey things."

The full call stack is also walkable at runtime (each frame records its caller).

⚠ TODO: Doc strings on type definitions.

Lambdas

Lambdas are anonymous functions with the same snapshot-capture semantics as def.

<expr>              -- zero-argument lambda
<x: expr>           -- one-argument lambda
<x, y: expr>        -- two-argument lambda

Calling

f(x, y)             -- positional call
f(x, kw=val)        -- keyword argument
f(x, y, kw=val)     -- mixed

Currying

f{…} returns a new function with arguments pre-bound. Any number of positional or
keyword arguments may be curried at once; curries may be cascaded.

f{x}                -- curry one positional arg
f{x, y}             -- curry two positional args at once
f{kw=val}           -- curry a keyword arg
f{x, kw=val}        -- positional and keyword together
f{x}{y}             -- cascaded curry — equivalent to f{x, y}
f{x}(y)             -- same as f(x, y)

Curried positional args are always safe. Curried keyword args are safe for parameters
passed only by keyword. Positional and keyword curries accumulate and are all passed
together when the function is finally called — the semantics are fully predictable, just
occasionally surprising when mixing styles.

The difference between f{x} and <f(x)> is that the former can accept further
arguments; the latter is a complete zero-argument thunk.

Cascaded Index Calls

f[a, b, c] is syntactic sugar for cascaded calls: f(a)(b)(c). Each argument triggers
a separate call on the result of the previous one.

table[row, col]     -- idiomatic: table(row)(col)
table[row][col]     -- also valid: chained brackets
f[x, y]             -- f(x)(y)

Reactive Iteration

Passing any zero-argument function to iter() creates a reactive iterator: it
yields the function's current value immediately, then re-fires whenever any dependency
of that function changes. Multiple dependency changes in the same async cycle produce
only one re-fire.

Because for calls iter() on its source, any zero-argument function works as a for
source directly:

for v in <x + y>:      -- re-fires whenever x or y changes
    print(v)

for v in my_func:      -- same, with a named zero-argument function
    print(v)

Note on module-level variables: Reactive tracking of module-level variables is
partially implemented — accesses are recognised and tracking is set up, but assignments
to module-level variables do not currently funnel through the tracker correctly, so
re-fires may not occur. ⚠ PARTIAL

Calling a blocking primitive inside a reactive iterator is currently an error.


Types

Declaring a Type

Type definitions edit an existing type if one with that name already exists, rather
than creating a new one. This is essential for live module upgrades: existing objects
retain their property storage intact, field defaults and modes can be updated, but fields
are not deleted if omitted (which allows amending types from other modules--see below).

A bare declaration (no body) is valid and useful for establishing a name before its
fields are defined, enabling mutually recursive types. A type body may also reference
its own type (self-recursive).

type Node                           -- bare declaration (no fields yet)
type Edge in Node                   -- declare with supertype

type Node:                          -- now define fields (edits the existing Node type)
    val id
    var next_node                   -- may reference Node (self-recursive)

Type with Fields

type Dog in Animal:
    val name                        -- immutable after construction
    val breed = "unknown"           -- immutable, class default (any expr, evaluated once at defn time)
    var age                         -- mutable
    var score = age * 2             -- mutable, with computed default
    one owner                       -- write-once; immutable once set
    dyn rank = <score / 10>         -- ⚠ TODO: computed on demand, cached, reactive

Field Modes

Mode Mutability Behaviour
val Immutable after construction. May be unknown. Class default evaluated once at type-definition time.
var Fully mutable. Writing None restores class default rather than forcing unknown (unless no default).
one Unknown until first write; immutable thereafter. If a class default exists, field is born immutable.
weak Like var but weak ref. Silently reverts to unknown on GC. ⚠ TODO
dyn Computed on demand; cached until a dependency changes. Cascades cleanly: a dyn that depends on another dyn triggers at most one recompute per cycle. ⚠ TODO

Field defaults may be any expression and are evaluated once at type-definition time.

Supertypes and Amending Types

Multiple inheritance is supported:

type Dog in Animal, Domestic

If supertypes are omitted, they default to just Object for a new type, but existing
type's supertypes are untouched in that case. This means you can re-open a type
any time to add new fields:

type Dog in Animal, Domestic:
    val name
    val breed

-- then, often in another module:

type Dog:           -- This does _not_ clear the supertypes
    var age
    var owner
    var health

-- at this point Dog has name, breed, age, owner, and health fields, and is still
-- a subclass of both Animal and Domestic (inheriting their fields as well).

Types as Constructors

Calling a type as a function constructs a new instance. Keyword arguments only — positional arguments are not supported (field order is not guaranteed to be stable, especially as types evolve).

d = Dog(name="Rex", age=3)

All fields are optional at construction. Omitted fields are left unknown (None), falling back to the type's class default if one exists. Construction always allocates a fresh object — there is no memoization or identity-merging as with Tuples.


Equality and Identity

Equality (==) is identity-based for mutable objects and value-based for
immutable scalars:

Type == semantics
Numbers, Strings, Datetimes Value equality
List, Map, Set, Object Identity — equal only if the same object
Tuple #[…] Identity, but same-content tuples are always the same object (memoized)

⚠ TODO: === content equality for collections.


Modules

Modules are identified by a UUID and a version number,
declared in #-prefixed header lines at the top of the file. The #
lines are parsed as space-separated key=value blobs (no spaces
around =, no quoting — value extends to the next whitespace).
uuid and version are currently required; version must be a
number (floats allowed, compared by magnitude).

# uuid=550e8400-e29b-41d4-a716-446655440000 version=1.3

Multiple # lines are fine; order and grouping do not matter.

Importing Modules

There are three ways to import a module:

import("foo.largo")           -- function call; returns the module object
import foo                    -- statement form; equivalent, binds result to `foo`
import foo, bar               -- import multiple modules at once
from foo import x, y          -- import specific names from a module into local scope

The import statement forms are syntactic sugar over the import() function. import foo is equivalent to foo = import("foo.largo"); from foo import x, y imports the module and then binds the named fields locally.

All forms wait for the module to finish executing (or reach its first wait point) before returning. Returns the module object, typically immediately unless it's the first import of that module.

When a module is imported, the already-loaded copy is returned by
default. If the stored version is older than the declared version,
the module is re-executed in its existing frame (module scope),
enabling live incremental upgrades without restarting the system or
invalidating existing data. It is up to the module to handle version
upgrading, typically by cascading checks on a module global "version"
as in:

if version\0 < 1:
    -- first version of module...
    ...
    version = 1

if version < 2:
    -- upgrade module from version 1 to 2
    ...
    version = 2

...

NOTE, however, that any such module global named "version" is
unrelated to the file revision version in the header. It's sensible
to keep them in sync but that is not enforced.

⚠ TODO: syntax= header field — allows non-DSL source formats (e.g. a visual
flow-chart editor) to be loaded side-by-side with standard Largo source, provided the
source file can begin with # header lines.

⚠ TODO: Crypto signatures for modules.


Exception Handling

⚠ TODO: User-level exception handling syntax is not yet defined.

Internally, break and continue compile to raises, and iterators use exceptions for
stop-signalling. The full infrastructure (SxTry, jump tables) is in place; the
user-facing try/catch surface syntax is TBD.


Async and Persistence

  • The runtime is single-threaded at the Largo level. A task runs until it calls a
    primitive that waits. No task switching occurs mid-expression.
  • Everything lives on disk, always. Checkpoints occur between compiled async steps.
    The system survives hard shutdown and resumes exactly where it left off.
  • Largo's async model is unrelated to Python async/await; the trampoline runs as a
    single Python task.

Spawning Tasks

spawn starts a function in a new, independent task. All three forms below are equivalent:

spawn(func)         -- call spawn() with a zero-argument function
spawn func()        -- statement form; the expression is implicitly lambda'd: spawn(<func()>)
spawn:              -- block form; the block body is made into an anonymous function and spawn()'d
    func()

The expression in spawn expr and the body in spawn: follow the same snapshot-capture semantics as def and lambdas: locals from the enclosing function scope are captured at the point of the spawn, but module-level variables are not captured (they are accessed live by name).

spawn currently returns nothing. Task handles and spawn foo as handle syntax are planned.

Sleeping

sleep(seconds)      -- suspend the current task for the given number of seconds

Waiting on a Condition

wait until expr     -- suspend until expr becomes true
wait while expr     -- suspend until expr becomes false

The expression is implicitly wrapped in a lambda and iterated reactively: the task wakes whenever any dependency of the expression changes, re-evaluates, and goes back to sleep if the condition is not yet satisfied. This is exactly equivalent to iterating the lambda with iter() and looping until the condition holds.

wait until foo.x > 10      -- wakes whenever foo.x changes; resumes when foo.x > 10
wait while task.running     -- wakes whenever task.running changes; resumes when it's false

Built-in Functions

These functions are defined in the system initialisation file and are available in every module without importing anything.

Output and Formatting

Function Description
print(x) Print a value to the console.
pprint(x) Developer-oriented deep formatting of a value, printed to console. Follows object references (including upward/parent ones) so output can be verbose; aimed at debugging, not display.
pformat(x) Same as pprint but returns the string rather than printing it.
str(x) Convert a string or number to a string. For other object types, returns a crude system-level handle description. (Proper dispatch to user-defined string conversions is planned.)
format_time(t) Format a time()-style Unix timestamp as a human-readable string.

Collections

Function Description
append(list, val) Append val to list in place.
truncate(list, len) Truncate (or extend) list to len items in place.
trunc(list, len) Return the first len items of list; returns list unchanged if already shorter.
length(x) Return the length of a List (sugar for x.__length__).
reversed(seq) Return a reversed version of seq.
list(itr) Collect an iterable into a new List.
set(itr) Collect an iterable into a new Set.
tuple(itr) Collect an iterable into a new Tuple.
sjoin(list, sep='') Join a list of strings with sep.
joinlines(list) Join a list of strings with newlines (sjoin{sep='\n'}).

Iteration

Function Description
iter(x) Return an iterator for x. Supported for collections (lists, maps, sets), strings (iterates characters), and zero-argument functions (reactive: yields the current value immediately, then re-fires whenever dependencies change).
kiter(x) Like iter, but yields key:value pairs. Supported for maps and lists (index:value).
span(a, b) Return a Range(start=a, stop=b). Sugar for a..b.

Tasks and Control

Function Description
spawn(func) Spawn func() in a new task. See Spawning Tasks.
sleep(seconds) Suspend the current task for the given duration.
resume(iterator) Advance an iterator, suspending until the next value is ready.

Modules

Function Description
import(path) Import a module, launching it in its own task if needed, and suspend until the module itself waits (or finishes). Returns the module object. This handshake prevents deadlock while still ensuring the module is initialised enough to use. Used by the import statement syntax.
import_fast(path) Import a module, launching it if needed, and return immediately without waiting. The module may still be starting up when this returns.

Misc

Function Description
raise(err) Raise an exception.
quote(s) Currently applies to strings only: returns a repr()-style quoted representation of the string.
ask_root(prompt) Post a question to the root console and suspend until an administrator answers it. Multiple tasks may have open questions simultaneously; the console lists all pending questions and lets the administrator select one to answer. Intended for maintenance and administration, not end-user interaction.
add_property(type, mode, name, default) Add a new property to an existing type at runtime. Equivalent to adding a field line to the original type definition.

Garbage Collection

Largo's garbage collector is currently mark-and-sweep only. It runs as a separate script and must be invoked manually while the application is not running.

Ref-counting GC (with periodic mark-and-sweep for cycles) is planned but is a significant project and has not yet been implemented.


Planned / Future Features

Feature Status
and, or, xor ⚠ TODO
in operator (membership / type / subtype test) ⚠ TODO
Chained comparisons x < y < z ⚠ PARTIAL
weak field mode ⚠ TODO
dyn field mode ⚠ TODO
while/for … then: (normal-exit handler) ⚠ TODO
User exception handling (try/catch) ⚠ TODO
Unpacking / splats ⚠ TODO
Bulk operations on collections (map, filter, etc.) ⚠ TODO
=== content equality ⚠ TODO
Memoized maps #[a=1], memoized sets #{…} ⚠ TODO
type deletes removed fields on re-definition ⚠ TODO
syntax= module header field ⚠ TODO
Permissions system for tasks / objects ⚠ TODO
Crypto signatures for modules ⚠ TODO
Doc strings on type definitions ⚠ TODO
Shell-like syntax for exec ⚠ TODO
Task handles / spawn foo as handle ⚠ TODO
Ref-counting garbage collector ⚠ TODO
Nested for clauses in comprehensions ⚠ TODO
Operator overloading / multiple dispatch ⚠ TODO
Frame-shift for loop (var1 -> var2) ⚠ Maybe

Planned Major Architectural Changes

Frames should be objects, not maps. This would give local
variables all the features of fields such as mode (val/var/one/
weak/etc) and tracking. Furthermore "functions" at this point become
constructors of the function's implied type signature, and the
difference between a function and a type constructor is simply that
the latter returns 'this' (self). Absent an explicit function, the
default constructor is simply to send keyword args to locals and then
return self (current type constructor); but now this becomes truly
just a default function body rather than a hard-coded dispatch, which
facilitates fully custom constructors. Note that 'val' slots would
need to be handled slightly differently: Currently you simply cannot
assign them outside of a constructor; instead we would need to throw
an error if they are accessed before being set (which unfortunately
begs for an assertive None). Likely this shift would make
non-snapshotted spawn blocks more sensible. Particularly if we intro
a "fork" which allows you to write multiple blocks which will eval in
parallel (launched in order) and meet up at the end (like a trio
nursery context) -- with well-defined local variable modes this
becomes more sound. Also this cleans up some current gross hacks
(None guarding in frame parent chain...).

Strings should be interned, to avoid the current rampant
proliferation of huge strings on the db. For '' strings, this would
be transparent at the largo level, but would take some doing under
the hood. A new, sigiled db type would be made for interned strings,
like Map, List and so on. Most of the code actually wouldn't
change that much because interned string Objects should work just
fine most of the places raw strings are currently used (field names,
map indices, etc), and if __str__ returns the string then that
covers many more places. The main change would be everywhere a
string is entered, whether constants or calculated, it would need to
be interned first, which will be a fair number of places... Also,
comparisons of constants will need to either call str() or intern the
contstant (e.g., field mode is currently a string; arguably this
should be made into a type anyway?). The intern table should be
defined with column order (string, oid) with an extra index on oid,
to avoid duplicating the strings (which may be on occasion huge).
But then for "" strings:

Mutable Strings should be returned by "" construction. Under the
hood these should be lists of interned and other mutable strings
(forming a tree), and should track their total length? The main
function of this is to make string arithmetic more efficient.
Interpolated strings end up just returning a list of parts (with no
replication of their interned components in the db); appending things
to a (mutable) string becomes very cheap. String concatenation is
essentially return [a, b] (plus sum of lengths). Then finally an
intern() call can traverse the tree (efficiently in python) to
assemble a single large string entered into the interned table when,
and only when, needed. Immediate interning of "" strings could be
done with #"foo{bar}" syntax, consistent with # as the general
interning/memoizing operator. Semantically, a sum including mutable
strings would mutate with them. So, e.g., s = '(' + x + ')' where
x is a mutable string would return a string, s, that would change
when you change x (here always being a parenthesized version of x).
Caller would have to decide when to treat strings as immutable by
convention vs using intern to snapshot. Alternately we could do
most of the above but disallow in-place mutation, giving immutable
strings that are simply more efficient arithmetically (important in
the context of disk-backed memory) which would be nearly transparent
from the largo perspective except that memoization would not work for
one type of string but would for another, which could be confusing.
Mutability actually makes this cleaner because it perfectly aligns
with the current mutable/immutable identity norm (immutables:
identity = value; mutables: identity = handle). [Side note: We
need to consider whether '#' should just become a normally dispatched
operator. Currently it is handled directly by the compiler for Tuples,
which allows Tuples to compile ahead of time, which is important to
retain. But we could try to handle constant propagation better in
the compiler so that, e.g., #[1, 2, 3] first computes the List and
then notices the List is a constant and so calls # during compilation;
a notion which could be extended to all operators [to be] flagged as
deterministic. Then, e.g., #"foo{10^5+72}" would theoretically be
computed and interned as a constant string at compile time... And
also syntactically, #mylist would work just as well as #[1, 2, 3]
whereas currently we have to call tuple(mylist). Arguably, more
simply for now, if we get the esrapy precedence right we could
make # an operator and just special-case #[] at compile time.]

Return should be re-implemented as an exception, perhaps
hard-coded at the function boundary to avoid populating jump tables
everywhere (if we switch to jump tables tagged on calls; if we stay
with the current jump table approach it doesn't matter much). Mainly
this makes implementation of try/finally much simpler. This would be
transparent within largo except for opening up the possibility of
intentionally catching returns.

Shell syntax could be added for invoking external applications.
But not using an external shell--argument parsing should be done at
compile time, so no worries about spaces in strings and such. E.g.,
something like:

$ tail -n {num} {filename}

should send the last num (a local largo variable) lines of the file
named by filename to stdout. And potentially something like this to
capture the output:

last_lines = $(tail -n {num} {filename})

If captured output (async) iterated lines by default, then this sort
of thing would work out of the box:

for line in $(tail -F {filename}):
    print("New line: {line}")

Obviously shell ability would be restricted. Imported modules would
not be allowed to do this by default. But it could make largo into
a pretty handy sysadmin scripting language, given the persistence,
permissions (todo), and change tracking (which can and will be
extended to tracking file changes and such).


Largo Cheat Sheet

⚠ TODO = not yet implemented (compile-time error) · ⚠ PARTIAL = incomplete · ⚠ TBD = syntax undecided


Comments

-- line comment
(| block comment — (| nestable |) |)

Literals

42        3.14        1.5e10          -- numbers (whole floats become ints)
'plain \n no interpolation'           -- single-quoted; backslash escapes
"format string {expr}"                -- double-quoted; {} interpolates
{{ }}                                 -- literal braces in format strings
$'it's fine'$                         -- guarded: no escape, quotes safe
$"embed {expr} and "quotes""$         -- guarded format string
$$$'triple-guarded'$$$                -- any matching number of $

Multi-line: any string auto-dedents to longest common leading whitespace of interior
lines. Closing delimiter on its own indented line → trailing \n.

In format strings, a {} preceded by whitespace indents multi-line values to the
column of {.


Collections

[]                          -- empty List
[1, 2, 3]                   -- List (mutable)
[:]                         -- empty Map
[a=1, b=2]                  -- Map, field-name keys
["k": v]                    -- Map, expression key
[a=1, "k": v, x+1: w]       -- field-name and expression keys freely mixed
#[]                         -- empty Tuple
#[1, 2, 3]                  -- Tuple (immutable, memoized — content = identity)
{}                          -- empty Set
{1, 2, 3}                   -- Set (mutable)

For Maps, field access is shorthand for string indexing: m.foo == m['foo'].

⚠ TODO: Object literal {a: 1, b: 2}. Immutable memoized maps #[a=1] and sets #{…}.

Comprehensions

[expr for x in seq]                    -- List comprehension
[key: val for x in seq]                -- Map comprehension
{expr for x in seq}                    -- Set comprehension
[expr for x in seq if cond]            -- filtered (works for all three types)
[i:x*2 for i:x in seq]                -- index/key capture
[x+y for x in a, y in b]              -- zip two iterables

Variables and Assignment

x = expr                    -- local assignment (right-associative)
x = y = 0                   -- chained, right-to-left
obj.field = expr             -- field assignment
obj[key] = expr              -- index assignment
(x = expr)                  -- assignment as sub-expression (parenthesize)

⚠ TODO: := in-place mutation.


Operators — Precedence (high → low)

# Operator Notes
1 f(args) call
1 f{args} curry → new function
1 f[a,b] cascaded calls: f(a)(b)f[a,b] idiomatic, f[a][b] also valid
1 x.field field access (unknown if unset; error if field doesn't exist on known type)
1 x? is x known (non-None)? → bool; false? is true
1 x! wait until x is known, then return it (useful for one fields)
2 a \ b default: a if known, else b
3 a ^ b exponentiation (right-assoc)
4 -x unary negation
5 * / // % mul / div / int-div / mod
6 + - add / subtract
7 a..b a... ...b range [a,b), [a,end), [0,b)
8 in membership / type test ⚠ TODO
9 < <= > >= == != comparison; chainable (ascending or descending) ⚠ PARTIAL
10 not logical not
11 and short-circuit and ⚠ TODO
12 or xor short-circuit or / xor ⚠ TODO
13 if…then…else ternary (right-assoc): if c then a else b
14 = := assignment (parenthesize as sub-expr)

Map/index access returns unknown (not error) for missing keys or unknown base objects.


Conditionals

if cond:
    body

if cond:
    body
else if cond2:
    body2
else:
    fallback

Loops

while cond:
    body

while cond as name:         -- named loop

for x in seq:
    body

for i:x in seq:             -- i = index/key, x = value
    body

for x in seq1, y in seq2:   -- zip-like
    body

for x in seq as name:
    body

break for                   -- label always required (default: "for" or "while")
continue while
break name

⚠ TODO: while cond then: / for x in seq then: — runs on normal exit, skipped by break.


Reactive Iteration

for calls iter() on its source. Any zero-argument function used as a source
becomes a reactive iterator: fires immediately, then re-fires whenever a dependency
changes. Multiple changes in one async cycle fire once.

for v in <x + y>:           -- lambda source; re-fires when x or y changes
    print(v)

for v in my_func:            -- named zero-arg function
    print(v)

⚠ PARTIAL: Reactive tracking of module-level variables is set up but assignments don't
currently funnel through the tracker — re-fires on module var changes may not occur.
Calling a blocking primitive inside a reactive iterator is currently an error.


Functions

def add(x, y):
    return x + y
{|
 | One-line summary.
 |
 | Optional details, available at runtime.
 | foo.summary  /  foo.details
 |}
def foo(x):
    ...

Nested functions and lambdas use snapshot semantics: enclosing function locals are
shallow-copied at definition time. Snapshotting stops at the module boundary — module-level
variables are referenced live, not captured. Caller locals cannot be modified.

⚠ TODO: Doc strings on type definitions.


Lambdas

<expr>              -- zero-arg lambda (also a reactive iterator source)
<x: expr>           -- one-arg
<x, y: expr>        -- two-arg

Calling and Currying

f(x, y)             -- positional call
f(x, kw=val)        -- keyword arg
f[x, y, z]          -- cascaded calls: f(x)(y)(z) — f[x][y][z] also valid
f{x}                -- curry: f with x prepended
f{x, y}             -- curry two args at once
f{kw=val}           -- curry keyword arg
f{x, kw=val}        -- mixed
f{x}{y}             -- cascaded curry == f{x, y}
f{x}(y)             -- == f(x, y)

f{x} vs <f(x)>: the curry accepts more args later; the lambda is a zero-arg thunk.


Types

type Foo                        -- bare declaration (useful for mutual recursion)
type Foo in Bar                 -- single inheritance
type Foo in Bar, Baz            -- multiple inheritance

type Dog in Animal:
    val name                    -- immutable after construction
    val breed = "unknown"       -- immutable, class default (any expr; evaluated once)
    var age                     -- mutable
    var score = age * 2         -- mutable, computed default
    one owner                   -- write-once
    weak ref                    -- weak reference; reverts to unknown on GC  ⚠ TODO
    dyn rank = <score / 10>     -- computed, cached, reactive  ⚠ TODO

d = Dog(name="Rex", age=3)      -- construct with keyword args only

Type definitions edit existing types (safe for live upgrades; preserves object data).

Mode Mutability
val Set at construction; immutable thereafter
var Fully mutable; writing None restores class default
one Unknown until first write; immutable after
weak Like var but weak reference; silently reverts to unknown on GC ⚠ TODO
dyn On-demand, cached, reactive ⚠ TODO

Equality

Type == semantics
Numbers, Strings, Datetimes Value equality
List, Map, Set, Object Identity (same object only)
Tuple #[…] Identity, but same-content tuples are always the same object

⚠ TODO: === content equality for collections.


Async and Tasks

spawn(func)         -- spawn func() in a new independent task
spawn func()        -- statement form (implicitly lambda'd)
spawn:              -- block form
    func()

sleep(seconds)      -- suspend current task

wait until expr     -- suspend until expr is true
wait while expr     -- suspend until expr is false

spawn captures enclosing function locals by snapshot; module-level variables are
accessed live. Task handles and spawn foo as handle are planned (⚠ TODO).


Modules

# uuid=550e8400-e29b-41d4-a716-446655440000 version=1.3

Header # lines contain space-separated key=value blobs. uuid and version
(numeric, ordered) are required. Re-importing a higher version re-runs the module
in its existing frame — live upgrade, no data loss.

import("foo.largo")           -- function call; returns module object
import foo                    -- statement form; binds result to `foo`
import foo, bar               -- import multiple modules
from foo import x, y          -- import specific names into local scope
import_fast(path)             -- import without waiting for module init

⚠ TODO: syntax= header field, crypto signatures.