Optimising for Natural Language Document Generation with @nlg

L4 can render your rules back into formatted English prose — in the VS Code Render tab, or with l4 render. The renderer is deterministic: it walks your code and turns each construct into a sentence or an outline. That means the quality of the generated prose is mostly in your hands. Well-named, well-shaped rules read almost like professionally drafted legal writing with no extra effort; awkward ones read like a transcript of an algorithm.

This tutorial shows how to get transparent English prose out of the renderer, using three increasingly powerful levers:

  1. Names — backticked identifiers and parameter names.
  2. Shape — mixfix word order, control flow, section titles, and arithmetic.
  3. @nlg — an authored sentence that overrides the structural rendering.

It finishes with how Legalese AI can then apply drafting policies to refine the result further.

Prerequisites


Lever 1 — Names do most of the work

The renderer prints identifiers and parameter names verbatim. Good names are the single highest-leverage thing you can do.

Name rules as the phrase you want to read

A backticked identifier can contain spaces, so name a rule as the noun phrase or clause it represents:

-- Renders: "Monthly property tax means ..."
`monthly property tax` MEANS ...

-- Renders: "Mpt means ..."
mpt MEANS ...

[!NOTE] Beginner programmers are routinely and pointedly reminded to use the first form whenever they reach for the latter.

Name parameters as nouns, not letters

Parameters appear in the rendered prose and in every @nlg slot. Name them the way they should read:

-- Renders: "... the buyer ... the seller ..."
GIVEN `the buyer` IS A Person
      `the seller` IS A Person

-- Renders: "... p ... q ..."
GIVEN p IS A Person
      q IS A Person

Sometimes existing legal writing will deliberately adopt this form: "A person (A) discriminates against another (B) if ..." (Equality Act 2010, s.13). In that situation the renderer will aim to obey the "legislative variable" style.

If a parameter has a record type, the renderer promotes the type name into a noun phrase: GIVEN claim IS A `Payment Claim` renders as "the payment claim". This works cleanly only when one parameter has that type — two Payment Claim parameters would collide on the same phrase.

Name record fields readably

Projections render as X's field, so field names carry straight into the prose:

DECLARE `Property Details` HAS
    `market value`         IS A NUMBER
    `monthly property tax` IS A NUMBER
-- "the property's market value", "the property's monthly property tax"

Lever 2 — Shape the code so it reads in order

Use mixfix so calls read as sentences

Put the words and the argument holes where they belong in the sentence. (See the mixfix tutorial for the full mechanics.)

GIVEN `the applicant` IS A Person
      `the programme` IS A Programme
      `application date` IS A DATE
GIVETH A BOOLEAN
DECIDE `as at an` `application date` `the applicant` `is eligible for` `the programme` IF ...

In a conventional programming language, this would be a function taking three arguments: eligibility(applicant, programme, date).

In L4, it is also a function taking three arguments, but the arguments are intermingled across the function name for the sake of readability.

End helper names in a preposition

A function whose name ends in a preposition (of, for, to, between, …) gets its arguments joined naturally, without a stray "with":

`the later of` x y MEANS IF x >= y THEN x ELSE y
-- "the later of the start date and the end date"

Let control flow stay structured

CONSIDER, IF/THEN/ELSE, and AND/OR render as indented outlines, not run-on sentences. Keep operative logic where the renderer can see it as structure rather than burying it inside an unrelated expression:

CONSIDER claim's status
WHEN Paid    THEN ...
WHEN Overdue THEN ...

renders as

depending on the claim's status:
- if it is Paid: ...
- if it is Overdue: ...

Keep arithmetic as arithmetic

Numeric expressions render in formula mode — with + − × ÷ and parentheses — not as nested "the sum of the product of …". Write the maths directly instead of wrapping it in prose helpers:

(`base rent` PLUS `service charge`) TIMES `months` PLUS `deposit`
-- "(base rent + service charge) × months + deposit"

Group rules into titled sections

Section markers organise a document into titled, numbered sections — they become headings in the rendered output and entries in the table of contents.

  • § `Section Name` starts a top-level section.
  • §§ `Subsection Name` nests one level deeper (§§§ deeper still).

The name is backtick-quoted, so it can be a full phrase. Every declaration after a marker belongs to that section until the next marker:

§ `Eligibility`

GIVEN `the applicant` IS A Person
`the applicant` `qualifies for EP` IF ...

§§ `Age requirements`

GIVEN `the applicant` IS A Person
`the applicant` `is of working age` IF ...

renders as

§ 1  Eligibility
    • The applicant qualifies for EP if ...
    1.1  Age requirements
        • The applicant is of working age if ...

The § numbers appear when Number sections is enabled in the Render tab (or --number-sections on the CLI); the headings and table-of-contents entries appear either way.

A flat file with no markers still gets sensible structure — its type definitions and rules are grouped into automatic Definitions and Provisions sections — but explicit §/§§ markers let you name and order the parts the way a reader of the contract or statute would expect. Imported modules that carry their own section titles keep them, rendering under their own heading rather than a generic one.


Lever 3 — @nlg: author the exact sentence

When structure and naming aren't enough — a recursive helper, domain jargon, or a formula you'd rather state in words — attach an @nlg annotation. It is the authoritative natural-language form of that definition.

Where it goes: end of the line

Write @nlg at the end of the construct's line, trailing the signature, with the body on the next line:

GIVEN x IS A NUMBER, y IS A NUMBER
GIVETH A NUMBER
`the greater of` x y @nlg the greater of %x% and %y%
  MEANS IF x >= y THEN x ELSE y

%param% slots

Inside the sentence, %name% refers to a parameter. The renderer fills each slot with:

  • the parameter name when it shows the definition itself, and
  • the actual argument at each call site.

So the rule above renders as "The greater of means the greater of x and y" in its own definition, and a call `the greater of` `start date` `end date` renders as "the greater of the start date and the end date".

It replaces the implementation

A function with an @nlg renders as its sentence, not as its body. This is what makes recursive library functions readable — for example filter ships with:

filter f list @nlg the items of %list% for which %f% holds
  MEANS ...

so filter `is eligible` applicants reads "the items of applicants for which is eligible holds" instead of exposing the recursion.

When to reach for it

Situation Why @nlg helps
Recursive / higher-order helpers Hide the implementation behind a description
Math you'd rather phrase in words "the pro-rated premium" instead of the formula
Domain terms of art Match the exact statutory or contractual wording
A name that can't be both valid code and good prose Decouple the two

Tips

  • Keep slots to the function's own parameters; the sentence should make sense with each slot read as a noun phrase.
  • Prefer naming and shape first, @nlg second — an @nlg is a maintenance cost (it can drift from the logic), so reserve it for where it earns its keep.
  • One sentence per definition. If you need branching prose, let the structure (CONSIDER/IF) render and annotate the leaves.

Putting it together

Before — terse names, no annotations:

GIVEN p IS A NUMBER, r IS A NUMBER, n IS A NUMBER
GIVETH A NUMBER
pmt p r n MEANS p TIMES r DIVIDED BY (1 MINUS (1 PLUS r) EXPONENT (0 MINUS n))

renders as a bare formula with opaque single letters.

After — descriptive names plus one @nlg:

GIVEN `the principal` IS A NUMBER
      `the monthly rate` IS A NUMBER
      `the term in months` IS A NUMBER
GIVETH A NUMBER
`the monthly repayment on` `the principal`
    `at` `the monthly rate` `over` `the term in months`
    @nlg the level monthly repayment on %the principal% at %the monthly rate% over %the term in months%
  MEANS ...

Now both the definition and every call site read as a sentence a lawyer can check.


Refining further with Legalese AI

The renderer gives you a faithful, deterministic baseline — the same input always produces the same prose, and it never invents facts. That baseline is the right foundation, but house style, tone, and jurisdiction conventions are editorial choices that go beyond what deterministic rules should decide.

That is where Legalese AI comes in. Once your rendered output is accurate, you can apply drafting policies — reusable style and language rules such as "use plain English", "prefer active voice", "expand defined terms on first use", or a firm's house style — and Legalese AI rewrites the rendered prose to match, while staying anchored to the deterministic output so the meaning is preserved.

In other words: names, shape, and @nlg get the content right; drafting policies get the style right. See Composing L4 with AI for how Legalese AI fits into the authoring loop.