On 2026-08-17 02:48, 'Lars Müller' via lua-l wrote:
> As others have said, this isn't really a bug. The grammar is
> token-based; tokens are properly defined in prose. There is no
> ambiguity here, the language is described by the formal grammar plus
> a precise description of the tokenization. The only minor issue is
> that the "lexical conventions" section in the reference manual
> neglects to mention that the tokenizer is greedy. I would consider
> this no big problem; virtually every tokenizer is, not every last
> detail needs to be written down.
I am not flagging it as fatal flaw. Just quirk that imo is not properly
documented. (Unlike "v=f\n()" vs "v= f()" parsing quirk,
which is properly described.)
> This does not call for an ugly solution however. Quite the opposite,
> just mirror the parsing. Parsing is text -> tokens ->
> AST. "Unparsing" should be AST -> tokens -> text. First convert an
> AST to a stream of tokens, then convert that stream of tokens to
> text, inserting separating suppressed tokens as necessary. If you're
> lazy, an emit(ttype, content) function which decides based on the
> last written token.
I agree with this approach.
But having "emit(ttype, content)" which has internal state is bad.
It makes her non-deterministic. And she has two responsibilities:
emit data and be aware of surroundings.
What we need is decision-making function that emits separator
depending of types of adjacent elements: emit_sep(prev_type, next_type).
So pseudocode becomes
// serialize key and value
// ...
next_event = event_start_index
emit_sep(prev_event, next_event)
serialize(KeyNode)
prev_event = next_event
// ...
We can move that "prev_event" tracking by adding internal state
to "emit_sep()". Again, tradeoff here is non-determinism.
But this function has only one responsibility:
// serialize key and value
// ...
emit_sep(event_start_index)
serialize(KeyNode)
// ...
Yeah, I think I feel the right implementation now. Thanks for hint!
> By the way, a hypothetical naive approach of directly feeding the
> grammar into some tool is also doomed to fail on many more token
> concatenations than "[[[" (this just happens to be an
> ambiguous-looking one). "if1thenend" is a single token, made up of
> the concatenation of four tokens. A fuzzer which simply applies the
> grammar rules may produce this along with many more abominations like
> it.
Yeah it's easy to write wrong code.
> Conversely, a parser directly based on the grammar will be way too
> permissive (this kind of thing happened to me some years ago when I
> tried to translate the grammar to a PEG for LPEG); a naive grammar
> will be ambiguous. Should "localx = 42" be "local x = 42"
> or "_G.localx = 42"?
Well, two alphanumerics MUST be separated. That follows from their grammar.
What to do with alphanumeric with something else depends of language.
F.e. in bash proper assignment is "a=b", not "a = b".
"a=1b=2" is invalid in Lua but may be valid in other language.
> Aside: I am not even sure the situation is salvageable in a CFG at
> all. In greedy tokenization, long strings are closed by the *first*
> matching delimiter. Is there a way to enforce this with a CFG? I
> don't think so. You can of course express valid long strings as
What is CFG?
> inner ::= '[' {.} ']'
> padded ::= inner | '=' padded '='
> long ::= '[' padded ']'
Mathematically nice grammar.
Reminds me "binary_number = ( '0' | '1' ) [binary_number]".
Not practical tho. You will have to do N calls just to read 2*N "=" chars.
> where . stands for any character. But I see no way to ensure that
> {.} must not contain a string of equal signs of length matching the
> delimiters, for all infinitely many possible numbers of delimiters.
> The following "program" would be valid per the naive CFG, but is
> obviously invalid when tokenized and subsequently parsed:
>
> --[[ blah blah blah ]]
> syntax error!
> blah blah blah ]]
Yeah long quotes are fun. In my parser grammar element can (also) be
function. And in my first implementation function for matching opening
quote was storing information of chunk length in some vault. This
information was used by function for matching closing quote.
(Later I rewrote that part to incomprehensible regexp "%[(%=*)%[.-%]%1%]"
because I wanted more speed.)
> - Lars
-- Martin