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.
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-notation0.14.0.The delimiters
Three characters delimit a reference, and they behave identically in every respect measured below:
Nothing else delimits —
“ ”,« »,[ ],{ },< >,|,/,$and~are ordinary text, so(x [a b])is two references, not one.The empty reference is not decoded as empty
Compare a single space, where the delimiters are consumed:
" "is a delimited reference and""is not. The only difference is that the content is empty.Identical in every style:
(x "")Ref("\"\"")(x '')Ref("''")(x)``Ref("")``Two adjacent empties merge
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
Not simple adjacency — a separator does not help:
Summary
(a " " b)Ref(" ")(a "" b)Ref("\"\"")(a "" "" b)Ref(" ")("" ("" 1))Err("x" ("" 1))("" ('' 1))OkSame 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.
(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 "" " "")<space>"<space>— already correct today, and must stay so("" ("" 1))("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:
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-codec0.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 197encodeand 199encode_linedocuments this parser will not read, all of that shape. A downstream project (link-assistant/router) avoids it with a%zsentinel for the empty reference — a private convention that makes the empty reference unwritable as itself, which is what should not be necessary.