The Ohm language#

Ohm is Lua 5.4 with LaTeX math mixed in. $…$ holds one expression, $$…$$ holds statements, and everything outside them is ordinary Lua.

local tab = browser.open("https://example.com", { hidden = true })
print($\frac{22}{7}$)                       -- inline math is an expression

$$
  n := 10                                   % display math holds statements
  \sum_{i=1}^{n} i^2                        % a bare expression is shown
  y := \Summarize_{\Psi} \circ \Compress_{\Gamma}(tab; chars=4000)
  \while_{n > 0}{ n = n - 1 }               % bodies are Ohm again
$$

Scripts compile to Lua line for line, so an error at .ohm line 12 is reported at line 12 — click it in the console and the editor jumps there.

Where `$` is and is not math#

A $ inside a Lua string or comment is literal:

local price = "$5"     -- fine
-- $x$                 -- fine
[[ $z$ ]]              -- fine

Inside math, \$, \{ and \} are escapes, % starts a comment to end of line, and a { … } body is Ohm again — so you can nest ordinary Lua and further $…$ inside it.

Inline math cannot span a blank line; if you forget the closing $, the error points at the opening one.

Values and operators#

You write You get
\frac{a}{b} a / b
\sqrt{x}, \sqrt[3]{x} square / cube root
\lfloor x \rfloor, \lceil x \rceil floor, ceil
|x| length of a string or list, absolute value of a number
a \cdot b, a \times b, a \div b multiplication, division
2x, 3(x+1), \pi r^2 implicit multiplication
a \leq b, a \neq b, a \geq b comparisons
a \land b, a \lor b, \neg a and, or, not
x \in S, x \notin S membership in a list or substring in a string
\{1, 2, 3\} a list
\{1, \dots, 10\} a range
x \mapsto x^2 a lambda; (a, b) \mapsto a + b for several parameters
\text{hello} a string
\mathrm{my_var} an identifier containing _
\pi, \infty, \top, \bot, \nil, \emptyset constants

Greek letters are plain identifiers: \alpha, \beta, \theta…

Precedence runs the way you would read it: \parallel loosest, then \circ, \lor, \land, comparisons, ranges, + -, then * / and juxtaposition. Unary minus binds a power, so -x^2 is -(x^2); \neg binds looser than comparison, so \neg x = 1 is not (x == 1).

Two things to know:

  • ^ does not chain. Write a^{b^c}, not a^b^c.
  • _ is the subscript operator, so identifiers cannot contain _. x_i means x[i]. Use \mathrm{snake_case} when you need an underscore in a name.

Statements#

$$
  x := 1                              % assignment (= also works at statement level)
  \if_{x > 0}{ print("positive") }\else{ print("not") }
  \while_{x < 10}{ x = x + 1 }
  \for_{i=1}^{5}{ print(i) }
  \forall t \in tabs { print(t.title()) }
  \forall k, v \in table { print(k, v) }
  \return x
$$

A bare expression on its own line is displayed; a bare call is not. So 1 + 1 prints 2, while print("hi") just runs.

Big operators are expressions:

$$
  total := \sum_{i=1}^{n} i^2
  best  := \max_{x \in scores} x
  prod  := \prod_{i=1}^{4} i
$$

Mapping over a collection:

$ \forall x \in \{1, 2, 3\} : x^2 $     -- {1, 4, 9}

Matrices#

A list is a matrix, and the ordinary arithmetic operators work on it.

$$
  urls := \{
    "https://a.example",
    "https://b.example",
    "https://c.example"
  \}
  T := \Tab \times urls          % one tab per url, opened all at once
$$

\Tab \times urls opens the tabs hidden — a matrix of ten urls should not take over the window. Use \Tabs_{\Gamma}(urls; hidden=false) when you want to watch, and \Close_{\Gamma}(T) when you are done.

\times is overloaded the way + is:

You write You get
6 \times 7 42 — numbers still multiply
\Tab \times urls a tab per url
M \times f f applied to every item, concurrently — each call is its own task, and the results keep the input order
A \times B every pair from two lists: the Cartesian product, the same as \Cross_{\Sigma}(A, B)
3 \times M the list repeated three times
local titles = $T \times (t \mapsto t.title())$
local notes  = $T \times (t \mapsto \Summarize_{\Psi}(t; words=40))$

Sending with `<-`#

destination <- value sends the right side into the left:

local answers = $T <- \text{What changed in browsers since 2020?}$   -- ask every tab
local more    = $answers <- \text{one more}$                          -- append to a list

For a tab, <- types the prompt into the page's main input, presses Enter and waits for the reply to settle — which is how you drive three chat sites at once. It is a heuristic (it picks the largest visible text box), so check the site if a reply comes back empty. A list of prompts is cycled across the tabs, one each.

\Chat_{\Lambda}(tab; prompt) is the same thing with a name, and takes timeout, step and settle if a site is slow.

Rearranging#

All \Sigma, all pure:

