Skip to content

Use compile-time matchers for RegEx expressions - #1500

Open
alex-thiessen-for-siemens wants to merge 2 commits into
jbeder:masterfrom
alex-thiessen-for-siemens:fix/regex-constexpr
Open

alex-thiessen-for-siemens wants to merge 2 commits into
jbeder:masterfrom
alex-thiessen-for-siemens:fix/regex-constexpr

Conversation

@alex-thiessen-for-siemens

@alex-thiessen-for-siemens alex-thiessen-for-siemens commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This PR modernizes the internal regular expression engine used by the
scanner and emitter, converting expressions to compile-time matchers
implemented in C++11.

Performance & Allocation Benchmarks

A dedicated microbenchmark (regex_benchmark in
util/regex_benchmark.cpp) was executed on Linux x86_64 to compare
against the baseline engine:

  1. Heap Allocations:
    • Baseline regex construction and matching: dynamic vector
      allocations during initialization and recursive evaluation.
    • New compile-time matchers: 0 dynamic heap allocations across
      construction, pattern matching, and destruction.
  2. Matcher Evaluation Latency:
    • Exp::Word: ~288 ns / iteration (0 allocations)
    • Exp::Chomp: ~22 ns / iteration (0 allocations)
    • Exp::DocStart: ~15 ns / iteration (0 allocations)
  3. Scanner / Emitter Integration:
    • Full parser throughput on realistic YAML documents is preserved or
      improved due to elimination of pointer indirection and heap lookups
      in hot scanning loops.

Verification

  • Added boundary tests, short-circuiting regression checks, and
    multi-character negation tests in test/regex_test.cpp.

Replace scanner and emitter expressions with private C++11 compile-time
matchers. Eliminate runtime AST construction, dynamic vector storage,
and static destruction in YAML::RegEx.

Keep stream matcher instantiation inside yaml-cpp so Windows DLL
consumers do not require unexported Stream internals.

Add compile-time and boundary tests and a reproducible benchmark for
matching and parser/emitter throughput.

Reference verification: not applicable
This changes only an internal matcher representation and lifetime
behavior; it does not change YAML syntax, schema resolution, or native
conversion semantics.
Add an integration death test covering jbeder#1281 to verify that emitting
from a static object's destructor during program termination does not
wrap plain scalars in double quotes due to destroyed matchers.

Add static assertions covering jbeder#1471 ensuring RegEx and CharBitSet
remain trivially destructible and trivially copyable.

Reference verification: not applicable
This covers static deinitialization emission to prevent regressions and
asserts compile-time trivial destructibility; it does not change YAML
syntax, schema resolution, or native conversion semantics.

Signed-off-by: Alex Thiessen <alex.thiessen@siemens.com>

@SGSSGene SGSSGene left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't read finished reading this PR yet, but I already have a few questions that I inlined.

As quick a recap: This replaces the RegEx implementation with a new implementation that does not rely on heap allocations (no malloc). This implies that there is nothing the destructors have to do for cleanup.
This in turn has the nice property, that if we enter a 'destructed' class, it still function as expected, since nothing has practically been destroyed?

Comment thread src/regex_yaml.h
Comment on lines +228 to +231
static int Match(const Source& source) {
const int first = First::Match(source);
return first >= 0 && Tail::Match(source) >= 0 ? first : -1;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how And is dealing with different length matching? To me it seems they should all have the same length? (maybe?)

Suggested change
static int Match(const Source& source) {
const int first = First::Match(source);
return first >= 0 && Tail::Match(source) >= 0 ? first : -1;
}
static int Match(const Source& source) {
const int first = First::Match(source);
return first >= 0 && Tail::Match(source) == first ? first : -1;
}

Comment thread src/regex_yaml.h
Comment on lines +248 to +250
static int Match(const Source& source) {
return Pattern::Match(source) >= 0 ? -1 : 1;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought Match is returning the number of matched characters?

Why is the return value 1 and not 0? (I am not 100% what I would expect from a Nor operator)

Comment thread src/exp.cpp
Comment on lines +15 to +19
template <typename Pattern>
const RegEx& Matcher() {
static constexpr RegEx expression = MakeStreamRegEx<Pattern>();
return expression;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correct, this is the only line that has a static member. (Which of course is instantiated many many times). Is that correct?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants