Skip to content

An empty reference is read three ways depending on context: "" keeps its delimiters, two in a row merge into a space, and nesting is a parse error #288

Description

@konard

An empty reference is handled three different ways depending on what surrounds it, and one of those readings is an outright parse error. The delimiters " ' and ` all behave the same, so this is one rule applied uniformly — and the fix should be too.

All measurements against links-notation 0.14.0.

The delimiters

Three characters delimit a reference, and they behave identically in every respect measured below:

parse_lino(r#"(x "a b")"#)   // Ref("a b")
parse_lino(r#"(x 'a b')"#)   // Ref("a b")
parse_lino("(x `a b`)")      // Ref("a b")

Nothing else delimits — “ ”, « », [ ], { }, < >, |, /, $ and ~ are ordinary text, so (x [a b]) is two references, not one.

The empty reference is not decoded as empty

parse_lino(r#"(a "" b)"#)
// Ok — [Ref("a"), Ref("\"\""), Ref("b")]
//                   ^^^^^^^^ the two-character text `""`; the delimiters became content

Compare a single space, where the delimiters are consumed:

parse_lino(r#"(a " " b)"#)
// Ok — [Ref("a"), Ref(" "), Ref("b")]

" " is a delimited reference and "" is not. The only difference is that the content is empty.

Identical in every style:

written decoded
(x "") Ref("\"\"")
(x '') Ref("''")
(x )`` Ref("")``

Two adjacent empties merge

parse_lino(r#"(a "" "" b)"#)   // [Ref("a"), Ref(" "), Ref("b")]
parse_lino(r#"(a '' '' b)"#)   // [Ref("a"), Ref(" "), Ref("b")]
parse_lino("(a `` `` b)")      // [Ref("a"), Ref(" "), Ref("b")]

Two references become one, and the survivor is a space appearing in neither input. This is the n-quote rule firing correctly — four adjacent delimiter characters delimit the space between them — but it means an empty reference cannot be written twice in a row without changing meaning.

Nesting is refused

parse_lino(r#"("" ("" 1))"#)   // Err: Verify
parse_lino(r#"('' ('' 1))"#)   // Err
parse_lino("(`` (`` 1))")      // Err

parse_lino(r#"("" ('' 1))"#)   // Ok — passes only because the styles differ
parse_lino(r#"("x" ("" 1))"#)  // Ok
parse_lino(r#"("" ("x" 1))"#)  // Ok

Not simple adjacency — a separator does not help:

parse_lino(r#"("" x ("" 1))"#) // Err
parse_lino(r#"("" 1 ("" 1))"#) // Err

Summary

input today intended
(a " " b) Ref(" ") correct
(a "" b) Ref("\"\"") empty reference
(a "" "" b) Ref(" ") two empty references
("" ("" 1)) Err parses, like ("x" ("" 1))
("" ('' 1)) Ok unchanged

Same construct, four behaviours depending on position and neighbour.

Proposed rule

Give the shortest delimiter priority: a bare delimiter pair is the empty reference, and a longer run is an n-quote delimiter as it is today.

written means
(a "" b) a, empty, b
(a "" "" b) a, empty, empty, b
(a ""x"" b) a, x, b — unchanged
(a """" b) a, empty, b — an n-quote-delimited empty is still empty
(x "" " "") the text <space>"<space>already correct today, and must stay so
("" ("" 1)) parses, matching ("x" ("" 1))

Every existing n-quote meaning survives; only the bare pair changes.

A run of delimiters that opens and closes a non-empty body already reads correctly and must not change:

parse_lino(r#"(x "" " "")"#)   // Ref(" \" ")   — the text `<space>"<space>`
parse_lino("(x ' \" ')")       // Ref(" \" ")   — identical

The four-quote run delimits, the inner " is content. Only the case where a delimiter pair encloses nothing is at issue. All three delimiter styles must behave identically, and mixing them must change nothing.

Please land this in every supported language

The notation is implemented in several languages and this must not diverge — a document written by one has to read identically in all. Please apply the rule and its conformance fixtures across all supported language implementations, and to the readers in lino-objects-codec (Rust, JS, Python, C#, sharing one fixture file).

Why it matters downstream

lino-objects-codec 0.6.0 writes (o: ("" (o: ("" 1)))) for {"": {"": 1}} — an ordinary value with an empty key. Its own round trip is clean, but fuzzing 15,000 values produced 197 encode and 199 encode_line documents this parser will not read, all of that shape. A downstream project (link-assistant/router) avoids it with a %z sentinel for the empty reference — a private convention that makes the empty reference unwritable as itself, which is what should not be necessary.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions