# Implement Example 7 Tailoring of LB25 (Unicode 15.0)

## Goal

Implement the "Example 7" tailoring of rule LB25 from UAX #14-49
(Unicode 15.0), so that the current test suite (`LineBreakTest.txt` for
Unicode 15.0) passes without any LB25 skips.

The tailored rules (from Example 7 of UAX #14-49) are:

    (PR | PO) × (OP | HY)? NU
    (OP | HY) × NU
    NU × (NU | SY | IS)
    NU (NU | SY | IS)* × (NU | SY | IS | CL | CP)
    NU (NU | SY | IS)* (CL | CP)? × (PO | PR)

These replace the simple pair-based LB25 in UAX #14-49:

    CL × PO    CP × PO    CL × PR    CP × PR
    NU × PO    NU × PR    PO × OP    PO × NU
    PR × OP    PR × NU    HY × NU    IS × NU
    NU × NU    SY × NU

## Pair Table

The `baTable` is *not* changed for LB25.  Instead, `get_lb_result_lookup()`
overrides the break result at runtime for the pairs that require extended
context.  This approach is used because the tailoring is only possible
when a fixup buffer (`brks`) is available — i.e. from `set_linebreaks()`.
When `lb_process_next_char()` is called directly (deprecated now), the
pair table's conservative `IND_BRK` values are used as-is.

Pairs overridden to `LINEBREAK_ALLOWBREAK` before the state machine runs
(the state machine may then restore them to `LINEBREAK_NOBREAK`):

| Pair     | Table value | Runtime override reason         |
|----------|-------------|---------------------------------|
| CL · PO  | IND_BRK     | New rule needs NU..CL context   |
| CL · PR  | IND_BRK     | New rule needs NU..CL context   |
| CP · PO  | IND_BRK     | New rule needs NU..CP context   |
| CP · PR  | IND_BRK     | New rule needs NU..CP context   |
| PO · OP  | IND_BRK     | New rule needs NU right context |
| PR · OP  | IND_BRK     | New rule needs NU right context |
| IS · NU  | IND_BRK     | New rule needs NU left context  |
| SY · NU  | IND_BRK     | New rule needs NU left context  |

Pairs that stay at their table value (same as untailored; state machine
may still strengthen them to `LINEBREAK_NOBREAK`):

| Pair     | Value   |
|----------|---------|
| NU · NU  | IND_BRK |
| NU · PO  | IND_BRK |
| NU · PR  | IND_BRK |
| PO · NU  | IND_BRK |
| PR · NU  | IND_BRK |
| HY · NU  | IND_BRK |

Note: `IS × NU` and `SY × NU` are overridden to ALLOWBREAK because the
tailored rule only allows them within a numeric expression
(`NU (SY|IS)* × NU`), not as standalone pairs.  The skipped tests at
lines 2873 and other places confirm this: they expect `IS ÷ NU` when
not preceded by NU.

Note: `HY × NU` stays `IND_BRK` (no runtime override) because
`(OP | HY) × NU` is a standalone sub-rule in the tailoring.  `IND_BRK`
(no-break when no SP intervenes) is the correct conservative value.

## State Machine Design

### States

```c
enum Lb25State
{
    LB25_NONE,       /* Not in numeric expression */
    LB25_PREFIX,     /* Saw PR or PO */
    LB25_PREFIXOP,   /* Saw (PR|PO) then OP or HY */
    LB25_NUM,        /* Inside NU (NU|SY|IS)* */
    LB25_NUMCLOSE,   /* Saw NU (NU|SY|IS)* (CL|CP) */
};
```

The `LB25_OPHY` state (for standalone OP/HY not preceded by PR/PO) was
considered during design but is not needed: `OP × NU` is already
`PRH_BRK` (LB14), and `HY × NU` is `IND_BRK` (correct for standalone
use).  No break override is required for either case, and no state needs
to be maintained.

### Transition Table

The state machine tracks context for the *left side* of the next break.
It runs as a post-table override in `get_lb_result_lookup()`, after the
runtime ALLOWBREAK overrides described above.

| State     | Incoming `lbcNew` | New State  | Break Override          |
|-----------|-------------------|------------|-------------------------|
| None      | PR, PO            | Prefix     | —                       |
| None      | NU                | Num        | —                       |
| None      | (else)            | None       | —                       |
| Prefix    | OP, HY            | PrefixOp   | (deferred)              |
| Prefix    | NU                | Num        | —                       |
| Prefix    | (else)            | (restart)  | —                       |
| PrefixOp  | NU                | Num        | fixup prior to NOBREAK  |
| PrefixOp  | (else)            | (restart)  | —                       |
| Num       | NU, SY, IS        | Num        | NOBREAK                 |
| Num       | CL, CP            | NumClose   | —  (LB13 handles it)    |
| Num       | PO, PR            | Prefix     | NOBREAK                 |
| Num       | (else)            | (restart)  | —                       |
| NumClose  | PO, PR            | Prefix     | NOBREAK                 |
| NumClose  | (else)            | (restart)  | —                       |

**(restart):** Check for a new start: PR/PO → Prefix, NU → Num, else →
None.  (OP/HY without a prior PR/PO is handled by the pair table.)

**(deferred):** The runtime-overridden ALLOWBREAK for `PO/PR × OP` is
written to `brks` as-is.  The position is recorded in `posLb25Fixup`
for potential fixup.

**fixup prior to NOBREAK:** When NU arrives in PrefixOp state, the
current break (OP × NU or HY × NU) is set to NOBREAK, **and** the
previously deferred position (PR/PO × OP or PR/PO × HY) is patched
to NOBREAK in the `brks` array.

### Rationale for each sub-rule

**`(PR | PO) × (OP | HY)? NU`:**
- `(PR|PO) × NU`: Table has IND_BRK; state machine gives NOBREAK as
  strengthening (not strictly required but consistent).
- `(PR|PO) × OP NU` or `(PR|PO) × HY NU`: Prefix → PrefixOp (deferred
  ALLOWBREAK written to brks), then PrefixOp + NU → Num (NOBREAK for
  OP/HY × NU, plus fixup of the deferred PR/PO × OP/HY position).

**`(OP | HY) × NU`:**
- OP × NU: Already `PRH_BRK` (LB14).  No override needed.
- HY × NU: Table has `IND_BRK`.  This is correct: the `×` in the
  tailored rule is satisfied by `IND_BRK` (no-break when no SP
  intervenes; LB18 handles the SP case).

**`NU × (NU | SY | IS)`:**
- NU × NU: Table has IND_BRK; state machine gives NOBREAK.
- NU × SY: Table has PRH_BRK (LB13: `× SY`).  No override needed.
- NU × IS: Runtime override to ALLOWBREAK; state machine gives NOBREAK.

**`NU (NU|SY|IS)* × (NU|SY|IS|CL|CP)`:**
- Extended context (SY × NU, IS × NU after a prior NU): Runtime
  override to ALLOWBREAK; state machine provides NOBREAK when in Num.
- CL/CP: Table has PRH_BRK (LB13).  No override needed; state
  transitions to NumClose.

**`NU (NU|SY|IS)* (CL|CP)? × (PO|PR)`:**
- Direct NU × PO/PR: Table has IND_BRK; state machine gives NOBREAK.
- Extended NU..SY × PO/PR or NU..CL × PO/PR: Runtime override to
  ALLOWBREAK; state machine gives NOBREAK.

### Fixup Mechanism

Only the `(PR|PO) × OP NU` / `(PR|PO) × HY NU` sub-rule needs fixup:

1. When Prefix + OP/HY → PrefixOp, the ALLOWBREAK (from runtime
   override) is written normally to `brks`.  The position index is
   recorded in `posLb25Fixup` inside `update_lb25_state()`.
2. When PrefixOp + NU → Num, `fLb25Mark` is set to `true` and the
   state returns `LB25_NUM`.  In `set_linebreaks()`, after writing
   `brks[posLast]`, the fixup block fires: `brks[posLb25Fixup]` is
   patched to NOBREAK, then both `posLb25Fixup` and `fLb25Mark` are
   reset.
3. When PrefixOp + anything else → restart, the deferred position is
   discarded (the ALLOWBREAK already in `brks` is correct).

Maximum fixup depth: 1 position.

Context fields added to `struct LineBreakContext`:

```c
size_t posLast;         /* current output position (set by set_linebreaks) */
enum Lb25State eLb25;   /* current LB25 state */
size_t posLb25Fixup;    /* index of the deferred position for fixup */
bool fLb25Mark;         /* true when a fixup is pending */
```

`posLast` is initialized to `INVALID_POS` (`(size_t)-1`) and set each
iteration by `set_linebreaks()`.  The LB25 tailoring block in
`get_lb_result_lookup()` is guarded by `posLast != INVALID_POS`, so it
only fires when called from `set_linebreaks()`.
