Hacker News new | ask | show | jobs
by ahelwer 1892 days ago
Here I am on day three of an attempt to modify a rust program with logic that would have taken about twenty minutes to implement in C# or Java. It has to do with operations on strings (use a regex to find a string in some text, escape all the regex-reserved characters in that string, then use the string as a regex to find other occurrences of itself in the text) so I get that I'm really thrown into the borrow-checker deep-end, but man writing rust feels like working on one of those puzzles where you try to fit a set of tiles inside a rectangle. You'll almost get it but then some edge is sticking out. So you move the tiles around but this leads to two edges sticking out now! So you do a whole bunch of additional exploring before ending up right back where you started with the one edge sticking out.

I even abandoned the learn-by-stackoverflow-search approach to rust and read the first seven or so chapters of the rust book. But even that hasn't helped very much. I know I am just currently in the painful, frustrating stage of learning where nothing really makes sense, and at some future point it will all click, but it really can't be overstated just what a wicked learning curve this language has.

12 comments

I don't know if this helpful, but from your description of your problem, here's how I'd do that in Rust:

- Use a regex to find a string in some text: use regexes, get a &str

- Escape all the regex reserved characters in that string: Okay this requires mutation but we can't mutate a &str, let's loop over the &str's characters (using the Chars iterator) and push them into a new String with the proper escapes

- The new String we own, so we can easily turn it into a regex and use it to search the original string.

That's where I always end up when I start playing with Rust too. At this point I genuinely think I've given up on belief that the "borrow checker" paradigm is ever really going to be broadly successful (vs. "screw it, just do it in <other managed language>").

Problems with well-constrained allocation paradigms do really well in rust. Problems with reading/parsing/storing messy data structures from external data just really, really hurt.

I think a big part of the problem is that the ownership analysis layer has the dual crises of being (1) really complicated and too hard to keep in your head as a single model and (2) specified in an ad-hoc way that resists formal rules. So eventually you end up in of those puzzles like you mention where... there's just no answer! No one's been where you have, and the lack of rigor means you end up trying to reverse engineer the compiler front end trying to find the trick that works. And then you give up and write it in Go or Python or whatever.

No, borrow checker's formal rules are rather simple. Here it is:

It is an error to access a place, when an access conflicts with a loan, and the loan is live.

You're describing the desired behavior of the compiler, but not what it actually implements.

In fact there are an infinite number of correct Rust programs which will never access memory incorrectly but which will still be rejected by the compiler, for the simple reason that Rust's authors, talented though they may be, have made no progress at all at solving the Halting Problem.

What Rust actually accepts is a subset of correct programs. And this subset is somewhat informally defined as "whatever the compiler could manage to prove". Real code hits against this limit occasionally, and when it does there's really no option other than "join the Rust team" or "try voodoo".

There are two problems here. Rust accepts a subset of correct programs that involve borrowing, that's absolutely true, but it gives you the tools to get around that: you either use Arc<Mutex<T>, where now you are in the same space that any refcounted GC language is in (with the extra flexibility that if you can "choose your guarantees"[1]), or use unsafe which lets you dereference memory freely in the same way that you would in C or C++.

The unstated problem is that there are patterns from other languages that are actually invalid and Rust is correctly denying, but not properly communicating it, which causes frustration. You can always make the argument that the failure modes are benign or rare, and for those cases noone is stopping you from actually using unsafe.

[1]: https://manishearth.github.io/blog/2015/05/27/wrapper-types-...

> The unstated problem is that there are patterns from other languages that are actually invalid

And this is why this argument persists. This is, to people outside the community, a semantic evasion:

1. It relies on a Rust-internal definition for "actually valid" (conforming to Rust's specific set of provability requirements) that doesn't correspond to what the rest of the world views as "correct" (not behaving incorrectly). Think about stuff like allocate-through-a-session-and-free-in-a-block paradigms (Apache was famous for this), or run-once-and-exit, or garbage collection, etc... Those things aren't "invalid" in any reasonable sense, they're just not what Rust programs do.

2. The definition for "valid" is (and this is my point above) entirely Rust-internal and ad hoc. It's not that we refuse to conform to your rules, really, it's that we don't know what they are!

There's an anecdote about a similar effect in the article too:

> Fun fact: while at Mozilla I heard multiple anecdotes of [very intelligent] Firefox developers thinking they had found a bug in Rust's borrow checker because they thought it was impossible for a flagged error to occur." However, after sufficient investigation the result was always (maybe with an exception or two because Mozilla adopted Rust very early) that the Rust compiler was correct and the developer's assertions about how code could behave was incorrect. In these cases, the Rust compiler likely prevented hard-to-debug bugs or even exploitable security vulnerabilities. I remember one developer exclaiming that if the bug had shipped, it would have taken weeks to debug and would likely have gone unfixed for years unless its severity warranted staffing.

Sometimes people think that they're right and that the compiler is wrong, but often, (not not necessarily always!) it's that they forgot some important piece of context.

(Oh, and Rust absolutely can do "allocate in a block and free at once" stuff, or "run once and exit" stuff...)

Rules are actually simple and not ad hoc. I in fact think Rust documentation should state rules and make them more prominent. In my experience, knowing the rules does not help you to keep them, but at least we will get less complaints of the sort "we don't know what the rules are".

To recap, here are the rules:

It is an error to access a place, when an access conflicts with a loan, and the loan is live. Access means source code construct to read or write, whether the construct is actually executed in runtime is irrelevant. Place is static approximation of a set of bytes in the memory. Place is either local (x), field (x.f), or upvar (local captured in closure). Indexing (x[i]) and method calls (x.m()) are approximated as local (x). Loan is either shared (&x) or mutable (&mut x). Read access conflicts with mutable loan, write access conflicts with all loans. Expression is live from the definition to the end of the containing statement. Declaration is live in connected region of control flow graph from the definition to potentially multiple last uses.

No, the rule I stated is what is actually implemented.

Like all type checking technology, the rule is about static behavior, not dynamic behavior. So halting problem is irrelevant.

Rust accepts all programs that follow the rule in static behavior. It's not a subset, and it's not a best effort to prove. It's the complete statement.

"Access" means source code construct to read or write. Rust doesn't care whether the construct is actually executed in runtime.

I believe the "subset of correct code" refers to things like the current non-support for things like

    let (left, right) = (&mut foo[..split], &mut foo[split..]);
where you have to rely on things like foo.split_at_mut(split), which are implemented as unsafe under the cover.

I see these are limitations but not show-stoppers in any way.

It's surely not a showstopper when it's on one line, but again the problem in practice is that those slices may be other operations or hidden behind other APIs, and happening at different depths in a complicated call tree, and you get an error only up at the top when what you're doing looks "clearly correct".

Yes, sure, the rule in question may be simple in the abstract (it's a compiler, after all -- they're just software doing straightforward things). But the ability for the poor programmer to detect which rule is being violated where is a lot more limited than the compiler is designed for. Thus the user with the upthread complaint, which is hardly unique.

I mean, at the end of the day if Rust wants to be an everyday language for this kind of everyday problem, the analysis paradigm needs to be communicated much better to everyday hackers (via docs, error messages, whatever). When Rust was new this seemed like just a technical problem to be solved with software maturity. I guess at this point after several years of regularly returning to play with Rust and being frustrated every time, I've mostly given up.

Yes, but the rule is still complete. What is going on is "place" is currently defined to be local x, or field x.f, or index x[i], but all x[i] are merged to x[*]. Rust's so-called NLL changed definition of "live". The point is, the rule is simple, formal, and complete. It is neither an ad hoc rule nor a best effort prover.
Link to the relevant spec then? Admittedly my intuition is a year stale at the moment, but I went looking hard for this at one point and found nothing other than source code.
No spec, because technical details are complex, but spec, when it is written, will just be an elaboration of "It is an error to access a place, when an access conflicts with a loan, and the loan is live". This has never changed since Rust 1.0, more than 5 years. Spec will just need to define "place" and "live" and that takes time.
There's no spec yet, but there are working groups working on it.
While this can be painful, my basic suggestion -- clone more. The temptation is to never clone, butin C++ people run copy constructors all the time without thinking about it (as they are called automatically).
I clone all the time to make the borrow checker leave me alone. If this code shows up as a hot spot in a profile I can spend the extra energy to optimize it.

I hope to evolve my understanding so that I can do the right thing at design time to minimize unnecessary clone()s.

Cloning means runtime inefficiency, but the cost is so pathetically small compared to the amount of code we run in Python and Java with their larger footprints. Yes. Clone more.
This is one of the psychological traps I've experienced writing Rust: in Python or Java you can't hope to make things more efficient at a certain point. You couldn't get clever with string re-use if you tried, etc. So you're content to just move forward and be practical.

Whereas in Rust you know that it's probably possible to use a &str there instead of a String. If you just bang on it a little longer, you can make it just a bit more optimal. The clone()s and the Vec::new()s are all explicit, making them feel heavier than those other languages, when in reality they're still quite a bit lighter.

You usually don't have to optimize them out! Your code will probably be faster than Java even with a bunch of clones, etc! But there's this temptation that's really hard for programmers to resist, to sink hours and hours into making things just slightly more optimal given the opportunity. You have to consciously talk yourself out of that if you want to be productive.

> The clone()s and the Vec::new()s are all explicit, making them feel heavier than those other languages, when in reality they're still quite a bit lighter.

That's… not exactly true. `Vec::new` is completely free so that's a different debate, but much as in C++ or C allocations in Rust are much more expensive than in managed languages:

1. system allocators genuinely suck, all of them, though some more than others (iirc macos' is especially bad)

2. managed languages can much more easily specialised allocation strategies (freelists, bump allocators, type-custom allocation strategies), this is either difficult / impossible (no custom allocators) or way more painful (manually pass custom allocators in) in Rust

As a result, allocations in Rust are really tremendously slow by default, it's not too rare to see questions about Rust programs which are slower than managed equivalents, in release mode. Because the rust program is allocation heavy (relatively), it might have 10% the allocations of the non-rust program but the allocations are 100 times more expensive.

Interesting, I hadn't heard this before. Do you have any resources I could read?

Edit: I do know that languages where strings are immutable use that fact to do lots of optimization (automatically sharing "copied" strings and just cloning reference-counters, for example), but you can accomplish some of this in Rust too with Rc<> or even persistent data structures if you really want to. And of course there are cases where it's more efficient to actually mutate a string, which these languages can't do. But it sounds like you're talking about something else?

There are three strategies that other languages use automatically that you can also apply in Rust by hand: string interning, small string optimization and "Copy-on-write". For the first and second, you can use some existing crate like https://docs.rs/string-interner/0.12.2/string_interner/ and https://github.com/rust-analyzer/smol_str. For the later, you can use Cow<'_, str> https://doc.rust-lang.org/std/borrow/enum.Cow.html. I'm having trouble thinking of a case where Rc<String> would be appropriate.
> You have to consciously talk yourself out of that if you want to be productive.

That's 100% correct but there's one problem: how do you write down your future improvement opportunities?

Skip 2 optimizations here, 3 optimizations there and after 30 PRs you can easily have notes spanning 100 bullet points on what to optimize one day in the future Soon™.

I am still not at what I'd call an expert level in Rust -- and the skill ceiling is quite high -- so maybe with time I'd start to automatically spot these optimization opportunities?

But before I am at that level I'd prefer to either keep hugely long notes or, ideally, have a tool recommend optimizations to me.

The simplest tool is to just grep for various constructs that copy or allocate. Clippy, the Rust linter, will find some of these (as well as other issues, some of which tend to cause performance problems).

The better tool is to profile the code, then optimize hotspots. There's a Rust Performance Book with a list of profilers known to work with Rust programs[1]. Rustc does support Profile Guided Optimization[2] which is often pretty good at speeding up the output even without any code changes.

[1] https://nnethercote.github.io/perf-book/profiling.html [2] https://doc.rust-lang.org/rustc/profile-guided-optimization....

Helpful links, thank you!

> The simplest tool is to just grep for various constructs that copy or allocate.

Do you happen to have a curated list somewhere?

The vast majority of the code you write is glue code, you can be as inefficient when writing it as you want and it doesn't matter. Optimize only what the profiler tells you your cpu is spending real time in.
Doesn't the compiler optimize out clones when it can?
There's a Cargo tool called "clippy" which will tell you if it finds unnecessary clones.
Probably not; detecting unnecessary clones is expensive for non-trivial cases. If a human can't prove that the clone isn't necessary (by writing the code in such a manner that the borrow checker accepts), I doubt the compiler could.
No, not really.
I've never seen C++ that isn't cautious about copying as little as possible.
A big difference is that C++ copies implicitly. Rust has the opposite default, it will never clone implicitly.
People do think about this all the time in C++ and it is very common to get code review comments about avoiding copies if you overlook such things.
Yup, and I wish the compiler would give a warning on a use of a copy constructor...
> use a regex to find a string in some text, escape all the regex-reserved characters in that string, then use the string as a regex to find other occurrences of itself in the text)

This took 2 min: https://play.rust-lang.org/?version=stable&mode=debug&editio...

and I don't recall what's the last time I used the regex crate.

I'm certainly doing all the regex stuff incorrectly, no idea how to match regex-reserved characters, no idea if I'm using the iterator API correctly (its the second example in the regex docs..), etc.

But the idea is simple, search the text for the regex, escape all the reserved characters, use that regex to search for occurrences of the same regex.

I guess that if you want a fully-vectorized, zero-allocation, zero-copy, multi-threaded, gpu... version of this that maxes out the theoretical peak of the hardware available, then you'll probably end up putting a considerable amount of work here.

But otherwise, for someone with literally 30 seconds of experience with the regex crate, it was as straightforward to implement as you mentioned. Just c&p their second example, and just added some glue. This is how I'd implement this in Python, or Java...

EDIT: other responses suggest cloning, but that seems to suggest that one has to explicitly and deliberately think about cloning. I instead just played a game of type golf using functions like `.to_string()` or `.collect` to "clone" stuff behind the scenes. My code is probably horrible, but is the first thing that came to mind.

Also, this function is just regex::escape https://docs.rs/regex/1.4.5/regex/fn.escape.html
You can't learn Rust by StackOverflow, that just doesn't work. It is well known, I repeat this everytime I see it happening, and they never listen. It's deeply frustrating.
You can't learn any language by StackOverflow, but some languages are more forgiving to people who don't actually know them.

If you don't really know Rust and in particular understand the borrow checker and how it works (and why it exists), you are going to have a bad time.

Rust in general has a learning curve, but once you do learn it there is a payoff. If Rust code compiles (and if you didn't use 'unsafe'), you can be pretty damn sure it is free of a long list of potential bugs including concurrency bugs, buffer overflows, null pointer dereferences, alignment problems, double-free, use-after-free, stack smashing, most memory leaks, and so on. The runtime cost of that assurance is less in Rust than in any other language I know, and sometimes the safety of Rust actually lets you do things in higher performing ways that would be dangerous to code in C. In C/C++ you have to knit your own straightjacket, and yours may not be as well thought out or efficient as the one the Rust compiler issues you.

I've really started to appreciate Rust's safety. If the code compiles there can still be bugs, but they're going to be higher up at the logic/algorithm level instead of the annoying sorts of bugs that constantly crop up in C/C++.

I mean yeah, that's why I went and read a bunch of the book. But I still run into lots of issues. Like what the hell is a borrow cow and when would I want to use it? And how do I fix the "temporary value dropped when borrowed" error in a series of maps on options?
I would recommend asking questions on the subreddit. There's a standing thread for asking questions, and people usually give high-quality help and respond quickly
If you don't mind sharing snippets for the diagnostics you couldn't figure out, I would love to see them in case we can mechanically suggest the appropriate code, but in general a well placed .clone(), .cloned() or .collect() will be what you want. Likely what's happening is you're iterating over a sequence (you can think of options as a sequence of only one iteration as well), but that iterator is over borrows of the sequence's data, instead of owned values. If you try to return that outside of its enclosing function, you will see that error.

As for a "borrow cow", you probably want to read https://doc.rust-lang.org/std/borrow/enum.Cow.html and https://deterministic.space/secret-life-of-cows.html. For what you are doing, it is an optimization to avoid unnecessary clones that is absolutely not required: make your code work with String and worry about Cow<'_, str> later.

Sure! So I'm trying to implement this feature: https://github.com/tree-sitter/tree-sitter/issues/982#issuec...

And here's my branch (you can see the latest commits to see the file I'm modifying): https://github.com/ahelwer/tree-sitter/tree/testfile-separat...

I haven't had a chance to go through and add clones everywhere, and will be away at a PT appointment for the next hour or so, but would appreciate any pointers you can give.

It passes cargo check with this change.

  diff --git a/cli/src/test.rs b/cli/src/test.rs
  index ac4807bf..8b9882ab 100644
  --- a/cli/src/test.rs
  +++ b/cli/src/test.rs
  @@ -410,16 +410,17 @@ fn parse_test_content(name: String, content: String, file_path: Option<PathBuf>)
           .map(|b| String::from_utf8_lossy(b).to_string())
           .map(|s| escape_reserved_regex_chars(&s));
       
  -    let suffixHeaderPattern : Option<String> = suffix
  +    let suffixHeaderPattern : Option<String> = suffix.as_ref()
           .map(|s| String::from(r"^===+") + &s + r"\r?\n([^=]*)\r?\n===+" + &s + r"\r?\n");
       
  -    let suffixDividerPattern: Option<String> = suffix
  +    let suffixDividerPattern: Option<String> = suffix.as_ref()
           .map(|s| String::from(r"^---+") + &s + r"\r?\n");
   
  -    let headerRegex = suffixHeaderPattern
  -        .and_then(|s| ByteRegexBuilder::new(&s[..]).multi_line(true).build().ok())
  -        .as_ref()
  -        .unwrap_or(&HEADER_REGEX);
  +    let headerRegexFromSuffixHeaderPattern = suffixHeaderPattern.as_ref()
  +        .and_then(|s| ByteRegexBuilder::new(&s[..]).multi_line(true).build().ok());
  +
  +    let headerRegex = headerRegexFromSuffixHeaderPattern
  +        .as_ref().unwrap_or(&HEADER_REGEX);
   
       // Identify all of the test descriptions using the `======` headers.
       for (header_start, header_end) in headerRegex
Thank you for this! the .as_ref() seems to solve a lot of the problems, since I guess just doing a straight .map() takes ownership of the contained string.
The error output for your code is

    error[E0382]: use of moved value: `suffix`
       --> cli/src/test.rs:416:48
        |
    406 |     let suffix = FIRST_HEADER_REGEX
        |         ------ move occurs because `suffix` has type `std::option::Option<std::string::String>`, which does not implement the `Copy` trait
    ...
    414 |         .map(|s| String::from(r"^===+") + &s + r"\r?\n([^=]*)\r?\n===+" + &s + r"\r?\n");
        |          ------------------------------------------------------------------------------- `suffix` moved due to this method call
    415 |
    416 |     let suffixDividerPattern: Option<String> = suffix
        |                                                ^^^^^^ value used here after move
        |
    note: this function consumes the receiver `self` by taking ownership of it, which moves `suffix`
       --> /Users/ekuber/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/src/rust/library/core/src/option.rs:451:38
        |
    451 |     pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Option<U> {
        |                                      ^^^^
    
    error[E0716]: temporary value dropped while borrowed
       --> cli/src/test.rs:419:23
        |
    419 |       let headerRegex = suffixHeaderPattern
        |  _______________________^
    420 | |         .and_then(|s| ByteRegexBuilder::new(&s[..]).multi_line(true).build().ok())
        | |__________________________________________________________________________________^ creates a temporary which is freed while still in use
    421 |           .as_ref()
    422 |           .unwrap_or(&HEADER_REGEX);
        |                                    - temporary value is freed at the end of this statement
    ...
    425 |       for (header_start, header_end) in headerRegex
        |                                         ----------- borrow later used here
        |
        = note: consider using a `let` binding to create a longer lived value

You need to change line 413 to turn `suffix` into an `Option<&str>` to avoid taking ownership of it:

    let suffixHeaderPattern: Option<String> = suffix
        .as_ref()
        .map(...);
and you need to change the `headerRegex` extraction to turn it also into an `Option<&str>` from an `Option<String>` by using `.as_ref()` before the `.and_then` call, which lets you avoid the `&s[..]` reborrow, that only lives until the end of that closure:

    let headerRegex = suffixHeaderPattern
        .as_ref()
        .and_then(|s| ByteRegexBuilder::new(s).multi_line(true).build().ok())
        .unwrap_or(HEADER_REGEX.clone());
Edit: I would also consider this to be a diagnostics bug, for at least the first case rustc should have suggested .as_ref(). For the second part, the compiler would ideally have pointed at the `&s[..]` as part of the problem, and the sibling comment has the change you likely want.

Edit 2: to further drive the point home that this should be a bug, this is the current output for a similar case while I was trying to minimize this:

    error[E0308]: mismatched types
      --> src/main.rs:10:22
       |
    10 |         .map(|s| bar(s));
       |          ---         ^ expected `&Struct`, found struct `Struct`
       |          |
       |          help: consider using `as_ref` instead: `as_ref().map`
https://play.rust-lang.org/?version=stable&mode=debug&editio...
The latter change introduces an unnecessary clone(). The correct approach is to instead introduce a variable so that headerRegex can keep being a reference to either that variable or to HEADER_REGEX.
> into an `Option<&str>` from an `Option<String>`

`Option::deref` is a good choice here.

I have to agree that at the beginning those unfortunate names (Arc, Cow) confused me more than they should, given that I know perfectly well what Copy on Write and reference count means.

If only it was spelled CoW and ARC I would very quickly grasp it (probably).

But it's an extremely minor inconvenience and arguably makes the whole thing very memorable

1. You mean Cow<'a, str>? It's an enum of either String or &'a str, typically used with 'static so you can either store an heap-allocated string or a static string without copying it to the heap

2. Given you mention "maps of option", probably you are doing something like opt.map(|x| &x.field) or equivalent which is obviously invalid (since you are returning a reference to a subobject of a function argument). Instead, you need to do opt.as_ref().map(|x| &x.field) (or as_mut() if it's mutable).

Clone more.
See that's seemingly good (and popular) advice, which I didn't find anywhere in the book.
I had a similar experience. It really made me question whether the borrow checker is worth it. I'm all for memory safety, but there are some pretty compelling low pause GCs out there now that can give you memory safety without the ergonomic problems.
I'm in a similar position. I highly recommend posting snippets of what you're trying to do in the Rust discord. The people are super nice and very helpful.

Honestly the friendliness of the community is a big part of why I'm learning Rust. I found it so much harder to get free help from volunteers with c code issues. Not that I deserve it or anything, but it's really nice and I try to contribute back by writing PRs to improve the docs once I understand something.

As someone who's been there, I strongly recommend asking for help (maybe posting this on HN counts?). Bashing your head against the wall against Rust without some correct code to look at is not (IMO) a great way to learn the language... I definitely get the appeal, as someone who hates asking for help, but it's just not worth it in this case, especially when you're starting out and aren't even sure what terms to look for.
Short suggestion: If performance isn't critical, sprinkle some .clone()s in there on your strings and the borrow-checker will get off your back

Long suggestion: Working with strings in Rust requires that you have a C++ mental model of what strings are and what properties (in the abstract sense of the word) they have. Rust will make sure you can't misuse them, and it will make them as ergonomic as they can be within that C++-like model, but it's a fundamentally different concept from what you have in languages like C# and Java, and if you're trying to look at things from that other perspective, you're going to keep running into walls over and over.

More broadly, when you're using Rust you need to be in the basic mindset of a C++ programmer. From there Rust will make things significantly easier in many ways, but if you don't start with the right mental model, you're going to have a bad time. Strings are probably the most striking case of this because the way they get treated in virtually all higher-level languages is so wildly different from the way they get treated at the low-level.

This is my experience with the language. It's a good language - but it's a tool at the end of the day. One with edges and trade offs like any other. I can't help but think the people like the author only feel "giddy" because its been hyped up on this website.
That seems rather trivial, and I'm not sure how you could possibly have any issues with the borrow checker.

Just do the first regex search, then escape into a String with regex::escape, then build a regex from the String and search with it.

Of course what you are doing is probably wrong since you should use a string matching crate rather than escaping characters and using a regex matching crate.

Since it's so trivial, I would be quite happy for you to implement the feature for me! https://github.com/tree-sitter/tree-sitter/issues/982

Just lol at the idea that you won't run into borrow checker issues when dealing with strings though.

Solving that issue isn't trivial. I just read it and I wouldn't know where to begin, probably because I don't understand the requirements.

I think what's being called "trivial" is doing a bit of regex searching. It's probably accurate to call that trivial for an experienced Rust programmer, but if you're just beginning, I don't think it's helpful to call anything trivial. I still remember my first exposure to Rust. It was different. It took a bit to grok. But once it clicked, things were much better.

As the maintainer of the regex crate, I invite you or anyone to ask for help using regexes. The regex repo has Discussions opened up, so it's appropriate to ask for help, even if they are beginner questions: https://github.com/rust-lang/regex/discussions

As usual though, try to provide as many details as you can. Giving the source code you have but can't get to work is a great start, for example.

Looking at their current iteration of the code, the problem is not in the regex crate's API, but rather on them not having the ownership system internalized yet.

https://doc.rust-lang.org/rust-by-example/ and https://github.com/rust-lang/rustlings might be good places to visit to gain that experience, as well as improving the specific diagnostics to be more actionable.

> the problem is not in the regex crate's API, but rather on them not having the ownership system internalized yet

Yes, I figured that to be the case, but still wanted to say, "Discussions are open for questions, even if they are beginner related, as long as it's related to regex'ing somehow." :-)

I'm not the person you replied to, but here's some Playground code I just wrote that might be helpful for you, based on the issue you linked:

https://play.rust-lang.org/?version=stable&mode=debug&editio...

I wrote code recently to parse a file with regexes and generate rust code from it. It's get-the-job-done code, not super clean, but you might find it useful.

https://github.com/danielzfranklin/drm-fourcc-rs/blob/main/b...

Maybe this can help you, I looked at your fork of tree-sitter to figure out what you were trying to do.

https://play.rust-lang.org/?version=stable&edition=2018&gist...

But what you really want is to get your head around ownership and borrowing, especially when it comes to the scoped RAII that rust has (e.g. freeing a String while a reference to it still exists and is being used would give you the "temporary value dropped while borrowed" error).

Yes, lots of up front things that pay off after your code base gets larger and/or you ship.