{"slug": "show-hn-e-a-language-you-dial-between-english-and-python", "title": "Show HN: E– – a language you dial between English and Python", "summary": "A developer released E-- (English--), a programming language written in canonical English that compiles deterministically to Python, separating LLM-based code generation from runtime execution to ensure reproducibility and debuggability. The tool is available under Apache License 2.0 and can be installed via PyPI.", "body_md": "A programming language you write in plain, canonical English — and that compiles\n\ndeterministicallyto Python.\n\nE-- (\"English--\") is English with the ambiguity removed: a closed grammar and a fixed vocabulary, with exactly one canonical phrasing per construct. It is meant to read and edit like English while still compiling to ordinary, reproducible Python.\n\nLLM-generated code is fuzzy at runtime: non-reproducible, expensive per call, hard to debug. E-- separates the LLM's role from execution — LLM (optionally) writes canonical E-- at authoring time; a deterministic parser compiles the E-- to Python; runtime is pure. Best of both worlds: LLM creativity when you need it, deterministic behavior forever after.\n\nInstall from PyPI:\n\n```\npip install e-minus-minus\n```\n\nTranspile a canonical E-- file to Python:\n\n```\nemm-transpile examples/describe.emm\n```\n\nThat's it. No LLM required for canonical E-- with no `{{ }}`\n\nslots. See\n\"Running E--\" below for more, and \"Resolving `{{ }}`\n\nslots\" for the LLM setup\nwhen you use free-English input or value slots. The LLM path is optional and\nlives behind the `[llm]`\n\nextra: `pip install \"e-minus-minus[llm]\"`\n\n.\n\nDeveloping on E-- itself? Clone the repo and invoke the CLI as a module while\nyour working tree is on `PYTHONPATH`\n\n:\n\n```\nPYTHONPATH=src python -m e_minus_minus.transpiler examples/describe.emm\n```\n\nE-- is licensed under **Apache License 2.0** (see [ LICENSE](/frmoded/e--/blob/main/LICENSE)) —\npermissive, with an explicit patent grant, so it can be embedded in commercial\nproducts freely.\n\nTwo clarifications:\n\n**The license covers the E-- tooling.** The Python that E-- generates is yours — the output is not encumbered by this project's license.**The LLM is your own.** E--'s normalizer and`{{ }}`\n\nresolution require a language model that you supply; that provider's terms are separate from this project.\n\nProgrammatic API:\n\n``` python\nfrom e_minus_minus import transpile\n\npython_source = transpile(emm_source)\n```\n\n`transpile()`\n\nis pure: no network, no side effects. Pass a `resolve_slot`\n\ncallable to handle `{{ ... }}`\n\nslots (see [ docs/spec.md](/frmoded/e--/blob/main/docs/spec.md) and\nthe CLI implementation in\n\n`src/transpiler.py`\n\nfor the injected-resolver\npattern).E-- is a two-stage pipeline, split so that the unreliable part and the deterministic part never mix:\n\n``` php\nFree English  --LLM (transpile-time)-->  Canonical E--  --plain parser-->  Python\n```\n\n**Normalizer (LLM, optional).** Turns free-form English into canonical E--. This is the only stage that deals with linguistic ambiguity.**Compiler (deterministic).** Turns canonical E-- into Python with an ordinary parser — no LLM, fully reproducible and debuggable.\n\n**The LLM runs only at transpile time, never at runtime.** Generated Python is\nalways pure and self-contained. The LLM is never allowed to decide program\nstructure; it is used only to fill clearly-delimited value slots written as\n`{{ ... }}`\n\n, and those resolutions are cached so builds stay reproducible.\n\nCanonical E--:\n\n```\nSet result to [[fibonacci]]( {{the first prime number greater than 5}} ).\nDo [[print]](result).\n```\n\ncompiles to:\n\n```\nresult = fibonacci(7)\nprint(result)\n```\n\nMarkers keep it unambiguous: `[[name]]`\n\nis a function call, a bare word is a\nvariable, `\"x\"`\n\n/`3`\n\nare literals, `<1, 2, 3>`\n\nis a list, and `{{ ... }}`\n\nis an\nEnglish phrase the transpiler resolves once and bakes in.\n\nE-- source files use the ** .emm** extension (English--). The deterministic\ncanonical-to-Python core is implemented; you can transpile and run\n\n`.emm`\n\nfiles\nfrom the command line.Given this canonical E-- source at `examples/describe.emm`\n\n:\n\n```\nDefine [[describe]] taking n:\n    If n is greater than 10:\n        Give back \"big\".\n    Give back \"small\".\n\nFor each n in <3, 42, 7>:\n    Do [[print]]([[describe]](n)).\n```\n\ntranspile it and print the generated Python to your screen:\n\n```\npython3 src/transpiler.py examples/describe.emm\n```\n\nprints:\n\n``` python\ndef describe(n):\n    if n > 10:\n        return \"big\"\n    return \"small\"\nfor n in [3, 42, 7]:\n    print(describe(n))\n```\n\nWrite the generated Python to a file instead of the screen:\n\n```\npython3 src/transpiler.py examples/describe.emm -o out.py\n```\n\nTranspile **and run** it, so you see the program's actual output:\n\n```\npython3 src/transpiler.py examples/describe.emm --run\n```\n\nprints:\n\n```\nsmall\nbig\nsmall\n```\n\nSee the generated Python **and** run it in one go with `--show`\n\n(alias `-s`\n\n):\n\n```\npython3 src/transpiler.py examples/describe.emm --run --show\n```\n\nprints the code and its output, separated by comment lines:\n\n``` python\n# --- generated Python ---\ndef describe(n):\n    if n > 10:\n        return \"big\"\n    return \"small\"\nfor n in [3, 42, 7]:\n    print(describe(n))\n# --- output ---\nsmall\nbig\nsmall\n```\n\nThe delimiters are Python comments, so the whole block stays copy-pasteable.\n`--show`\n\non its own (without `--run`\n\n) just prints the Python, like the default.\n\nNotes:\n\n- The\n`.emm`\n\nextension is the convention for E-- source files. `{{ ... }}`\n\nLLM value slots**are** runnable — see \"Resolving`{{ }}`\n\nslots\" below for the one-time setup. Files with no slots (like`examples/describe.emm`\n\n) need no key and`--run`\n\nworks with no model.\n\nA `{{ ... }}`\n\nslot is an English phrase that the transpiler resolves to a Python\nexpression **once, at transpile time**, using an LLM — then caches the result so\nlater builds are offline and reproducible. Files with **no** `{{ }}`\n\nslots need\nno API key and no setup.\n\nTo run a slot example end to end:\n\n```\n# 1. create and activate a virtual env\npython3 -m venv .venv && source .venv/bin/activate\n\n# 2. install dependencies (the Anthropic SDK)\npip install -r requirements.txt\n\n# 3. set your Anthropic API key\nexport ANTHROPIC_API_KEY=\"sk-ant-...\"\n\n# 4. transpile and run a slot example\npython3 src/transpiler.py examples/primes.emm --show --run\n```\n\n`examples/primes.emm`\n\nis minimal:\n\n```\nFor each p in {{the first five prime numbers, as a Python list}}:\n    Do [[print]](p).\n```\n\nwhich transpiles to:\n\n```\nfor p in [2, 3, 5, 7, 11]:\n    print(p)\n```\n\nand prints `2 3 5 7 11`\n\n(one per line). The `{{ ... }}`\n\nslot resolved once via\nthe LLM to the concrete list `[2, 3, 5, 7, 11]`\n\n, was cached, and gets used for\nevery subsequent build with no API call.\n\nThe first run calls the model (Anthropic Haiku) to resolve each slot, writes the\nresolved Python expression to ** .emm_cache.json**, and bakes it into the\noutput. Every later run is an\n\n**offline cache hit**— no model call, identical result. The cache file maps the exact slot text to its resolved expression and is meant to be\n\n**committed**, so resolved values stay diffable and reviewable.\n\nEditing a slot's text is a cache miss and re-resolves; deleting the cache forces\nfull re-resolution. Files without `{{ }}`\n\nslots (like `examples/describe.emm`\n\n)\nnever touch the API.\n\nYou can also build a slot example inline without any file setup:\n\n```\nprintf 'Set year to {{the current year, as an integer literal}}.\\nDo [[print]](year).\\n' > hello.emm\nemm-transpile hello.emm --show --run\n```\n\nThe slot at line 1 is at **expression** position (inside `Set year to ...`\n\n), so\nthe LLM returns a single Python expression — e.g. `2026`\n\n— and the compiler\nsplices it in:\n\n```\nyear = 2026\nprint(year)\n```\n\nPrints `2026`\n\n. Second run is offline (cache hit).\n\nA `{{ ... }}`\n\nslot can appear at a **statement** position, not just an\nexpression position — putting it on its own line, at a block's indentation,\ndelegates one or more Python statements to the LLM. Author writes the\nsurrounding structure; the LLM fills the region.\n\n```\nDefine [[summarize]] taking numbers:\n    {{ compute mean, median and count of numbers into mean_v median_v count_v }}\n    Do [[print]](count_v).\n    Do [[print]](mean_v).\n    Give back mean_v.\n```\n\nAt transpile time the statement slot resolves to real Python:\n\n``` python\ndef summarize(numbers):\n    from statistics import mean, median\n    mean_v = mean(numbers)\n    median_v = median(numbers)\n    count_v = len(numbers)\n    print(count_v)\n    print(mean_v)\n    return mean_v\n```\n\n**Trade-off**: `[[wikilinks]]`\n\nor callable references inside a code-slot's\nresolved Python are **opaque** to any downstream tool that inspects the E--\nsource. Author knowingly accepts DAG invisibility inside code-slot regions in\nexchange for region-level delegation. Use expression slots when you need\ngraph visibility; use code slots when you're delegating structure the LLM\nknows better than you do.\n\n`examples/code_slot_example.emm`\n\nis a runnable code-slot demo:\n\n```\nDefine [[summarize]] taking numbers:\n    {{ compute the mean, median and count of numbers into named variables mean_v median_v and count_v }}\n    Do [[print]](count_v).\n    Do [[print]](mean_v).\n    Do [[print]](median_v).\n    Give back mean_v.\n\nSet data to <2, 3, 5, 7, 11, 13>.\nDo [[summarize]](data).\n```\n\nTranspile and run it:\n\n```\nemm-transpile examples/code_slot_example.emm --show --run\n```\n\nThe statement slot on line 2 resolves to real Python that binds `mean_v`\n\n,\n`median_v`\n\n, `count_v`\n\n— e.g. via `from statistics import mean, median`\n\nplus\nthree assignments. Because it's a code slot, it can add the `import`\n\non its\nown line, which a value slot cannot.\n\n**Code slots let you delegate imports.** A value slot at expression position\ncan only emit a single Python expression, so getting the current year with a\nvalue slot produces the awkward\n`__import__('datetime').datetime.now().year`\n\n. A code slot at statement\nposition sidesteps the constraint:\n\n``` python\nprintf '{{ import datetime and set x to the current year }}\\nDo [[print]](x).\\n' > hello.emm\nemm-transpile hello.emm --show --run\n```\n\nThe LLM resolves the statement slot to:\n\n``` python\nimport datetime\nx = datetime.datetime.now().year\nprint(x)\n```\n\nPrints the current year. Same syntax as a value slot; position determines shape.\n\nYou don't have to write canonical E-- by hand. The transpiler's first phase\n**normalizes** free-English E-- into canonical E-- with an LLM, then compiles\nthe canonical form to Python — one input, two outputs. An English source\n(`examples/describe_en.en`\n\n) reads like prose:\n\n```\nDefine a function called describe that takes a number n. If n is greater than\nten, give back the string \"big\". Otherwise, give back the string \"small\".\nThen, for each n in the list 3, 42 and 7, print describe of n.\n```\n\nNormalize it to canonical and run the result, saving the canonical form too:\n\n```\npython3 src/transpiler.py examples/describe_en.en --canonical-out out.em --run\n```\n\n`out.em`\n\nholds the canonical E-- (equivalent to `examples/describe.emm`\n\n) and the\nprogram prints `small / big / small`\n\n.\n\nTwo properties make this safe and cheap:\n\n**The parser is the canonical-detector.** Whether a file \"is already canonical\" is decided by trying to parse it deterministically — no LLM, no heuristic. An**already-canonical file needs no API key**: normalization short-circuits before any model call. Only genuinely English input hits the model.** Fixed point + cache.**Feeding the canonical output (`out.em`\n\n) back in parses as canonical, so Phase 1 does nothing and reproduces the same outputs. Normalizations are cached in a committed`.emm_norm_cache.json`\n\n(keyed by source text), so re-running English input is an offline cache hit. Setup is the same as for slots:`pip install -r requirements.txt`\n\nand`export ANTHROPIC_API_KEY=...`\n\n.\n\nNormalization and `{{ }}`\n\nslot resolution are independent, separately cached LLM\ntouchpoints — a canonical file with all slots cached makes **zero** live calls.\n\nEarly design. The language is specified in [ docs/spec.md](/frmoded/e--/blob/main/docs/spec.md). The\ndeterministic canonical-to-Python core (lexer, parser, emitter) is implemented\nwith a runnable CLI — see \"Running E--\" above — and\n\n`{{ }}`\n\nslot resolution is\nwired up (Anthropic Haiku + a committed cache; see \"Resolving `{{ }}`\n\nslots\").\nThe LLM normalizer (free English → canonical) is wired up at whole-file\ngranularity (see \"Writing in free English\"); per-region normalization is the\nnext refinement.— the language specification (source of truth).`docs/spec.md`\n\n/`docs/cowork-protocol.md`\n\n— internal development workflow.`docs/cc-prompt-queue.md`", "url": "https://wpnews.pro/news/show-hn-e-a-language-you-dial-between-english-and-python", "canonical_source": "https://github.com/frmoded/e--", "published_at": "2026-07-10 19:08:13+00:00", "updated_at": "2026-07-10 19:35:37.781692+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence", "large-language-models"], "entities": ["E--", "Python", "Apache License 2.0", "PyPI"], "alternates": {"html": "https://wpnews.pro/news/show-hn-e-a-language-you-dial-between-english-and-python", "markdown": "https://wpnews.pro/news/show-hn-e-a-language-you-dial-between-english-and-python.md", "text": "https://wpnews.pro/news/show-hn-e-a-language-you-dial-between-english-and-python.txt", "jsonld": "https://wpnews.pro/news/show-hn-e-a-language-you-dial-between-english-and-python.jsonld"}}