Precisely because the language is complicated and less amenable to LR parsing.
Note also that (as I alluded to above) the parsing technique that Tree-sitter uses, "LR parsing", makes some things more difficult to parse than they'd be with another kind of parser. This is a deliberate trade-off, because LR parsing makes certain features of Tree-sitter, like fast re-parsing in response to input changes, much much easier.
There has been, however, discussion about the need to clean up some of the lesser-used language feature, but obviously doing so carries risks.
It is a little terrifying in the sense that I'd not want to write language level tools (eg: syntax highlighter).
But if you have scheme on one end and natural language on the other, ruby leans à bit towards natural language - but in a good way. In some ways ruby isn't that different from Smalltalk - but it has a lot (sometimes I think too many, sometimes not) conveniences.
Parantheses and brackets are largely optional "where it makes sense". Conditionals support postfix, eg these are equivalent:
if should_send?()
send_mail({to: '[email protected]'})
end
send_mail to: '[email protected]' if should_send?Everything is kind-of-but-not-really an object, a reference, and a function, all at the same time - which sounds complicated but in my head... turns out to be pretty simple. Everything's just kind of different flavors of the same thing. `attr_accessor` is a good place to see this in action.
The flexibility comes more from the variety of available core language options (procs, blocks, and lambdas) and core libraries (map/each/collect, for example), not from a variety of underlying concepts.
It also seems somehow to be completely declarative? How have you managed to transform Ruby parsing to be context-free? For example where's the set of what's currently a local variable so you can distinguish from method calls?
To be fair, we're cheating a little bit because the Ruby grammar relies so heavily on an external scannar, which is just under 1,000 lines of C++: https://github.com/tree-sitter/tree-sitter-ruby/blob/master/...
I really want to try tree-sitter for using in an actual Ruby implementation because it's so beautiful!
There's no symbol table in the parser, so at parse time, we don't distinguish those cases:
$ cat test.rb
module Test
def test1
x = 14; x
end
def test2
y = 14; x
end
end
$ tree-sitter parse test.rb
(program [0, 0] - [9, 0]
(module [0, 0] - [8, 3]
name: (constant [0, 7] - [0, 11])
(method [1, 2] - [3, 5]
name: (identifier [1, 6] - [1, 11])
(assignment [2, 4] - [2, 10]
left: (identifier [2, 4] - [2, 5])
right: (integer [2, 8] - [2, 10]))
(identifier [2, 12] - [2, 13]))
(method [5, 2] - [7, 5]
name: (identifier [5, 6] - [5, 11])
(assignment [6, 4] - [6, 10]
left: (identifier [6, 4] - [6, 5])
right: (integer [6, 8] - [6, 10]))
(identifier [6, 12] - [6, 13]))))
In both cases the bit after the semicolon just parses as (identifier).For some use cases (e.g. syntax highlighting, depending on your colorization rules) it doesn't matter, and so we don't want to pay the cost. If it does matter (like in an actual implementation), then you'd have to implement this yourself and drive it by the parse tree you get from tree-sitter.
So you can't read anything from a method call! I can make it so, if you're doing a class method (of any kind) you have to invoke the constructor, as described in "What is a method?" There's also a few new techniques like "new_class_method", which requires creating an object (of some kind) for that class... but what about that? It's not "I've just fixed Tree-sitter's problem"; it's that Tree-sitter hasn't yet resolved the problem yet - there are other parsing problems besides Tree-sitter in Ruby itself like those of classes (and classes are not part of Tree-sitter) and things that are known as "type-traits" and so on - so as it's not quite enough it can be done by other things. The reason for using LR grammar is that when it comes to this - what do I want from that grammar?
The point I'm making here is that LR doesn't give a reason for what you're doing. As a programmer you are trying to write code that is portable because - if it works in a domain you don't understand (such as Ruby) - then you don't know what you're doing is wrong. There can be a domain (as in any language) that's a lot more complex than this - but since we've got that, how can I be sure it won't mess up the code I'm writing?
What code? The parser? How can it be simpler than its syntax? It has syntax and semantics, which is strictly more than the syntax.
> The point I'm making here is that LR doesn't give a reason for what you're doing.
What do you mean 'what you're doing'?
One thing I love about tree-sitter is how both the grammar and the resulting ASTs are so readable. I can come back to this project after months of not contributing and pick up right where I left off.
With tree-sitter you're hand-writing a 1k file. With Bison you're hand-writing a 13k file.
- https://github.com/tree-sitter/tree-sitter-ruby/blob/32cd5a0... - https://github.com/tree-sitter/tree-sitter-ruby/blob/32cd5a0...
So about 2k loc.
The trickiest (and most verbose) parts of the external scanner have to do with heredocs and the various ways to declare literals (strings, symbols, regexes, etc).
https://github.com/tree-sitter/tree-sitter-ruby/blob/master/...
I find it absolutely amazing that a grammar for something as complicated as Ruby can be so concise. Less than a thousand lines. The corresponding Bison grammar is 13k lines. And I think the tree-sitter one is scannerless so also includes the lexer?! How do they do it?