GAMA-L 2.0 core (executable)
This is a real parser + static checker + tree-walking interpreter, not the old
transpiler. It is a core subset of SPEC_2.0.md; the rest is still roadmap.
Run
python3 gamal2.py run examples/invoice.gm
python3 gamal2.py check examples/invoice.gm
python3 gamal2.py test
Package manager (local + git)
python3 gamal2.py pkg init .
python3 gamal2.py pkg install .
python3 gamal2.py pkg update .
python3 gamal2.py pkg list .
python3 gamal2.py pkg search registry.json
python3 gamal2.py pkg add registry.json math 0.1.0
[dependencies]
math = { path = "math" }
lib = { git = "https://example.com/lib.git", rev = "v1.2.0" }
Path dependencies must stay inside the project root; symlinks are rejected.
Git dependencies clone into .gamal/cache/<name> and are pinned in
.gamal/lock.json; reinstall fails on a lock mismatch until pkg update.
Modules resolve from .gamal/modules/<name> via import "name/file.gm".
A local/static registry is a JSON file (path, file://, or http(s)://)
consumed by pkg search|resolve|add. For a loopback/private HTTP registry set
GAMA_L_REGISTRY_ALLOW_PRIVATE=1 (the client blocks private hosts by default).
Serve one with:
python3 gama_registry_server.py --registry registry.json --port 8089
Any git URL (including private remotes) works with the host's git credentials.
Editor support (LSP, stdio)
python3 gamal2.py lsp
JSON-RPC language server: initialize, didOpen/didChange/didSave
diagnostics, completion, hover, signature help, definition, references,
document symbols, semantic tokens, code actions, rename, document formatting,
shutdown, exit. Malformed frames return a JSON-RPC error and no longer
kill the server; multiline strings keep correct line/column positions.
Positions are converted between LSP UTF-16 and Python indexes.
Completion covers the current keywords, types (Function, Result, Table,
Json, Null, …), all builtins with signature details, document let/fn
names, and import aliases. For import "lib.gm" as <cursor> the only
inferable alias is a sanitized module filename stem; it is omitted when it
collides with a top-level name or an existing alias. After alias., completion
and hover read the imported file's direct top-level fn declarations
(namespaced completion does not list module let constants yet).
Signature help works for known builtins, user fn/lambda-typed let values,
and namespaced module functions.
Generic named functions are reflected in hover and signature help with their
declared type parameters (fn id<T>(x: T) -> T, fn pair<A, B>(...)) and a
Type parameters: ... (position N) summary; parameters that use a type
parameter carry a matching type parameter note. Completion after id<
offers type parameters that are in scope from an enclosing generic function,
then the callee's declared type parameters for context, then the built-in type
names; fn id< offers the built-in type names only (type-parameter names are
chosen by the author and cannot be inferred). The core parser supports
id<Int>(1) for locally declared generic functions (count/name checks,
formatter round-trip); imported/namespaced generic calls and nested type
arguments are not recognized. LSP signature help also recovers explicit
arguments when the base name resolves to a generic fn declaration, and
semantic tokens mark declared type-parameter names as types.
Match-arm patterns get best-effort LSP support even when the core parser
rejects the pattern: arm-head completion offers records, enums and variants,
Point { ... } offers the record's fields (inherited ones included), hover on
a pattern record name shows its fields/arity, and hover on a bound variable
infers the field type (record patterns) or the List<T> element type. These
record-struct, record-constructor, list and if-guard forms are LSP-only and
remain parse errors in the core; a guard after if falls back to normal
expression completion, and malformed or unclosed matches stay quiet instead
of crashing.
The core exposes a module's top-level let constants as alias.name, typed
and evaluated at import time, and module functions see the same renamed
binding. Any other non-declaration top-level statement under an alias
(print, prove, verify, loops, assignments, expression statements) is
rejected (use is allowed as a no-op).
Builtin parameter names are LSP-only labels, since the core has no
builtin parameter names. Diagnostics still use the in-memory check(text)
path, so a document with an import reports the core's "import requires
check_file/run_file" message even though the file would check/run fine on
disk.
A syntax-only VS Code package is in vscode/ (TextMate grammar + snippets +
a dependency-free LSP client). It highlights import ... as namespaces,
Function, namespaced calls, lambdas, record with { ... } updates,
payload/multi-payload enums, aliased variants (geo.Color.Red), export fn/let/record/enum, selective export { ... } lists (including as
renames), and the postfix ? operator. Snippets cover those features
(recordwith, payloadenum, multipayloadenum, payload, pair,
aliasvariant, matchpayload, exportfn, exportlet, exportrecord,
tryexpr) plus impas, callns, iife, and fnlettype. Grammar limits:
record-update scopes end at the first }, only three-part alias chains are
scoped, and the as Name clause of renamed exports is not given a dedicated
scope yet. The extension requires a trusted workspace
before spawning Python, supports gama-lsp from PATH or a configured script,
and restarts with bounded backoff.
Distribution
pyproject.toml defines the gama-l package and console scripts gama-l,
gama-lsp, gama-doctor, gama-registry, gama-release; modules are
gamal2, gama_lsp, gama_cli, gama_fmt, gama_doctor, gama_mail,
gama_registry, gama_registry_server, gama_ai, release.
The CLI wrapper is gama_cli.py. release.py build writes deterministic
wheel/tar, SHA256SUMS, and RELEASE.json; release.py verify checks them.
Reproducible release verification
scripts/verify_release.sh runs the whole release smoke test from a clean
temporary directory (no artifacts are written into the repo):
release.py buildtwice and compares the SHA256 of the wheel and sdist to prove the build is byte-reproducible;release.py verifyplussha256sum -c SHA256SUMS, and checks theRELEASE.jsonmanifest;- installs the wheel into a fresh virtualenv and runs
gama-l --help,gama-l run examples/hello.gm, andgama-l doctor --json(8/8); - validates
vscode/package.jsonand every referenced extension asset, and packages the extension whenvsceis available offline (otherwise it reports the missing tool and never publishes).
bash scripts/verify_release.sh
The wheel ships code only; gama-l doctor needs a tree with examples/, so
run it from the source checkout or pass --root to a directory that exposes
examples/.
Release dry run (recommended gate)
scripts/release_dry_run.sh is the single "everything except publish" gate. It
builds twice, verifies hashes, installs the wheel into a fresh virtualenv, runs
the shipped CLI and doctor, checks the wheel with twine when available,
packages the VSIX when vsce is available, and prints a manifest with versions,
sizes, hashes, and tool availability. All work happens in a temporary directory.
cd gama-lang/v2
bash scripts/release_dry_run.sh
# Optional: keep the verified artifacts + manifest (outside the repo, e.g. CI)
KEEP_DIST=/tmp/gama-l-dist bash scripts/release_dry_run.sh
Exact checks:
release.py build --out dist-aandrelease.py build --out dist-b; the wheel and sdist SHA-256 must match across both builds.release.py verify --out dist-aandsha256sum -c SHA256SUMS.python3 -m venv, install the wheel with--no-index --no-deps, thengama-l --help,gama-l run examples/hello.gm, andgama-l doctor --json --root <temp-root>(8/8).twine checkon the wheel when twine is installed.vsce package(or cachednpx --no-install @vscode/vsce) when available.- A manifest containing the core version, extension version, artifact sizes and hashes, doctor result, and tool matrix.
Limitations:
release.pywrites a deterministic source archive, not a PEP 517 sdist (it has noPKG-INFO).twine checktherefore validates the wheel only; the sdist is covered byrelease.py verifyandsha256sum. The sdist is not directly PyPI-uploadable.twineandvsceare optional. Missing tools are reported and skipped; real build/checksum/install/CLI/doctor failures exit non-zero.- Byte-reproducibility is checked within a single run on the same Python/toolchain (two builds); builds on different Python versions are not compared.
- Artifacts are never written into the repository (the
dist/directories are git-ignored) and nothing is published.KEEP_DISTis only for CI/scratch space, not for committing. gama-l doctorruns against the installed wheel by pointing--rootat a temporary directory that symlinks the sourceexamples/.
Release workflow (tag → artifacts; publish is opt-in)
.github/workflows/release.yml runs on v* tag pushes and on manual dispatch.
It runs the full test suite (Python 3.10 and 3.12), then the release job runs
scripts/release_dry_run.sh with KEEP_DIST set, validates the workflow YAML,
and uploads dist (wheel, sdist, SHA256SUMS, RELEASE.json, manifest, and
VSIX when packaged) as the gama-l-release artifact.
- Concurrency:
release-${{ github.ref }}withcancel-in-progress: false. - Timeouts: 20 minutes for tests, 30 minutes for release.
- A tag push never publishes: it builds and uploads artifacts only.
- Publishing is opt-in via manual dispatch with
publish: trueand repository secrets. The PyPI step requiresPYPI_API_TOKEN; the VS Code Marketplace step requiresVSCE_PAT. Without those secrets the publish steps are skipped and only artifacts are uploaded.
Doctor
python3 gamal2.py doctor
python3 gamal2.py doctor --json
Checks Python, module imports, every example parse/run, formatter idempotence, LSP API (including rename/formatting/code actions), SQL write guard, and a local package install.
Formatting
python3 gamal2.py fmt file.gm # print
python3 gamal2.py fmt --check file.gm # rc 1 if unformatted
python3 gamal2.py fmt --write file.gm # write in place
Opt-in bytecode IR (CLI)
The standalone bytecode IR prototype in gama_ir.py is exposed as an opt-in
CLI path. The default run command keeps using the reference tree-walking
interpreter; nothing about that backend changes.
python3 gamal2.py ir run FILE.gm # compile on gama_ir and run on the IR VM
python3 gamal2.py ir compile FILE.gm # print the instruction listing
python3 gamal2.py ir disasm FILE.gm # alias for compile
python3 gamal2.py ir compile --json FILE.gm
ir run parses and type-checks the file with the normal front end, compiles the
checked AST with gama_ir, and prints the VM output. import "lib.gm"
statements are resolved by the same core loader run uses (flattened, with the
project-root bound, cycle detection and depth limit), so imported functions,
constants and records work in the IR backend too. Aliased imports
(import "lib.gm" as m) flatten into dotted declarations (m.fn, m.pi,
m.Point, m.Color, m.Option.Some) and are supported: m.fn(...), m.pi,
m.Color.Red, payload constructors m.Option.Some(5) and positional record
construction m.Point(1, 2) (including let p: m.Point) resolve against the
same tables as local names. Enum payload declarations, constructors and match
bindings compile the same way locally and through aliases.
gama_ir.compile_program on a bare source string has no filesystem context for
imports — use gama_ir.compile_file for files. A construct outside the IR
subset exits nonzero with IRUnsupported: IR does not support ... (line, col);
an IR runtime failure reports IRRuntimeError; parse/check/file errors keep
their usual message. compile/disasm print one index OPCODE [arg] line per
instruction (or {"file": ..., "instructions": [...]} with --json). This is
also reachable as gama-l ir ... through the packaged console script.
Exact supported subset:
- Statements:
let/:=,print,if/else(incl.else ifchains),while,forover list/text,break,continue,matchonOk/Errand enum variants (nullary, one-payload and multi-payload patterns), namedfndefinitions,return(inside functions), expression statements,prove name = expr/verify expr(hash-chained receipts with optional HMAC and the optional persistent JSONL ledger — see below), anduseas a no-op. - Expressions:
Int/Decimal/Text/Bool/null, list[a, b]and map{k: v}literals, variables, enum variants and payload constructors (Color.Red,Some(5),Option.Some(5),Pair(1, 2)),!/-,+ - * / %,== != < <= > >=, short-circuit&&/||, indexingbase[i](list/text, referenceintcoercion, negative indices), calls to named user-defined functions (recursion and default parameters included), lambda expressionsfn(x: Int) -> Int { ... }compiled to IR function values that capture the defining lexical frame, calls through function values (f(2)for aFunction-typed binding or parameter), named functions read as values (let g = add, including namespaced functions), expression callees such as IIFEs(fn ...)(41)and returned lambdasmakeAdder(5)(37), and Result propagationexpr?inside a function returningResult/Any(anErrreturns from that function;?elsewhere isIRUnsupported), and pipe expressionsbase | filter <expr> | select a, b | sum a | limit <expr> | count | map a. Pipe stages run left to right on the same value exactly like the reference interpreter:filter/limitsub-expressions are compiled to their own code and evaluated with the active row bound (filterrows shadow outer bindings;limitsees the enclosing scope), andselect/sum/count/mapuse the interpreter's runtime operations, evaluation order and error messages. The normal checker rules apply, so column stages (select,sum,map, and column-referencingfilter) need aTable/Anybase;count,limitand constantfilterwork on lists, and the runtime also accepts list/text bases from unchecked ASTs. A hand-built AST with an unknown stage keyword or missing stage argument is rejected withIRUnsupported. - Builtins:
len,text,number,round,Ok,Erronly.
Host callables: run_program(ast, env={...}), run_file and
compile_file(path, env=...) accept plain Python callables in the initial
environment. A call evaluates its arguments on the VM, invokes the Python
function with those values and returns the result unchanged as an IR value.
Host callables are ordinary first-class values: they can be aliased, passed as
Function arguments, returned from IR functions and called through variables
or expression callees ((host)(1), makeHost()(2)). Positional arity is read
from inspect.signature when available and checked before arguments run;
signatures that cannot be introspected defer to the call. Host exceptions
become IRRuntimeError at the call site's line/col (host callable NAME raised TypeError: ...; an IRRuntimeError from the host is re-raised unchanged).
Host calls count against GAMA_L_MAX_CALL_DEPTH; Python-side work inside the
callable is not counted by the IR step/expression caps, and no interpreter is
instantiated. Calls are positional only (the language has no keyword
arguments), and host objects are not traversed for methods: bind
obj.method itself. Checked sources/files may refer to host names only when
the same env is passed to compile_program/compile_file; those names are
declared as Function for checking (runtime precedence still gives a declared
IR function priority over an env binding of the same name).
Not supported (raises IRUnsupported; the normal interpreter still supports
them): unresolved import nodes (file imports go through
compile_file), bare Attr expression nodes, record destructuring patterns
(Point(x, y) and nested forms are interpreter-only; enum payload variants are
fully supported), list patterns ([], [a, b], [a, ...rest], nested lists),
match guards (pattern if cond) and top-level variable patterns (x if ...),
and all other builtins (sql, ask, split, unwrap,
wait_user, mail, remember/recall, ...). First-class calls are checked for
callability and arity before their arguments are evaluated and raise
IRRuntimeError with the interpreter's messages. The IR now enforces the same runtime limits as the
interpreter (GAMA_L_MAX_STEPS, GAMA_L_MAX_EXPR_STEPS, GAMA_L_LOOP_LIMIT,
GAMA_L_MAX_OUTPUT_CHARS, GAMA_L_MAX_CALL_DEPTH) with matching wording and
raises IRRuntimeError (a GamaError) on exceed. Map
literals are JSON values, so text-keyed maps are not indexable. Successful
programs produce byte-for-byte the same output as the interpreter, and runtime
error messages keep the interpreter's text and line/col.
prove/verify run on VM proof state, never on a gamal2.Interpreter
instance. They reuse the reference module-level canonical-JSON, hash-payload
and ledger-scan helpers, so receipts, sha256/id/prev links, HMAC sig
(GAMA_L_PROOF_KEY, GAMA_L_REQUIRE_SIG) and intent/lease fields
(GAMA_L_PROOF_INTENT/GAMA_L_PROOF_LEASE, GAMA_L_REQUIRE_INTENT_LEASE)
match the interpreter exactly. GAMA_L_PROOF_LEDGER=<file> loads and validates
the JSONL chain at startup, appends under one exclusive POSIX flock with a
tail re-read, and refuses to extend a malformed/forked ledger; without fcntl
cross-process atomicity is best-effort. Proof statements inherit the IR's
general runtime limits exactly like ordinary statements.
Implemented
import "file.gm"with cycle detection (relative to the importing file)- module namespaces:
import "lib.gm" as math, thenmath.add(2,3)andmath.pi; plain imports still flatten (including top-levellet); aliased modules expose top-levelfndefinitions,letconstants,recordtypes andenumdeclarations, evaluated at import time, and reject side-effect top-level statements (useis allowed as a harmless no-op). Enums are namespaced (geo.Color,geo.Color.Red); a module that declares at least one explicitexportexposes only its exported enums through the alias. - explicit module exports:
export fn,export let,export recordandexport enummark the alias-visible surface; other top-level declarations stay private but remain callable inside the module. A module with at least oneexportexposes only exported names throughimport ... as; a module with noexportkeeps the legacy behaviour (all top-levelfn/let/record/enumvisible). Plain imports always flatten all declarations under their original names for compatibility.exportis only allowed at top level; duplicate export names and renamed targets that collide with another top-level name are rejected. Renamed exports useexport <decl> ... as <Ident>: forfnthe canonical position is in the header aftereffects(export fn add(a: Int, b: Int) -> Int as plus { ... }), whilelet/record/enumuse a trailing clause (export let pi = 3.14 as PI,export record Point { ... } as Pt,export enum Color { ... } as Hue). The alias exposes only the new name (geo.plus,geo.PI,geo.Pt,geo.Hue); module code keeps using the original names. Selective re-export lists are also supported:export { add, pi as PI, Point, Color }names local top-levelfn/let/record/enumdeclarations, optionally renaming each entry on the alias surface (geo.PI) while the module and plain imports keep the original name (pi). Entries are comma- or newline-separated with an optional trailing comma;export {}is rejected. A module with a list (alone or mixed with declaration-level exports) exposes exactly the union of its exported names and hides every unlisted top-level declaration. The list may only name declarations of that same module: imported aliases (import ... as inner) and plain-imported names are not re-exportable and produceGamaError, as do unknown names, duplicate entries, duplicate alias targets, and alias targets that collide with an existing top-level name. letwith optional type annotation,:=assignmentfnand SPECmakewith typed parameters, defaults, return type,effects { ... }- generic type parameters on named functions with type erasure:
fn id<T>(x: T) -> T { return x },fn pair<A, B>(a: A, b: B) -> Text { ... }. Type parameters are valid in parameter, return andletannotations and are erased toAnyby the checker/runtime, so a call accepts any argument type. The checker infers a call result from the arguments when the return type is a type parameter (id(5): Int,id("x"): Text) and otherwise erases toAny; non-type-parameter positions keep normal arity/type checks. Duplicate type parameters and duplicate parameter names are rejected. Generic functions remain ordinary first-class named functions (recursion, defaults, higher-order use, and IR compilation with erasure all work). Limits: no higher-kinded bounds; union and intersection (T: A & B) bounds and named-function bounds are supported, and generic record/enum bounds are covered by their declarations. Generic lambdas are covered separately. The IR backend supports named-function-as-value for plain and generic functions (type erasure applies to the latter) - explicit call type arguments on local generic functions:
id<Int>(5),pair<Int, Text>(...); the checker validates type-argument count and names, argument compatibility uses the explicit bindings, and the runtime/IR ignore them (erasure). Imported/namespaced generic calls and nested type arguments are not recognized; formatter round-trips explicit arguments. - union generic bounds:
fn add<T: Int | Decimal>(a: T, b: T) -> T,fn show<T: Text | Bool>(x: T) -> Text; inferred/explicit arguments match any alternative, unknown/duplicate alternatives rejected, formatter round-tripsT: A | B. - intersection generic bounds:
fn tag<T: Named & Aged>(x: T) -> Text; every component must be satisfied (records structurally require all fields of each bound), explicit/inferred args checked, formatter round-tripsT: A & B, IR erases. - nested record
withpaths:o with { inner.x = 9 }deep-copies only records on the updated path; multiple paths merge, original/aliases unchanged; formatter/IR supported. - generic record/enum bounds:
record Box<T: Text> { value: T },enum Opt<T: Int> { Some(T), None }; annotation and constructor inference enforce the nominal bound, aliases rename bound refs, formatter/IR erase. - return type inference when
->is omitted: all lexicalreturnexpressions merge (Int/Decimal -> Decimal; Any absorbs; incompatible -> GamaTypeError; no return -> Any; recursion -> Any); inferred results are visible to callers, methods, lambdas and forward references. - effect inference when
effectsis omitted: direct and callee effects are computed as a least fixpoint; expliciteffects {}stays an empty contract and explicit sets still reject uncovered effects; inferred sets stay out-of-band so formatting is unchanged. - LSP inferred signatures: hover/signature help show
(inferred: T)and inferred effects when annotations are omitted. - generic records with type erasure:
record Box<T> { value: T },record Pair<A, B> { first: A, second: B }, constructorsBox(5)/Pair("x", 2), annotationsBox<Int>/Pair<Text, Int>(arity checked) and explicit constructor argumentsBox<Int>(5). Type parameters are usable in field types and method signatures (fn get(self: Box<T>) -> T); they erase toAny, while concrete fields keep their type. Aletannotation binds type parameters for the constructor check (let b: Box<Int> = Box("x")is rejected, while erasedBox("x")is accepted), nested refs (Box<Box<Int>>) erase to the nominal base, duplicate type parameters/field names are rejected, namespaced records work (geo.Box<Int>,geo.Box(5)), and formatter/IR erase explicitly (the IR also compiles method-bearing records). Limits: no bounds, no method-level type parameters (methods use only the record's parameters), and no explicit type arguments on namespaced constructors (geo.Box<Int>(5); annotate the binding instead) - top-level type aliases:
type UserId = Int,type IntBox = Box<Int>,type PointList = List<Point>; aliases are transparent and expanded recursively by the checker inletannotations, parameters, returns, record fields, enum payloads and generic arguments. Generic targets are arity-checked (Box<Int, Text>and bareBoxare rejected), duplicate/unknown/cyclic aliases and collisions with builtin types/records/enums are rejected, aliases may be exported and renamed (export type UserId = Int as Id), exposed through selectiveexport { ... }lists and aliased imports (geo.Id), the formatter round-trips them and the IR emits aNOP(complete erasure). Union aliasestype Scalar = Int | Decimal/type MaybeText = Text | Boolexpand to a union: a value is accepted when it is compatible with at least one branch forlet, parameters, record fields and enum payloads/patterns; a union return accepts any branch. Union-to-union assignment uses branch inclusion, while narrowing a union value back to a single type is rejected (no flow-sensitive narrowing) except for the existingInt/Decimalinterchangeability. Branches may be base types, records, enums, non-union aliases, or generic record refs (erased to the nominal base); unknown/duplicate branches, nested union aliases and union arity misuse are rejected. Generic aliasestype Boxed<T> = Box<T>are supported with arity checks and substitution (Boxed<Int>->Box<Int>, nested uses included); bounds on alias parameters are not supported - first-class functions and lambdas:
fn(x: Int) -> Int { ... },let f = ...,f(41), passing functions as values, named functions as values after definition, IIFE(fn...)(41), and recursive lambdas if/elseblocks,returnfor x in list/text,while cond,break/continue, list/text indexingOk/ErrResult values andmatcharms (Ok(v),Err(e),_)expr?Result propagation: unwrapsOk, returnsErrfrom the enclosing function; the function must returnResult- core builtins:
unwrap,unwrap_or,is_ok,is_err;split,join,contains,trim,upper,lower,replace,starts_with,ends_with;keys,values,has; numericabs,min,max,floor,ceil; text/listreverse,slice,index_of,sort; conversionto_int(to_textis already covered bytext) - literals: text, int, decimal, bool, null, list, map
- arithmetic/comparison/boolean operators; a newline immediately after a binary operator continues the expression on the next line (statement boundaries still require the operator to be on the same line)
- pipes:
filter,select,sum,limit,count,map ask(offline mock, or OpenAI-compatibleGAMA_L_ASK_URL),sql(demo table, or read-only SQLite viaGAMA_L_DB),remember/recallwait_user(non-interactive returnsDenied; falsy)- guarded
sql_write(INSERT/UPDATE/DELETE; needsGAMA_L_ALLOW_WRITE=1, rejects DDL/stacked statements) - guarded SMTP
mail(needsGAMA_L_MAIL_ENABLED=1; config viaGAMA_L_SMTP_*) - SPEC syntax aliases:
make,ask "..." with context,remember a.b = v,recall a.b,wait user "...",use sql round(value, digits)- stdlib semantics:
abspreserves Int/Decimal;min/maxtake one or more numbers;floor/ceilreturn Int;reverse/slicework on Text and List (slice(seq, start, end)is half-open with explicit0 <= start <= end <= len, no negative indices);index_ofreturns the first index or-1;sortreturns a new ascending list and requires all numbers or all Text (stable, as Python's sort);to_intaccepts Int, an integral Decimal, or Text of an optional sign plus digits (surrounding whitespace is trimmed) askusesgama_aiwhenGAMA_L_AI_URLis set; config viaGAMA_L_AI_KEY,GAMA_L_AI_MODEL,GAMA_L_AI_ALLOW_HTTP=1for local HTTP; HTTPS by default, retries 429/5xx, 1 MiB response cap; redirects are refusedprove(hash-chained receipt, optional HMAC viaGAMA_L_PROOF_KEY, optional persistent JSONL ledger viaGAMA_L_PROOF_LEDGER, optional intent/lease binding viaGAMA_L_PROOF_INTENT/GAMA_L_PROOF_LEASE) andverify(tamper detection, intent/lease mismatch rejection;GAMA_L_REQUIRE_INTENT_LEASE=1requires both env values and a match). Concurrent appends to one ledger are serialized (POSIXflock) across tail-read, hash and append, so parallel runs extend a single chain; a forked or malformed ledger is refused byproveand reported byverify- runtime limits via
GAMA_L_MAX_CALL_DEPTH(default 200; the effective cap is additionally clamped to(sys.getrecursionlimit() - 100) // 4, so an explicit higher value cannot exhaust the CPython stack — deep recursion raisesGamaError, neverRecursionError) andGAMA_L_MAX_OUTPUT_CHARS(default 1000000) - optional global step limit via
GAMA_L_MAX_STEPS(unset, blank, malformed, or negative = disabled): counts every executed statement dispatch (including nested block and function-body statements) plus one tick perfor/whilebody iteration; expression evaluation and the function call itself are not counted separately. On exceed raisesGamaRuntimeErrorwith the count, the limit, and the source line/col.GAMA_L_LOOP_LIMIT(default 1000000) still boundswhileiterations independently; a present but blank or non-integer value raisesGamaRuntimeErrorinstead of leakingValueError - optional expression step limit via
GAMA_L_MAX_EXPR_STEPS(unset, blank, malformed, or negative = disabled): counts everyInterpreter.exprevaluation; nested calls count individually (e.g.1 + 2is three steps: theBinaryplus bothLiterals). On exceed raisesGamaRuntimeErrorwith the count, the limit, and the source line/col. It is independent ofGAMA_L_MAX_STEPS, call depth, output, and loop limits, and remains counted when the cap is disabled - the standalone bytecode IR (
gama_ir.py) enforces all five limits with interpreter parity:STEP/LOOP_TICK/EXPR_STEPVM ops reproduce statement-dispatch, loop-iteration and expression-node counting (including default-argument and pipe-stage evaluation and the extra receiver lookup for dotted method calls),GAMA_L_LOOP_LIMITkeeps strict integer parsing, andGAMA_L_MAX_OUTPUT_CHARS/GAMA_L_MAX_CALL_DEPTHreuse the interpreter's defaults and effective call-depth cap. Exceeding a limit raisesIRRuntimeError(aGamaError) with the interpreter's wording and line/col.RunResultexposessteps,expr_stepsandout_charsso counts can be compared directly. Scope difference: the IR counts compiled statement/expression nodes (one per emitted node, in evaluation order); constructs outside its subset are rejected at compile time, so they are never counted - standalone bytecode IR prototype (
gama_ir.py) for a documented subset: statements/expressions,if/else if,while,for, list/map literals and indexing,break/continue,fn,Ok/Err+match,expr?Result propagation inside aResult/Anyfunction, and the core builtinslen/text/number/round; also records/enums, field access and enum variants; plain and aliased imports resolve throughcompile_file(m.fn(...),m.pi,m.Point(1, 2),let p: m.Point,m.Color.Red, payload constructorsm.Option.Some(5)and enum matches with payload bindings work); record copy/withupdates (p with { x = 3 }) compile to a new same-type value (shallow, declaration order) and in-place field assignment (p.x := 3, nestedl.a.x := 5, namespacedgeo.origin.x := 7) mutates the receiver in place with interpreter-matching value evaluation order and runtime errors; generic named functions compile like ordinary functions (type parameters erased); pipe expressions (filter/select/sum/limit/count/map) compile to a singlePIPEinstruction that preserves the interpreter's evaluation order, row scoping and error messages; generic enums compile with type erasure (annotations and payload type arguments are not statically enforced by the IR, but enum identity, variants and payload arity are preserved); record methods compile with a per-nominal method table (receiver bound toself, defaults, method-to-method calls, recursion, nested receiverso.inner.m()and namespacedgeo.Pointmethods) and dispatch only after named functions/enum constructors, matching the interpreter; first-class functions/lambdas, named function values and calls through values compile with closure capture, and host-injected Python callables (run_program/run_file/compile_fileenv=...) dispatch with evaluated args, signature-based arity, wrapped host exceptions and normal call-depth accounting; record inheritance (Child extends Parent) flattens fields/methods and dispatches dynamically; runtime limits (GAMA_L_MAX_STEPS,GAMA_L_MAX_EXPR_STEPS, loop/output/call-depth) match the interpreter; opt-in viagamal2 ir run|compile(see above), the interpreter and defaultrunbackend are unchanged - enums: nullary
enum Color { Red, Green, Blue }and payloadenum Option { Some(Int, Text), None }; values areEnum.Variant(nullary) orVariant(args...)/Enum.Variant(args...)(one or more payloads), annotationlet x: Option = Some(5), nominal equality (cross-enum==is a type error), andmatcharmsEnum.Variant,Variant,Variant(x),Enum.Variant(x),_. Payload printing is deterministic:Enum.Variant(<field-value>)where the payload is rendered like a record field value (text JSON-quoted, decimals plain fixed form,nullfor null) and nullary variants printEnum.Variant. Wrong payload type/arity, unknown or ambiguous constructors, and nullary/payload pattern mismatches raiseGamaTypeError. Generic enums (enum Option<T> { Some(T), None }) are supported: type parameters and nested type refs are valid payload types and annotations (Option<Int>,Option<Option<Int>>) with recursive arity checking; type parameters erase toAnywhile concrete payloads keep their checks, and namespaced generic enums (geo.Option<Int>,geo.Option.Some(5)) work. Limits: a bare constructor must be unique across enums (otherwise qualifyEnum.Variant(...)); nullary variants cannot be called with(); payload variants cannot be used as a bare value; enums remain top-level and distinct from records/functions - nominal record (struct) types:
record Point { x: Int, y: Int }, positional constructionPoint(1, 2), named constructionPoint { x: 3, y: 4 }(local record names only), typed fields, dotted accessp.x(including nestedl.a.x), in-place field assignmentp.x := 3/l.a.x := 5, copy/withupdatesp with { x = 3, y = 4 },let p: Point = ..., and nominal/value equality. Constructors enforce arity, missing/extra/duplicate fields and field types (GamaTypeError); unknown field access is rejected statically when the base type is a record. Assignment resolves the longest dotted variable prefix as the receiver and mutates the record in place (aliases observe the update; namespaced constants such asgeo.origin.x := 7work).p with { ... }type-checks the receiver as a record, rejects unknown/duplicate fields and wrong value types, and returns a new record; the receiver and its aliases are untouched. Unchanged nested record values are shallow-copied (shared), so mutating one through the copy is visible through the receiver, while a replaced field is independent. Wrong field/type, a non-record receiver and an unknown base variable areGamaTypeError. Printing is deterministic: declaration-order fields,Name { x: 1, y: "text" }, decimals with their plain fixed form, and text JSON-quoted. Record destructuring patterns are supported inmatch:Point(x, y)binds the flattened declaration/inheritance field order (inherited fields first),_skips a field, and nested patterns such asOuter(Point(a, b), n)are allowed when the field type is a record. The checker requires a known record name, exact positional arity, nested pattern/field type agreement and unique binding names, and gives each binding its field type in the arm body. Matching is exact-nominal: a child value does not match a parent pattern (it falls through), and_is the usual fallback. The formatter round-trips these patterns; the IR backend rejects them withIRUnsupported. Records are exported through aliased imports (geo.Point) and their type names are namespaced; positional constructiongeo.Point(1, 2)andwithupdates such asgeo.origin with { x = 1 }work, while named construction with a dotted name (geo.Point { ... }) is not accepted by the parser. Gaps: no overloaded/generic methods; inheritance (record Child extends Parent) is supported nominally; static functions are declared asfn Point.origin(...), constructor calls are not first-class values,recordis contextual (not a reserved word), named construction requires a local declaration, the IR backend now supports records/enums for the documented subset (copy/withis supported with the same shallow semantics, and in-place field assignment mutates the receiver with interpreter parity), and LSP has record/enum completion, hover, definition, references and rename - list patterns and match guards:
match xs { [] => ..., [a] => ..., [a, b] => ..., [a, ...rest] => ..., _ => ... }andmatch n { x if x > 0 => ..., _ => ... }. A fixed list pattern requires the exact length;...restmust be last, requires a binding name (or_to skip), matches any longer tail and binds a freshList; nested lists ([[a], b]), literal items (0,"x",true,false,null),_and variable items are supported. The checker requires the subject to beList/Anyfor list arms, rejects duplicate binding names across items, nested lists and the rest binding, and types item bindings asAnyandrestasList(List has no element type parameter); literal matching is type-aware, sotruedoes not match1. Guards are parsed after the pattern, evaluated after all bindings are installed (soif len(rest) > 0works), must beBool, and a false guard falls through to the next arm; a final guard miss raises the normalmatch: no arm matched. A bare lower-case label is a variable pattern binding the whole subject (needed for scalar guards); qualified and upper-case labels keep their enum/record meaning. Record destructuring patterns are unchanged. The formatter round-trips all forms; the IR backend rejects list patterns, guards and variable patterns withIRUnsupported(existing Ok/Err/enum arms are unchanged). - LSP completion/hover for records/enums (fields, variants, declarations) and VS Code grammar/snippets
- reproducible benchmark suite (
scripts/bench.py) for interpreter vs IR and core operations - record methods:
record Point { x: Int; y: Int; fn sum(self: Point) -> Int { return self.x + self.y } }, callp.sum(),selfbound to the receiver, method-to-method calls, defaults, recursion and receiver mutation; per nominal record; methods may use the record's type parameters but cannot declare their own; no overloading; static functions are declared asfn Point.origin(); inheritance is supported nominally; the IR compiles these with interpreter parity (including unchecked unknown-method/arity/non-record errors) - generic lambdas:
fn<T>(x: T) -> T { ... },fn<A, B>(...), erasure, higher-order use; explicit type args supported forlet NAME = fn<...>only - generic type bounds on named functions:
fn show<T: Text>(x: T) -> Text,fn numeric<T: Int>(...); one nominal bound per parameter (base type, record or enum), checked at inferred/explicit calls, erased at runtime - record static functions:
fn Point.origin() -> Point,fn Point.of(x: Int) -> Point, callPoint.origin(); no receiver/self, namespacedgeo.Point.of; value methods unchanged - record inheritance:
record Dog extends Animal { ... }; inherited fields come first, positional construction takes parent then child fields, methods are inherited with exact-signature overrides,Childis a nominal subtype ofParentin let/params/returns/fields, and equality stays nominal. Cycles/unknown parents/duplicate fields are rejected; the IR compiles inheritance with interpreter parity (inherited methods, overrides and dynamic dispatch on the actual value). LSP completion/hover/symbols show inherited members andextends. - structural record compatibility (static checks only): a record value
Sis compatible with a target recordTwhenSdeclares everyTfield with a compatible type; extraSfields are allowed, missing/wrong-type fields are rejected. It applies where nominal compatibility was already used:letannotations, function arguments and returns, record field values (positional and named construction,with, field assignment), method arguments and generic-constructor checks. Field types recurse (nested records compose) and cyclic record graphs are coinductive. Runtime stays nominal: values keep their record type for printing and equality (Point3 == Pointremains a static type error), while field access and method calls use the static target type (aPoint3value held in aPointbinding runsPoint's method). Namespaced records work (geo.Point3used asgeo.Point). Limits: fields only (methods on the value are irrelevant), generic record types are compared after erasure, and there is no coercion or value slicing. The bytecode IR performs no type checking of its own: it accepts programs the checker allows and preserves the same field and static-method dispatch at runtime (so unknown-method/arity errors still surface only at runtime) - AST-based formatter for the current language (comments and multi-line strings preserved; idempotent; records/enums included; deterministic width wrapping at 88 columns by default, configurable via
fmt --width, covering expressions, declarations andfn/control-flow headers) - static type errors and static effect violations
- CLI
run,check,fmt,test,pkg,lsp,ir
Benchmark (reproducible, offline)
scripts/bench.py measures the existing parse/check/run pipeline without
modifying the interpreter, IR prototype, or CLI. It uses only the Python
standard library and never touches the network.
python3 scripts/bench.py --runs 5 --seed 0
python3 scripts/bench.py --runs 5 --json
What is measured (separately, so the numbers add up):
parse_ms— lexer + parser only (gamal2.parse).check_ms— static checker on a pre-parsed AST (parse excluded).run_ms— tree-walking interpreter on a checked AST (parse/check excluded).- IR (only for the documented IR subset) —
compile_msandrun_msare reported separately;speedupis interpreterrunmedian / IRrunmedian. Programs outside the IR subset are listed with the concreteIRUnsupportedreason, not skipped or faked. Where IR runs, its output is compared against the interpreter's output (outputs_match). - Startup/import — inside-child import times for
gamal2andgamal2+gama_ir, plus cold Python process startup. - Memory —
resource.ru_maxrssfrom a fresh child after importing both modules (process high-water mark, not per-program memory; unavailable on platforms withoutresource). - Formatter — end-to-end
gamal2.fmt_sourceon a mixed sample.
Methodology: --runs N timed samples per measurement and --warmup W untimed
iterations; median/min/max (and raw samples in JSON). Program execution order is
shuffled with --seed (results are sorted by name); the same seed/arguments
produce the same structure and IR outputs. JSON includes schema, tool,
runs, warmup, seed, one entry per program, and explicit notes.
Limits (do not overclaim): the programs are small and are not a representative workload or a general language-performance claim; timings are single-machine micro-benchmarks and include CPython overhead; the IR is an opt-in prototype, not the default backend; peak RSS is a whole-process high-water mark; and a faster number here is not a conformance or correctness result. Use more runs and a quiet machine for comparisons, and read medians rather than single samples.
Tests
python3 -m unittest discover -s tests -v # 3470/3470
python3 gamal2.py doctor --json # 8/8 checks
../../scripts/ci_local.sh # py_compile + tests + doctor + VS Code JSON/smoke
Conformance coverage: arithmetic/pipes (20 cases), type errors, effect
violations (including sql.write/mail stubs), HITL returns Denied,
proof/tamper, duplicate proof keys, optional HMAC proof signatures (GAMA_L_PROOF_KEY; GAMA_L_REQUIRE_SIG=1 requires them),
imports/cycles/diamonds, multi-table SQLite schema checks, git/path package
dependencies, LSP completion/hover/definition/references/symbols/tokens/code actions/
rename/formatting/didChange, SPEC syntax aliases/defaults, loops/indexing,
records (construction/access/naming/equality/namespaces), Result/match, VS Code LSP client,
HTTP/local registry, AI connector, guarded SQL writes and SMTP mail, offline
ask = Unavailable, 11 examples, canonical fmt check/write, doctor, and
reproducible release metadata.
Conformance: see CONFORMANCE.md (SPEC 2.0 T1–T10: PASS 10/10, including optional cross-run proof ledger).
Not implemented yet
- deployed/public package registry (the HTTP server is implemented and testable)
- external intent/lease registry with expiry/revocation (env-bound intent/lease receipts are implemented; there is no external registry)
- full expression-level/bidirectional type inference (return/effect inference and structural compatibility are implemented); higher-kinded bounds
- formatter remaining edges: a comment before a binary operator has no legal source position (the grammar only breaks after an operator), so the defensive renderer keeps it after the operator; comments in a commented
fneffects set use source order (uncommented sets stay sorted); indivisible tokens longer than the width still overflow - VS Code Marketplace signing/publishing (VSIX packaging is reproducible locally)
- publishing a built release by default: the release workflow has secret-gated, opt-in PyPI/Marketplace steps, but tag pushes only build and upload artifacts
The old v0.1 transpiler remains in gama-lang/ for reference. Do not call this
full 2.0 until the remaining conformance tests and connectors are done.
Metadata
Release files for gama-l 2.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| gama_l-2.0.0.tar.gz | 486.3 kB | Without provenance | Details for gama_l-2.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| gama_l-2.0.0-py3-none-any.whl | Python 3 | none | any | Details for gama_l-2.0.0-py3-none-any.whl |
Total release size: 711.8 kB
Release files / gama_l-2.0.0.tar.gz
| Download URL | gama_l-2.0.0.tar.gz |
|---|---|
| Size | 486.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b46d0c702016e85f0fd976c9a1d8b658bd783eb5cd03dc5a87959d4b6737c308
|
|
BLAKE2b-256 checksum How to use checksums |
ce9b0554449b526ae9e3ccc91efc346713e4dd17b5af3d2b19b366bba392c898
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Release files / gama_l-2.0.0-py3-none-any.whl
| Download URL | gama_l-2.0.0-py3-none-any.whl |
|---|---|
| Size | 225.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
532e6f50c8d3d8f18d17bc26fd6427ee705e9fb583ba70d142bc7dc675122c69
|
|
BLAKE2b-256 checksum How to use checksums |
05214d101751e025105d7b76efc096e031c18d3eeb79c0b59bd1143a3fc4597c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Security for gama-l 2.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.