Comments (61)
@ilslv @mpkorstanje it seems to me that the discussion becomes a little bit overheated. Let's sum it up a little with facts and coclusions, to push it into a constructive direction.
The current grammar described in ARCHITECTURE.md
:
- Somewhat implementation-driven as describes some superset of
Cucumber Expressions
followed by post-validation rules for the sake of better spanning info and error messages in the current parser implementations. - Not context-free, while doesn't have to.
- Covered with a tests suite to ensure the current implementations match it.
This imposes problems for alternative implementations:
- either to follow closely the original implementation (which is too restrictive regarding implementation approaches and performance reasoning);
- or to implement something new only pretending to call itself
Cucumber Expressions
, but in fact, having no ability to reason whether it's the right implementation or not (which is bad for future proofing and compatibility).
We'd really like to have implementation freedom, while preserve the ability to formally check correctness. For this purpose @ilslv proposed to add a "formal spec" grammar, which is context-free and implementation-free, but describes the actual Cucumber Expressions
precise enough, so any implementation can refer to its as the source of truth.
But @mpkorstanje is against having a formal spec without an ability to verify whether implementations do follow it, as it will introduce an obvious drift between the actual spec and its implementations.
Please, correct me if I'm wrong.
So, from my point of view:
It worths, of course, to provide a precise "formal spec" grammar proposed by @ilslv. But we should also provide a generic set of tests for it which would be suitable for any implementation.
With this, we can satisfy @mpkorstanje point about "actual correctness", while also provide third-party alternative implementations (like ours) with an obvious way to verify its correctness. Having this tests suite will allow to strip out major part of existing tests, leaving only the implementation-specific ones (like spanning/errors handling, or language-dependent stuff).
Is there any arguments against this way? Or is there any issue that I do miss?
from cucumber-expressions.
@mattwynne thanks for the invitation! But I'd like to have discussion here manly for 2 reasons:
- In my experience Slack conversations have tendency to branch off into some other topic and are hard to follow.
- History of Slack conversations gets lost eventually, while it's good to have full discussion of technical decision (especially controversial ones like this). That allows future contributors to better understand what options where available at the time and why exact decision was made.
We also have a regular Zoom call on Thursdays at 7am PST (4pm CET) that you'd be more than welcome to join us for.
If issue won't be resolved by Thursday I thinks I will be able to join you on the call. They have tendency to be more constructive, as time is limited 😅
from cucumber-expressions.
@ilslv the grammar as written is context-sensitive and not in EBNF form (EBNF also can't express context sensitive grammars).
Provided grammar describes superset of Cucumber Expressions and I don't really see reasons to do it.
Users of Cucumbers are not typically familiar with the concepts involved in parsers or grammars. As such the typical error messages from a parser are incomprehensible. By accepting a super set of all cucumber expressions we can more clearly provide error messages for typical mistakes such as using optional parameter types, empty optionals, etc, etc by pointing the entire AST node that is in the wrong place rather then a single character.
text = (- text-to-escape) | ('\', text-to-escape)
This puts each character into a single AST node. This isn't great for languages with garbage collection and makes debugging much harder.
alternation = single-alternation, ('/', single_alternation)+
It is now longer obvious that alternation
is bounded by whitespace, parameter types or start/end of expression which means that we are losing some of the intention.
parameter = '{', name*, '}'
name = (- name_to_escape) | ('\', name-to-escape)
name-to-escape = '{' | '}' | '(' | '/' | '\'
The *-to-escape
is pretty neat. This is definitively worth including.
Now I've only given this a cursory review but other then the issues mentioned above I don't see any immediate technical problem with the provided EBNF. Might be worth updating the documentation somewhat.
from cucumber-expressions.
@ilslv could you please send a pull request with your proposed changes?
from cucumber-expressions.
So the least resourceful, but good enough, solution would be to have a programmable way for mapping "formal spec" AST into the current one, or vice versa, or having even another reduced representation, so both "formal spec" AST and the current AST may be translated into for correct comparing. This will allow us to fully reuse exisitng implementation and tests.
from cucumber-expressions.
@mpkorstanje if I understand correctly you want to make every PR atomic. This means after every merge implementation should closely resemble grammar. I don't consider myself comfortable enough this any other implantation language, especially taking into account number of users. So only help I can do is to provide context-free grammar in separate peaces.
So I see following roadmap:
- Slightly fix current grammar to remove errors and ambiguities
This part doesn't require implementation tinkering, so can be performed by me. - Make grammar context-free while still describing superset grammar and correct implementations
I guess we are talking about this grammar
cucumber-expression = ( alternation | optional | parameter | text )*
alternation = alternative*, ( "/" + alternative* )+
alternative = optional | text
optional = "(", option*, ")"
option = optional | parameter | text
parameter = "{", name*, "}"
name = whitespace | .
text = whitespace | ")" | "}" | .
- If all goes well, maybe update grammar to describe exact
Cucumber Expressions
and update implementations accordingly
from cucumber-expressions.
How exciting to have a Rust port! ❤️
This looks good to me. Any feedback @mpkorstanje?
@ilslv would you be interested in donating your parser implementation to this repo (under a rust
directory)? That way we can more easily share the same test suite, align the release cycle and keep everything in one place. We'd also set up a GitHub Action to release it as a crate whenever we make a release.
from cucumber-expressions.
@aslakhellesoy my implementation may be incompatible with test suit in this repository. As I could see tests under parser
directory parse nested optionals just find and error only on expansion to the regex in matching
directory. My implementation is 1-step that matches only correct Cucumber Expressions
.
from cucumber-expressions.
Do you think it would be possible to make your implementation compatible with the official test suite? If not, do you think there is something we could change in the official implementation's test suite (or implementation) in order to make it easier for your implementation to be compatible?
from cucumber-expressions.
@aslakhellesoy you added the EBNF phrase at 0875a40#diff-8f6366fd8e7a5fa30f7f05879999195b7cf5fa1e3d157822eb37f0e91d226cfdR7. Note that the code highlighting should still use EBNF because that makes the pseudo-grammar somewhat readable but the grammar itself is not EBNF.
from cucumber-expressions.
it would be possible to make your implementation compatible with the official test suite
Yes, but it would introduce some performance overhead, which we would like to avoid.
do you think there is something we could change in the official implementation's test suite (or implementation) in order to make it easier for your implementation to be compatible
It would be possible, but I don't think it costs the effort. As I mentioned only subset of the official test suite is incompatible. The most important tests for final matching and regex transformation are ok. I even think that only this test case would be incompatible. This happens because other implementations are using current ARCHITECTURE.md
approach of generating AST which is wider than Cucumber Expression
and error only later. My implementation generates only right Cucumber Expression
from the start.
Mainly because of those difference I would hold on merging Rust implementation here. But you can still reference it using submodules or just mention it in the README.md
.
from cucumber-expressions.
typical error messages from a parser are incomprehensible
Yes, but only if you are using some sort of parser-generator. Rust implementation has exactly the same errors, which can point at troublesome characters or range of characters. All this while preserving minimal overhead (1 dynamic allocation on every nested expression and even it can be avoided, I just don't think it costs the effort for now).
This puts each character into a single AST node. This isn't great for languages with garbage collection and makes debugging much harder.
There is no need to translate provided grammar 1-to-1. I propose adding it only to provide some formal specification for the Cucumber Expressions
language.
My proposal is following: let's add formal EBNF specification for the language while preserving the old one and add documentation that it only exists only for implementation suggestion. This would allow Rust implementation (or any other one) to strictly follow it, because for now there is no way for me to reason that Rust implementation is correct, as there is no formal specification for Cucumber Expressions
.
If you are on board with that idea, I will happily prepare PR documenting all this.
from cucumber-expressions.
No. If we only have a formal specification without a closely matching implementation and test set in this repository we are only going to see uncontrolled drift.
Though I don't believe a formal specification has to be in EBNF. As long as the context sensitive form can be procedurally be written to EBNF it can serve as a formal specification for your purposes.
from cucumber-expressions.
No. If we only have a formal specification without a closely matching implementation and test set in this repository we are only going to see uncontrolled drift.
It looks like there is a misunderstanding, as I never proposed to remove anything, only add a formal specification. Let me sum up everything discussed in this issue.
What are the proposed changes?
I would like to see formal specification with some context-free grammar. Original issue shows the EBNF example.
It is now longer obvious that
alternation
is bounded by whitespace, parameter types or start/end of expression which means that we are losing some of the intention.
It's not obvious, but alternation
is still bounded by whitespace, because of following rules
text-in-alternative = (- alternative-to-escape) | ('\', alternative-to-escape)
alternative-to-escape = ' ' | '(' | '{' | '/' | '\'
You have to escape whitespace for it to match.
I agree that it's hard to spot. That's why I'm proposing only to add formal specification, leaving existing one as one of implementation possibilities.
Why do we need this change?
For now there as there is no formal specification, only some non-existing grammar, I can't reason why Rust implementation is correct.
Why not to use existing guidance from ARCHITECTURE.md
?
ARCHITECTURE.md
doesn't give full picture.
There is no way to know how escaping works in different AST elements only from it. I had to dig throughout all the test cases to be sure, that I didn't miss anything. We should provide formal specification to reason about implementation correctness.ARCHITECTURE.md
forces to use suboptimal implementation.
It encourages to generate middleware AST that is wider, thanCucumber Expression
and that is using context-sensitive grammar with lookbehinds and lookaheads. As shown by the Rust implementation there is a way to avoid both of those overheads by generating validCucumber Expression
from the start, basing on context-free grammar. All that while still preserving friendly error messages.
from cucumber-expressions.
from cucumber-expressions.
There is no misunderstanding.
From the discussion in cucumber-rs/cucumber-expressions#1 I see that you have asked to implement the parser in a way that closely matches the specification. You are now looking to update the specification to match your implementation.
However the proposed grammar is does not accept the superset of all Cucumber valid expressions. As such we do not have a reference implementation or test set that accurately describes it. Additionally if the EBNF can not be derived from the context-sensitive grammar it can not be said to be correct. If it can be derived from the context sensitive grammar it need not be explicitly included.
Currently the EBNF can't derived from the context sensitive grammar because it is not well specified. So I'm specifically requesting a fix to the context-sensitive grammar rather then an addition of an otherwise untested, unverified (in this repo) piece of specification.
from cucumber-expressions.
update the specification to match your implementation
No, I'm trying to introduce formal specification as there is none yet.
does not accept the superset of all Cucumber valid expressions
Yes, thats the point to describe Cucumber Expressions
through it's own formal specification, rather then relying on superset specification in non-existent grammar with number of notes on post-validation.
EBNF can not be derived from the context-sensitive
Yes, because Cucumber Expressions
specification doesn't need to be context-sensitive.
it can not be said to be correct
There is no notion of correctness right now, as no formal specification exists.
fix to the context-sensitive grammar
My grammar is context-free. Can you please provide concrete examples, where my attempt at formalisation fails?
from cucumber-expressions.
@tyranron thanks for pulling this discussion together and @ilslv thanks for all your contributions so far. I share Aslak's excitement at seeing a Cucumber in Rust!
If people would like to talk about this in a more real-time context, we can be found on the https://cucumber.io/community#slack in the #committers channel.
We also have a regular Zoom call on Thursdays at 7am PST (4pm CET) that you'd be more than welcome to join us for. If you hop into the Slack we can share the Zoom link in there or add you to the invite.
from cucumber-expressions.
@ilslv no, my request is very simple:
To fix the context sensitive grammar in such a way that a the EBNF can be logically derived from it.
I'm not opposed to replacing the context sensitive grammar with an EBNF if the test suite, implementations in this repository follow suit. However that is a significant commitment.
As such fixing the context sensitive grammar in such a way that a the EBNF can be logically derived from it should be a reasonable compromise.
Third party implementations will be able to use a "formal" grammar while this project retains the canonical grammar with closely matching implementation and tests.
In the long term we can then look at improving the test suite, grammar and implementations.
from cucumber-expressions.
To fix the context sensitive grammar in such a way that a the EBNF can be logically derived from it.
I don't understand what EBNF can be logically derived from it
actually means.
We have 2 grammars:
ARCHITECRURE.md
- Context-sensitive
- Describes superset of
Cucucumber Expressions
- Written in non-existent grammar with number of notes on post-validation
- Proposed grammar
- Context-free
- Describes what I believe is exact
Cucucumber Expressions
spec - Written in EBNF
What do you mean by fix the context sensitive grammar
?
Do you want final grammar to be context-sensitive? With EBNF it couldn't be achieved and it leads to unnecessary performance overhead for implementation to be compliant.
Do you still want resulting grammar to represent superset of Cucumber Expression
? I'm strongly against it, as it won't let us say that particular implementation is correct
. And I don't think this will be abled to achieved with EBNF too. And this also leads to performance overhead I described earlier.
The only other complaint I saw was regarding alternation
not been obviously bounded by whitespace and I already responded that actually it is bounded. And there is no other way to make it more clear to my knowledge.
I still don't understand what do you actually want to fix
.
from cucumber-expressions.
I'm looking to retain a close similarity between the grammar, tests and implementation in this repo while also make it possible for you to derive a grammar in EBNF form without asking you to also change the implementations and tests in this repo.
Now for our purposes we can say that grammars are equivalent if they accept the same language. So if the language accepted by cucumber expressions is context free you should be able to derive the context free grammar from the context sensitive grammar in a structured manner (or be able to proof equivalency in another way).
Currently I don't believe this is possible because the context sensitive grammar contains several errors and ambiguities. This inability to derive a context free grammar from the context sensitive one is what I'm asking you to fix. Additionally the the fixed grammar should still be context sensitive and accept the superset if Cucumber expressions with optional parameter types, empty optionals, ect.
So once fixed we will have one context sensitive grammar that matches our implementation and tests from which you can logically derive a context free one.
from cucumber-expressions.
- I want to guarantee that Rust implementation is correct. For that I need to formalize Cucumber Expressions language. While you are asking me to fix language which is superset of Cucumber Expressions. Even if I do it, there would be no way to say that Rust implementation is correct.
- There is no way to express context-sensitive language in EBNF. If you want to fix existing language while preserving context-sensitivity we need to choose another way of expressing it.
- It is mathematically impossible to
derive
context-free language form context-sensitive in general case.
All in all we have different intentions. You just want to fix existing superset grammar and by doing it to force implementations to be suboptimal to be somehow correct
(while still this word can't be applied to language without spec).
While I want to introduce formal specification for Cucumber Expressions language to be able guarantee correctness not only for Rust implementations, but also to every other implementation.
from cucumber-expressions.
I don't think it is impossible. This isn't the general case of rewriting any arbitrary grammar but rather this the case of rewriting this specific grammar. Assuming your EBNF is correct we know context free form exists.
But if you do think it is impossible then the only mutually acceptable alternative is to rewrite the grammar, tests, and existing implementations.
from cucumber-expressions.
programmable way for mapping "formal spec" AST into the current one, or vice versa
Simply mapping from formal spec into superset grammar won't give us much info. It's almost the same as having grammar that accepts only empty string and will always map in superset.
Mapping from superset grammar into formal spec isn't possible, as first one is superset of another.
Post-validation is key problem here.
The only thing, that will bring us to being somewhat correct is to feed all implementations some input from manual tests and fuzzers and compare resulting Cucumber Expressions
AST or errors produced while parsing.
Another problem is that tests inside this repo aren't suitable. parser folder contains expansion of superset AST as seen by this testcase. I don't also know if current implementations can produce exact Cucumber Expressions
AST.
from cucumber-expressions.
I also noticed one interesting implementation detail. Whitespaces are treated as a some sort of special case
---
expression: three hungry/blind mice
expected_ast:
type: EXPRESSION_NODE
start: 0
end: 23
nodes:
- type: TEXT_NODE
start: 0
end: 5
token: three
- type: TEXT_NODE
start: 5
end: 6
token: " "
- type: ALTERNATION_NODE
start: 6
end: 18
nodes:
- type: ALTERNATIVE_NODE
start: 6
end: 12
nodes:
- type: TEXT_NODE
start: 6
end: 12
token: hungry
- type: ALTERNATIVE_NODE
start: 13
end: 18
nodes:
- type: TEXT_NODE
start: 13
end: 18
token: blind
- type: TEXT_NODE
start: 18
end: 19
token: " "
- type: TEXT_NODE
start: 19
end: 23
token: mice
Rust implementation does completely the same thing. So It looks like existing implementation already work around context-sensitive grammar by being context-free. This can mean that we no longer can call them correct
. In that case bringing context-free grammar would only make it closer to existing implementations.
from cucumber-expressions.
Rust implementation does completely the same thing. So It looks like existing implementation already work around context-sensitive grammar by being context-free.
I'm not sure about the point you are making.
As we've already established, we can assume the language accepted by the grammar to be context-free. The grammar however is written in context sensitive form for clarity. I'm open to updating the grammar provided the implementations match the grammar.
E.g this would have to change:
from cucumber-expressions.
I'm not sure about the point you are making.
Your previous argument was about not being able to change the grammar into being context-free, as current implementations are based. I'm, on the other hand, saying that argument doesn't make sense to me, if current implementations already don't follow provided grammar.
I've finally looked into the implementation and from I can see ether grammar is more broken, than I thought, or implementation is wrong.
Here we can see call into parseTokensUntil
which should be doing alternative* + ( '/' + alternative* )+ + (?=right-boundary)
parsing part. Let's peek inside
As I understand algoritm is following:
- Check
lookingAtAny(tokens, current, endTokens)
- Parse more with
Result result = parseToken(expression, parsers, tokens, current);
- If parsed something, go to 1, or throw Exception otherwise
If so, this is not how lookahead defined at (?=right-boundary)
should work. Lookahead checks after each parsed character. This is what allows us to something like this in regex (stopping greedy parser using lookahead after the first character).
So, if I understand algorithm correctly, we have 3 possible cases:
- Implementation is wrong and should implement real lookahead.
- Implementation is right and grammar is wrong.
- Implementation and grammar are correct and
(?=right-boundary)
doesn't mean lookahead and means something likeapply previous parser and see if next element is right-boundary while not consuming it
Which is it?
from cucumber-expressions.
I'd say #3 is the most accurate, but again, I don't see how this changes any of the conclusions made so far. If we change the grammar, we change the implementations and tests to match.
from cucumber-expressions.
@mpkorstanje this is significant, because real lookahead requires context-sensitive grammar, while implemented one doesn't. I believe that it's possible to formalise pretty easy-to-understand context-free grammar which fully compliant with implementation.
from cucumber-expressions.
So to avoid drift, make it easy to verify correctness and provide a reference implementation the grammar and implementations in this repository should closely reflect each other.
So when for example we change:
alternation = (?<=left-boundary), alternative*, ( "/" + alternative* )+, (?=right-boundary)
left-boundary = whitespace | "}" | "^"
right-boundary = whitespace | "{" | "$"
alternative = optional | parameter | text
To:
alternation = single-alternation, ('/', single_alternation)+
single-alternation = ((text-in-alternative+, optional*)
| (optional+, text-in-alternative+))+
text-in-alternative = (- alternative-to-escape) | ('\', alternative-to-escape)
alternative-to-escape = ' ' | '(' | '{' | '/' | '\'
That change should be reflected in the implementation of the alternationParser
. So rather then checking the context before and after parsing a subtree as it does now it should try to parse a single-alternation
followed by ('/', single_alternation)+
.
from cucumber-expressions.
That change should be reflected in the implementation of the alternationParser.
I didn't mean old proposal when said I believe that it's possible to formalise pretty easy-to-understand context-free grammar which fully compliant with implementation.
It will be different from initial approach
But to introduce new context-free grammar (which I believe is now really possible, when I understand what (?=)
means in current grammar) we need to fix some problems.
First question of mine is about special handling of ^
and $
in alternations. The is no tests for it and Rust implementation actually ignores this case.
alternation = (?<=left-boundary), alternative*, ( "/" + alternative* )+, (?=right-boundary)
left-boundary = whitespace | "}" | "^"
right-boundary = whitespace | "{" | "$"
alternative = optional | parameter | text
text = whitespace | ")" | "}" | .
Is actually equivalent to
alternation = (?<=left-boundary), alternative*, ( "/" + alternative* )+, (?=right-boundary)
left-boundary = whitespace | "}"
right-boundary = whitespace | "{"
alternative = optional | parameter | text
text = whitespace | ")" | "}" | .
text
in alternative
will always consume ^
or $
before (?=right-boundary)
or (?<=left-boundary)
check: example 1 and 2. Or did I miss something?
from cucumber-expressions.
The ^
and $
are start and end of line. You can tell by looking at the part of the implementation of alternationParser
that checks the boundaries. The boundary tokens are listed there.
I'm not sure how they ended up qouted though that's clearly wrong.
from cucumber-expressions.
@mpkorstanje thanks for clarifying it.
cucumber-expression = ( alternation | optional | parameter | text )*
alternation = (?<=left-boundary), alternative*, ( "/" + alternative* )+, (?=right-boundary)
left-boundary = whitespace | "}" | ^
right-boundary = whitespace | "{" | $
alternative = optional | parameter | text
optional = "(", option*, ")"
option = optional | parameter | text
parameter = "{", name*, "}"
name = whitespace | .
text = whitespace | ")" | "}" | .
As alternative
may contain parameter
and cucumber-expression
tries to parse alternation
first, input like {word}a/b
should be theoretically parsed into following AST
- type: ALTERNATION_NODE
- type: ALTERNATIVE_NODE
- type: PARAMETER_NODE
- token: word
- type: TEXT_NODE
- token: a
- type: ALTERNATIVE_NODE
- type: TEXT_NODE
- token: b
But following test shows another picture. For some reason parameter
is treated as separate node, not as a part of alternation
. I don't quite understand, why this is the case?
from cucumber-expressions.
Mmh. I think that might be a development artifact.
For example {a}/{b}
could be a valid cucumber expression. However in practice that makes it cumbersome to use stemming like {n}st/nd/rd/th
because the parameter would have to he repeated in every alternative.
So an alternative never includes a parameter.
So we can change
alternative = optional | parameter | text
To
alternative = optional | text
from cucumber-expressions.
@mpkorstanje ok, now we have the following grammar
cucumber-expression = ( alternation | optional | parameter | text )*
alternation = (?<=left-boundary), alternative*, ( "/" + alternative* )+, (?=right-boundary)
left-boundary = whitespace | "}" | ^
right-boundary = whitespace | "{" | $
alternative = optional | text
optional = "(", option*, ")"
option = optional | parameter | text
parameter = "{", name*, "}"
name = whitespace | .
text = whitespace | ")" | "}" | .
which is equivalent to
cucumber-expression = ( alternation | optional | parameter | text )*
alternation = (?<=left-boundary), alternative*, ( "/" + alternative* )+
left-boundary = whitespace | "}" | ^
alternative = optional | text
optional = "(", option*, ")"
option = optional | parameter | text
parameter = "{", name*, "}"
name = whitespace | .
text = whitespace | ")" | "}" | .
(?=right-boundary)
is redundant, because text
inside alternative
will consume every character except whitespace | "{" | $
, so this check is always true.
from cucumber-expressions.
Can you update the implementation and grammar to reflect that?
from cucumber-expressions.
@mpkorstanje I'm not too comfortable with any languages other implementations are using. But more important thing is that we don't have to update implementation right the way, as provided grammar is equivalent to implemented one.
from cucumber-expressions.
Even more
cucumber-expression = ( alternation | optional | parameter | text )*
alternation = (?<=left-boundary), alternative*, ( "/" + alternative* )+
left-boundary = whitespace | "}" | ^
alternative = optional | text
optional = "(", option*, ")"
option = optional | parameter | text
parameter = "{", name*, "}"
name = whitespace | .
text = whitespace | ")" | "}" | .
is equivalent to
cucumber-expression = ( alternation | optional | parameter | text )*
alternation = alternative*, ( "/" + alternative* )+
alternative = optional | text
optional = "(", option*, ")"
option = optional | parameter | text
parameter = "{", name*, "}"
name = whitespace | .
text = whitespace | ")" | "}" | .
This happens because alternation
is greedy and comes first in cucumber-expression
. So previous characters in the start of alternation
parsing are always ^
or something previously parsed by optional | parameter | text
which wasn't accepted in alternation
. This happens to be only exactly whitespace | "}" | ^
so (?<=left-boundary)
is redundant too, as always returns true
.
Final grammar which is equivalent to the existing one is just relaxed version of my original proposal. By relaxed
I mean that it simply accepts invalid Cucumber Expressions
too, while mine doesn't.
from cucumber-expressions.
Cheers! We can use this info to update the grammar and implementations sometime in the future then.
from cucumber-expressions.
Fixing the grammar / parser to use the (- X-to-escape) | ('\', X-to-escape)
doesn't look so hard either.
from cucumber-expressions.
How do you "span" the invalid subtrees in error messages without a fully parsed AST?
Example:
Hello (optional {parameter}) world
^--------^
The parser would not fail at the first {
, how do you know to span the pointers to the next element and not also include other syntax errors like Hello (optional {parameter {} ) world
?
from cucumber-expressions.
Hello (optional {parameter}) world
^---------^
This one is pretty easy to tackle: If I'm started to parse optional
there can't be unescaped {
. Once I encounter one, I try to parse parameter
. If successful, that means I have an entire parameter
inside of optional. Otherwise this is an error too, just another one: unescaped reserved character.
So in case of Hello (optional {parameter {} ) world
this will error about parameter
containing {
, because we've started to parse parameter
inside an optional
, which errored.
Also helpful idea of splitting errors into recoverable once, and irreconcilable. As the name suggests, recoverable allows us to continue parsing with another parser. For example in abc
we first try to parse alternative
, recoverably fail, then with option
and so on, until encounter text
. While in case of abc/
we'll try to parse alternative
, which will encounter /
, after that we know, that all subsequent errors aren't recoverable, as only alternative
s can have unescaped /
inside. So having empty alternation is immediate irrecoverable error.
from cucumber-expressions.
Can you show an example of that?
from cucumber-expressions.
@mpkorstanje Rust syntax is quite alien-looking, but here you go
from cucumber-expressions.
Mmh. This error message doesn't look quite right:
from cucumber-expressions.
I also don't see where you would construct the ^---------^
span.
from cucumber-expressions.
Mmh. This error message doesn't look quite right
I've united all IlligalIn..
into UnescapedReservedCharacter
as final suggestion of escaping this character is the same
I also don't see where you would construct the ^---------^ span.
Actually I don't do it. I allow users to supply string wrapped in a LocatedSpan
which will track where the captured slice is coming from. In this case exact error message can be constructed, but there is possibility to skip this tracking by supplying raw string.
from cucumber-expressions.
Cheers! We can use this info to update the grammar
So, do you want update grammar to exact Cucumber Expressions
or still use superset AST
?
from cucumber-expressions.
So far I don't see how I can generate the error messages with the ^---------^
span without a superset.
from cucumber-expressions.
@mpkorstanje I've already described solution with LocatedSpan.
Codesample:
let err = Expression::regex("I have {int cucumbers in my belly").expect_err();
match err {
Error::UnescapedReservedCharacter(e) => {
assert_eq!(*e, "{");
assert_eq!(
repeat(' ')
.take(e.offset())
.chain(once('^'))
.chain(repeat('-').take(e.len()))
.chain(once('^')),
" ^-^".chars(),
)
}
e => panic!("wrong err: {}", e),
}
let err = Expression::regex("I have ({int}) cucumbers in my belly").expect_err();
match err {
Error::ParameterInOptional(e) => {
assert_eq!(*e, "int");
assert_eq!(
repeat(' ')
.take(e.offset())
.chain(once('^'))
.chain(repeat('-').take(e.len()))
.chain(once('^')),
" ^---^".chars(),
)
}
e => panic!("wrong err: {}", e),
}
More code examples are available here
from cucumber-expressions.
Unfortunately that doesn't show how to compute the bounds that go into the span.
from cucumber-expressions.
@mpkorstanje it does exactly that.
e.offset()
returns offset of the span in original input.
e.len()
returns length of the span.
repeat(' ').take(e.offset()).chain(once('^')).chain(repeat('-').take(e.len())).chain(once('^'))
does exactly what you were looking for.
from cucumber-expressions.
So where do those values come from?
from cucumber-expressions.
@mpkorstanje as I mentioned before, implementation abstracts over input type: https://github.com/ilslv/cucumber-expressions-1/blob/main/src/parse.rs#L427-L435
Rather than accepting String
, it accepts something that implements Offset
. So LocatedSpan implements Offset
by remembering it and slicing inner String
.
from cucumber-expressions.
@mpkorstanje the idea is that we carry the location information along while descending in a parser.
-
Given the string as input:
let err = Expression::regex("I have ({int}) cucumbers in my belly").expect_err();
-
We do wrap it into a
LocatedSpan
before feeding to actual parser (which operates on an already location capable inputs):parse::expression(Spanned::new(value))
The
LocatedSpan::new()
sets the positions as:offset
starts at 0,line
starts at 1, andcolumn
starts at 1. -
Then we descent in a parser and input sub-parts to other functions. When we slice that sub-part of an input, we adjust the
offset
appropriately. In Rust this implementation detail is abstracted behindSlice
implementation, but it can be viewed here: theoffset
is adjusted when we call slicing operator on aLocatedSpan
wrapper. -
So, at the moment of emitting an error, we have all the location information in-place. Again, in Rust implementation, the error value itself holds a failed piece of input along with its location info (as long as it's wrapped into
LocatedSpan
), which, in turn, allows us to call those.offset()
and.len()
methonds on the returned error value.
from cucumber-expressions.
So when encountering a {
inside an optional, how do you find the matching }
to span the entire parameter without parsing? For example:
Hello (optional {parameter}) world
^--------^
Best I can find is:
Some('{') => {
if let Ok((_, par)) = peek(parameter)(input.clone()) {
return Error::ParameterInOptional(
input.take(par.0.input_len() + 2),
)
.failure();
}
return Error::UnescapedReservedCharacter(input.take(1))
.failure();
}
And it looks like peek(parameter)
is doing the parsing.
from cucumber-expressions.
already described here
This one is pretty easy to tackle: If I'm started to parse
optional
there can't be unescaped{
. Once I encounter one, I try to parseparameter
. If successful, that means I have an entireparameter
inside ofoptional
. Otherwise this is an error too, just another one: unescaped reserved character.
And it looks like peek(parameter) is doing the parsing.
yes, it tries to parse parameter
Can you please formulate all the questions about Rust implementation, so I can tackle them all at once in depth? Because it looks like we are walking in circles.
from cucumber-expressions.
Right so you are parsing invalid ASTs and rejecting them early. This rejection seems rather ad-hoc and I don't see a good reason to do it.
The moment we'd add a test case with multiple AST problems you'd have to change the entire architecture. For example:
Hello (optional {parameter}) (optional {parameter})
^--------^ ^--------^
from cucumber-expressions.
This rejection seems rather ad-hoc and I don't see a good reason to do it.
Most other parsers do exactly the same thing. This is not an ad-hoc solution, but the only possible way to implement zero-cost performant parser. This entire conversation still doesn't have good reason to parse entire superset AST.
The moment we'd add a test case with multiple AST problems you'd have to change the entire architecture.
No, we still can continue parsing and collecting error and return batch of them. This won't require architectural changes. The only reason I've implemented this way is to close to existing implementations, which error early too. Example
Also I don't see error messages as a part of language specification.
from cucumber-expressions.
Would any of you be interested in an ensemble programming session where we work on this together? I feel like it might speed up the communication a bit if we could work on it in real time, and it could be fun!
I know some people prefer working asynchronously though, so no pressure, I just thought I'd suggest it.
If folks are interested I can do the work to figure out our schedules and find a good time.
from cucumber-expressions.
Fundamentally, to keep the implementation and spec close together this means rewriting the existing implementations. This takes time. I currently don't have that sort of time to commit to this.
I'm being asked to make a judgement call about using the superset of Cucumber expressions or exact set in the EBNF. I can't make this judgement call. To cut the task down to size it might even be convenient to in a first iteration make the grammar context-free. And in a second iteration remove the redundant parts of the grammar.
from cucumber-expressions.
Related Issues (20)
- more powerful optional text HOT 12
- Allow free-form text in optionals HOT 1
- Expression for an argument list HOT 4
- Improve Expression creation performance HOT 2
- Use action-publish-pypi in cucumber-expressions
- Optimize Cucumber expression creation in .Net implementation
- Action Required: Fix Renovate Configuration
- Bad CucumberExpression creation performance HOT 6
- Golang big.Float is not a big decimal HOT 12
- Feature: named capture groups HOT 13
- Missing source for source map
- RegEx "expressions" and defineParameterType feature file examples ?
- Add the ability to ignore a cucumber expression parameter just like a non capturing regex HOT 12
- Nested capture groups are not passed to custom param definition transformer HOT 8
- `end` as a method name / prop can cause some issues HOT 4
- LICENSE file is empty HOT 1
- Typo in error message for use of alternation inside an optional
- Unable to parse negative number in Norwegian HOT 11
- Released Ruby gem is missing LICENSE file. HOT 4
- Unable to create step definition with | operator with latest version HOT 1
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from cucumber-expressions.