ifcparse: give the tokenizer a compile-time policy for what it decodes

spf_lexer::next() becomes next<Policy>(). full_tokens, the default, is
what the parser has always had. index_tokens is what the lazy index
needs: a string is ended but not decoded, and a number, enumeration or
binary comes back as Token_LITERAL with only its position; names,
keywords and operators are read as before. Each policy compiles to its
own loop from the one implementation, so there is no second tokenizer.

character_decoder gains skip(): the same state machine as the
conversion with the collection compiled out, so an escape such as \S\'
(an apostrophe as the page character) ends the string at the same byte
under both policies. A byte-level scan would have ended it early.

Also fixes a comment that follows a token without whitespace, ",/* x */",
which skip_comment() never saw because the slash had been consumed.

TXG (58 MB), single thread: tokenizing the whole file 194 MB/s with
full_tokens, 249 MB/s with index_tokens; through 64 KB pages 196 and
205 MB/s. The parse itself is unchanged.

This commit was written by an AI coding tool and has not been verified by
a human.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wcN7XquTfUi4vsKQ4KchL
This commit is contained in:
Dion Moult
2026-09-14 07:08:48 +10:00
parent a026fc698c
commit 9829ebf001
6 changed files with 150 additions and 9 deletions
@@ -294,3 +294,51 @@ TEST_CASE("Only a 22-character GlobalId is indexed", "[ifcparse]") {
wall.set_attribute_value(0, std::string("1F$7lN9$r5MOA_lpAoNM52"));
CHECK(file.instance_by_guid("1F$7lN9$r5MOA_lpAoNM52").id() == 2);
}
TEST_CASE("The index token policy ends every token where the full policy does, without decoding", "[ifcparse]") {
// Doubled quotes, a \S\' escape (an apostrophe as the page character,
// which a byte scan would take for the end of the string), a \X2\
// escape, a comment, binaries, enumerations, numbers and names.
const std::string data =
"#1=IFCWALL('it''s','a\\S\\'b','\\X2\\00E9\\X0\\c',/* #9 */ #2, \"0A\", .T., -1.5E-3, 42, $, *, (IFCLABEL('x'), #3));\n";
ifcopenshell::file_reader<ifcopenshell::full_buffer_impl> full_reader(data, ifcopenshell::caller_fed_tag{});
ifcopenshell::file_reader<ifcopenshell::full_buffer_impl> index_reader(data, ifcopenshell::caller_fed_tag{});
ifcopenshell::spf_lexer<ifcopenshell::file_reader<ifcopenshell::full_buffer_impl>> full(&full_reader), index(&index_reader);
size_t count = 0;
std::vector<unsigned> names;
while (true) {
ifcopenshell::token a = full.next(), b = index.next<ifcopenshell::index_tokens>();
REQUIRE((bool)a == (bool)b);
if (!a) {
break;
}
++count;
CHECK(a.start_pos == b.start_pos);
CHECK(full_reader.tell() == index_reader.tell());
if (a.is_identifier()) {
REQUIRE(b.is_identifier());
CHECK(a.as_identifier() == b.as_identifier());
names.push_back(b.as_identifier());
} else if (a.is_keyword()) {
REQUIRE(b.is_keyword());
CHECK(a.as_string() == b.as_string());
} else if (a.is_operator()) {
REQUIRE(b.is_operator());
CHECK(a.value_char == b.value_char);
} else if (a.is_string()) {
CHECK(b.type == ifcopenshell::token::Token_STRING);
} else {
CHECK(b.type == ifcopenshell::token::Token_LITERAL);
}
full.reset_pool();
index.reset_pool();
}
CHECK(count == 34);
CHECK(names == std::vector<unsigned>{1, 2, 3});
// And the full policy decoded the escapes.
ifcopenshell::file_reader<ifcopenshell::full_buffer_impl> again(data, ifcopenshell::caller_fed_tag{});
ifcopenshell::spf_lexer<ifcopenshell::file_reader<ifcopenshell::full_buffer_impl>> lexer(&again);
lexer.next(); lexer.next(); lexer.next(); lexer.next();
CHECK(lexer.next().as_string() == "it's");
lexer.next();
CHECK(lexer.next().as_string() == "a\xc2\xa7" "b");
}