Operator Does
\Shuffle_{\Sigma}(M; seed) random order; a seed repeats it
\Rotate_{\Sigma}(M; by=1) rotate
\Reverse_{\Sigma}(M) reverse
\Transpose_{\Sigma}(M) swap rows and columns
\Zip_{\Sigma}(A, B) interleave into rows
\Chunk_{\Sigma}(M; size=2) split into rows of n
\Pick_{\Sigma}(M; n=1, seed) random sample
\Flatten_{\Sigma}(M), \Unique_{\Sigma}(M), \Sort_{\Sigma}(M; by) the obvious
\Map_{\Sigma}(M; f) the same as M \times f

So "ask three sites, then judge the answers without knowing which came from where" is:

local answers = $T <- question$
local blind   = $\Shuffle_{\Sigma}(answers)$
local best    = $\Debate_{\Sigma}(blind; question=question)$

Coding a corpus#

Grounded-theory operators, for turning a pile of text into codes:

$$
  codes := \OpenCode_{\Psi}(corpus; k=8)
  axial := \AxialCode_{\Psi}(codes)
  core  := \SelectiveCode_{\Psi}(axial; question="what makes onboarding fail?")
$$
print($\Codebook_{\Psi}(codes)$)

\OpenCode returns codes with definitions and verbatim quotes; \AxialCode groups them into categories with the relations between them; \SelectiveCode picks the core category and writes the storyline.

AI operators#

49 built-in operators live in 5 families. Write \Name_{\Family}(input; config). Every one is documented in the operator reference, and in the editor as you type.

Family Operators
\Psi reasoning Ask CoT ToT GoT SelfAsk Plan Reflect Critique Summarize OpenCode AxialCode SelectiveCode Codebook
\Gamma context Retrieve Search Fetch Rank Filter Compress MergeContext Ground Cite Tabs Close
\mathcal{M} memory MemRead MemWrite MemAppend MemSearch MemSummarize MemForget
\Sigma aggregation Join Vote Debate Map Shuffle Rotate Reverse Transpose Zip Chunk Pick Cross Flatten Unique Sort
\Lambda framework / agent ReAct Verify Fork Chat
$$
  hits    := \Search_{\Gamma}(question; k=5)
  context := \Retrieve_{\Gamma}(question; hits, k=6)
  answer  := \CoT_{\Psi}(question; context)
  final   := \Cite_{\Gamma}(answer; context)
$$

Config can be positional or named — \CoT(q; C) and \CoT(q; context=C) are the same.

Composition with \circ and concurrency with \parallel:

$ \Summarize_{\Psi} \circ \Compress_{\Gamma}(tab; chars=4000) $
$ a \parallel b \parallel c $      -- runs all three at once, returns the results

Define your own:

ohm.defop("Psi", "Shorten", function(text, cfg)
  return ai.generate("Shorten to " .. (cfg.words or 20) .. " words:\n" .. text)
end)
print($\Shorten_{\Psi}(longText; words=10)$)

Memory, context and parameters#

  • \mathcal{M} is a table that persists between runs of the same script: $$ \mathcal{M}["seen"] := (\mathcal{M}["seen"] or 0) + 1 $$
  • \Gamma is ctx — { script, project, args, tab }.
  • \Theta is params — the default model profile for this run.

Concurrency#

Host calls look synchronous but are not: the runtime schedules coroutines and resumes them as results arrive.

local results = ohm.all(
  function() return $\CoT_{\Psi}(q1)$ end,
  function() return $\CoT_{\Psi}(q2)$ end
)

ohm.spawn / ohm.join give finer control, and \parallel compiles to the same machinery. If every task ends up waiting on another, you get an error rather than a hang.

The host API#

Object Functions
browser open(url, opts), tabs(), active(), get(id)
a tab go text html info url title eval query queryAll click type press waitFor scroll screenshot show hide activate close
ai generate(prompt, opts), json(prompt, opts), profiles(), saveProfile(), useProfile()
fs read write list exists — inside the scripts folder only
project use list info links addLink docs readDoc writeDoc data readData
prompts list render run
curves list get eval sample
experiments list run
specs list get save markdown
models dir list search files download
ohm spawn join all sleep defop inspect text show display

Both tab.text() and tab:text() work everywhere.

Full details: Script API.

Sandbox#

Scripts cannot use io, require, dofile or os.execute, and fs is confined to the scripts folder. They can open tabs, drive pages over CDP, call models and read and write project files.

Treat scripts as code you trust, the same as anything else you run on your machine: an imported script can use your configured model profiles, and ohm.host reaches the host API directly.

Errors#

Compile errors appear in the console as name:line:column: message and highlight in the editor. Runtime errors carry the .ohm line.

Two mistakes worth naming because the compiler cannot warn about them yet:

  • a ~= b does not mean "not equal". ~ is treated as LaTeX spacing, so it compiles to a == b. Write \neq or !=.
  • % is a comment in math, not modulo. Write \bmod.

See the language spec for the full grammar, the generated code for each construct, and the known gaps.