Remove destruciveness footguns and add further documentation

This commit is contained in:
Elias Batek 2025-02-12 02:13:34 +01:00
parent 5c7538421f
commit a2fe6f1fb4
1 changed files with 126 additions and 24 deletions

142
ini.d
View File

@ -17,6 +17,51 @@
return parseIniDocument(readText(filePath)); return parseIniDocument(readText(filePath));
} }
--- ---
### On destructiveness and GC usage
Depending on the dialect and string type,
[IniParser] can operate in one of these three modes:
$(LIST
* Non-destructive with no heap alloc (incl. `@nogc`)
* Non-destructive (uses the GC)
* Destructive with no heap alloc (incl. `@nogc`)
)
a) If a given dialect requests no mutation of the input data
(i.e. no escape sequences, no concaternation of substrings etc.)
and is therefore possible to implement with slicing operations only,
the parser will be non-destructive and not do any heap allocations.
Such a parser is verifiably `@nogc`, too.
b) In cases where a dialect requires data-mutating operations,
there are two ways for a parser to implement them:
b.0) Either perform those mutations on the input data itself
and alter the contents of that buffer.
Because of the destructive nature of this operation,
it can be performed only once safely.
(Such an implementation could optionally fix up the modified data
to become valid and parsable again.
Though doing so would come with a performance overhead.)
b.1) Or allocate a new buffer for the result of the operation.
This also has the advantage that it works with `immutable` and `const`
input data.
For convenience reasons the GC is used to perform such allocations.
Use [IniParser.isDestructive] to check for the operating mode.
The construct a non-destructive parser despite a mutable input data,
specify `const(char)[]` as the value of the `string` template parameter.
---
char[] mutableInput = [ /* … */ ];
auto parser = makeIniParser!(dialect, const(char)[])(mutableInput);
assert(parser.isDestructive == false);
---
+/ +/
module arsd.ini; module arsd.ini;
@ -336,6 +381,18 @@ struct IniParser(
public { public {
/// ///
alias Token = IniToken!string; alias Token = IniToken!string;
// dfmt off
///
enum isDestructive = (
(operatingMode!string == OperatingMode.destructive)
&& (
dialect.hasFeature(Dialect.concatSubstrings)
|| dialect.hasFeature(Dialect.escapeSequences)
|| dialect.hasFeature(Dialect.lineFolding)
)
);
// dfmt on
} }
private { private {
@ -364,17 +421,16 @@ struct IniParser(
public { public {
/// ///
bool empty() const { bool empty() const @nogc {
return _empty; return _empty;
} }
/// ///
inout(Token) front() inout { inout(Token) front() inout @nogc {
return _front; return _front;
} }
/// private void popFrontImpl() {
void popFront() {
if (_source.length == 0) { if (_source.length == 0) {
_empty = true; _empty = true;
return; return;
@ -383,11 +439,37 @@ struct IniParser(
_front = this.fetchFront(); _front = this.fetchFront();
} }
/*
This is a workaround.
The compiler doesnt feel like inferring `@nogc` properly otherwise.
cannot call non-@nogc function
`arsd.ini.makeIniParser!(IniDialect.concatSubstrings, char[]).makeIniParser`
which calls
`arsd.ini.IniParser!(IniDialect.concatSubstrings, char[]).IniParser.this`
which calls
`arsd.ini.IniParser!(IniDialect.concatSubstrings, char[]).IniParser.popFront`
*/
static if (isDestructive) {
/// ///
inout(typeof(this)) save() inout { void popFront() @nogc {
popFrontImpl();
}
} else {
///
void popFront() {
popFrontImpl();
}
}
// Destructive parsers make very poor Forward Ranges.
static if (!isDestructive) {
///
inout(typeof(this)) save() inout @nogc {
return this; return this;
} }
} }
}
// extras // extras
public { public {
@ -705,7 +787,7 @@ struct IniParser(
Token token = this.lexSubstringImpl!tokenType(); Token token = this.lexSubstringImpl!tokenType();
auto next = this.save(); auto next = this; // copy
next._bypassConcatSubstrings = true; next._bypassConcatSubstrings = true;
next.popFront(); next.popFront();
@ -885,6 +967,9 @@ struct IniFilteredParser(
/// ///
public alias Token = IniToken!string; public alias Token = IniToken!string;
///
public enum isDestructive = IniParser!(dialect, string).isDestructive;
private IniParser!(dialect, string) _parser; private IniParser!(dialect, string) _parser;
public @safe pure nothrow: public @safe pure nothrow:
@ -901,10 +986,10 @@ public @safe pure nothrow:
} }
/// ///
bool empty() const => _parser.empty; bool empty() const @nogc => _parser.empty;
/// ///
inout(Token) front() inout => _parser.front; inout(Token) front() inout @nogc => _parser.front;
/// ///
void popFront() { void popFront() {
@ -912,14 +997,16 @@ public @safe pure nothrow:
_parser.skipIrrelevant(true); _parser.skipIrrelevant(true);
} }
static if (!isDestructive) {
/// ///
inout(typeof(this)) save() inout { inout(typeof(this)) save() inout @nogc {
return this; return this;
} }
} }
}
/// ///
@safe unittest { @safe @nogc unittest {
// INI document (demo data) // INI document (demo data)
static immutable string rawIniDocument = `; This is a comment. static immutable string rawIniDocument = `; This is a comment.
[section1] [section1]
@ -960,7 +1047,7 @@ oachkatzl = schwoaf ;try pronouncing that
assert(values == 2); assert(values == 2);
} }
@safe unittest { @safe @nogc unittest {
static immutable string rawIniDocument = `; This is a comment. static immutable string rawIniDocument = `; This is a comment.
[section1] [section1]
s1key1 = value1 s1key1 = value1
@ -1079,7 +1166,7 @@ s2key2 = value no.4
assert(parser.empty()); assert(parser.empty());
} }
@safe unittest { @safe @nogc unittest {
static immutable rawIni = "#not-a = comment"; static immutable rawIni = "#not-a = comment";
auto parser = makeIniParser(rawIni); auto parser = makeIniParser(rawIni);
@ -1094,7 +1181,7 @@ s2key2 = value no.4
assert(parser.empty); assert(parser.empty);
} }
@safe unittest { @safe @nogc unittest {
static immutable rawIni = "#actually_a = comment\r\n\t#another one\r\n\t\t ; oh, and a third one"; static immutable rawIni = "#actually_a = comment\r\n\t#another one\r\n\t\t ; oh, and a third one";
enum dialect = (Dialect.hashLineComments | Dialect.lineComments); enum dialect = (Dialect.hashLineComments | Dialect.lineComments);
auto parser = makeIniParser!dialect(rawIni); auto parser = makeIniParser!dialect(rawIni);
@ -1114,7 +1201,7 @@ s2key2 = value no.4
assert(parser.empty); assert(parser.empty);
} }
@safe unittest { @safe @nogc unittest {
static immutable rawIni = ";not a = line comment\nkey = value ;not-a-comment \nfoo = bar # not a comment\t"; static immutable rawIni = ";not a = line comment\nkey = value ;not-a-comment \nfoo = bar # not a comment\t";
enum dialect = Dialect.lite; enum dialect = Dialect.lite;
auto parser = makeIniParser!dialect(rawIni); auto parser = makeIniParser!dialect(rawIni);
@ -1149,7 +1236,7 @@ s2key2 = value no.4
} }
} }
@safe unittest { @safe @nogc unittest {
static immutable rawIni = "; line comment 0\t\n\nkey = value ; comment-1\nfoo = bar #comment 2\n"; static immutable rawIni = "; line comment 0\t\n\nkey = value ; comment-1\nfoo = bar #comment 2\n";
enum dialect = (Dialect.inlineComments | Dialect.hashInlineComments); enum dialect = (Dialect.inlineComments | Dialect.hashInlineComments);
auto parser = makeIniParser!dialect(rawIni); auto parser = makeIniParser!dialect(rawIni);
@ -1191,7 +1278,7 @@ s2key2 = value no.4
assert(parser.skipIrrelevant(false)); assert(parser.skipIrrelevant(false));
} }
@safe unittest { @safe @nogc unittest {
static immutable rawIni = "key = value;inline"; static immutable rawIni = "key = value;inline";
enum dialect = Dialect.inlineComments; enum dialect = Dialect.inlineComments;
auto parser = makeIniParser!dialect(rawIni); auto parser = makeIniParser!dialect(rawIni);
@ -1211,7 +1298,7 @@ s2key2 = value no.4
assert(parser.empty); assert(parser.empty);
} }
@safe unittest { @safe @nogc unittest {
static immutable rawIni = "key: value\n" static immutable rawIni = "key: value\n"
~ "foo= bar\n" ~ "foo= bar\n"
~ "lol :rofl\n" ~ "lol :rofl\n"
@ -1274,7 +1361,7 @@ s2key2 = value no.4
assert(parser.skipIrrelevant()); assert(parser.skipIrrelevant());
} }
@safe unittest { @safe @nogc unittest {
static immutable rawIni = static immutable rawIni =
"\"foo=bar\"=foobar\n" "\"foo=bar\"=foobar\n"
~ "'foo = bar' = foo_bar\n" ~ "'foo = bar' = foo_bar\n"
@ -1392,7 +1479,7 @@ IniParser!(dialect, string) makeIniParser(
} }
/// ///
@safe unittest { @safe @nogc unittest {
string regular; string regular;
auto parser1 = makeIniParser(regular); auto parser1 = makeIniParser(regular);
assert(parser1.empty); // exclude from docs assert(parser1.empty); // exclude from docs
@ -1404,6 +1491,21 @@ IniParser!(dialect, string) makeIniParser(
const(char)[] constChars; const(char)[] constChars;
auto parser3 = makeIniParser(constChars); auto parser3 = makeIniParser(constChars);
assert(parser3.empty); // exclude from docs assert(parser3.empty); // exclude from docs
assert(!parser1.isDestructive); // exclude from docs
assert(!parser2.isDestructive); // exclude from docs
assert(!parser3.isDestructive); // exclude from docs
}
@safe unittest {
char[] mutableInput;
enum dialect = Dialect.concatSubstrings;
auto parser1 = makeIniParser!(dialect, const(char)[])(mutableInput);
auto parser2 = (() @nogc => makeIniParser!(dialect)(mutableInput))();
assert(!parser1.isDestructive);
assert(parser2.isDestructive);
} }
/++ /++
@ -1427,7 +1529,7 @@ IniFilteredParser!(dialect, string) makeIniFilteredParser(
} }
/// ///
@safe unittest { @safe @nogc unittest {
string regular; string regular;
auto parser1 = makeIniFilteredParser(regular); auto parser1 = makeIniFilteredParser(regular);
assert(parser1.empty); // exclude from docs assert(parser1.empty); // exclude from docs