Skip to content

Embedding API (Python)

ragul/embed.py lets you call Ragul directly from Python — no subprocess, no shelling out to the ragul CLI, no parsing stdout. This is the entry point for:

  • A Python REPL or notebook — run Ragul snippets cell-by-cell and get the resulting values back as plain Python objects.
  • A library dependency — another Python project can embed a Ragul pipeline as one step in a larger program.
  • Tooling — anything that wants Ragul's diagnostics as structured Python objects (RagulDiagnostic) instead of formatted CLI text.

Status

The embedding API is new and unreleased (see CHANGELOG.md, [Unreleased]). It ships as ragul.run, ragul.eval, and ragul.RagulError, re-exported from the top-level ragul package.

import ragul

ragul.eval(expr)

Evaluate a single Ragul expression/pipeline and return its value as a native Python object.

import ragul

result = ragul.eval("3-4-add")
print(result)   # 7

Under the hood this is sugar for ragul.run(): it wraps expr as a single assignment sentence (result-into {expr}-it.), runs it, and returns the result binding. Anything that is legal on the right-hand side of -into / -be works here — arithmetic chains, string suffixes, list pipelines, and so on:

ragul.eval("10-3-add-2-mul")   # -> (10 + 3) * 2 = 26

ragul.run(source, filename="<embed>")

Run a Ragul program and get back its final top-level bindings as a plain Python dict[str, Any].

import ragul

source = (
    'sum-into  10-3-add-it.\n'
    'product-into  sum-2-mul-it.\n'
)
bindings = ragul.run(source)
print(bindings)   # {'sum': 13, 'product': 26}

filename is only used to label diagnostics (compile errors will report this string as the "file"). Pass something meaningful — e.g. a notebook cell id — if you're embedding Ragul in a tool that runs many separate snippets.

ragul.run() follows the exact same Lex → Parse → Interpret pipeline as the ragul futtat / ragul run CLI command — it does not run the type checker. That has two consequences worth knowing:

  • Static checks that only exist in the type checker (E001, E003, E004, E005, E006, E007, E009, W001 — see Error Codes) are not enforced by ragul.run() / ragul.eval(). If you want those checks, run ragul ellenőriz / ragul check on the source separately (via the CLI, or by driving ragul.pipeline.orchestrator.CompilerPipeline directly with the "ellenőriz" command).
  • Because effect-boundary checking (E004) is one of those type-checker-only rules, effect suffixes such as -print-doing / -nyomtat-va do execute immediately even in a bare top-level sentence passed to ragul.run() — there is no static gate stopping them the way there would be if you ran ragul ellenőriz on the same source.

Known limitation: bare top-level sentences only

ragul.run() only captures bindings made by bare top-level sentences — the same shape used above, with no scope wrapper and no leading tab. This is not how a normal .ragul file is written; ordinary programs wrap their body in a named effect scope:

program-ours-effect
    sum-into  10-3-add-it.

Do not do this with ragul.run(). If you wrap the source in program-ours-effect / -nk-hatás, the assignments happen inside that child scope's own environment, which is never merged back into the top-level bindings run() returns — you get back {} instead of an error. This is a pre-existing interpreter environment-scoping limitation, tracked separately from the embedding API itself (see CHANGELOG.md, [Unreleased]).


Errors — ragul.RagulError

Both run() and eval() raise ragul.RagulError — never a bare exit code — on a compile-time (lexer/parser) failure. This is deliberate: embedded callers want exceptions, not shell exit code conventions.

import ragul

try:
    ragul.run("broken-t-va.\n")   # case suffix (-t) before action suffix (-va): E002
except ragul.RagulError as e:
    print(e)                 # formatted diagnostic text, same as the CLI shows
    for d in e.diagnostics:  # list[RagulDiagnostic]
        print(d.code, d.file, d.line, d.message)
  • e.diagnostics is the list of RagulDiagnostic objects (see ragul/errors.py) produced by whichever phase failed — the same objects the CLI prints.
  • str(e) joins each diagnostic's .format() output with newlines, so printing the exception directly gives you CLI-quality error text.

Printing diagnostics on Windows

A formatted RagulDiagnostic can contain non-ASCII characters — for example the suggestion line is prefixed with → (U+2192 RIGHTWARDS ARROW). A plain Windows terminal defaults to a legacy codepage (e.g. cp1252) that can't encode that character, so print(e) / print(d) can raise UnicodeEncodeError and crash your script. ragul/main.py works around this for the CLI by reconfiguring stdout/stderr to UTF-8 at startup:

import sys
if hasattr(sys.stdout, "reconfigure"):
    sys.stdout.reconfigure(encoding="utf-8", errors="replace")
if hasattr(sys.stderr, "reconfigure"):
    sys.stderr.reconfigure(encoding="utf-8", errors="replace")

Do the same near the top of any script that prints a RagulError / RagulDiagnostic directly — see examples/embedding/quickstart.py, which needs exactly this to run on a default Windows console.

Runtime errors carry no diagnostics

Because ragul.run() / ragul.eval() skip the type checker, most problems that would be a clean, structured diagnostic under ragul ellenőriz instead surface at runtime — e.g. an undefined root, or a suffix applied to a value of the wrong type. The interpreter itself catches those, prints a message to stderr, and returns a non-zero exit code — it does not add anything to the diagnostics list. embed.run() still raises ragul.RagulError in that case (since the underlying TaskResult.ok is False), but e.diagnostics will be an empty list and str(e) will be an empty string. If you need the actual message, check stderr, or treat "a RagulError with no diagnostics" as "a runtime failure" in your own error handling.


When to reach for this vs. the CLI

Use case Recommended
Running a .ragul project file, want console I/O and exit codes ragul futtat / ragul run (CLI)
Calling a small Ragul pipeline from a Python script or notebook, want the result as a Python value ragul.eval() / ragul.run()
Embedding Ragul as one step of a larger Python program ragul.run()
Type-checking source without running it, from Python not yet exposed as a dedicated function — use the CLI (ragul ellenőriz) or drive ragul.pipeline.orchestrator.CompilerPipeline().run("ellenőriz", ...) directly

See also examples/embedding/quickstart.py for a runnable script covering both functions, the bare-top-level-sentences limitation, and the error path.