From 67ea94c8eea05831c9b33980392aa44a34fb9c43 Mon Sep 17 00:00:00 2001 From: Siddharth Mishra Date: Wed, 1 Jul 2026 17:28:29 -0700 Subject: [PATCH 01/11] feat(parsercombinator): scannerless combinator DSL with per-grammar context Add Include/Misra/ParserCombinator.h: a documented, debuggable, scannerless parser-combinator DSL. Parsers are plain `static inline` functions; StrIter and PcParserStatus stay inside the block frames (PcSeq/PcChoice), the arms (PcAlt/PcTryAlt and their Then variants), and the two atoms (PcSatisfyChar / PcSatisfyStr) -- a grammar rule never names either one. Thread a per-grammar `ParserCtx` (carrying the allocator parsers build AST nodes from, plus the growing AST root / symbol tables) through every parser as `ctx`, enabling immediate-mode parsing: build long-lived output as the input is consumed. Bin/ParseC.c: an arithmetic calculator as the running example and playground for the DSL, to be grown into a full C parser. --- Bin/ParseC.c | 147 ++++++++++++++ Include/Misra/ParserCombinator.h | 336 +++++++++++++++++++++++++++++++ 2 files changed, 483 insertions(+) create mode 100644 Bin/ParseC.c create mode 100644 Include/Misra/ParserCombinator.h diff --git a/Bin/ParseC.c b/Bin/ParseC.c new file mode 100644 index 0000000..a86f3e2 --- /dev/null +++ b/Bin/ParseC.c @@ -0,0 +1,147 @@ +#include +#include +#include + +/// +/// Per-grammar context threaded through every parser as `ctx`. This calculator +/// evaluates to a `u64` and allocates nothing, so it only carries the allocator +/// that a real (AST-building) grammar would allocate nodes from -- here it just +/// demonstrates the threading and stands ready for the C parser to grow into. +/// +typedef struct ParserCtx { + /// Where parsers allocate long-lived output (AST nodes). Unused by the u64 calculator. + Allocator *alloc; +} ParserCtx; + +/// +/// A tiny arithmetic calculator -- the running example / playground for the DSL, +/// loosest precedence first: +/// expr = additive +/// additive = multiplicative ( ('+'|'-') multiplicative )* left-assoc +/// multiplicative = atom ( ('*'|'/') atom )* left-assoc +/// atom = '(' expr ')' | "pi" | number +/// number = digit+ (folded into a u64 as we go) +/// "pi" (-> 3) exercises the literal-string matcher. No rule touches StrIter or +/// the status directly -- only the atoms and the block frames do. +/// + +PcParser(CharIfExist, char, char); +PcParser(Digit, char); +PcParser(Number, u64); +PcParser(Pi, u64); +PcParser(AddOp, char); +PcParser(MulOp, char); +PcParser(Atom, u64); +PcParser(Parenthesized, u64); +PcParser(Multiplicative, u64); +PcParser(Additive, u64); +PcParser(Expr, u64); + +/// fundamental: match a specific char (the `expect` input), capture it +PcParser(CharIfExist, char, char) { + PcSatisfyChar(c, c == expect) { + *value = c; + } +} + +/// char-class atom via the char predicate +PcParser(Digit, char) { + PcSatisfyChar(c, c >= '0' && c <= '9') { + *value = c; + } +} + +/// number = digit+, folded left into a u64 as the run is consumed +PcParser(Number, u64) { + char d; + PcSeq() { + PcMatch(Digit, &d); + *value = (u64)(d - '0'); + PcMany(Digit, &d) { + *value = *value * 10 + (u64)(d - '0'); + } + } +} + +/// literal-string atom: the "pi" keyword +PcParser(Pi, u64) { + PcSatisfyStr("pi") { + *value = 3; + } +} + +PcParser(AddOp, char) { + PcChoice() { + PcAlt(CharIfExist, '+', value); + PcAlt(CharIfExist, '-', value); + } +} + +PcParser(MulOp, char) { + PcChoice() { + PcAlt(CharIfExist, '*', value); + PcAlt(CharIfExist, '/', value); + } +} + +PcParser(Parenthesized, u64) { + char paren; + PcSeq() { + PcMatch(CharIfExist, '(', &paren); + PcMatch(Expr, value); + PcMatch(CharIfExist, ')', &paren); + } +} + +PcParser(Atom, u64) { + PcChoice() { + PcTryAlt(Parenthesized, value); + PcAlt(Pi, value); + PcAlt(Number, value); + } +} + +PcParser(Multiplicative, u64) { + char op; + u64 rhs; + PcSeq() { + PcMatch(Atom, value); + PcMany(MulOp, &op) { + PcMatch(Atom, &rhs); + *value = (op == '*') ? (*value * rhs) : (rhs ? *value / rhs : 0); + } + } +} + +PcParser(Additive, u64) { + char op; + u64 rhs; + PcSeq() { + PcMatch(Multiplicative, value); + PcMany(AddOp, &op) { + PcMatch(Multiplicative, &rhs); + *value = (op == '+') ? (*value + rhs) : (*value - rhs); + } + } +} + +PcParser(Expr, u64) { + return PcParse(Additive, value); +} + +int main() { + HeapAllocator heap = HeapAllocatorInit(); + ParserCtx ctx = {.alloc = ALLOCATOR_OF(&heap)}; + char buf[] = "1+2*3+4"; + StrIter in = StrIterFromZstr(buf); + u64 out = 0; + PcParserStatus st = pc_parser_Expr(&in, &ctx, &out); + + if (st & PC_PARSER_STATUS_SUCCESS) + LOG_INFO("parsed value = {}", out); + else + LOG_INFO("parse failed"); + + HeapAllocatorDeinit(&heap); + return 0; +} diff --git a/Include/Misra/ParserCombinator.h b/Include/Misra/ParserCombinator.h new file mode 100644 index 0000000..2fab56d --- /dev/null +++ b/Include/Misra/ParserCombinator.h @@ -0,0 +1,336 @@ +/// file : misra/parsercombinator.h +/// author : Siddharth Mishra (admin@brightprogrammer.in) +/// This is free and unencumbered software released into the public domain. +/// +/// Parser combinator provides arbitrary parser creation capabilities. +/// +/// Definition: +/// A parser combinator is a high order function over parsers that takes +/// a sequence (in the sense that ordering may matter) of parsers and returns +/// a new parser that is derived from the provided parsers. +/// +/// Parser: +/// type Parser a = String -> Result (a, String) +/// A parser takes a string and returns a value of type a, and the remaining string +/// left to parse. +/// +/// Algebraic interfaces: +/// - Functor: Take the result of a parser and transform it without changing the parser +/// itself. Like parsing a digit sequence and converting it to Int or Float. +/// - Applicative: Run parsers in sequence where later parsers dont depend on +/// earlier parsers. [Context-Free nature] +/// - Monad: Run parsers in sequence where later parser depends on the earlier +/// parser's value. [Context-Sensitive nature] +/// - Alternative: Choice and repetition. +/// + +/// Last year I was tasked to write a C++ demangler in pure C and was getting paid to do it. +/// I knew this person, and was like a mentor to me, I failed them miserably. The parser I wrote +/// was PEG (as I learn now), which was basically a top-down recursive-descent left-to-right parser that I built using +/// very basic macro-defined parser combinators. I really respect this person, and I lost +/// their trust and respect, which hurts whenever my internal dialogue reminds me of it. +/// If I get this right this time, this part of my code is my homage to them. +/// +/// There were macros to define parser rules and then to use the rules. There were macros +/// to help you parse in a sequence or alternation and consume the input along the way. +/// +/// The parser was in the end designed in such a way that it would parse the input as it +/// is consumed, zero tokenization. It was able to automatically backtrack because of it's +/// design, and I was very proud of it. A few things came biting me in the ass very badly +/// later down the line : +/// - [Heavy macro usage], in the sense that macro was expanding to multiple lines +/// and was not dispatching code to handler functions. The issue with this is +/// that debugging is a real PITA, because debuggers dont expand macros. +/// Essentially, single-stepping code in GDB to watch the parser run was hell! +/// - [Left-recursive] nature of the C++ name mangling grammar itself. Left recursive +/// grammar is a real hell if you are writing a recursive descent parser and you +/// let this nature of grammar slip away from your eyes. There are multiple normalized +/// forms that can help you but it's still a pain, for very large grammar (that C++ name +/// mangling grammar already is) is a real pain. +/// - [Backtracking] parsers have issue with `Alternative` combinators. They try the next +/// in alternation when the first one fails. Libraries like Parsec in Haskell split this +/// into two behaviors : +/// - If parsing the very first in an alternation fails and it failed by not cosuming a single +/// token, then do not parse anything in the alternative sequence at all. +/// - You can force the alternation parsing anyhow by a `try` keyword that forces the combinator +/// to continue trying alternatives. +/// I believe this would've solved a big chunk of my issues. +/// - [Ordering of alternatives] mattered in my parser. Sometimes I wanted the parser to take +/// a longer parsing route, but the way I wrote the grammar using combinators made the parser +/// take the first smallest route it was able to parse. This created a lot of debugging time +/// which was already hard. +/// - Normalized grammars are hard to make any sense of! If possible, normalize parts of the grammars. +/// Depending on how long each rule is, the number of rules blow up and stop making any sense. +/// +/// Now, I'm standing on the same cliff, and this time I want to make a different decision. +/// +/// I'm using an LLM to learn how other languages and libraries have achieved the same thing. +/// I know this is possible, and I just have to make some tweaks in the way I designed my +/// combinators the last time. The approach this time is to try to imitate how functional +/// languages achieve this same thing. The functional part can be emulated using macros +/// as code-generators hopefully. +/// +/// On the [Heavy macro usage] point, I know that I use lots of macros in this library. +/// That is mainly because I know the real reason debuggability vanishes is not because +/// it's a macro, but because I used macros in a way that it hid the implementation. +/// In this library I made sure that the macros are very thin wrappers and are essentially +/// code-dispatchers first and then code-generators. This ensures that most of the buggy +/// remains debuggable through a function that actually handles the implementation. +/// + +#ifndef MISRA_PARSER_COMBINATOR_H +#define MISRA_PARSER_COMBINATOR_H + +#include +#include +#include + +/// +/// A parser combinator does not only need a way to parse a string. It also needs to convert +/// it to a representation that any phase after parsing can use and analyze. This may heavily +/// depend on the language being parsed. +/// +/// The mechanism this library uses is a per-grammar context (`ParserCtx`, see below) threaded +/// through every parser. The grammar author places their in-progress structure (an AST root, +/// symbol tables, ...) and an allocator on it, so parsers can build long-lived output as the +/// input is consumed -- immediate-mode parsing with no separate tree-walking pass. +/// + +/// +/// This is the design philosophy behind the macros below: +/// +/// - A parser is an ordinary `static inline` function. `PcParser` writes its signature so it +/// is steppable in a debugger; the only thing a grammar body ever calls into is another +/// parser (through `PcMatch`/`PcAlt`/...), never opaque combinator glue. +/// - `StrIter` (the input stream) and `PcParserStatus` (the result) are internal constructs. +/// They appear only inside the block frames, the arms, and the two atoms -- a grammar rule +/// built on top of these never names either one. +/// - Scannerless: parsers consume the source character-by-character through a `StrIter`; there +/// is no tokenization pass. +/// + +/// +/// Status returned by every parser. Two independent bits: whether the parse succeeded, and +/// whether any input was consumed (which stays meaningful even when the parse later fails, +/// because it decides commit-vs-backtrack in an enclosing choice). Grammar bodies never read +/// or write this directly; the macros construct and inspect it. +/// +typedef u32 PcParserStatus; + +enum { + /// The parse did not succeed. + PC_PARSER_STATUS_FAILED = 0, + /// The parse succeeded. + PC_PARSER_STATUS_SUCCESS = 1, + /// Input was advanced (matters for backtracking even on a failed parse). + PC_PARSER_STATUS_CONSUMED = 1u << 1 +}; + +/// +/// ParserCtx is NOT defined here. Each grammar (a C parser, a JSON parser, ...) declares its +/// own `typedef struct { ... } ParserCtx;` before using `PcParser`, and threads a pointer to +/// it through every parser as `ctx`. It carries whatever the grammar needs to build output as +/// it parses, typically: +/// - an `Allocator *` the parsers allocate AST nodes from (giving them a lifetime beyond the +/// parse), and +/// - the growing root of the AST / a symbol table for context-sensitive rules. +/// The combinator only forwards `ctx`; it never looks inside it. +/// + +/// Mangled name of the parser function for rule `Name`. +#define PcGenParserName(Name) pc_parser_##Name + +/// Portable "may be unused" attribute for the `ctx` parameter: not every parser needs the +/// context (recognizers, pure arithmetic, ...), and those should not warn. +#if defined(__GNUC__) || defined(__clang__) +# define PC_MAYBE_UNUSED __attribute__((unused)) +#else +# define PC_MAYBE_UNUSED +#endif + +/// +/// Invoke a parser by name, threading the stream `in` and context `ctx` automatically and +/// forwarding every extra argument verbatim. Used internally by `PcMatch`/`PcAlt`/...; call +/// it directly only to delegate one rule wholesale to another (`return PcParse(Other, value);`). +/// Convention: trailing pointer arguments are outputs, the ones between are inputs. +/// +#define PcParse(Name, ...) PcGenParserName(Name)(in, ctx, __VA_ARGS__) + +/// +/// Define (or, when followed by `;`, forward-declare) a parser. Overloaded by arity: +/// +/// PcParser(Name, BuildT) -> (StrIter *in, ParserCtx *ctx, BuildT *value) +/// PcParser(Name, InT, BuildT) -> (StrIter *in, ParserCtx *ctx, InT expect, BuildT *value) +/// +/// The names a body reads are `in` (stream), `ctx` (grammar context), `expect` (the input, in +/// the 3-arg form), and `value` (the output). The consumer must have a `ParserCtx` type in scope. +/// +/// SUCCESS: The body returns `PC_PARSER_STATUS_SUCCESS` (or'd with `CONSUMED` when it advanced +/// `in`); the parsed result has been written to `*value`. +/// FAILURE: The body returns `PC_PARSER_STATUS_FAILED` (or'd with `CONSUMED` when it advanced +/// before failing); `*value` is left unspecified. +/// +#define PcParser(...) OVERLOAD(PcParser, __VA_ARGS__) +#define PcParser_2(Name, BuildT) \ + static inline PcParserStatus PcGenParserName(Name)(StrIter * in, ParserCtx * ctx PC_MAYBE_UNUSED, BuildT * value) +#define PcParser_3(Name, InT, BuildT) \ + static inline PcParserStatus \ + PcGenParserName(Name)(StrIter * in, ParserCtx * ctx PC_MAYBE_UNUSED, InT expect, BuildT * value) + +/// +/// The consumed bit for a parser, derived from the stream position against a snapshot: a parse +/// that rewound leaves `pos` unchanged (not consumed); one that committed leaves it advanced +/// (consumed). This is why backtracking accounts for itself -- no separate flag is threaded. +/// +#define PC_CONSUMED(start) (in->pos != (start).pos ? PC_PARSER_STATUS_CONSUMED : 0u) + +/// +/// Block frames -- the whole body of a rule is one of these (or a bare `PcSatisfy*`/delegation). +/// Each scopes its bookkeeping in the `for`-init struct so frames nest and sit side by side +/// without name clashes, and each owns the rule's `return`. +/// +/// PcSeq: run the steps (`PcMatch`/`PcMany`/...) in order; a failing step returns early. If the +/// body runs to the end, the rule succeeds (consuming whatever the steps consumed). +/// +/// SUCCESS: All steps matched; returns SUCCESS with the accumulated CONSUMED bit. +/// FAILURE: A step failed and already returned; control never reaches the frame's success return. +/// +#define PcSeq() \ + for (struct { \ + StrIter start; \ + bool ran; \ + } pc_seq = {*in, false}; \ + ; \ + pc_seq.ran = true) \ + if (pc_seq.ran) \ + return PC_CONSUMED(pc_seq.start) | PC_PARSER_STATUS_SUCCESS; \ + else + +/// +/// PcChoice: try the arms (`PcAlt`/`PcTryAlt`/...) top to bottom; the first that matches wins and +/// its value is the rule's value. If none match, the rule fails. +/// +/// SUCCESS: An arm matched; returns SUCCESS with CONSUMED reflecting the arm. +/// FAILURE: No arm matched; returns FAILED, CONSUMED set if the rule advanced before failing +/// (a committed arm that consumed then failed) -- driving the caller's commit-vs-backtrack. +/// +#define PcChoice() \ + for (struct { \ + StrIter mark; \ + PcParserStatus st; \ + bool ran, matched, done; \ + } pc_ch = {*in, 0, false, false, false}; \ + ; \ + pc_ch.ran = true) \ + if (pc_ch.ran) \ + return (pc_ch.matched ? PC_PARSER_STATUS_SUCCESS : PC_PARSER_STATUS_FAILED) | PC_CONSUMED(pc_ch.mark); \ + else + +/// +/// Sequence steps (used inside `PcSeq`). Both forward their extra args straight to the parser +/// call, so the parser's own signature validates arity and types at the call site. +/// +/// PcMatch: run parser `Name`; on failure, fail the whole rule (returning with the sequence's +/// consumed bit). On success, continue with the next step. +/// +#define PcMatch(Name, ...) \ + do { \ + if (!(PcParse(Name, __VA_ARGS__) & PC_PARSER_STATUS_SUCCESS)) \ + return PC_CONSUMED(pc_seq.start) | PC_PARSER_STATUS_FAILED; \ + } while (0) + +/// +/// PcMany: zero-or-more. Run parser `Name` repeatedly; the body runs once per match. A match +/// that consumed nothing would spin forever, so the loop stops on it (it never aborts and never +/// allocates); the failing/empty iteration is rewound so its bytes are not eaten. Always +/// "succeeds" -- it is a step that simply stops. +/// +#define PcMany(Name, ...) \ + for (StrIter UNPL(pc_many_) = *in; \ + (UNPL(pc_many_) = *in, \ + (PcParse(Name, __VA_ARGS__) & PC_PARSER_STATUS_SUCCESS) && in->pos != UNPL(pc_many_).pos) ? \ + true : \ + ((*in = UNPL(pc_many_)), false);) + +/// +/// Choice arms (used inside `PcChoice`). All forward their extra args to the parser call. +/// +/// `PcAlt`/`PcTryAlt` are BODILESS: the matched parser's output IS the arm's value. Each is a +/// complete statement, so writing a `{ ... }` after one is a compile error. +/// `PcAltThen`/`PcTryAltThen` take a build body, run on a match, to transform the parsed value. +/// +/// Plain arms COMMIT: if the arm consumed input then failed, the choice fails (consumed) rather +/// than trying later arms. `Try` arms BACKTRACK: any failure rewinds and the next arm is tried. +/// +#define PcAlt(Name, ...) \ + ((void)(pc_ch.done || \ + ((pc_ch.st = PcParse(Name, __VA_ARGS__)) & PC_PARSER_STATUS_SUCCESS ? \ + ((pc_ch.matched = pc_ch.done = true)) : \ + ((pc_ch.st & PC_PARSER_STATUS_CONSUMED) ? (pc_ch.done = true, false) : (*in = pc_ch.mark, false))))) + +#define PcTryAlt(Name, ...) \ + ((void)(pc_ch.done || ((pc_ch.st = PcParse(Name, __VA_ARGS__)) & PC_PARSER_STATUS_SUCCESS ? \ + ((pc_ch.matched = pc_ch.done = true)) : \ + (*in = pc_ch.mark, false)))) + +#define PcAltThen(Name, ...) \ + if (!pc_ch.done && \ + ((pc_ch.st = PcParse(Name, __VA_ARGS__)) & PC_PARSER_STATUS_SUCCESS ? \ + ((pc_ch.matched = pc_ch.done = true)) : \ + ((pc_ch.st & PC_PARSER_STATUS_CONSUMED) ? (pc_ch.done = true, false) : (*in = pc_ch.mark, false)))) + +#define PcTryAltThen(Name, ...) \ + if (!pc_ch.done && \ + ((pc_ch.st = PcParse(Name, __VA_ARGS__)) & PC_PARSER_STATUS_SUCCESS ? ((pc_ch.matched = pc_ch.done = true)) : \ + (*in = pc_ch.mark, false))) + +/// +/// Atoms -- the only place `StrIter` and `PcParserStatus` are handled directly. Every fundamental +/// parser (a char class, a keyword, ...) is written on top of one of these, so it never pokes the +/// stream or builds a status by hand. +/// +/// PcSatisfyChar: match one char, bound as `Var`, for which `Pred` holds; run the build body, +/// then succeed (consuming the char). If `Pred` is false at the current position, fail without +/// consuming. `Var` is loop-scoped (it does not leak into the parser body). +/// +/// SUCCESS: `Pred(Var)` held; the char is consumed, the body has run; returns SUCCESS|CONSUMED. +/// FAILURE: `Pred(Var)` did not hold (or there was no char); nothing consumed; returns FAILED. +/// +#define PcSatisfyChar(Var, Pred) \ + for (char Var = 0;;) \ + for (bool UNPL(pc_sc_ran) = false;; UNPL(pc_sc_ran) = true) \ + if (UNPL(pc_sc_ran)) \ + return PC_PARSER_STATUS_SUCCESS | PC_PARSER_STATUS_CONSUMED; \ + else if (!(StrIterPeek(in, &Var) && (Pred))) \ + return PC_PARSER_STATUS_FAILED; \ + else if ((StrIterMove(in, 1), true)) + +/// +/// PcSatisfyStr: match the literal string `Expect` (a `Zstr`; pass `StrBegin(&s)` for a `Str`) at +/// the current position. The whole string is peeked before committing, so a mismatch consumes +/// nothing (a clean failure, usable in a choice without a commit surprise); a full match consumes +/// it and runs the build body. +/// +/// SUCCESS: `Expect` was present; it is consumed, the body has run; returns SUCCESS|CONSUMED. +/// FAILURE: `Expect` was not present; nothing consumed; returns FAILED. +/// +#define PcSatisfyStr(Expect) \ + u64 UNPL(pc_ss_len) = ZstrLen(Expect); \ + bool UNPL(pc_ss_ok) = true; \ + for (u64 UNPL(pc_ss_i) = 0; UNPL(pc_ss_i) < UNPL(pc_ss_len); UNPL(pc_ss_i)++) { \ + char UNPL(pc_ss_c); \ + if (!StrIterPeekAt(in, (i64)UNPL(pc_ss_i), &UNPL(pc_ss_c)) || UNPL(pc_ss_c) != (Expect)[UNPL(pc_ss_i)]) { \ + UNPL(pc_ss_ok) = false; \ + break; \ + } \ + } \ + if (UNPL(pc_ss_ok)) \ + StrIterMustMove(in, (i64)UNPL(pc_ss_len)); \ + for (bool UNPL(pc_ss_ran) = false;; UNPL(pc_ss_ran) = true) \ + if (!UNPL(pc_ss_ok)) \ + return PC_PARSER_STATUS_FAILED; \ + else if (UNPL(pc_ss_ran)) \ + return PC_PARSER_STATUS_SUCCESS | PC_PARSER_STATUS_CONSUMED; \ + else + +#endif // MISRA_PARSER_COMBINATOR From b65d2f1bf1d011400d10f9fa5e0016c2839e1c1a Mon Sep 17 00:00:00 2001 From: Siddharth Mishra Date: Wed, 1 Jul 2026 20:35:48 -0700 Subject: [PATCH 02/11] feat(parsercombinator): extend the DSL and grow ParseC into the CalC calculator DSL (ParserCombinator.h): - PcParse is OVERLOAD'd by arity, symmetric with PcParser (no __VA_ARGS__) - recognizer family for no-output "validator" parsers: PcRecognizer / PcRecognize / PcRecognizeMany - PcOpt: optional PcSeq step - PcReject: context-sensitive rule failure that hides the consumed/status bits - PcRun: driver-side entry point so a caller never spells pc_parser_ CalC (Bin/CalC.c, renamed from ParseC.c): - immediate-mode calculator: Num (tagged i64/f64) with promotion, variables in a Map(Str, Num) on ParserCtx, unary minus, %, parens, scannerless whitespace, seeded pi/e - REPL or -c via ArgParse; success/error reported by the driver - tokens accumulate through the atoms into a Str and convert via StrToU64 / StrToF64 -- no forged Str views, and no grammar rule touches StrIter/status --- Bin/CalC.c | 367 +++++++++++++++++++++++++++++++ Bin/ParseC.c | 147 ------------- Include/Misra/ParserCombinator.h | 92 +++++++- 3 files changed, 454 insertions(+), 152 deletions(-) create mode 100644 Bin/CalC.c delete mode 100644 Bin/ParseC.c diff --git a/Bin/CalC.c b/Bin/CalC.c new file mode 100644 index 0000000..56f6389 --- /dev/null +++ b/Bin/CalC.c @@ -0,0 +1,367 @@ +#include +#include + +/// +/// CalC -- a small immediate-mode calculator, the running playground for the +/// parser-combinator DSL. It parses and evaluates as it reads (no AST): every +/// rule folds its result straight into a `Num`. Variables live in the grammar +/// context, so a result can be named and reused on a later line. +/// +/// line = assignment | expr +/// assignment = identifier '=' expr (stores into ctx->vars) +/// expr = additive +/// additive = multiplicative ( ('+'|'-') multiplicative )* left-assoc +/// multiplicative = unary ( ('*'|'/'|'%') unary )* left-assoc +/// unary = ('-'|'+')? atom +/// atom = '(' expr ')' | identifier | number +/// number = digit+ ( '.' digit+ )? (int, or float if a '.') +/// identifier = letter+ (a variable name) +/// +/// Whitespace is scannerless: each token consumes trailing whitespace and the +/// top rule skips leading whitespace once. `pi` and `e` are seeded variables. +/// + +typedef struct Num { + bool is_float; + union { + i64 i; + f64 f; + }; +} Num; + +static Num num_int(i64 v) { + return (Num) {.is_float = false, .i = v}; +} +static Num num_flt(f64 v) { + return (Num) {.is_float = true, .f = v}; +} +static f64 num_f(Num n) { + return n.is_float ? n.f : (f64)n.i; +} + +static Num num_add(Num a, Num b) { + return (a.is_float || b.is_float) ? num_flt(num_f(a) + num_f(b)) : num_int(a.i + b.i); +} +static Num num_sub(Num a, Num b) { + return (a.is_float || b.is_float) ? num_flt(num_f(a) - num_f(b)) : num_int(a.i - b.i); +} +static Num num_mul(Num a, Num b) { + return (a.is_float || b.is_float) ? num_flt(num_f(a) * num_f(b)) : num_int(a.i * b.i); +} +static Num num_div(Num a, Num b) { + if (a.is_float || b.is_float) { + f64 d = num_f(b); + return num_flt(d != 0.0 ? num_f(a) / d : 0.0); + } + return num_int(b.i != 0 ? a.i / b.i : 0); +} +static Num num_mod(Num a, Num b) { + if (a.is_float || b.is_float) + return num_flt(0.0); + return num_int(b.i != 0 ? a.i % b.i : 0); +} +static Num num_neg(Num a) { + return a.is_float ? num_flt(-a.f) : num_int(-a.i); +} + +typedef Map(Str, Num) Vars; + +/// +/// The grammar context threaded through every parser as `ctx`. For CalC it is +/// the variable environment; the map owns its allocator (and deep-copies names), +/// so a later grammar's `ctx` is where an AST root / arena would join it. +/// +typedef struct ParserCtx { + Vars vars; +} ParserCtx; + +PcRecognizer(WsChar); +PcRecognizer(Ws); +PcParser(DigitCh, char); +PcParser(LetterCh, char); +PcParser(CharIfExist, char, char); +PcParser(Sym, char, char); +PcParser(SignCh, char); +PcParser(Number, Num); +PcParser(Identifier, Str); +PcParser(VarRef, Num); +PcParser(AddOp, char); +PcParser(MulOp, char); +PcParser(Parenthesized, Num); +PcParser(Atom, Num); +PcParser(Unary, Num); +PcParser(Multiplicative, Num); +PcParser(Additive, Num); +PcParser(Expr, Num); +PcParser(Assignment, Num); +PcParser(Line, Num); +PcParser(Calc, Num); + +PcRecognizer(WsChar) { + PcSatisfyChar(c, c == ' ' || c == '\t' || c == '\n' || c == '\r') {} +} + +PcRecognizer(Ws) { + PcRecognizeMany(WsChar); +} + +PcParser(DigitCh, char) { + PcSatisfyChar(c, c >= '0' && c <= '9') { + *value = c; + } +} + +PcParser(LetterCh, char) { + PcSatisfyChar(c, (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_') { + *value = c; + } +} + +/// fundamental: match a specific char (the `expect` input), capture it +PcParser(CharIfExist, char, char) { + PcSatisfyChar(c, c == expect) { + *value = c; + } +} + +/// a token char: match `expect`, then eat trailing whitespace +PcParser(Sym, char, char) { + PcSeq() { + PcMatch(CharIfExist, expect, value); + PcRecognize(Ws); + } +} + +PcParser(SignCh, char) { + PcChoice() { + PcAlt(Sym, '-', value); + PcAlt(Sym, '+', value); + } +} + +/// number = digit+ ( '.' digit+ )? -- accumulated char-by-char into a Str, then +/// converted by the library's own parser; int unless a '.' makes it a float +PcParser(Number, Num) { + char d; + bool is_float = false; + PcSeq() { + StrInitStack(tok, 64) { + PcMatch(DigitCh, &d); + StrPushBack(&tok, d); + PcMany(DigitCh, &d) { + StrPushBack(&tok, d); + } + PcOpt(CharIfExist, '.', &d) { + is_float = true; + StrPushBack(&tok, d); + PcMany(DigitCh, &d) { + StrPushBack(&tok, d); + } + } + if (is_float) { + f64 f = 0; + StrToF64(&tok, &f, NULL); + *value = num_flt(f); + } else { + u64 u = 0; + StrToU64(&tok, &u, NULL); + *value = num_int((i64)u); + } + } + PcRecognize(Ws); + } +} + +/// identifier = letter+ ; the matched letters are appended into the caller's Str +PcParser(Identifier, Str) { + char l; + PcSeq() { + PcMatch(LetterCh, &l); + StrPushBack(value, l); + PcMany(LetterCh, &l) { + StrPushBack(value, l); + } + PcRecognize(Ws); + } +} + +/// variable reference: look the name up in the environment; undefined -> reject +PcParser(VarRef, Num) { + PcSeq() { + StrInitStack(name, 64) { + PcMatch(Identifier, &name); + Num *slot = MapGetFirstPtr(&ctx->vars, name); + if (!slot) + PcReject(); + *value = *slot; + } + } +} + +PcParser(AddOp, char) { + PcChoice() { + PcAlt(Sym, '+', value); + PcAlt(Sym, '-', value); + } +} + +PcParser(MulOp, char) { + PcChoice() { + PcAlt(Sym, '*', value); + PcAlt(Sym, '/', value); + PcAlt(Sym, '%', value); + } +} + +PcParser(Parenthesized, Num) { + char paren; + PcSeq() { + PcMatch(Sym, '(', &paren); + PcMatch(Expr, value); + PcMatch(Sym, ')', &paren); + } +} + +PcParser(Atom, Num) { + PcChoice() { + PcAlt(Parenthesized, value); + PcAlt(VarRef, value); + PcAlt(Number, value); + } +} + +PcParser(Unary, Num) { + char sign = '+'; + PcSeq() { + PcOpt(SignCh, &sign) {} + PcMatch(Atom, value); + if (sign == '-') + *value = num_neg(*value); + } +} + +PcParser(Multiplicative, Num) { + char op; + Num rhs; + PcSeq() { + PcMatch(Unary, value); + PcMany(MulOp, &op) { + PcMatch(Unary, &rhs); + *value = (op == '*') ? num_mul(*value, rhs) : (op == '/') ? num_div(*value, rhs) : num_mod(*value, rhs); + } + } +} + +PcParser(Additive, Num) { + char op; + Num rhs; + PcSeq() { + PcMatch(Multiplicative, value); + PcMany(AddOp, &op) { + PcMatch(Multiplicative, &rhs); + *value = (op == '+') ? num_add(*value, rhs) : num_sub(*value, rhs); + } + } +} + +PcParser(Expr, Num) { + return PcParse(Additive, value); +} + +/// assignment = identifier '=' expr ; evaluate, then bind the name (the map copies it) +PcParser(Assignment, Num) { + char eq; + PcSeq() { + StrInitStack(name, 64) { + PcMatch(Identifier, &name); + PcMatch(Sym, '=', &eq); + PcMatch(Expr, value); + (void)MapInsertR(&ctx->vars, name, *value); + } + } +} + +PcParser(Line, Num) { + PcChoice() { + PcTryAlt(Assignment, value); + PcAlt(Expr, value); + } +} + +PcParser(Calc, Num) { + PcSeq() { + PcRecognize(Ws); + PcMatch(Line, value); + } +} + +/// bind a predefined variable; the map deep-copies the key, so the temporary is freed +static void seed(Vars *vars, HeapAllocator *heap, Zstr name, Num v) { + Str key = StrInitFromCstr(name, ZstrLen(name), heap); + (void)MapInsertR(vars, key, v); + StrDeinit(&key); +} + +static void eval_line(ParserCtx *ctx, const char *src, u64 len) { + StrIter in = StrIterFromCstr((char *)src, len); + Num out = {0}; + PcParserStatus st = PcRun(Calc, &in, &out); + if ((st & PC_PARSER_STATUS_SUCCESS) && in.pos == in.length) { + if (out.is_float) + LOG_INFO("{}", out.f); + else + LOG_INFO("{}", out.i); + } else { + LOG_INFO("error at column {}", in.pos + 1); + } +} + +int main(int argc, char **argv) { + HeapAllocator heap = HeapAllocatorInit(); + + ParserCtx ctx = {.vars = MapInitWithDeepCopy(str_hash, str_compare, str_init_copy, str_deinit, NULL, NULL, &heap)}; + seed(&ctx.vars, &heap, "pi", num_flt(3.14159265358979323846)); + seed(&ctx.vars, &heap, "e", num_flt(2.71828182845904523536)); + + ArgParse args = ArgParseInit("calc", "an immediate-mode parser-combinator calculator", &heap); + Zstr expr = NULL; + ArgOptional(&args, "-c", "--command", &expr, "evaluate one expression and exit"); + ArgRun rc = ArgParseRun(&args, argc, argv); + + int status = 0; + if (rc == ARG_RUN_ERROR) { + status = 1; + } else if (rc != ARG_RUN_HELP) { + if (expr != NULL) { + eval_line(&ctx, expr, ZstrLen(expr)); + } else { + File fin = FileStdin(); + StrInitStack(line, 4096) { + for (;;) { + StrClear(&line); + bool eof = false; + for (;;) { + char c; + i64 n = FileRead(&fin, &c, 1); + if (n <= 0) { + eof = true; + break; + } + if (c == '\n') + break; + StrPushBack(&line, c); + } + if (StrLen(&line) > 0) + eval_line(&ctx, StrBegin(&line), StrLen(&line)); + if (eof) + break; + } + } + } + } + + ArgParseDeinit(&args); + MapDeinit(&ctx.vars); + HeapAllocatorDeinit(&heap); + return status; +} diff --git a/Bin/ParseC.c b/Bin/ParseC.c deleted file mode 100644 index a86f3e2..0000000 --- a/Bin/ParseC.c +++ /dev/null @@ -1,147 +0,0 @@ -#include -#include -#include - -/// -/// Per-grammar context threaded through every parser as `ctx`. This calculator -/// evaluates to a `u64` and allocates nothing, so it only carries the allocator -/// that a real (AST-building) grammar would allocate nodes from -- here it just -/// demonstrates the threading and stands ready for the C parser to grow into. -/// -typedef struct ParserCtx { - /// Where parsers allocate long-lived output (AST nodes). Unused by the u64 calculator. - Allocator *alloc; -} ParserCtx; - -/// -/// A tiny arithmetic calculator -- the running example / playground for the DSL, -/// loosest precedence first: -/// expr = additive -/// additive = multiplicative ( ('+'|'-') multiplicative )* left-assoc -/// multiplicative = atom ( ('*'|'/') atom )* left-assoc -/// atom = '(' expr ')' | "pi" | number -/// number = digit+ (folded into a u64 as we go) -/// "pi" (-> 3) exercises the literal-string matcher. No rule touches StrIter or -/// the status directly -- only the atoms and the block frames do. -/// - -PcParser(CharIfExist, char, char); -PcParser(Digit, char); -PcParser(Number, u64); -PcParser(Pi, u64); -PcParser(AddOp, char); -PcParser(MulOp, char); -PcParser(Atom, u64); -PcParser(Parenthesized, u64); -PcParser(Multiplicative, u64); -PcParser(Additive, u64); -PcParser(Expr, u64); - -/// fundamental: match a specific char (the `expect` input), capture it -PcParser(CharIfExist, char, char) { - PcSatisfyChar(c, c == expect) { - *value = c; - } -} - -/// char-class atom via the char predicate -PcParser(Digit, char) { - PcSatisfyChar(c, c >= '0' && c <= '9') { - *value = c; - } -} - -/// number = digit+, folded left into a u64 as the run is consumed -PcParser(Number, u64) { - char d; - PcSeq() { - PcMatch(Digit, &d); - *value = (u64)(d - '0'); - PcMany(Digit, &d) { - *value = *value * 10 + (u64)(d - '0'); - } - } -} - -/// literal-string atom: the "pi" keyword -PcParser(Pi, u64) { - PcSatisfyStr("pi") { - *value = 3; - } -} - -PcParser(AddOp, char) { - PcChoice() { - PcAlt(CharIfExist, '+', value); - PcAlt(CharIfExist, '-', value); - } -} - -PcParser(MulOp, char) { - PcChoice() { - PcAlt(CharIfExist, '*', value); - PcAlt(CharIfExist, '/', value); - } -} - -PcParser(Parenthesized, u64) { - char paren; - PcSeq() { - PcMatch(CharIfExist, '(', &paren); - PcMatch(Expr, value); - PcMatch(CharIfExist, ')', &paren); - } -} - -PcParser(Atom, u64) { - PcChoice() { - PcTryAlt(Parenthesized, value); - PcAlt(Pi, value); - PcAlt(Number, value); - } -} - -PcParser(Multiplicative, u64) { - char op; - u64 rhs; - PcSeq() { - PcMatch(Atom, value); - PcMany(MulOp, &op) { - PcMatch(Atom, &rhs); - *value = (op == '*') ? (*value * rhs) : (rhs ? *value / rhs : 0); - } - } -} - -PcParser(Additive, u64) { - char op; - u64 rhs; - PcSeq() { - PcMatch(Multiplicative, value); - PcMany(AddOp, &op) { - PcMatch(Multiplicative, &rhs); - *value = (op == '+') ? (*value + rhs) : (*value - rhs); - } - } -} - -PcParser(Expr, u64) { - return PcParse(Additive, value); -} - -int main() { - HeapAllocator heap = HeapAllocatorInit(); - ParserCtx ctx = {.alloc = ALLOCATOR_OF(&heap)}; - char buf[] = "1+2*3+4"; - StrIter in = StrIterFromZstr(buf); - u64 out = 0; - PcParserStatus st = pc_parser_Expr(&in, &ctx, &out); - - if (st & PC_PARSER_STATUS_SUCCESS) - LOG_INFO("parsed value = {}", out); - else - LOG_INFO("parse failed"); - - HeapAllocatorDeinit(&heap); - return 0; -} diff --git a/Include/Misra/ParserCombinator.h b/Include/Misra/ParserCombinator.h index 2fab56d..4c3d6db 100644 --- a/Include/Misra/ParserCombinator.h +++ b/Include/Misra/ParserCombinator.h @@ -149,12 +149,55 @@ enum { #endif /// -/// Invoke a parser by name, threading the stream `in` and context `ctx` automatically and -/// forwarding every extra argument verbatim. Used internally by `PcMatch`/`PcAlt`/...; call -/// it directly only to delegate one rule wholesale to another (`return PcParse(Other, value);`). -/// Convention: trailing pointer arguments are outputs, the ones between are inputs. +/// Invoke an output-producing parser by name, threading the stream `in` and context `ctx` +/// automatically. Overloaded by arity, mirroring `PcParser` -- the last argument is the output: /// -#define PcParse(Name, ...) PcGenParserName(Name)(in, ctx, __VA_ARGS__) +/// PcParse(Name, Out) -> pc_parser_Name(in, ctx, Out) +/// PcParse(Name, In, Out) -> pc_parser_Name(in, ctx, In, Out) +/// +/// Used internally by `PcMatch`/`PcAlt`/`PcMany`/`PcOpt`; call it directly only to delegate one +/// rule wholesale to another (`return PcParse(Other, value);`). Because an output parser always +/// carries the output argument, this family never sees an empty argument list -- so there is no +/// `__VA_OPT__` here (the recognizer family below has none either, for the same reason: every +/// arity writes its call explicitly). +/// +#define PcParse(...) OVERLOAD(PcParse, __VA_ARGS__) +#define PcParse_2(Name, Out) PcGenParserName(Name)(in, ctx, Out) +#define PcParse_3(Name, In, Out) PcGenParserName(Name)(in, ctx, In, Out) + +/// +/// The recognizer family: parsers that produce NO output -- they only succeed/fail (and consume), +/// the "validator" style. Identical to the `PcParser`/`PcParse` family with the trailing output +/// slot removed, so the arities shift down by one: 1-arg (no input) or 2-arg (an input to match). +/// +/// PcRecognizer(Name) -> (StrIter *in, ParserCtx *ctx) define, no input +/// PcRecognizer(Name, InT) -> (StrIter *in, ParserCtx *ctx, InT expect) define, one input +/// PcRecognize(Name) -> pc_parser_Name(in, ctx) call, no input +/// PcRecognize(Name, In) -> pc_parser_Name(in, ctx, In) call, one input +/// +/// SUCCESS: The body returns `PC_PARSER_STATUS_SUCCESS` (| `CONSUMED` when it advanced `in`). +/// FAILURE: The body returns `PC_PARSER_STATUS_FAILED` (| `CONSUMED` when it advanced then failed). +/// +#define PcRecognizer(...) OVERLOAD(PcRecognizer, __VA_ARGS__) +#define PcRecognizer_1(Name) \ + static inline PcParserStatus PcGenParserName(Name)(StrIter * in, ParserCtx * ctx PC_MAYBE_UNUSED) +#define PcRecognizer_2(Name, InT) \ + static inline PcParserStatus PcGenParserName(Name)(StrIter * in, ParserCtx * ctx PC_MAYBE_UNUSED, InT expect) + +#define PcRecognize(...) OVERLOAD(PcRecognize, __VA_ARGS__) +#define PcRecognize_1(Name) PcGenParserName(Name)(in, ctx) +#define PcRecognize_2(Name, In) PcGenParserName(Name)(in, ctx, In) + +/// +/// Run a parser from OUTSIDE a parser body -- the entry point a driver (a REPL, a CLI) uses to +/// kick off the top rule, so it never spells the mangled `pc_parser_` by hand. `PcParse` +/// threads the ambient stream `in` for a rule; a driver owns its own stream, so it passes a +/// pointer to it explicitly. `ctx` is still taken from the enclosing scope. The remaining args +/// (the outputs) forward to the parser call. +/// +/// PcRun(Name, &in, &out) -> pc_parser_Name(&in, ctx, &out) +/// +#define PcRun(Name, InPtr, ...) PcGenParserName(Name)(InPtr, ctx, __VA_ARGS__) /// /// Define (or, when followed by `;`, forward-declare) a parser. Overloaded by arity: @@ -239,6 +282,15 @@ enum { return PC_CONSUMED(pc_seq.start) | PC_PARSER_STATUS_FAILED; \ } while (0) +/// +/// PcReject: fail the current rule outright from its body -- the escape hatch for a +/// context-sensitive rejection no combinator can express (an undefined variable, a name that is +/// not a typedef, ...). Reports the sequence's consumed bit, exactly as a failing `PcMatch` would, +/// so an enclosing choice commits/backtracks correctly. A `PcSeq` step, like `PcMatch`; a rule +/// uses it so it never has to name the consumed bit or the status itself. +/// +#define PcReject() return PC_CONSUMED(pc_seq.start) | PC_PARSER_STATUS_FAILED + /// /// PcMany: zero-or-more. Run parser `Name` repeatedly; the body runs once per match. A match /// that consumed nothing would spin forever, so the loop stops on it (it never aborts and never @@ -252,6 +304,36 @@ enum { true : \ ((*in = UNPL(pc_many_)), false);) +/// +/// PcOpt: zero-or-one. Try parser `Name`; if it matches, run the body once (the parsed value is +/// available through whatever output pointer was passed). If it does not match, rewind and skip +/// the body. Never fails the sequence -- it is a step that is simply optional. +/// +#define PcOpt(Name, ...) \ + StrIter UNPL(pc_opt_) = *in; \ + if ((PcParse(Name, __VA_ARGS__) & PC_PARSER_STATUS_SUCCESS) ? true : (*in = UNPL(pc_opt_), false)) + +/// +/// PcRecognizeMany: the recognizer-flavoured `PcMany` -- a whole recognizer body that runs a +/// sub-recognizer zero-or-more times and then returns success. Terminal (it owns the `return`), so +/// it is the entire body of a "recognize zero-or-more of one thing" parser (e.g. whitespace), not +/// a mid-sequence step. Same empty-match guard as `PcMany`: a non-consuming match stops the loop +/// and is rewound, so it can never spin. Overloaded 1-arg / 2-arg like `PcRecognize`. +/// +#define PcRecognizeMany(...) OVERLOAD(PcRecognizeMany, __VA_ARGS__) +#define PcRecognizeMany_1(Name) PC_RECOGNIZE_MANY(PcRecognize_1(Name)) +#define PcRecognizeMany_2(Name, In) PC_RECOGNIZE_MANY(PcRecognize_2(Name, In)) +#define PC_RECOGNIZE_MANY(call) \ + do { \ + StrIter UNPL(pc_rm_start) = *in; \ + for (StrIter UNPL(pc_rm_mark) = *in; \ + (UNPL(pc_rm_mark) = *in, ((call) & PC_PARSER_STATUS_SUCCESS) && in->pos != UNPL(pc_rm_mark).pos) ? \ + true : \ + ((*in = UNPL(pc_rm_mark)), false);) \ + ; \ + return PC_CONSUMED(UNPL(pc_rm_start)) | PC_PARSER_STATUS_SUCCESS; \ + } while (0) + /// /// Choice arms (used inside `PcChoice`). All forward their extra args to the parser call. /// From 5bbef2a297dae4d6a56ba4d69c06c84ad19c22dc Mon Sep 17 00:00:00 2001 From: Siddharth Mishra Date: Wed, 1 Jul 2026 22:32:01 -0700 Subject: [PATCH 03/11] feat(parsercombinator): greedy diagnostics for CalC, and vendor it as a tool Error reporting as a shared, greedy model (the rustc-style sink, not first-fail): DSL (ParserCombinator.h): - PcReport (level + input span + message) and PcReportError/Warn/Info: record a diagnostic to ctx->reports and keep going, so a rule poisons its value instead of unwinding and one input surfaces as many errors as it has. - macro-hygiene fixes surfaced by a fan-out audit: PcSatisfyStr now evaluates its Expect argument once (bound to a UNPL local); PcOpt is a single for-scoped statement (was an enclosing-scope decl + bare if), so it nests, sits one-per-line, and takes an unbraced body without a dangling-else. CalC (Bin/CalC.c): - greedy diagnostics: a poison bit on Num that propagates through arithmetic, undefined-variable / division-by-zero / overflow reported-and-continued, and a rustc-style renderer (source line + carets) that parsers never touch. - REPL prints results/errors via WriteFmt/WriteFmtLn (+ a prompt), not LOG. - the driver checks leftover input through the Iter API (StrIterRemainingLength / StrIterIndex), not raw field access. - assignment upserts (Map is a multimap, so replace not duplicate) and does not bind a poisoned result. Build: - vendor calc as a real Bin tool in meson.build (gated on Map + File, install:false), built through the existing build/ dir. --- Bin/CalC.c | 164 +++++++++++++++++++++++-------- Include/Misra/ParserCombinator.h | 58 +++++++++-- meson.build | 16 +++ 3 files changed, 190 insertions(+), 48 deletions(-) diff --git a/Bin/CalC.c b/Bin/CalC.c index 56f6389..2d7a526 100644 --- a/Bin/CalC.c +++ b/Bin/CalC.c @@ -5,7 +5,9 @@ /// CalC -- a small immediate-mode calculator, the running playground for the /// parser-combinator DSL. It parses and evaluates as it reads (no AST): every /// rule folds its result straight into a `Num`. Variables live in the grammar -/// context, so a result can be named and reused on a later line. +/// context, and errors are collected greedily: a broken sub-expression records a +/// `PcReport`, poisons its value, and parsing continues, so one line reports as +/// many problems as it has. /// /// line = assignment | expr /// assignment = identifier '=' expr (stores into ctx->vars) @@ -17,12 +19,10 @@ /// number = digit+ ( '.' digit+ )? (int, or float if a '.') /// identifier = letter+ (a variable name) /// -/// Whitespace is scannerless: each token consumes trailing whitespace and the -/// top rule skips leading whitespace once. `pi` and `e` are seeded variables. -/// typedef struct Num { bool is_float; + bool is_error; ///< poison: a broken sub-expression; propagates, never aborts union { i64 i; f64 f; @@ -30,49 +30,63 @@ typedef struct Num { } Num; static Num num_int(i64 v) { - return (Num) {.is_float = false, .i = v}; + return (Num) {.i = v}; } static Num num_flt(f64 v) { return (Num) {.is_float = true, .f = v}; } +static Num num_poison(void) { + return (Num) {.is_error = true}; +} static f64 num_f(Num n) { return n.is_float ? n.f : (f64)n.i; } +static bool num_is_zero(Num n) { + return n.is_float ? (n.f == 0.0) : (n.i == 0); +} static Num num_add(Num a, Num b) { + if (a.is_error || b.is_error) + return num_poison(); return (a.is_float || b.is_float) ? num_flt(num_f(a) + num_f(b)) : num_int(a.i + b.i); } static Num num_sub(Num a, Num b) { + if (a.is_error || b.is_error) + return num_poison(); return (a.is_float || b.is_float) ? num_flt(num_f(a) - num_f(b)) : num_int(a.i - b.i); } static Num num_mul(Num a, Num b) { + if (a.is_error || b.is_error) + return num_poison(); return (a.is_float || b.is_float) ? num_flt(num_f(a) * num_f(b)) : num_int(a.i * b.i); } static Num num_div(Num a, Num b) { - if (a.is_float || b.is_float) { - f64 d = num_f(b); - return num_flt(d != 0.0 ? num_f(a) / d : 0.0); - } - return num_int(b.i != 0 ? a.i / b.i : 0); + if (a.is_error || b.is_error || num_is_zero(b)) + return num_poison(); + return (a.is_float || b.is_float) ? num_flt(num_f(a) / num_f(b)) : num_int(a.i / b.i); } static Num num_mod(Num a, Num b) { - if (a.is_float || b.is_float) - return num_flt(0.0); - return num_int(b.i != 0 ? a.i % b.i : 0); + if (a.is_error || b.is_error || num_is_zero(b) || a.is_float || b.is_float) + return num_poison(); + return num_int(a.i % b.i); } static Num num_neg(Num a) { + if (a.is_error) + return num_poison(); return a.is_float ? num_flt(-a.f) : num_int(-a.i); } typedef Map(Str, Num) Vars; +typedef Vec(PcReport) Reports; /// -/// The grammar context threaded through every parser as `ctx`. For CalC it is -/// the variable environment; the map owns its allocator (and deep-copies names), -/// so a later grammar's `ctx` is where an AST root / arena would join it. +/// The grammar context threaded through every parser as `ctx`: the variable +/// environment plus the diagnostics sink the `PcReport*` macros append to. Both +/// are containers that carry their own allocator. /// typedef struct ParserCtx { - Vars vars; + Vars vars; + Reports reports; } ParserCtx; PcRecognizer(WsChar); @@ -139,8 +153,8 @@ PcParser(SignCh, char) { } } -/// number = digit+ ( '.' digit+ )? -- accumulated char-by-char into a Str, then -/// converted by the library's own parser; int unless a '.' makes it a float +/// number = digit+ ( '.' digit+ )? -- accumulated into a Str, converted by the +/// library parser; a value that does not fit is reported and poisoned, not aborted PcParser(Number, Num) { char d; bool is_float = false; @@ -160,12 +174,20 @@ PcParser(Number, Num) { } if (is_float) { f64 f = 0; - StrToF64(&tok, &f, NULL); - *value = num_flt(f); + if (StrToF64(&tok, &f, NULL)) + *value = num_flt(f); + else { + PcReportError("malformed number"); + *value = num_poison(); + } } else { u64 u = 0; - StrToU64(&tok, &u, NULL); - *value = num_int((i64)u); + if (StrToU64(&tok, &u, NULL)) + *value = num_int((i64)u); + else { + PcReportError("number does not fit in an integer"); + *value = num_poison(); + } } } PcRecognize(Ws); @@ -185,15 +207,18 @@ PcParser(Identifier, Str) { } } -/// variable reference: look the name up in the environment; undefined -> reject +/// variable reference: look the name up; undefined -> report and poison (keep going) PcParser(VarRef, Num) { PcSeq() { StrInitStack(name, 64) { PcMatch(Identifier, &name); Num *slot = MapGetFirstPtr(&ctx->vars, name); - if (!slot) - PcReject(); - *value = *slot; + if (slot) + *value = *slot; + else { + PcReportError("undefined variable"); + *value = num_poison(); + } } } } @@ -247,7 +272,15 @@ PcParser(Multiplicative, Num) { PcMatch(Unary, value); PcMany(MulOp, &op) { PcMatch(Unary, &rhs); - *value = (op == '*') ? num_mul(*value, rhs) : (op == '/') ? num_div(*value, rhs) : num_mod(*value, rhs); + bool fresh = !value->is_error && !rhs.is_error; + if (fresh && (op == '/' || op == '%') && num_is_zero(rhs)) { + PcReportError("division by zero"); + *value = num_poison(); + } else if (fresh && op == '%' && (value->is_float || rhs.is_float)) { + PcReportError("modulo needs integer operands"); + *value = num_poison(); + } else + *value = (op == '*') ? num_mul(*value, rhs) : (op == '/') ? num_div(*value, rhs) : num_mod(*value, rhs); } } } @@ -276,7 +309,10 @@ PcParser(Assignment, Num) { PcMatch(Identifier, &name); PcMatch(Sym, '=', &eq); PcMatch(Expr, value); - (void)MapInsertR(&ctx->vars, name, *value); + // upsert (the map is a multimap, so replace rather than add a duplicate); a broken + // result does not bind the name + if (!value->is_error) + MapSetOnlyR(&ctx->vars, name, *value); } } } @@ -298,28 +334,66 @@ PcParser(Calc, Num) { /// bind a predefined variable; the map deep-copies the key, so the temporary is freed static void seed(Vars *vars, HeapAllocator *heap, Zstr name, Num v) { Str key = StrInitFromCstr(name, ZstrLen(name), heap); - (void)MapInsertR(vars, key, v); + MapInsertR(vars, key, v); StrDeinit(&key); } -static void eval_line(ParserCtx *ctx, const char *src, u64 len) { - StrIter in = StrIterFromCstr((char *)src, len); +static Zstr level_word(ReportLevel level) { + switch (level) { + case REPORT_ERROR : + return "error"; + case REPORT_WARN : + return "warning"; + default : + return "note"; + } +} + +/// draw each report rustc-style: level + message, the source line, then carets +/// under its span. Parsers never touch this -- they only record a `PcReport`. +static void render(Str *src, Reports *reports) { + const char *bytes = StrBegin(src); + for (u64 r = 0; r < VecLen(reports); r++) { + PcReport rep = VecAt(reports, r); + u64 end = rep.end; + while (end > rep.start && (bytes[end - 1] == ' ' || bytes[end - 1] == '\t')) + end--; + WriteFmtLn("{}: {}", level_word(rep.level), rep.message); + WriteFmtLn(" {}", *src); + StrInitStack(caret, 512) { + char space = ' ', hat = '^'; + for (u64 c = 0; c < rep.start; c++) + StrPushBackR(&caret, space); + for (u64 c = rep.start; c < end; c++) + StrPushBackR(&caret, hat); + WriteFmtLn(" {}", caret); + } + } +} + +static void eval_line(ParserCtx *ctx, Str *line) { + VecClear(&ctx->reports); + StrIter in = StrIterFromStr(*line); Num out = {0}; PcParserStatus st = PcRun(Calc, &in, &out); - if ((st & PC_PARSER_STATUS_SUCCESS) && in.pos == in.length) { + if (VecLen(&ctx->reports) > 0) + render(line, &ctx->reports); + else if ((st & PC_PARSER_STATUS_SUCCESS) && !StrIterRemainingLength(&in)) { if (out.is_float) - LOG_INFO("{}", out.f); + WriteFmtLn("{}", out.f); else - LOG_INFO("{}", out.i); - } else { - LOG_INFO("error at column {}", in.pos + 1); - } + WriteFmtLn("{}", out.i); + } else + WriteFmtLn("error at column {}", StrIterIndex(&in) + 1); } int main(int argc, char **argv) { HeapAllocator heap = HeapAllocatorInit(); - ParserCtx ctx = {.vars = MapInitWithDeepCopy(str_hash, str_compare, str_init_copy, str_deinit, NULL, NULL, &heap)}; + ParserCtx ctx = { + .vars = MapInitWithDeepCopy(str_hash, str_compare, str_init_copy, str_deinit, NULL, NULL, &heap), + .reports = VecInit(&heap), + }; seed(&ctx.vars, &heap, "pi", num_flt(3.14159265358979323846)); seed(&ctx.vars, &heap, "e", num_flt(2.71828182845904523536)); @@ -333,11 +407,14 @@ int main(int argc, char **argv) { status = 1; } else if (rc != ARG_RUN_HELP) { if (expr != NULL) { - eval_line(&ctx, expr, ZstrLen(expr)); + Str cmd = StrInitFromCstr(expr, ZstrLen(expr), &heap); + eval_line(&ctx, &cmd); + StrDeinit(&cmd); } else { File fin = FileStdin(); StrInitStack(line, 4096) { for (;;) { + WriteFmt("> "); StrClear(&line); bool eof = false; for (;;) { @@ -352,15 +429,18 @@ int main(int argc, char **argv) { StrPushBack(&line, c); } if (StrLen(&line) > 0) - eval_line(&ctx, StrBegin(&line), StrLen(&line)); - if (eof) + eval_line(&ctx, &line); + if (eof) { + WriteFmtLn(""); break; + } } } } } ArgParseDeinit(&args); + VecDeinit(&ctx.reports); MapDeinit(&ctx.vars); HeapAllocatorDeinit(&heap); return status; diff --git a/Include/Misra/ParserCombinator.h b/Include/Misra/ParserCombinator.h index 4c3d6db..bf143bf 100644 --- a/Include/Misra/ParserCombinator.h +++ b/Include/Misra/ParserCombinator.h @@ -126,13 +126,35 @@ enum { PC_PARSER_STATUS_CONSUMED = 1u << 1 }; +/// +/// Diagnostics. Error reporting is a shared model, not per-grammar: a parser records what it has +/// to say (a level + a span into the input + its own words) and stays out of the rendering. The +/// grammar author never draws a caret or computes a column -- a renderer turns these into output. +/// Reporting does NOT unwind (it is not "raise"): a rule records a `PcReport`, substitutes a +/// placeholder/poison value, and keeps parsing, so one input yields as many errors as possible. +/// +typedef enum { + REPORT_INFO, + REPORT_WARN, + REPORT_ERROR +} ReportLevel; + +typedef struct PcReport { + u64 start; ///< span start, an index into the input (see `IterIndex`) + u64 end; ///< span end, exclusive + ReportLevel level; + Zstr message; ///< the parser's words; the specifics show under the caret +} PcReport; + /// /// ParserCtx is NOT defined here. Each grammar (a C parser, a JSON parser, ...) declares its /// own `typedef struct { ... } ParserCtx;` before using `PcParser`, and threads a pointer to /// it through every parser as `ctx`. It carries whatever the grammar needs to build output as /// it parses, typically: /// - an `Allocator *` the parsers allocate AST nodes from (giving them a lifetime beyond the -/// parse), and +/// parse), +/// - a `Vec(PcReport) reports;` sink the `PcReport*` macros append to (required by those +/// macros), and /// - the growing root of the AST / a symbol table for context-sensitive rules. /// The combinator only forwards `ctx`; it never looks inside it. /// @@ -291,6 +313,21 @@ enum { /// #define PcReject() return PC_CONSUMED(pc_seq.start) | PC_PARSER_STATUS_FAILED +/// +/// Record a diagnostic and KEEP GOING -- the greedy alternative to `PcReject`. Appends a +/// `PcReport` (spanning what the current `PcSeq` frame has consumed so far) to `ctx->reports`, +/// then returns nothing, so the rule substitutes a poison value and parsing continues to collect +/// more errors. `ctx` must carry a `Vec(PcReport) reports;`. A `PcSeq` step, like `PcReject`. +/// +#define PcReportError(Msg) PC_REPORT(REPORT_ERROR, Msg) +#define PcReportWarn(Msg) PC_REPORT(REPORT_WARN, Msg) +#define PcReportInfo(Msg) PC_REPORT(REPORT_INFO, Msg) +#define PC_REPORT(Level, Msg) \ + VecPushBack( \ + &ctx->reports, \ + ((PcReport) {.start = (pc_seq.start).pos, .end = IterIndex(in), .level = (Level), .message = (Msg)}) \ + ) + /// /// PcMany: zero-or-more. Run parser `Name` repeatedly; the body runs once per match. A match /// that consumed nothing would spin forever, so the loop stops on it (it never aborts and never @@ -307,11 +344,18 @@ enum { /// /// PcOpt: zero-or-one. Try parser `Name`; if it matches, run the body once (the parsed value is /// available through whatever output pointer was passed). If it does not match, rewind and skip -/// the body. Never fails the sequence -- it is a step that is simply optional. +/// the body. Never fails the sequence -- it is a step that is simply optional. Like `PcMany`, it +/// is one `for` statement with its bookkeeping in the loop scope, so it nests, sits next to other +/// steps on the same line, and takes an unbraced body without a dangling-`else` surprise. /// #define PcOpt(Name, ...) \ - StrIter UNPL(pc_opt_) = *in; \ - if ((PcParse(Name, __VA_ARGS__) & PC_PARSER_STATUS_SUCCESS) ? true : (*in = UNPL(pc_opt_), false)) + for (struct { \ + StrIter mark; \ + bool ran; \ + } UNPL(pc_opt_) = {*in, false}; \ + !UNPL(pc_opt_).ran && \ + ((UNPL(pc_opt_).ran = true), \ + (PcParse(Name, __VA_ARGS__) & PC_PARSER_STATUS_SUCCESS) ? true : (*in = UNPL(pc_opt_).mark, false));) /// /// PcRecognizeMany: the recognizer-flavoured `PcMany` -- a whole recognizer body that runs a @@ -397,11 +441,13 @@ enum { /// FAILURE: `Expect` was not present; nothing consumed; returns FAILED. /// #define PcSatisfyStr(Expect) \ - u64 UNPL(pc_ss_len) = ZstrLen(Expect); \ + Zstr UNPL(pc_ss_exp) = (Expect); \ + u64 UNPL(pc_ss_len) = ZstrLen(UNPL(pc_ss_exp)); \ bool UNPL(pc_ss_ok) = true; \ for (u64 UNPL(pc_ss_i) = 0; UNPL(pc_ss_i) < UNPL(pc_ss_len); UNPL(pc_ss_i)++) { \ char UNPL(pc_ss_c); \ - if (!StrIterPeekAt(in, (i64)UNPL(pc_ss_i), &UNPL(pc_ss_c)) || UNPL(pc_ss_c) != (Expect)[UNPL(pc_ss_i)]) { \ + if (!StrIterPeekAt(in, (i64)UNPL(pc_ss_i), &UNPL(pc_ss_c)) || \ + UNPL(pc_ss_c) != UNPL(pc_ss_exp)[UNPL(pc_ss_i)]) { \ UNPL(pc_ss_ok) = false; \ break; \ } \ diff --git a/meson.build b/meson.build index 31252fc..04e3d87 100644 --- a/meson.build +++ b/meson.build @@ -742,6 +742,22 @@ if opt_sys_dns ) endif +# calc: an immediate-mode parser-combinator calculator (Bin/CalC.c) -- the +# worked example / playground for Include/Misra/ParserCombinator.h. Built and +# kept green by the tree, but not installed: it demonstrates the DSL, it is not +# a shipping utility. Needs Map (variables) and File (the stdin REPL). +if opt_map and opt_file + executable( + 'calc', + files('Bin/CalC.c') + freestanding_extra_sources, + link_with: [misra_std], + c_args: common_c_args, + include_directories: inc_misra, + install: false, + link_args: freestanding_link_args + ) +endif + # Fuzzing harness pulls in List explicitly through Fuzz/Harness/ListInt.c. if opt_list executable( From d0b7e00f892aea65f8ebb0bfa5da170ee351eb70 Mon Sep 17 00:00:00 2001 From: Siddharth Mishra Date: Thu, 2 Jul 2026 00:04:06 -0700 Subject: [PATCH 04/11] fix(container): evaluate the L-form insert argument exactly once VecInsertL / VecInsertFastL (and everything routing through them -- VecPushBackL, StrPushBackL, ...) and ListInsertL passed the inserted value TWICE: `&LVAL_AS(T, lval)` (a stable copy to insert) and `&(lval)` (the source to zero on the move). A side-effecting lvalue like `VecPushBack(&v, arr[i++])` therefore ran its side effect twice. Collapse each to a single `&(lval)`: `vec_insert_one_l` / `list_insert_one_l` now take one `void *source` and do both the copy-in and the zero-on-success through it (the shape Map's L-form already used). LVAL_AS stays in the R-forms, which is where immediates/rvalues legitimately need it -- so `VecPushBack(v, )` still (correctly) will not compile a move of a temporary. Full suite green (119/119). --- Include/Misra/Std/Container/List/Insert.h | 2 +- Include/Misra/Std/Container/List/Private.h | 2 +- Include/Misra/Std/Container/Vec/Insert.h | 11 ++--------- Include/Misra/Std/Container/Vec/Private.h | 9 +-------- Source/Misra/Std/Container/List.c | 4 ++-- Source/Misra/Std/Container/Vec.c | 13 +++---------- 6 files changed, 10 insertions(+), 31 deletions(-) diff --git a/Include/Misra/Std/Container/List/Insert.h b/Include/Misra/Std/Container/List/Insert.h index d36d63d..d29f6eb 100644 --- a/Include/Misra/Std/Container/List/Insert.h +++ b/Include/Misra/Std/Container/List/Insert.h @@ -33,7 +33,7 @@ #define ListInsertL(l, lval, idx) \ (ValidateList(l), \ CHECK_TYPE_EQUIVALENCE(TYPE_OF(lval), LIST_DATA_TYPE(l)), \ - list_insert_one_l(GENERIC_LIST(l), &LVAL_AS(LIST_DATA_TYPE(l), lval), &(lval), sizeof(LIST_DATA_TYPE(l)), (idx))) + list_insert_one_l(GENERIC_LIST(l), &(lval), sizeof(LIST_DATA_TYPE(l)), (idx))) /// /// Insert a single element at the given index. R-value form: source is treated diff --git a/Include/Misra/Std/Container/List/Private.h b/Include/Misra/Std/Container/List/Private.h index f56839e..b48196d 100644 --- a/Include/Misra/Std/Container/List/Private.h +++ b/Include/Misra/Std/Container/List/Private.h @@ -27,7 +27,7 @@ GenericListNode *get_node_relative_to_list_node(GenericListNode *node, i64 ridx) GenericListNode *get_node_random_access(GenericList *list, GenericListNode *node, u64 nidx, i64 ridx); GenericListNode *get_node_for_list_iteration(GenericList *list, GenericListNode *node, u64 nidx, u64 target_idx); -bool list_insert_one_l(GenericList *list, const void *item_copy, void *source, u64 item_size, u64 idx); +bool list_insert_one_l(GenericList *list, void *source, u64 item_size, u64 idx); bool list_insert_one_r(GenericList *list, const void *item_copy, u64 item_size, u64 idx); bool list_insert_range_l(GenericList *list, void *items, u64 item_size, u64 count); bool list_insert_range_r(GenericList *list, const void *items, u64 item_size, u64 count); diff --git a/Include/Misra/Std/Container/Vec/Insert.h b/Include/Misra/Std/Container/Vec/Insert.h index c39c5b0..d9b656e 100644 --- a/Include/Misra/Std/Container/Vec/Insert.h +++ b/Include/Misra/Std/Container/Vec/Insert.h @@ -39,7 +39,7 @@ #define VecInsertL(v, lval, idx) \ (ValidateVec(v), \ CHECK_TYPE_EQUIVALENCE(TYPE_OF(lval), VEC_DATATYPE(v)), \ - vec_insert_one_l(GENERIC_VEC(v), &LVAL_AS(VEC_DATATYPE(v), lval), &(lval), sizeof(VEC_DATATYPE(v)), (idx), true)) + vec_insert_one_l(GENERIC_VEC(v), &(lval), sizeof(VEC_DATATYPE(v)), (idx), true)) /// /// Insert a single element at the given index, preserving order of trailing @@ -101,14 +101,7 @@ #define VecInsertFastL(v, lval, idx) \ (ValidateVec(v), \ CHECK_TYPE_EQUIVALENCE(TYPE_OF(lval), VEC_DATATYPE(v)), \ - vec_insert_one_l( \ - GENERIC_VEC(v), \ - &LVAL_AS(VEC_DATATYPE(v), lval), \ - &(lval), \ - sizeof(VEC_DATATYPE(v)), \ - (idx), \ - false \ - )) + vec_insert_one_l(GENERIC_VEC(v), &(lval), sizeof(VEC_DATATYPE(v)), (idx), false)) /// /// Insert a single element using fast (order-not-preserving) placement. diff --git a/Include/Misra/Std/Container/Vec/Private.h b/Include/Misra/Std/Container/Vec/Private.h index 8a36001..7413cd7 100644 --- a/Include/Misra/Std/Container/Vec/Private.h +++ b/Include/Misra/Std/Container/Vec/Private.h @@ -31,14 +31,7 @@ extern "C" { size find_idx_vec(GenericVec *vec, const void *item_data, size item_size, GenericCompare comp); void validate_vec(const GenericVec *v); - bool vec_insert_one_l( - GenericVec *vec, - const void *item_copy, - void *source, - size item_size, - size idx, - bool preserve_order - ); + bool vec_insert_one_l(GenericVec *vec, void *source, size item_size, size idx, bool preserve_order); bool vec_insert_one_r(GenericVec *vec, const void *item_copy, size item_size, size idx, bool preserve_order); bool vec_insert_range_l(GenericVec *vec, void *items, size item_size, size idx, size count, bool preserve_order); bool vec_insert_range_r( diff --git a/Source/Misra/Std/Container/List.c b/Source/Misra/Std/Container/List.c index b2f68c2..9e3c2ef 100644 --- a/Source/Misra/Std/Container/List.c +++ b/Source/Misra/Std/Container/List.c @@ -547,8 +547,8 @@ GenericListNode *get_node_for_list_iteration(GenericList *list, GenericListNode return get_node_random_access(list, node, nidx, (i64)target_idx - (i64)nidx); } -bool list_insert_one_l(GenericList *list, const void *item_copy, void *source, u64 item_size, u64 idx) { - return list_zero_source_on_success(list, source, item_size, insert_into_list(list, item_copy, item_size, idx)); +bool list_insert_one_l(GenericList *list, void *source, u64 item_size, u64 idx) { + return list_zero_source_on_success(list, source, item_size, insert_into_list(list, source, item_size, idx)); } bool list_insert_one_r(GenericList *list, const void *item_copy, u64 item_size, u64 idx) { diff --git a/Source/Misra/Std/Container/Vec.c b/Source/Misra/Std/Container/Vec.c index 2113c2f..6ce3de9 100644 --- a/Source/Misra/Std/Container/Vec.c +++ b/Source/Misra/Std/Container/Vec.c @@ -538,16 +538,9 @@ void validate_vec(const GenericVec *v) { ((GenericVec *)(void *)v)->__magic &= ~MAGIC_VALIDATED_BIT; } -bool vec_insert_one_l( - GenericVec *vec, - const void *item_copy, - void *source, - size item_size, - size idx, - bool preserve_order -) { - bool success = preserve_order ? insert_range_into_vec(vec, item_copy, item_size, idx, 1) : - insert_range_fast_into_vec(vec, item_copy, item_size, idx, 1); +bool vec_insert_one_l(GenericVec *vec, void *source, size item_size, size idx, bool preserve_order) { + bool success = preserve_order ? insert_range_into_vec(vec, source, item_size, idx, 1) : + insert_range_fast_into_vec(vec, source, item_size, idx, 1); return vec_zero_source_on_success(vec, source, item_size, success); } From b9bdf1ee226031415a027c6d989e38f9d5c64861 Mon Sep 17 00:00:00 2001 From: Siddharth Mishra Date: Thu, 2 Jul 2026 00:04:27 -0700 Subject: [PATCH 05/11] feat(parsercombinator): cursor-span reports, syntax recovery, PC_ naming - PcReportErrorHere/WarnHere/InfoHere: report a diagnostic spanning the single character at the cursor (frame-free), for "expected X" errors where nothing was consumed -- the twin of the frame-span PcReport* family. - PcElse (PcChoice fallback: body runs iff no arm matched, without a rule naming pc_ch) + PcRecover (resync skip); CalC's Atom uses them so a bad operand is reported and the parse resyncs, surfacing many errors per line, not just the first. - rename the diagnostic API to the module prefix: ReportLevel -> PcReportLevel, REPORT_{INFO,WARN,ERROR} -> PC_REPORT_{INFO,WARN,ERROR}. - PC_REPORT / PC_REPORT_HERE push through VecPushBackR (R-form) so the report's compound literal, including its message, is evaluated once. --- Bin/CalC.c | 11 ++++-- Include/Misra/ParserCombinator.h | 57 +++++++++++++++++++++++++------- 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/Bin/CalC.c b/Bin/CalC.c index 2d7a526..65abfc5 100644 --- a/Bin/CalC.c +++ b/Bin/CalC.c @@ -252,6 +252,11 @@ PcParser(Atom, Num) { PcAlt(Parenthesized, value); PcAlt(VarRef, value); PcAlt(Number, value); + PcElse() { + PcReportErrorHere("expected an operand"); + PcRecover(c, c == '+' || c == '-' || c == '*' || c == '/' || c == '%' || c == ')'); + *value = num_poison(); + } } } @@ -338,11 +343,11 @@ static void seed(Vars *vars, HeapAllocator *heap, Zstr name, Num v) { StrDeinit(&key); } -static Zstr level_word(ReportLevel level) { +static Zstr level_word(PcReportLevel level) { switch (level) { - case REPORT_ERROR : + case PC_REPORT_ERROR : return "error"; - case REPORT_WARN : + case PC_REPORT_WARN : return "warning"; default : return "note"; diff --git a/Include/Misra/ParserCombinator.h b/Include/Misra/ParserCombinator.h index bf143bf..cdf07e6 100644 --- a/Include/Misra/ParserCombinator.h +++ b/Include/Misra/ParserCombinator.h @@ -134,16 +134,16 @@ enum { /// placeholder/poison value, and keeps parsing, so one input yields as many errors as possible. /// typedef enum { - REPORT_INFO, - REPORT_WARN, - REPORT_ERROR -} ReportLevel; + PC_REPORT_INFO, + PC_REPORT_WARN, + PC_REPORT_ERROR +} PcReportLevel; typedef struct PcReport { - u64 start; ///< span start, an index into the input (see `IterIndex`) - u64 end; ///< span end, exclusive - ReportLevel level; - Zstr message; ///< the parser's words; the specifics show under the caret + u64 start; ///< span start, an index into the input (see `IterIndex`) + u64 end; ///< span end, exclusive + PcReportLevel level; + Zstr message; ///< the parser's words; the specifics show under the caret } PcReport; /// @@ -319,15 +319,30 @@ typedef struct PcReport { /// then returns nothing, so the rule substitutes a poison value and parsing continues to collect /// more errors. `ctx` must carry a `Vec(PcReport) reports;`. A `PcSeq` step, like `PcReject`. /// -#define PcReportError(Msg) PC_REPORT(REPORT_ERROR, Msg) -#define PcReportWarn(Msg) PC_REPORT(REPORT_WARN, Msg) -#define PcReportInfo(Msg) PC_REPORT(REPORT_INFO, Msg) +#define PcReportError(Msg) PC_REPORT(PC_REPORT_ERROR, Msg) +#define PcReportWarn(Msg) PC_REPORT(PC_REPORT_WARN, Msg) +#define PcReportInfo(Msg) PC_REPORT(PC_REPORT_INFO, Msg) #define PC_REPORT(Level, Msg) \ - VecPushBack( \ + VecPushBackR( \ &ctx->reports, \ ((PcReport) {.start = (pc_seq.start).pos, .end = IterIndex(in), .level = (Level), .message = (Msg)}) \ ) +/// +/// The `...Here` variants report a diagnostic whose span is the single character at the current +/// cursor, rather than the enclosing `PcSeq` frame. Use them where nothing has been consumed and +/// there is no frame span to point at -- an "expected X"-style error, e.g. inside a `PcChoice` +/// where every arm has rewound. They read only the cursor, so they need no `pc_seq`. +/// +#define PcReportErrorHere(Msg) PC_REPORT_HERE(PC_REPORT_ERROR, Msg) +#define PcReportWarnHere(Msg) PC_REPORT_HERE(PC_REPORT_WARN, Msg) +#define PcReportInfoHere(Msg) PC_REPORT_HERE(PC_REPORT_INFO, Msg) +#define PC_REPORT_HERE(Level, Msg) \ + VecPushBackR( \ + &ctx->reports, \ + ((PcReport) {.start = IterIndex(in), .end = IterIndex(in) + 1, .level = (Level), .message = (Msg)}) \ + ) + /// /// PcMany: zero-or-more. Run parser `Name` repeatedly; the body runs once per match. A match /// that consumed nothing would spin forever, so the loop stops on it (it never aborts and never @@ -410,6 +425,24 @@ typedef struct PcReport { ((pc_ch.st = PcParse(Name, __VA_ARGS__)) & PC_PARSER_STATUS_SUCCESS ? ((pc_ch.matched = pc_ch.done = true)) : \ (*in = pc_ch.mark, false))) +/// +/// PcElse: the fallback arm of a `PcChoice`. Its body runs iff NO arm matched (every arm failed +/// cleanly, consuming nothing), and it marks the choice handled so the rule succeeds with whatever +/// the body writes to the output (typically a report plus a poison value). Put it last. A rule +/// uses it so it never has to name `pc_ch`. +/// +#define PcElse() if (!pc_ch.done && (pc_ch.matched = pc_ch.done = true)) + +/// +/// PcRecover: recovery skip. Advance the stream until the current character satisfies `IsSync` (a +/// resynchronization boundary) or the stream ends; `Var` is the loop-scoped peeked char. Pairs +/// with a report and a poison value to recover from a syntax error and carry on, so one input +/// surfaces more than just the first structural break. +/// +#define PcRecover(Var, IsSync) \ + for (char Var = 0; StrIterPeek(in, &Var) && !(IsSync);) \ + StrIterMove(in, 1) + /// /// Atoms -- the only place `StrIter` and `PcParserStatus` are handled directly. Every fundamental /// parser (a char class, a keyword, ...) is written on top of one of these, so it never pokes the From 1179837a01dcb513fcf708f57d9329cfa8f6776f Mon Sep 17 00:00:00 2001 From: Siddharth Mishra Date: Thu, 2 Jul 2026 14:49:48 -0700 Subject: [PATCH 06/11] fix(vec): break exits the reverse VecForeach loops The reverse VecForeach*ReverseIdx macros wrapped the iteration loop in two run-once for-loops (to inject the var/run_once declarations), so a break in the body exited those wrappers, not the iteration loop -- iteration silently continued to the next element while forward variants stopped. Mirror the forward variants (iteration loop innermost) so break exits it; the shrink-during-iteration guard is preserved. Adds forward + reverse early-break tests. --- Include/Misra/Std/Container/Vec/Foreach.h | 12 ++-- Tests/Std/Vec/Foreach.c | 68 +++++++++++++++++++++++ 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/Include/Misra/Std/Container/Vec/Foreach.h b/Include/Misra/Std/Container/Vec/Foreach.h index 3bef967..ca82ed2 100644 --- a/Include/Misra/Std/Container/Vec/Foreach.h +++ b/Include/Misra/Std/Container/Vec/Foreach.h @@ -54,9 +54,9 @@ #define VecForeachReverseIdx(v, var, idx) \ for (TYPE_OF(v) UNPL(pv) = (v); UNPL(pv); UNPL(pv) = NULL) \ if ((ValidateVec(UNPL(pv)), 1) && UNPL(pv)->length > 0) \ - for (u64 idx = UNPL(pv)->length; idx-- > 0 && idx < UNPL(pv)->length;) \ - for (u8 UNPL(run_once) = 1; UNPL(run_once); UNPL(run_once) = 0) \ - for (VEC_DATATYPE(UNPL(pv)) var = VecAt(UNPL(pv), idx); UNPL(run_once); UNPL(run_once) = 0) + for (u64 idx = UNPL(pv)->length, UNPL(d) = 1; UNPL(d); UNPL(d)--) \ + for (VEC_DATATYPE(UNPL(pv)) var = {0}; \ + idx-- > 0 && idx < UNPL(pv)->length && (var = VecAt(UNPL(pv), idx), 1);) /// /// Iterate over each element `var` of given vector `v` at each index `idx` into the vector. @@ -107,9 +107,9 @@ #define VecForeachPtrReverseIdx(v, var, idx) \ for (TYPE_OF(v) UNPL(pv) = (v); UNPL(pv); UNPL(pv) = NULL) \ if ((ValidateVec(UNPL(pv)), 1) && UNPL(pv)->length > 0) \ - for (u64 idx = UNPL(pv)->length; idx-- > 0 && idx < UNPL(pv)->length;) \ - for (u8 UNPL(run_once) = 1; UNPL(run_once); UNPL(run_once) = 0) \ - for (VEC_DATATYPE(UNPL(pv)) *var = VecPtrAt(UNPL(pv), idx); UNPL(run_once); UNPL(run_once) = 0) + for (u64 idx = UNPL(pv)->length, UNPL(d) = 1; UNPL(d); UNPL(d)--) \ + for (VEC_DATATYPE(UNPL(pv)) *var = NULL; \ + idx-- > 0 && idx < UNPL(pv)->length && (var = VecPtrAt(UNPL(pv), idx), 1);) /// /// Walk each element of `v` forward, binding `var` to the element value. diff --git a/Tests/Std/Vec/Foreach.c b/Tests/Std/Vec/Foreach.c index ce2d24e..3d5ab06 100644 --- a/Tests/Std/Vec/Foreach.c +++ b/Tests/Std/Vec/Foreach.c @@ -16,6 +16,8 @@ bool test_vec_foreach_reverse(void); bool test_vec_foreach_reverse_idx(void); bool test_vec_foreach_ptr_reverse(void); bool test_vec_foreach_ptr_reverse_idx(void); +bool test_vec_foreach_early_break(void); +bool test_vec_foreach_reverse_early_break(void); bool test_vec_foreach_out_of_bounds_access(void); bool test_vec_foreach_idx_out_of_bounds_access(void); @@ -571,6 +573,70 @@ bool test_vec_foreach_idx_basic_out_of_bounds_access(void) { return true; } +// Early break: `break` inside a forward VecForeach must stop iteration immediately. +bool test_vec_foreach_early_break(void) { + WriteFmt("Testing early break through forward VecForeach variants\n"); + + typedef Vec(int) IntVec; + IntVec vec = VecInit(&alloc); + for (int i = 0; i < 5; i++) + VecPushBackR(&vec, i); // 0,1,2,3,4 + + bool result = true; + + int visited = 0, last = -1; + VecForeach(&vec, item) { + visited++; + last = item; + if (item == 2) + break; + } + result = result && (visited == 3) && (last == 2); + + visited = 0; + last = -1; + VecForeachPtr(&vec, item_ptr) { + visited++; + last = *item_ptr; + if (*item_ptr == 2) + break; + } + result = result && (visited == 3) && (last == 2); + + visited = 0; + VecForeachIdx(&vec, item, idx) { + visited++; + (void)item; + if (idx == 2) + break; + } + result = result && (visited == 3); + + VecDeinit(&vec); + return result; +} + +// Early break through the reverse variant (visits 4,3,2,1,0; break at 2 -> 3 visits). +bool test_vec_foreach_reverse_early_break(void) { + WriteFmt("Testing early break through reverse VecForeach\n"); + + typedef Vec(int) IntVec; + IntVec vec = VecInit(&alloc); + for (int i = 0; i < 5; i++) + VecPushBackR(&vec, i); + + int visited = 0, last = -1; + VecForeachReverse(&vec, item) { + visited++; + last = item; + if (item == 2) + break; + } + + VecDeinit(&vec); + return (visited == 3) && (last == 2); +} + // Main function that runs all tests int main(void) { alloc = DefaultAllocatorInit(); @@ -586,6 +652,8 @@ int main(void) { test_vec_foreach_reverse_idx, test_vec_foreach_ptr_reverse, test_vec_foreach_ptr_reverse_idx, + test_vec_foreach_early_break, + test_vec_foreach_reverse_early_break, test_vec_foreach_out_of_bounds_access, test_vec_foreach_idx_out_of_bounds_access, test_vec_foreach_idx_basic_out_of_bounds_access, From 85f468ebddf1330cabe01ec4601c75aa384d2cdb Mon Sep 17 00:00:00 2001 From: Siddharth Mishra Date: Thu, 2 Jul 2026 14:51:04 -0700 Subject: [PATCH 07/11] feat(parsercombinator): transactional context with automatic backtracking rollback Weaves a savepoint contract into the DSL: a grammar defines PcParserCtx + PcParserCtxMark + PcParserCtxSnapshot/PcParserCtxRollback, and every combinator that can abandon an attempt (PcChoice arms, PcMatchZeroOrMore/PcMatchOneOrMore, PcOpt, the recognizer repeats) snapshots the context on entry and rolls it back when the attempt is abandoned -- a failed parse leaves the context untouched, a committed one keeps its mutations. The grammar author writes only mutations, never snapshot/rollback. Also renames the repetition combinators for honesty (PcMatchZeroOrMore/PcMatchOneOrMore, PcRecognizeZeroOrMore/PcRecognizeOneOrMore, adding the one-or-more variants) and ParserCtx -> PcParserCtx. CalC drops its Map for an append-only Vec(Binding) (rollback = truncate), and VarRef finds the newest binding with VecForeachPtrReverse + break. --- Bin/CalC.c | 76 ++++++---- Include/Misra/ParserCombinator.h | 240 +++++++++++++++++++++++-------- 2 files changed, 225 insertions(+), 91 deletions(-) diff --git a/Bin/CalC.c b/Bin/CalC.c index 65abfc5..d142c94 100644 --- a/Bin/CalC.c +++ b/Bin/CalC.c @@ -76,7 +76,16 @@ static Num num_neg(Num a) { return a.is_float ? num_flt(-a.f) : num_int(-a.i); } -typedef Map(Str, Num) Vars; +typedef struct Binding { + Str name; + Num value; +} Binding; +typedef Vec(Binding) Bindings; + +static void binding_deinit(void *copy, const Allocator *alloc) { + (void)alloc; + StrDeinit(&((Binding *)copy)->name); +} typedef Vec(PcReport) Reports; /// @@ -84,10 +93,22 @@ typedef Vec(PcReport) Reports; /// environment plus the diagnostics sink the `PcReport*` macros append to. Both /// are containers that carry their own allocator. /// -typedef struct ParserCtx { - Vars vars; - Reports reports; -} ParserCtx; +typedef struct PcParserCtx { + Bindings vars; + Reports reports; +} PcParserCtx; + +typedef struct { + u64 len; +} PcParserCtxMark; + +static PcParserCtxMark PcParserCtxSnapshot(PcParserCtx *ctx) { + return (PcParserCtxMark) {.len = VecLen(&ctx->vars)}; +} + +static void PcParserCtxRollback(PcParserCtx *ctx, PcParserCtxMark mark) { + VecResize(&ctx->vars, mark.len); +} PcRecognizer(WsChar); PcRecognizer(Ws); @@ -116,7 +137,7 @@ PcRecognizer(WsChar) { } PcRecognizer(Ws) { - PcRecognizeMany(WsChar); + PcRecognizeZeroOrMore(WsChar); } PcParser(DigitCh, char) { @@ -160,15 +181,13 @@ PcParser(Number, Num) { bool is_float = false; PcSeq() { StrInitStack(tok, 64) { - PcMatch(DigitCh, &d); - StrPushBack(&tok, d); - PcMany(DigitCh, &d) { + PcMatchOneOrMore(DigitCh, &d) { StrPushBack(&tok, d); } PcOpt(CharIfExist, '.', &d) { is_float = true; StrPushBack(&tok, d); - PcMany(DigitCh, &d) { + PcMatchZeroOrMore(DigitCh, &d) { StrPushBack(&tok, d); } } @@ -198,9 +217,7 @@ PcParser(Number, Num) { PcParser(Identifier, Str) { char l; PcSeq() { - PcMatch(LetterCh, &l); - StrPushBack(value, l); - PcMany(LetterCh, &l) { + PcMatchOneOrMore(LetterCh, &l) { StrPushBack(value, l); } PcRecognize(Ws); @@ -212,7 +229,11 @@ PcParser(VarRef, Num) { PcSeq() { StrInitStack(name, 64) { PcMatch(Identifier, &name); - Num *slot = MapGetFirstPtr(&ctx->vars, name); + Num *slot = NULL; + VecForeachPtrReverse(&ctx->vars, b) if (StrCmp(&b->name, &name) == 0) { + slot = &b->value; + break; + } if (slot) *value = *slot; else { @@ -275,7 +296,7 @@ PcParser(Multiplicative, Num) { Num rhs; PcSeq() { PcMatch(Unary, value); - PcMany(MulOp, &op) { + PcMatchZeroOrMore(MulOp, &op) { PcMatch(Unary, &rhs); bool fresh = !value->is_error && !rhs.is_error; if (fresh && (op == '/' || op == '%') && num_is_zero(rhs)) { @@ -295,7 +316,7 @@ PcParser(Additive, Num) { Num rhs; PcSeq() { PcMatch(Multiplicative, value); - PcMany(AddOp, &op) { + PcMatchZeroOrMore(AddOp, &op) { PcMatch(Multiplicative, &rhs); *value = (op == '+') ? num_add(*value, rhs) : num_sub(*value, rhs); } @@ -314,10 +335,10 @@ PcParser(Assignment, Num) { PcMatch(Identifier, &name); PcMatch(Sym, '=', &eq); PcMatch(Expr, value); - // upsert (the map is a multimap, so replace rather than add a duplicate); a broken - // result does not bind the name - if (!value->is_error) - MapSetOnlyR(&ctx->vars, name, *value); + if (!value->is_error) { + Binding b = {.name = StrInitFromStr(&name, VecAllocator(&ctx->vars)), .value = *value}; + VecPushBack(&ctx->vars, b); + } } } } @@ -337,10 +358,9 @@ PcParser(Calc, Num) { } /// bind a predefined variable; the map deep-copies the key, so the temporary is freed -static void seed(Vars *vars, HeapAllocator *heap, Zstr name, Num v) { - Str key = StrInitFromCstr(name, ZstrLen(name), heap); - MapInsertR(vars, key, v); - StrDeinit(&key); +static void seed(Bindings *vars, HeapAllocator *heap, Zstr name, Num v) { + Binding b = {.name = StrInitFromCstr(name, ZstrLen(name), heap), .value = v}; + VecPushBack(vars, b); } static Zstr level_word(PcReportLevel level) { @@ -376,7 +396,7 @@ static void render(Str *src, Reports *reports) { } } -static void eval_line(ParserCtx *ctx, Str *line) { +static void eval_line(PcParserCtx *ctx, Str *line) { VecClear(&ctx->reports); StrIter in = StrIterFromStr(*line); Num out = {0}; @@ -395,8 +415,8 @@ static void eval_line(ParserCtx *ctx, Str *line) { int main(int argc, char **argv) { HeapAllocator heap = HeapAllocatorInit(); - ParserCtx ctx = { - .vars = MapInitWithDeepCopy(str_hash, str_compare, str_init_copy, str_deinit, NULL, NULL, &heap), + PcParserCtx ctx = { + .vars = VecInitWithDeepCopy(NULL, binding_deinit, &heap), .reports = VecInit(&heap), }; seed(&ctx.vars, &heap, "pi", num_flt(3.14159265358979323846)); @@ -446,7 +466,7 @@ int main(int argc, char **argv) { ArgParseDeinit(&args); VecDeinit(&ctx.reports); - MapDeinit(&ctx.vars); + VecDeinit(&ctx.vars); HeapAllocatorDeinit(&heap); return status; } diff --git a/Include/Misra/ParserCombinator.h b/Include/Misra/ParserCombinator.h index cdf07e6..dc1cd8b 100644 --- a/Include/Misra/ParserCombinator.h +++ b/Include/Misra/ParserCombinator.h @@ -90,7 +90,7 @@ /// it to a representation that any phase after parsing can use and analyze. This may heavily /// depend on the language being parsed. /// -/// The mechanism this library uses is a per-grammar context (`ParserCtx`, see below) threaded +/// The mechanism this library uses is a per-grammar context (`PcParserCtx`, see below) threaded /// through every parser. The grammar author places their in-progress structure (an AST root, /// symbol tables, ...) and an allocator on it, so parsers can build long-lived output as the /// input is consumed -- immediate-mode parsing with no separate tree-walking pass. @@ -147,16 +147,67 @@ typedef struct PcReport { } PcReport; /// -/// ParserCtx is NOT defined here. Each grammar (a C parser, a JSON parser, ...) declares its -/// own `typedef struct { ... } ParserCtx;` before using `PcParser`, and threads a pointer to -/// it through every parser as `ctx`. It carries whatever the grammar needs to build output as -/// it parses, typically: -/// - an `Allocator *` the parsers allocate AST nodes from (giving them a lifetime beyond the -/// parse), -/// - a `Vec(PcReport) reports;` sink the `PcReport*` macros append to (required by those -/// macros), and -/// - the growing root of the AST / a symbol table for context-sensitive rules. -/// The combinator only forwards `ctx`; it never looks inside it. +/// The parser context and its savepoint contract +/// ============================================== +/// +/// `PcParserCtx` is NOT defined here. Each grammar (a C parser, a JSON parser, ...) declares its +/// own `typedef struct { ... } PcParserCtx;` before using `PcParser`, and it is threaded through +/// every parser as `ctx`. It carries whatever the grammar accumulates as it parses -- typically an +/// allocator, a `Vec(PcReport) reports;` sink (required by the `PcReport*` macros), and any +/// context-sensitive state the grammar discovers along the way (a symbol table, the set of typedef +/// names, ...). The combinator only forwards `ctx`; it never looks inside it. +/// +/// Because a backtracking parser abandons speculative attempts, any state a parser writes into +/// `ctx` while trying an alternative that later fails must be undone -- otherwise a rule that +/// discovered a symbol on a path the parse did not take would leave that symbol behind. The DSL +/// undoes it automatically, provided the grammar supplies a small savepoint contract next to its +/// `PcParserCtx`: +/// +/// - a type `PcParserCtxMark` an opaque saved-state handle. +/// - a func `PcParserCtxMark PcParserCtxSnapshot(PcParserCtx *ctx)` +/// capture the current context. +/// - a func `void PcParserCtxRollback(PcParserCtx *ctx, PcParserCtxMark mark)` +/// restore a previously captured +/// context. +/// +/// The grammar author writes ONLY the context mutations (bind a symbol, record a type). The author +/// never calls snapshot or rollback -- the combinators do, around every attempt that may be +/// abandoned. The division of labour is: you mutate, the DSL saves and restores. +/// +/// Contract -- what the two functions must guarantee +/// ------------------------------------------------- +/// Picture the context as a state machine with three states, relative to the most recent mark: +/// +/// SETTLED no mark outstanding; the context is the committed baseline. +/// MARKED a mark is held and the context still equals it (nothing changed since). +/// DIRTY a mark is held and the context has changed since it was taken. +/// +/// Snapshot (a mutation) +/// SETTLED ------------------> MARKED -------------------> DIRTY --. +/// ^ | | <--' (a mutation) +/// | Rollback(mark) | Rollback(mark) | +/// '-----------------------------+------------------------------' +/// +/// - Snapshot does NOT change the context; it returns a mark bound to the current state. +/// - A mutation is the whole CLASS of context-changing operations the grammar performs; every one +/// must be reversible with respect to every outstanding mark. +/// - Rollback(mark) restores the context to exactly what it was when `mark` was taken, however +/// many mutations happened since. A mark may be rolled back to more than once (an alternation +/// rewinds to the same mark once per failing arm), so rollback must be repeatable. +/// +/// How the combinators use it during backtracking +/// ---------------------------------------------- +/// Before a combinator tries an attempt it might abandon -- an arm of a `PcChoice`, the body of a +/// `PcOpt`, one round of a `PcMatchZeroOrMore`/`PcMatchOneOrMore` -- it takes a mark. If the attempt is abandoned (an arm +/// fails without committing, an optional is absent, a repetition stops), the context is rolled back +/// to that mark, so a failed attempt leaves the context exactly as it found it. If the attempt +/// commits (it consumed input and belongs to the parse), its mutations stay. A grammar therefore +/// reads and writes `ctx` as ordinary imperative code and still parses soundly under backtracking, +/// with no manual save or restore anywhere in the grammar. +/// +/// The `reports` sink is deliberately OUTSIDE this contract: diagnostics accumulate monotonically +/// and are never rolled back, so an error explaining why a branch failed survives even after the +/// parser backtracks past it. Snapshot and rollback concern the grammar's semantic state only. /// /// Mangled name of the parser function for rule `Name`. @@ -177,7 +228,7 @@ typedef struct PcReport { /// PcParse(Name, Out) -> pc_parser_Name(in, ctx, Out) /// PcParse(Name, In, Out) -> pc_parser_Name(in, ctx, In, Out) /// -/// Used internally by `PcMatch`/`PcAlt`/`PcMany`/`PcOpt`; call it directly only to delegate one +/// Used internally by `PcMatch`/`PcAlt`/`PcMatchOneOrMore`/`PcOpt`; call it directly only to delegate one /// rule wholesale to another (`return PcParse(Other, value);`). Because an output parser always /// carries the output argument, this family never sees an empty argument list -- so there is no /// `__VA_OPT__` here (the recognizer family below has none either, for the same reason: every @@ -192,8 +243,8 @@ typedef struct PcReport { /// the "validator" style. Identical to the `PcParser`/`PcParse` family with the trailing output /// slot removed, so the arities shift down by one: 1-arg (no input) or 2-arg (an input to match). /// -/// PcRecognizer(Name) -> (StrIter *in, ParserCtx *ctx) define, no input -/// PcRecognizer(Name, InT) -> (StrIter *in, ParserCtx *ctx, InT expect) define, one input +/// PcRecognizer(Name) -> (StrIter *in, PcParserCtx *ctx) define, no input +/// PcRecognizer(Name, InT) -> (StrIter *in, PcParserCtx *ctx, InT expect) define, one input /// PcRecognize(Name) -> pc_parser_Name(in, ctx) call, no input /// PcRecognize(Name, In) -> pc_parser_Name(in, ctx, In) call, one input /// @@ -202,9 +253,9 @@ typedef struct PcReport { /// #define PcRecognizer(...) OVERLOAD(PcRecognizer, __VA_ARGS__) #define PcRecognizer_1(Name) \ - static inline PcParserStatus PcGenParserName(Name)(StrIter * in, ParserCtx * ctx PC_MAYBE_UNUSED) + static inline PcParserStatus PcGenParserName(Name)(StrIter * in, PcParserCtx * ctx PC_MAYBE_UNUSED) #define PcRecognizer_2(Name, InT) \ - static inline PcParserStatus PcGenParserName(Name)(StrIter * in, ParserCtx * ctx PC_MAYBE_UNUSED, InT expect) + static inline PcParserStatus PcGenParserName(Name)(StrIter * in, PcParserCtx * ctx PC_MAYBE_UNUSED, InT expect) #define PcRecognize(...) OVERLOAD(PcRecognize, __VA_ARGS__) #define PcRecognize_1(Name) PcGenParserName(Name)(in, ctx) @@ -224,11 +275,11 @@ typedef struct PcReport { /// /// Define (or, when followed by `;`, forward-declare) a parser. Overloaded by arity: /// -/// PcParser(Name, BuildT) -> (StrIter *in, ParserCtx *ctx, BuildT *value) -/// PcParser(Name, InT, BuildT) -> (StrIter *in, ParserCtx *ctx, InT expect, BuildT *value) +/// PcParser(Name, BuildT) -> (StrIter *in, PcParserCtx *ctx, BuildT *value) +/// PcParser(Name, InT, BuildT) -> (StrIter *in, PcParserCtx *ctx, InT expect, BuildT *value) /// /// The names a body reads are `in` (stream), `ctx` (grammar context), `expect` (the input, in -/// the 3-arg form), and `value` (the output). The consumer must have a `ParserCtx` type in scope. +/// the 3-arg form), and `value` (the output). The consumer must have a `PcParserCtx` type in scope. /// /// SUCCESS: The body returns `PC_PARSER_STATUS_SUCCESS` (or'd with `CONSUMED` when it advanced /// `in`); the parsed result has been written to `*value`. @@ -237,10 +288,10 @@ typedef struct PcReport { /// #define PcParser(...) OVERLOAD(PcParser, __VA_ARGS__) #define PcParser_2(Name, BuildT) \ - static inline PcParserStatus PcGenParserName(Name)(StrIter * in, ParserCtx * ctx PC_MAYBE_UNUSED, BuildT * value) + static inline PcParserStatus PcGenParserName(Name)(StrIter * in, PcParserCtx * ctx PC_MAYBE_UNUSED, BuildT * value) #define PcParser_3(Name, InT, BuildT) \ static inline PcParserStatus \ - PcGenParserName(Name)(StrIter * in, ParserCtx * ctx PC_MAYBE_UNUSED, InT expect, BuildT * value) + PcGenParserName(Name)(StrIter * in, PcParserCtx * ctx PC_MAYBE_UNUSED, InT expect, BuildT * value) /// /// The consumed bit for a parser, derived from the stream position against a snapshot: a parse @@ -254,7 +305,7 @@ typedef struct PcReport { /// Each scopes its bookkeeping in the `for`-init struct so frames nest and sit side by side /// without name clashes, and each owns the rule's `return`. /// -/// PcSeq: run the steps (`PcMatch`/`PcMany`/...) in order; a failing step returns early. If the +/// PcSeq: run the steps (`PcMatch`/`PcMatchOneOrMore`/...) in order; a failing step returns early. If the /// body runs to the end, the rule succeeds (consuming whatever the steps consumed). /// /// SUCCESS: All steps matched; returns SUCCESS with the accumulated CONSUMED bit. @@ -281,10 +332,11 @@ typedef struct PcReport { /// #define PcChoice() \ for (struct { \ - StrIter mark; \ - PcParserStatus st; \ - bool ran, matched, done; \ - } pc_ch = {*in, 0, false, false, false}; \ + StrIter mark; \ + PcParserCtxMark ctx_mark; \ + PcParserStatus st; \ + bool ran, matched, done; \ + } pc_ch = {*in, PcParserCtxSnapshot(ctx), 0, false, false, false}; \ ; \ pc_ch.ran = true) \ if (pc_ch.ran) \ @@ -344,52 +396,112 @@ typedef struct PcReport { ) /// -/// PcMany: zero-or-more. Run parser `Name` repeatedly; the body runs once per match. A match -/// that consumed nothing would spin forever, so the loop stops on it (it never aborts and never -/// allocates); the failing/empty iteration is rewound so its bytes are not eaten. Always -/// "succeeds" -- it is a step that simply stops. +/// PcMatchZeroOrMore: zero-or-more ("any number", including none). Run parser `Name` repeatedly; the body runs +/// once per match. A match that consumed nothing would spin forever, so the loop stops on it (it +/// never aborts and never allocates); the failing/empty iteration is rewound so its bytes are not +/// eaten. Always "succeeds" -- it is a step that simply stops. /// -#define PcMany(Name, ...) \ - for (StrIter UNPL(pc_many_) = *in; \ - (UNPL(pc_many_) = *in, \ - (PcParse(Name, __VA_ARGS__) & PC_PARSER_STATUS_SUCCESS) && in->pos != UNPL(pc_many_).pos) ? \ +#define PcMatchZeroOrMore(Name, ...) \ + for (struct { \ + StrIter mark; \ + PcParserCtxMark ctx_mark; \ + } UNPL(pc_zom_) = {*in, PcParserCtxSnapshot(ctx)}; \ + (UNPL(pc_zom_).mark = *in, \ + UNPL(pc_zom_).ctx_mark = PcParserCtxSnapshot(ctx), \ + (PcParse(Name, __VA_ARGS__) & PC_PARSER_STATUS_SUCCESS) && in->pos != UNPL(pc_zom_).mark.pos) ? \ true : \ - ((*in = UNPL(pc_many_)), false);) + (*in = UNPL(pc_zom_).mark, PcParserCtxRollback(ctx, UNPL(pc_zom_).ctx_mark), false);) + +/// +/// PcMatchOneOrMore: one-or-more. Like `PcMatchZeroOrMore`, but at least one match is required: if the first attempt does +/// not match, the whole rule fails (reporting the sequence's consumed bit, exactly as a failing +/// `PcMatch` would), so it is a `PcSeq` step. The body runs once per match, the first included -- +/// folding the common "match one, then any more" shape into a single step. +/// +#define PcMatchOneOrMore(Name, ...) \ + for (struct { \ + StrIter mark; \ + PcParserCtxMark ctx_mark; \ + bool done; \ + } UNPL(pc_oom1_) = {*in, PcParserCtxSnapshot(ctx), false}; \ + !UNPL(pc_oom1_).done; \ + UNPL(pc_oom1_).done = true) \ + if ((UNPL(pc_oom1_).mark = *in, \ + UNPL(pc_oom1_).ctx_mark = PcParserCtxSnapshot(ctx), \ + !((PcParse(Name, __VA_ARGS__) & PC_PARSER_STATUS_SUCCESS) && in->pos != UNPL(pc_oom1_).mark.pos))) \ + return ( \ + *in = UNPL(pc_oom1_).mark, \ + PcParserCtxRollback(ctx, UNPL(pc_oom1_).ctx_mark), \ + PC_CONSUMED(pc_seq.start) | PC_PARSER_STATUS_FAILED \ + ); \ + else \ + for (struct { \ + StrIter mark; \ + PcParserCtxMark ctx_mark; \ + bool first; \ + } UNPL(pc_oomN_) = {*in, PcParserCtxSnapshot(ctx), true}; \ + UNPL(pc_oomN_).first ? \ + true : \ + (UNPL(pc_oomN_).mark = *in, \ + UNPL(pc_oomN_).ctx_mark = PcParserCtxSnapshot(ctx), \ + (PcParse(Name, __VA_ARGS__) & PC_PARSER_STATUS_SUCCESS) && in->pos != UNPL(pc_oomN_).mark.pos ? \ + true : \ + (*in = UNPL(pc_oomN_).mark, PcParserCtxRollback(ctx, UNPL(pc_oomN_).ctx_mark), false)); \ + UNPL(pc_oomN_).first = false) /// /// PcOpt: zero-or-one. Try parser `Name`; if it matches, run the body once (the parsed value is /// available through whatever output pointer was passed). If it does not match, rewind and skip -/// the body. Never fails the sequence -- it is a step that is simply optional. Like `PcMany`, it +/// the body. Never fails the sequence -- it is a step that is simply optional. Like `PcMatchOneOrMore`, it /// is one `for` statement with its bookkeeping in the loop scope, so it nests, sits next to other /// steps on the same line, and takes an unbraced body without a dangling-`else` surprise. /// #define PcOpt(Name, ...) \ for (struct { \ - StrIter mark; \ - bool ran; \ - } UNPL(pc_opt_) = {*in, false}; \ + StrIter mark; \ + PcParserCtxMark ctx_mark; \ + bool ran; \ + } UNPL(pc_opt_) = {*in, PcParserCtxSnapshot(ctx), false}; \ !UNPL(pc_opt_).ran && \ ((UNPL(pc_opt_).ran = true), \ - (PcParse(Name, __VA_ARGS__) & PC_PARSER_STATUS_SUCCESS) ? true : (*in = UNPL(pc_opt_).mark, false));) + (PcParse(Name, __VA_ARGS__) & PC_PARSER_STATUS_SUCCESS) ? \ + true : \ + (*in = UNPL(pc_opt_).mark, PcParserCtxRollback(ctx, UNPL(pc_opt_).ctx_mark), false));) /// -/// PcRecognizeMany: the recognizer-flavoured `PcMany` -- a whole recognizer body that runs a -/// sub-recognizer zero-or-more times and then returns success. Terminal (it owns the `return`), so -/// it is the entire body of a "recognize zero-or-more of one thing" parser (e.g. whitespace), not -/// a mid-sequence step. Same empty-match guard as `PcMany`: a non-consuming match stops the loop +/// PcRecognizeZeroOrMore: the recognizer-flavoured `PcMatchZeroOrMore` -- a whole recognizer body that runs a +/// sub-recognizer zero-or-more times ("any number", including none) and then returns success. +/// Terminal (it owns the `return`), so it is the entire body of a "recognize zero-or-more of one +/// thing" parser (e.g. whitespace), not a mid-sequence step. A non-consuming match stops the loop /// and is rewound, so it can never spin. Overloaded 1-arg / 2-arg like `PcRecognize`. /// -#define PcRecognizeMany(...) OVERLOAD(PcRecognizeMany, __VA_ARGS__) -#define PcRecognizeMany_1(Name) PC_RECOGNIZE_MANY(PcRecognize_1(Name)) -#define PcRecognizeMany_2(Name, In) PC_RECOGNIZE_MANY(PcRecognize_2(Name, In)) -#define PC_RECOGNIZE_MANY(call) \ +#define PcRecognizeZeroOrMore(...) OVERLOAD(PcRecognizeZeroOrMore, __VA_ARGS__) +#define PcRecognizeZeroOrMore_1(Name) PC_RECOGNIZE_REPEAT(PcRecognize_1(Name), 0) +#define PcRecognizeZeroOrMore_2(Name, In) PC_RECOGNIZE_REPEAT(PcRecognize_2(Name, In), 0) + +/// +/// PcRecognizeOneOrMore: the one-or-more counterpart of `PcRecognizeZeroOrMore` -- the whole body of a +/// "recognize one-or-more of one thing" recognizer. Identical, except it returns failure when it +/// matched nothing at all. Terminal, like `PcRecognizeZeroOrMore`. +/// +#define PcRecognizeOneOrMore(...) OVERLOAD(PcRecognizeOneOrMore, __VA_ARGS__) +#define PcRecognizeOneOrMore_1(Name) PC_RECOGNIZE_REPEAT(PcRecognize_1(Name), 1) +#define PcRecognizeOneOrMore_2(Name, In) PC_RECOGNIZE_REPEAT(PcRecognize_2(Name, In), 1) +#define PC_RECOGNIZE_REPEAT(call, min_one) \ do { \ StrIter UNPL(pc_rm_start) = *in; \ - for (StrIter UNPL(pc_rm_mark) = *in; \ - (UNPL(pc_rm_mark) = *in, ((call) & PC_PARSER_STATUS_SUCCESS) && in->pos != UNPL(pc_rm_mark).pos) ? \ + for (struct { \ + StrIter mark; \ + PcParserCtxMark ctx_mark; \ + } UNPL(pc_rm_) = {*in, PcParserCtxSnapshot(ctx)}; \ + (UNPL(pc_rm_).mark = *in, \ + UNPL(pc_rm_).ctx_mark = PcParserCtxSnapshot(ctx), \ + ((call) & PC_PARSER_STATUS_SUCCESS) && in->pos != UNPL(pc_rm_).mark.pos) ? \ true : \ - ((*in = UNPL(pc_rm_mark)), false);) \ + (*in = UNPL(pc_rm_).mark, PcParserCtxRollback(ctx, UNPL(pc_rm_).ctx_mark), false);) \ ; \ + if ((min_one) && in->pos == UNPL(pc_rm_start).pos) \ + return PC_PARSER_STATUS_FAILED; \ return PC_CONSUMED(UNPL(pc_rm_start)) | PC_PARSER_STATUS_SUCCESS; \ } while (0) @@ -404,26 +516,28 @@ typedef struct PcReport { /// than trying later arms. `Try` arms BACKTRACK: any failure rewinds and the next arm is tried. /// #define PcAlt(Name, ...) \ - ((void)(pc_ch.done || \ - ((pc_ch.st = PcParse(Name, __VA_ARGS__)) & PC_PARSER_STATUS_SUCCESS ? \ - ((pc_ch.matched = pc_ch.done = true)) : \ - ((pc_ch.st & PC_PARSER_STATUS_CONSUMED) ? (pc_ch.done = true, false) : (*in = pc_ch.mark, false))))) + ((void)(pc_ch.done || ((pc_ch.st = PcParse(Name, __VA_ARGS__)) & PC_PARSER_STATUS_SUCCESS ? \ + ((pc_ch.matched = pc_ch.done = true)) : \ + ((pc_ch.st & PC_PARSER_STATUS_CONSUMED) ? \ + (pc_ch.done = true, false) : \ + (*in = pc_ch.mark, PcParserCtxRollback(ctx, pc_ch.ctx_mark), false))))) #define PcTryAlt(Name, ...) \ ((void)(pc_ch.done || ((pc_ch.st = PcParse(Name, __VA_ARGS__)) & PC_PARSER_STATUS_SUCCESS ? \ ((pc_ch.matched = pc_ch.done = true)) : \ - (*in = pc_ch.mark, false)))) + (*in = pc_ch.mark, PcParserCtxRollback(ctx, pc_ch.ctx_mark), false)))) #define PcAltThen(Name, ...) \ - if (!pc_ch.done && \ - ((pc_ch.st = PcParse(Name, __VA_ARGS__)) & PC_PARSER_STATUS_SUCCESS ? \ - ((pc_ch.matched = pc_ch.done = true)) : \ - ((pc_ch.st & PC_PARSER_STATUS_CONSUMED) ? (pc_ch.done = true, false) : (*in = pc_ch.mark, false)))) + if (!pc_ch.done && ((pc_ch.st = PcParse(Name, __VA_ARGS__)) & PC_PARSER_STATUS_SUCCESS ? \ + ((pc_ch.matched = pc_ch.done = true)) : \ + ((pc_ch.st & PC_PARSER_STATUS_CONSUMED) ? \ + (pc_ch.done = true, false) : \ + (*in = pc_ch.mark, PcParserCtxRollback(ctx, pc_ch.ctx_mark), false)))) #define PcTryAltThen(Name, ...) \ - if (!pc_ch.done && \ - ((pc_ch.st = PcParse(Name, __VA_ARGS__)) & PC_PARSER_STATUS_SUCCESS ? ((pc_ch.matched = pc_ch.done = true)) : \ - (*in = pc_ch.mark, false))) + if (!pc_ch.done && ((pc_ch.st = PcParse(Name, __VA_ARGS__)) & PC_PARSER_STATUS_SUCCESS ? \ + ((pc_ch.matched = pc_ch.done = true)) : \ + (*in = pc_ch.mark, PcParserCtxRollback(ctx, pc_ch.ctx_mark), false))) /// /// PcElse: the fallback arm of a `PcChoice`. Its body runs iff NO arm matched (every arm failed From 0d20ef1ed8e270edf5d84ebba78bc5cb54de6dc2 Mon Sep 17 00:00:00 2001 From: Siddharth Mishra Date: Thu, 2 Jul 2026 17:31:52 -0700 Subject: [PATCH 08/11] feat(parsercombinator): binary parsing on the DSL, with Tzif as the first grammar Deliver fixed-width byte-reader parsers out of the box so a binary grammar composes them directly instead of hand-writing a reader per field: PcU8/PcI8 and Pc{U,I}{16,32,64}{BE,LE}, declared in the header and defined in Source/Misra/ParserCombinator.c over a BufIter. They live outside the grammar's translation unit, so a grammar's context must be a tagged `struct PcParserCtx` for the opaque pointer to line up. Add the combinators the arrays need: PcMatchExactlyN (index-bearing, as VecForeachIdx is to VecForeach) and PcRecognizeExactlyN, plus PcExpect (the recognizer twin of PcMatch) and PcSkipBytes. Buf gains signed fixed-width reads (BufReadI, two's-complement wrappers over the unsigned readers) and parameter docs on every fixed-width reader. Reimplement the Tzif resolver on the DSL: Header is a parser producing a value, the version and transition-time width are PcChoice decisions rather than runtime fields, each resolution phase is its own parser, and the context holds only the instant being resolved. Validated by the existing Tzif test suite. --- Include/Misra/ParserCombinator.h | 137 ++++++++++--- Include/Misra/Std/Container/Buf.h | 193 +++++++++++++++++- Source/Misra/ParserCombinator.c | 40 ++++ Source/Misra/Parsers/Tzif.c | 320 +++++++++++++++++------------- meson.build | 1 + 5 files changed, 528 insertions(+), 163 deletions(-) create mode 100644 Source/Misra/ParserCombinator.c diff --git a/Include/Misra/ParserCombinator.h b/Include/Misra/ParserCombinator.h index dc1cd8b..cc02583 100644 --- a/Include/Misra/ParserCombinator.h +++ b/Include/Misra/ParserCombinator.h @@ -84,6 +84,17 @@ #include #include #include +#include + +/// +/// The cursor type. Text grammars use the default `StrIter` (`Iter(char)`); a byte-oriented +/// grammar sets `#define PC_ITER BufIter` (`Iter(const u8)`) before including this header. The +/// block frames are cursor-agnostic (they only use `in->pos` / `*in` / `IterIndex`); only the +/// atoms are element-specific -- `PcSatisfy*` for characters, the `PcU*` family below for bytes. +/// +#ifndef PC_ITER +# define PC_ITER StrIter +#endif /// /// A parser combinator does not only need a way to parse a string. It also needs to convert @@ -243,8 +254,8 @@ typedef struct PcReport { /// the "validator" style. Identical to the `PcParser`/`PcParse` family with the trailing output /// slot removed, so the arities shift down by one: 1-arg (no input) or 2-arg (an input to match). /// -/// PcRecognizer(Name) -> (StrIter *in, PcParserCtx *ctx) define, no input -/// PcRecognizer(Name, InT) -> (StrIter *in, PcParserCtx *ctx, InT expect) define, one input +/// PcRecognizer(Name) -> (PC_ITER *in, PcParserCtx *ctx) define, no input +/// PcRecognizer(Name, InT) -> (PC_ITER *in, PcParserCtx *ctx, InT expect) define, one input /// PcRecognize(Name) -> pc_parser_Name(in, ctx) call, no input /// PcRecognize(Name, In) -> pc_parser_Name(in, ctx, In) call, one input /// @@ -253,9 +264,9 @@ typedef struct PcReport { /// #define PcRecognizer(...) OVERLOAD(PcRecognizer, __VA_ARGS__) #define PcRecognizer_1(Name) \ - static inline PcParserStatus PcGenParserName(Name)(StrIter * in, PcParserCtx * ctx PC_MAYBE_UNUSED) + static inline PcParserStatus PcGenParserName(Name)(PC_ITER * in, PcParserCtx * ctx PC_MAYBE_UNUSED) #define PcRecognizer_2(Name, InT) \ - static inline PcParserStatus PcGenParserName(Name)(StrIter * in, PcParserCtx * ctx PC_MAYBE_UNUSED, InT expect) + static inline PcParserStatus PcGenParserName(Name)(PC_ITER * in, PcParserCtx * ctx PC_MAYBE_UNUSED, InT expect) #define PcRecognize(...) OVERLOAD(PcRecognize, __VA_ARGS__) #define PcRecognize_1(Name) PcGenParserName(Name)(in, ctx) @@ -275,8 +286,8 @@ typedef struct PcReport { /// /// Define (or, when followed by `;`, forward-declare) a parser. Overloaded by arity: /// -/// PcParser(Name, BuildT) -> (StrIter *in, PcParserCtx *ctx, BuildT *value) -/// PcParser(Name, InT, BuildT) -> (StrIter *in, PcParserCtx *ctx, InT expect, BuildT *value) +/// PcParser(Name, BuildT) -> (PC_ITER *in, PcParserCtx *ctx, BuildT *value) +/// PcParser(Name, InT, BuildT) -> (PC_ITER *in, PcParserCtx *ctx, InT expect, BuildT *value) /// /// The names a body reads are `in` (stream), `ctx` (grammar context), `expect` (the input, in /// the 3-arg form), and `value` (the output). The consumer must have a `PcParserCtx` type in scope. @@ -288,10 +299,10 @@ typedef struct PcReport { /// #define PcParser(...) OVERLOAD(PcParser, __VA_ARGS__) #define PcParser_2(Name, BuildT) \ - static inline PcParserStatus PcGenParserName(Name)(StrIter * in, PcParserCtx * ctx PC_MAYBE_UNUSED, BuildT * value) + static inline PcParserStatus PcGenParserName(Name)(PC_ITER * in, PcParserCtx * ctx PC_MAYBE_UNUSED, BuildT * value) #define PcParser_3(Name, InT, BuildT) \ static inline PcParserStatus \ - PcGenParserName(Name)(StrIter * in, PcParserCtx * ctx PC_MAYBE_UNUSED, InT expect, BuildT * value) + PcGenParserName(Name)(PC_ITER * in, PcParserCtx * ctx PC_MAYBE_UNUSED, InT expect, BuildT * value) /// /// The consumed bit for a parser, derived from the stream position against a snapshot: a parse @@ -313,7 +324,7 @@ typedef struct PcReport { /// #define PcSeq() \ for (struct { \ - StrIter start; \ + PC_ITER start; \ bool ran; \ } pc_seq = {*in, false}; \ ; \ @@ -332,7 +343,7 @@ typedef struct PcReport { /// #define PcChoice() \ for (struct { \ - StrIter mark; \ + PC_ITER mark; \ PcParserCtxMark ctx_mark; \ PcParserStatus st; \ bool ran, matched, done; \ @@ -403,7 +414,7 @@ typedef struct PcReport { /// #define PcMatchZeroOrMore(Name, ...) \ for (struct { \ - StrIter mark; \ + PC_ITER mark; \ PcParserCtxMark ctx_mark; \ } UNPL(pc_zom_) = {*in, PcParserCtxSnapshot(ctx)}; \ (UNPL(pc_zom_).mark = *in, \ @@ -420,7 +431,7 @@ typedef struct PcReport { /// #define PcMatchOneOrMore(Name, ...) \ for (struct { \ - StrIter mark; \ + PC_ITER mark; \ PcParserCtxMark ctx_mark; \ bool done; \ } UNPL(pc_oom1_) = {*in, PcParserCtxSnapshot(ctx), false}; \ @@ -436,7 +447,7 @@ typedef struct PcReport { ); \ else \ for (struct { \ - StrIter mark; \ + PC_ITER mark; \ PcParserCtxMark ctx_mark; \ bool first; \ } UNPL(pc_oomN_) = {*in, PcParserCtxSnapshot(ctx), true}; \ @@ -458,7 +469,7 @@ typedef struct PcReport { /// #define PcOpt(Name, ...) \ for (struct { \ - StrIter mark; \ + PC_ITER mark; \ PcParserCtxMark ctx_mark; \ bool ran; \ } UNPL(pc_opt_) = {*in, PcParserCtxSnapshot(ctx), false}; \ @@ -489,9 +500,9 @@ typedef struct PcReport { #define PcRecognizeOneOrMore_2(Name, In) PC_RECOGNIZE_REPEAT(PcRecognize_2(Name, In), 1) #define PC_RECOGNIZE_REPEAT(call, min_one) \ do { \ - StrIter UNPL(pc_rm_start) = *in; \ + PC_ITER UNPL(pc_rm_start) = *in; \ for (struct { \ - StrIter mark; \ + PC_ITER mark; \ PcParserCtxMark ctx_mark; \ } UNPL(pc_rm_) = {*in, PcParserCtxSnapshot(ctx)}; \ (UNPL(pc_rm_).mark = *in, \ @@ -554,8 +565,8 @@ typedef struct PcReport { /// surfaces more than just the first structural break. /// #define PcRecover(Var, IsSync) \ - for (char Var = 0; StrIterPeek(in, &Var) && !(IsSync);) \ - StrIterMove(in, 1) + for (char Var = 0; IterPeekAt(in, 0, &Var) && !(IsSync);) \ + IterMove(in, 1) /// /// Atoms -- the only place `StrIter` and `PcParserStatus` are handled directly. Every fundamental @@ -574,9 +585,9 @@ typedef struct PcReport { for (bool UNPL(pc_sc_ran) = false;; UNPL(pc_sc_ran) = true) \ if (UNPL(pc_sc_ran)) \ return PC_PARSER_STATUS_SUCCESS | PC_PARSER_STATUS_CONSUMED; \ - else if (!(StrIterPeek(in, &Var) && (Pred))) \ + else if (!(IterPeekAt(in, 0, &Var) && (Pred))) \ return PC_PARSER_STATUS_FAILED; \ - else if ((StrIterMove(in, 1), true)) + else if ((IterMove(in, 1), true)) /// /// PcSatisfyStr: match the literal string `Expect` (a `Zstr`; pass `StrBegin(&s)` for a `Str`) at @@ -593,14 +604,13 @@ typedef struct PcReport { bool UNPL(pc_ss_ok) = true; \ for (u64 UNPL(pc_ss_i) = 0; UNPL(pc_ss_i) < UNPL(pc_ss_len); UNPL(pc_ss_i)++) { \ char UNPL(pc_ss_c); \ - if (!StrIterPeekAt(in, (i64)UNPL(pc_ss_i), &UNPL(pc_ss_c)) || \ - UNPL(pc_ss_c) != UNPL(pc_ss_exp)[UNPL(pc_ss_i)]) { \ + if (!IterPeekAt(in, (i64)UNPL(pc_ss_i), &UNPL(pc_ss_c)) || UNPL(pc_ss_c) != UNPL(pc_ss_exp)[UNPL(pc_ss_i)]) { \ UNPL(pc_ss_ok) = false; \ break; \ } \ } \ if (UNPL(pc_ss_ok)) \ - StrIterMustMove(in, (i64)UNPL(pc_ss_len)); \ + IterMustMove(in, (i64)UNPL(pc_ss_len)); \ for (bool UNPL(pc_ss_ran) = false;; UNPL(pc_ss_ran) = true) \ if (!UNPL(pc_ss_ok)) \ return PC_PARSER_STATUS_FAILED; \ @@ -608,4 +618,85 @@ typedef struct PcReport { return PC_PARSER_STATUS_SUCCESS | PC_PARSER_STATUS_CONSUMED; \ else +/// +/// Binary fields -- for a grammar with `#define PC_ITER BufIter`. The fixed-width readers are NOT +/// written per grammar: `PcU8`/`PcI8`, `Pc{U,I}16BE`/`LE`, `Pc{U,I}32BE`/`LE`, `Pc{U,I}64BE`/`LE` +/// are fundamental parsers delivered out of the box (declared here, defined in ParserCombinator.c) +/// and used through `PcMatch` / `PcAlt` -- e.g. `PcMatch(PcI32BE, &field)`. Each reads one field +/// from the cursor at the stated endianness and signedness; a short buffer fails the rule. Magic is +/// `PcSatisfyStr`, not a reader. +/// +/// The delivered parsers live outside the grammar's translation unit, so they see the context only +/// as an opaque `struct PcParserCtx` (which they ignore) -- so a grammar MUST define its context as +/// a tagged `typedef struct PcParserCtx { ... } PcParserCtx;`, so the opaque pointer lines up. +/// +struct PcParserCtx; +PcParserStatus pc_parser_PcU8(BufIter *in, struct PcParserCtx *ctx, u8 *value); +PcParserStatus pc_parser_PcU16BE(BufIter *in, struct PcParserCtx *ctx, u16 *value); +PcParserStatus pc_parser_PcU16LE(BufIter *in, struct PcParserCtx *ctx, u16 *value); +PcParserStatus pc_parser_PcU32BE(BufIter *in, struct PcParserCtx *ctx, u32 *value); +PcParserStatus pc_parser_PcU32LE(BufIter *in, struct PcParserCtx *ctx, u32 *value); +PcParserStatus pc_parser_PcU64BE(BufIter *in, struct PcParserCtx *ctx, u64 *value); +PcParserStatus pc_parser_PcU64LE(BufIter *in, struct PcParserCtx *ctx, u64 *value); +PcParserStatus pc_parser_PcI8(BufIter *in, struct PcParserCtx *ctx, i8 *value); +PcParserStatus pc_parser_PcI16BE(BufIter *in, struct PcParserCtx *ctx, i16 *value); +PcParserStatus pc_parser_PcI16LE(BufIter *in, struct PcParserCtx *ctx, i16 *value); +PcParserStatus pc_parser_PcI32BE(BufIter *in, struct PcParserCtx *ctx, i32 *value); +PcParserStatus pc_parser_PcI32LE(BufIter *in, struct PcParserCtx *ctx, i32 *value); +PcParserStatus pc_parser_PcI64BE(BufIter *in, struct PcParserCtx *ctx, i64 *value); +PcParserStatus pc_parser_PcI64LE(BufIter *in, struct PcParserCtx *ctx, i64 *value); + +/// +/// PC_BYTE_ATOM: the terminal body of a fundamental byte reader (the delivered parsers and +/// `PcSkipBytes`) -- read/advance or fail the rule; on success return success|consumed. +/// +#define PC_BYTE_ATOM(call) \ + do { \ + if (!(call)) \ + return PC_PARSER_STATUS_FAILED; \ + return PC_PARSER_STATUS_SUCCESS | PC_PARSER_STATUS_CONSUMED; \ + } while (0) + +/// +/// PcSkipBytes: advance `N` bytes; a short buffer fails. TERMINAL, the body of a recognizer. +/// +#define PcSkipBytes(N) PC_BYTE_ATOM(IterMove(in, (i64)(N))) + +/// +/// PcExpect: a `PcSeq` step that runs a RECOGNIZER (no output) and fails the rule if it did not +/// match -- the recognizer twin of `PcMatch`, for fixed markers (a magic recognizer, a skip). +/// +#define PcExpect(...) \ + do { \ + if (!(PcRecognize(__VA_ARGS__) & PC_PARSER_STATUS_SUCCESS)) \ + return PC_CONSUMED(pc_seq.start) | PC_PARSER_STATUS_FAILED; \ + } while (0) + +/// +/// PcMatchExactlyN: run parser `Name` exactly `N` times as a `PcSeq` step, binding `Idx` (a `u64`) +/// to the iteration index for the body to read -- the counted, index-bearing sibling of +/// `PcMatchZeroOrMore` (as `VecForeachIdx` is to `VecForeach`). Any of the `N` failing fails the +/// rule (with the sequence's consumed bit). The body runs after each match. +/// +#define PcMatchExactlyN(N, Idx, Name, ...) \ + for (u64 Idx = 0; Idx < (u64)(N); Idx++) \ + if (!(PcParse(Name, __VA_ARGS__) & PC_PARSER_STATUS_SUCCESS)) \ + return PC_CONSUMED(pc_seq.start) | PC_PARSER_STATUS_FAILED; \ + else + +/// +/// PcRecognizeExactlyN: run recognizer `Name` exactly `N` times -- the counted sibling of +/// `PcRecognizeZeroOrMore`. TERMINAL (the whole recognizer body); any failure fails. +/// +#define PcRecognizeExactlyN(...) OVERLOAD(PcRecognizeExactlyN, __VA_ARGS__) +#define PcRecognizeExactlyN_2(N, Name) PC_RECOGNIZE_EXACTLY((N), PcRecognize_1(Name)) +#define PcRecognizeExactlyN_3(N, Name, In) PC_RECOGNIZE_EXACTLY((N), PcRecognize_2(Name, In)) +#define PC_RECOGNIZE_EXACTLY(N, call) \ + do { \ + for (u64 UNPL(pc_rxn_) = 0; UNPL(pc_rxn_) < (u64)(N); UNPL(pc_rxn_)++) \ + if (!((call) & PC_PARSER_STATUS_SUCCESS)) \ + return PC_PARSER_STATUS_FAILED; \ + return PC_PARSER_STATUS_SUCCESS | PC_PARSER_STATUS_CONSUMED; \ + } while (0) + #endif // MISRA_PARSER_COMBINATOR diff --git a/Include/Misra/Std/Container/Buf.h b/Include/Misra/Std/Container/Buf.h index 1326452..93cc17e 100644 --- a/Include/Misra/Std/Container/Buf.h +++ b/Include/Misra/Std/Container/Buf.h @@ -59,7 +59,7 @@ typedef Iter(const u8) BufIter; /// /// TAGS: Buf, Clear, Reuse /// -#define BufClear(b) VecClear(b) +#define BufClear(b) VecClear(b) // Read-only accessors. The leading `((void)0, ...)` makes each macro a // comma expression, and the result of a C comma expression is not an @@ -73,7 +73,7 @@ typedef Iter(const u8) BufIter; /// /// TAGS: Buf, Length, Accessor /// -#define BufLength(b) ((void)0, (b)->length) +#define BufLength(b) ((void)0, (b)->length) /// /// Pointer to the contiguous byte storage backing `b`. Read-only at @@ -83,7 +83,7 @@ typedef Iter(const u8) BufIter; /// /// TAGS: Buf, Data, Accessor /// -#define BufData(b) ((void)0, (b)->data) +#define BufData(b) ((void)0, (b)->data) /// /// Allocator backing `b`'s storage. Read-only; rebinding the @@ -91,7 +91,7 @@ typedef Iter(const u8) BufIter; /// /// TAGS: Buf, Allocator, Accessor /// -#define BufAllocator(b) ((void)0, (b)->allocator) +#define BufAllocator(b) ((void)0, (b)->allocator) /// /// Ensure `b` has capacity for at least `n` bytes without changing @@ -118,7 +118,7 @@ typedef Iter(const u8) BufIter; /// /// TAGS: Buf, Resize, Capacity, Allocation /// -#define BufResize(b, n) VecResize((b), (n)) +#define BufResize(b, n) VecResize((b), (n)) /// /// Construct a `BufIter` over `[data, data + length)`. The iterator @@ -178,6 +178,9 @@ static inline bool BufPushBytes(Buf *b, const u8 *data, size n) { /// /// Read a single byte at the cursor and advance one byte. /// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded value. +/// /// SUCCESS : Returns `true`; `*out` holds the byte at `c->pos` and /// `c->pos` has advanced by one. /// FAILURE : Returns `false` when fewer than one byte remains in `c` @@ -197,6 +200,9 @@ static inline bool BufReadU8(BufIter *c, u8 *out) { /// /// Read two little-endian bytes (low byte first) and advance two bytes. /// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded value. +/// /// SUCCESS : Returns `true`; `*out` holds the decoded `u16` and /// `c->pos` has advanced by two. /// FAILURE : Returns `false` when fewer than two bytes remain in `c`; @@ -216,6 +222,9 @@ static inline bool BufReadU16LE(BufIter *c, u16 *out) { /// /// Read two big-endian bytes (high byte first) and advance two bytes. /// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded value. +/// /// SUCCESS : Returns `true`; `*out` holds the decoded `u16` and /// `c->pos` has advanced by two. /// FAILURE : Returns `false` when fewer than two bytes remain in `c`; @@ -235,6 +244,9 @@ static inline bool BufReadU16BE(BufIter *c, u16 *out) { /// /// Read four little-endian bytes and advance four bytes. /// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded value. +/// /// SUCCESS : Returns `true`; `*out` holds the decoded `u32` and /// `c->pos` has advanced by four. /// FAILURE : Returns `false` when fewer than four bytes remain in `c`; @@ -255,6 +267,9 @@ static inline bool BufReadU32LE(BufIter *c, u32 *out) { /// /// Read four big-endian bytes and advance four bytes. /// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded value. +/// /// SUCCESS : Returns `true`; `*out` holds the decoded `u32` and /// `c->pos` has advanced by four. /// FAILURE : Returns `false` when fewer than four bytes remain in `c`; @@ -275,6 +290,9 @@ static inline bool BufReadU32BE(BufIter *c, u32 *out) { /// /// Read eight little-endian bytes and advance eight bytes. /// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded value. +/// /// SUCCESS : Returns `true`; `*out` holds the decoded `u64` and /// `c->pos` has advanced by eight. /// FAILURE : Returns `false` when fewer than eight bytes remain in `c`; @@ -298,6 +316,9 @@ static inline bool BufReadU64LE(BufIter *c, u64 *out) { /// /// Read eight big-endian bytes and advance eight bytes. /// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded value. +/// /// SUCCESS : Returns `true`; `*out` holds the decoded `u64` and /// `c->pos` has advanced by eight. /// FAILURE : Returns `false` when fewer than eight bytes remain in `c`; @@ -318,6 +339,168 @@ static inline bool BufReadU64BE(BufIter *c, u64 *out) { return true; } +/// +/// Read a single byte at the cursor as a two's-complement signed value and +/// advance one byte. +/// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded signed value. +/// +/// SUCCESS : Returns `true`; `*out` holds the signed byte at `c->pos` and +/// `c->pos` has advanced by one. +/// FAILURE : Returns `false` when fewer than one byte remains in `c` +/// (`c->pos >= c->length`); `*out` is unchanged and `c->pos` +/// is unchanged. +/// +/// TAGS: Buf, Read, I8 +/// +static inline bool BufReadI8(BufIter *c, i8 *out) { + u8 raw = 0; + if (!BufReadU8(c, &raw)) { + return false; + } + *out = (i8)raw; + return true; +} + +/// +/// Read two little-endian bytes (low byte first) as a two's-complement signed +/// value and advance two bytes. +/// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded signed value. +/// +/// SUCCESS : Returns `true`; `*out` holds the decoded `i16` and +/// `c->pos` has advanced by two. +/// FAILURE : Returns `false` when fewer than two bytes remain in `c`; +/// `*out` is unchanged and `c->pos` is unchanged. +/// +/// TAGS: Buf, Read, I16, LittleEndian +/// +static inline bool BufReadI16LE(BufIter *c, i16 *out) { + u16 raw = 0; + if (!BufReadU16LE(c, &raw)) { + return false; + } + *out = (i16)raw; + return true; +} + +/// +/// Read two big-endian bytes (high byte first) as a two's-complement signed +/// value and advance two bytes. +/// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded signed value. +/// +/// SUCCESS : Returns `true`; `*out` holds the decoded `i16` and +/// `c->pos` has advanced by two. +/// FAILURE : Returns `false` when fewer than two bytes remain in `c`; +/// `*out` is unchanged and `c->pos` is unchanged. +/// +/// TAGS: Buf, Read, I16, BigEndian +/// +static inline bool BufReadI16BE(BufIter *c, i16 *out) { + u16 raw = 0; + if (!BufReadU16BE(c, &raw)) { + return false; + } + *out = (i16)raw; + return true; +} + +/// +/// Read four little-endian bytes as a two's-complement signed value and advance +/// four bytes. +/// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded signed value. +/// +/// SUCCESS : Returns `true`; `*out` holds the decoded `i32` and +/// `c->pos` has advanced by four. +/// FAILURE : Returns `false` when fewer than four bytes remain in `c`; +/// `*out` is unchanged and `c->pos` is unchanged. +/// +/// TAGS: Buf, Read, I32, LittleEndian +/// +static inline bool BufReadI32LE(BufIter *c, i32 *out) { + u32 raw = 0; + if (!BufReadU32LE(c, &raw)) { + return false; + } + *out = (i32)raw; + return true; +} + +/// +/// Read four big-endian bytes as a two's-complement signed value and advance +/// four bytes. +/// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded signed value. +/// +/// SUCCESS : Returns `true`; `*out` holds the decoded `i32` and +/// `c->pos` has advanced by four. +/// FAILURE : Returns `false` when fewer than four bytes remain in `c`; +/// `*out` is unchanged and `c->pos` is unchanged. +/// +/// TAGS: Buf, Read, I32, BigEndian +/// +static inline bool BufReadI32BE(BufIter *c, i32 *out) { + u32 raw = 0; + if (!BufReadU32BE(c, &raw)) { + return false; + } + *out = (i32)raw; + return true; +} + +/// +/// Read eight little-endian bytes as a two's-complement signed value and advance +/// eight bytes. +/// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded signed value. +/// +/// SUCCESS : Returns `true`; `*out` holds the decoded `i64` and +/// `c->pos` has advanced by eight. +/// FAILURE : Returns `false` when fewer than eight bytes remain in `c`; +/// `*out` is unchanged and `c->pos` is unchanged. +/// +/// TAGS: Buf, Read, I64, LittleEndian +/// +static inline bool BufReadI64LE(BufIter *c, i64 *out) { + u64 raw = 0; + if (!BufReadU64LE(c, &raw)) { + return false; + } + *out = (i64)raw; + return true; +} + +/// +/// Read eight big-endian bytes as a two's-complement signed value and advance +/// eight bytes. +/// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded signed value. +/// +/// SUCCESS : Returns `true`; `*out` holds the decoded `i64` and +/// `c->pos` has advanced by eight. +/// FAILURE : Returns `false` when fewer than eight bytes remain in `c`; +/// `*out` is unchanged and `c->pos` is unchanged. +/// +/// TAGS: Buf, Read, I64, BigEndian +/// +static inline bool BufReadI64BE(BufIter *c, i64 *out) { + u64 raw = 0; + if (!BufReadU64BE(c, &raw)) { + return false; + } + *out = (i64)raw; + return true; +} + /// LEB128 unsigned. Fails on truncation or width overflow. /// /// SUCCESS : Returns `true`; `*out` holds the decoded value and `c->pos` diff --git a/Source/Misra/ParserCombinator.c b/Source/Misra/ParserCombinator.c new file mode 100644 index 0000000..b738510 --- /dev/null +++ b/Source/Misra/ParserCombinator.c @@ -0,0 +1,40 @@ +/// file : ParserCombinator.c +/// author : Siddharth Mishra (admin@brightprogrammer.in) +/// This is free and unencumbered software released into the public domain. +/// +/// The fundamental byte-reader parsers the DSL delivers out of the box, so a +/// binary grammar composes `PcMatch(PcU32BE, &field)` / `PcMatch(PcI32BE, &f)` +/// without hand-writing a wrapper per field. Each reads one fixed-width field +/// from the `BufIter` cursor at the stated endianness through Buf's direct +/// `BufRead{U,I}*`; a short buffer fails the rule (never aborts). Defined here +/// rather than inline so they live outside every grammar's translation unit -- +/// which is why they see the context only as an opaque `struct PcParserCtx *` +/// (unused; a grammar's context is tagged `struct PcParserCtx` so the pointer +/// lines up at the call). + +#include +#include + +#define PC_DELIVER_READER(Name, Type, Reader) \ + PcParserStatus pc_parser_##Name(BufIter *in, struct PcParserCtx *ctx, Type *value) { \ + (void)ctx; \ + PC_BYTE_ATOM(Reader(in, value)); \ + } + +PC_DELIVER_READER(PcU8, u8, BufReadU8) +PC_DELIVER_READER(PcU16BE, u16, BufReadU16BE) +PC_DELIVER_READER(PcU16LE, u16, BufReadU16LE) +PC_DELIVER_READER(PcU32BE, u32, BufReadU32BE) +PC_DELIVER_READER(PcU32LE, u32, BufReadU32LE) +PC_DELIVER_READER(PcU64BE, u64, BufReadU64BE) +PC_DELIVER_READER(PcU64LE, u64, BufReadU64LE) + +PC_DELIVER_READER(PcI8, i8, BufReadI8) +PC_DELIVER_READER(PcI16BE, i16, BufReadI16BE) +PC_DELIVER_READER(PcI16LE, i16, BufReadI16LE) +PC_DELIVER_READER(PcI32BE, i32, BufReadI32BE) +PC_DELIVER_READER(PcI32LE, i32, BufReadI32LE) +PC_DELIVER_READER(PcI64BE, i64, BufReadI64BE) +PC_DELIVER_READER(PcI64LE, i64, BufReadI64LE) + +#undef PC_DELIVER_READER diff --git a/Source/Misra/Parsers/Tzif.c b/Source/Misra/Parsers/Tzif.c index 4fbdd59..c6752d8 100644 --- a/Source/Misra/Parsers/Tzif.c +++ b/Source/Misra/Parsers/Tzif.c @@ -2,13 +2,18 @@ /// author : Siddharth Mishra (admin@brightprogrammer.in) /// This is free and unencumbered software released into the public domain. /// -/// TZif (`/etc/localtime`) offset resolver backing `ClockLocal`. See -/// `Tzif.h` for the contract and the scope note. All on-disk decoding -/// goes through `BufReadFmt` big-endian (`{>Nr}`) directives -- TZif is -/// a big-endian format (RFC 8536) -- so there are no packed structs and -/// no manual byteswaps, matching the rest of the Parsers/ family. Every -/// read is the non-`Must` `BufReadFmt` / `IterMove`, which return false -/// on a short file instead of aborting: `/etc/localtime` is untrusted. +/// TZif (`/etc/localtime`) offset resolver backing `ClockLocal`. See `Tzif.h` +/// for the contract and the scope note. Written on the parser combinator DSL +/// over a `BufIter` (`#define PC_ITER BufIter`): TZif is big-endian (RFC 8536). +/// The grammar reads the header (a produced value) and the version chooses the +/// block (`Dispatch`); each version arm reads transition times at its own width +/// (`FindBestTransitionTimeV1` vs `...V2`), so the width is a control-flow +/// decision, not a field anyone checks. Each resolution phase is its own parser +/// that takes what it needs and returns what it found, the arrays stream through +/// `PcMatchExactlyN`, and the context holds only the one ambient fact the phases +/// read down the stack: the instant being resolved. Every read is +/// bounds-checked and fails the rule (never aborts) on a short file -- +/// `/etc/localtime` is untrusted -- surfacing as a soft `false`. #include @@ -17,25 +22,12 @@ #include #include -// TZif fixed-layout header (RFC 8536 4.1): "TZif" magic, 1-byte -// version, 15 reserved bytes, then six big-endian u32 counts. 44 bytes. -#define TZIF_HEADER_SIZE 44 - -// The six u32 counts, read in one shot. -#define FMT_TZIF_COUNTS_BE \ - "{>4r}" /* isutcnt */ \ - "{>4r}" /* isstdcnt */ \ - "{>4r}" /* leapcnt */ \ - "{>4r}" /* timecnt */ \ - "{>4r}" /* typecnt */ \ - "{>4r}" /* charcnt */ - -// One ttinfo record (RFC 8536 4.2): i32 utoff, u8 isdst, u8 desigidx. -#define FMT_TZIF_TTINFO_BE \ - "{>4r}" /* utoff */ \ - "{>1r}" /* isdst */ \ - "{>1r}" /* desigidx */ +#define PC_ITER BufIter +#include +// TZif fixed-layout header (RFC 8536 4.1): "TZif" magic, version, 15 reserved +// bytes, then six big-endian u32 counts. Produced by `Header`, threaded into the +// block parsers as an input. typedef struct { u8 version; u32 isutcnt; @@ -46,144 +38,202 @@ typedef struct { u32 charcnt; } TzifHeader; -static bool tzif_read_header(BufIter *it, TzifHeader *h) { - if ((u64)IterRemainingLength(it) < TZIF_HEADER_SIZE) - return false; - - // "TZif" magic matched inline; a non-TZif file fails the read here. - u8 ver = 0; - if (!BufReadFmt(it, "TZif{>1r}", ver)) - return false; - if (!IterMove(it, 15)) // reserved - return false; +// One count-prefixed array to scan for a record at a selected index: `count` +// records, keep the one at `sel` (-1 = nothing selected). +typedef struct { + u32 count; + i64 sel; +} PhaseIn; - if (!BufReadFmt(it, FMT_TZIF_COUNTS_BE, h->isutcnt, h->isstdcnt, h->leapcnt, h->timecnt, h->typecnt, h->charcnt)) - return false; - h->version = ver; - return true; +// One ttinfo record's decoded fields. +typedef struct { + i32 utoff; + u8 isdst; +} TtInfoVal; + +// The one ambient fact the phases read down the call stack: the instant being +// resolved. Everything a phase *produces* is a return value, not a field here, +// and the block's time width is decided by which version arm runs, not stored. +typedef struct PcParserCtx { + i64 unix_seconds; +} PcParserCtx; + +// Backtracking savepoint: the context is a flat value, so a mark is a copy of it +// and rollback restores it -- a `PcChoice` arm leaves nothing behind. +typedef PcParserCtx PcParserCtxMark; +static PcParserCtxMark PcParserCtxSnapshot(PcParserCtx *ctx) { + return *ctx; +} +static void PcParserCtxRollback(PcParserCtx *ctx, PcParserCtxMark mark) { + *ctx = mark; } -// Size of a version-1 data block, used only to skip it on the way to -// the version-2+ block. v1 transition times and leap records are 4 and -// 4+4 bytes respectively. +// Size of a version-1 data block, used only to skip it on the way to the +// version-2+ block (transition times 4 bytes, leap records 4+4). static u64 tzif_v1_data_size(const TzifHeader *h) { return (u64)h->timecnt * 4 + (u64)h->timecnt * 1 + (u64)h->typecnt * 6 + (u64)h->charcnt * 1 + (u64)h->leapcnt * 8 + (u64)h->isstdcnt * 1 + (u64)h->isutcnt * 1; } -// Resolve the offset from a data block whose transition times are -// `time_width` bytes (4 for v1, 8 for v2+). The cursor must sit at the -// start of the transition-time array. Streams through the block without -// storing it: find the last transition <= `unix_seconds`, take its -// type's utoff, with the first standard-time (isdst==0) ttinfo as the -// pre-history / past-table fallback. -static bool tzif_resolve(BufIter *it, u32 time_width, u32 timecnt, u32 typecnt, i64 unix_seconds, i32 *out_offset) { - if (typecnt == 0) - return false; +// The magic and a generic byte skip. +PcRecognizer(TzifMagic) { + PcSatisfyStr("TZif") {} +} +PcRecognizer(Skip, u64) { + PcSkipBytes(expect); +} + +// "TZif" + version + 15 reserved + six big-endian u32 counts. +PcParser(Header, TzifHeader) { + PcSeq() { + PcExpect(TzifMagic); + PcMatch(PcU8, &value->version); + PcExpect(Skip, 15); + PcMatch(PcU32BE, &value->isutcnt); + PcMatch(PcU32BE, &value->isstdcnt); + PcMatch(PcU32BE, &value->leapcnt); + PcMatch(PcU32BE, &value->timecnt); + PcMatch(PcU32BE, &value->typecnt); + PcMatch(PcU32BE, &value->charcnt); + } +} - // Phase 1: transitions are ascending; remember the index of the - // last one not in the future. - bool found = false; - i64 best = 0; - for (u32 i = 0; i < timecnt; ++i) { - i64 t = 0; - if (time_width == 8) { - if (!BufReadFmt(it, "{>8r}", t)) - return false; - } else { - i32 t32 = 0; - if (!BufReadFmt(it, "{>4r}", t32)) - return false; - t = t32; +// One ttinfo record: utoff (signed), the isdst flag, and a designation index we +// skip. `PcI32BE` reads straight into the signed field -- no unsigned temporary. +PcParser(TtInfo, TtInfoVal) { + PcSeq() { + PcMatch(PcI32BE, &value->utoff); + PcMatch(PcU8, &value->isdst); + PcExpect(Skip, 1); + } +} + +// Phase 1: transitions are ascending; the last index not in the future, or -1 if +// they are all ahead of the instant. The time is signed and its width is the +// version's (32-bit v1, 64-bit v2) -- so the two arms read via `PcI32BE` and +// `PcI64BE`, and that reader choice is the only difference between them. +PcParser(FindBestTransitionTimeV1, u32, i64) { + i32 t = 0; + PcSeq() { + *value = -1; + PcMatchExactlyN(expect, i, PcI32BE, &t) { + if (t <= ctx->unix_seconds) // i32 t sign-extends against the i64 instant + *value = (i64)i; } - if (t <= unix_seconds) { - found = true; - best = (i64)i; + } +} +PcParser(FindBestTransitionTimeV2, u32, i64) { + i64 t = 0; + PcSeq() { + *value = -1; + PcMatchExactlyN(expect, i, PcI64BE, &t) { + if (t <= ctx->unix_seconds) + *value = (i64)i; } } +} - // Phase 2: the type-index array is `timecnt` bytes; pick the one at - // `best` and step the cursor to the array's end either way. - u8 chosen = 0; - bool have_chosen = false; - if (found) { - if (best > 0 && !IterMove(it, best)) - return false; - if (!BufReadFmt(it, "{>1r}", chosen)) - return false; - have_chosen = true; - i64 rest = (i64)timecnt - best - 1; - if (rest > 0 && !IterMove(it, rest)) - return false; - } else if (timecnt > 0 && !IterMove(it, (i64)timecnt)) { - return false; +// Phase 2: the type-index array; the index recorded at `sel`, or -1 when there +// was no transition to select. +PcParser(PickTransitionType, PhaseIn, i64) { + u8 tix = 0; + PcSeq() { + *value = -1; + PcMatchExactlyN(expect.count, i, PcU8, &tix) { + if ((i64)i == expect.sel) + *value = (i64)tix; + } } +} - // Phase 3: ttinfo records. Grab the chosen type's utoff; remember - // the first standard-time offset for the fallback. - bool have_std = false; - i32 std_off = 0; - bool have_off = false; - i32 off = 0; - for (u32 i = 0; i < typecnt; ++i) { - i32 utoff = 0; - u8 isdst = 0, desigidx = 0; - if (!BufReadFmt(it, FMT_TZIF_TTINFO_BE, utoff, isdst, desigidx)) - return false; - if (have_chosen && i == (u32)chosen) { - off = utoff; - have_off = true; - } - if (!have_std && isdst == 0) { - std_off = utoff; - have_std = true; +// Phase 3: the ttinfo records; the selected type's utoff, else the first +// standard-time (isdst==0) type as the pre-history fallback. No usable type -> +// the rule rejects. +PcParser(PickUtOffset, PhaseIn, i32) { + TtInfoVal tt = {0}; + i32 off = 0; + i32 std_off = 0; + bool have_off = false; + bool have_std = false; + PcSeq() { + PcMatchExactlyN(expect.count, i, TtInfo, &tt) { + if ((i64)i == expect.sel) { + off = tt.utoff; + have_off = true; + } + if (!have_std && tt.isdst == 0) { + std_off = tt.utoff; + have_std = true; + } } + if (have_off) + *value = off; + else if (have_std) + *value = std_off; + else + PcReject(); } +} - if (have_off) { - *out_offset = off; - return true; +// Version dispatch. v1: resolve the block right here (4-byte times). v2+: skip +// the v1 block, read the v2 header, resolve its block (8-byte times). Each arm's +// guard rejects before consuming, so the choice falls through cleanly. +PcParser(ResolveV1, TzifHeader, i32) { + i64 best = 0; + i64 chosen = 0; + PcSeq() { + if (expect.version >= '2') + PcReject(); + PcMatch(FindBestTransitionTimeV1, expect.timecnt, &best); + PcMatch(PickTransitionType, ((PhaseIn) {expect.timecnt, best}), &chosen); + PcMatch(PickUtOffset, ((PhaseIn) {expect.typecnt, chosen}), value); + } +} +PcParser(ResolveV2, TzifHeader, i32) { + TzifHeader h2 = {0}; + i64 best = 0; + i64 chosen = 0; + PcSeq() { + if (expect.version < '2') + PcReject(); + PcExpect(Skip, tzif_v1_data_size(&expect)); + PcMatch(Header, &h2); + PcMatch(FindBestTransitionTimeV2, h2.timecnt, &best); + PcMatch(PickTransitionType, ((PhaseIn) {h2.timecnt, best}), &chosen); + PcMatch(PickUtOffset, ((PhaseIn) {h2.typecnt, chosen}), value); + } +} +PcParser(Dispatch, TzifHeader, i32) { + PcChoice() { + PcAlt(ResolveV1, expect, value); + PcAlt(ResolveV2, expect, value); } - if (have_std) { - *out_offset = std_off; - return true; +} + +// TZif: the fixed header, then the version-appropriate data block. +PcParser(Tzif, i32) { + TzifHeader h = {0}; + PcSeq() { + PcMatch(Header, &h); + PcMatch(Dispatch, h, value); } - return false; } bool TzifOffsetFromBuf(const u8 *data, size len, i64 unix_seconds, i32 *out_offset_seconds) { if (!data || !out_offset_seconds) return false; - BufIter it = BufIterFromMemory((u8 *)data, len); - TzifHeader h1; - if (!tzif_read_header(&it, &h1)) { - LOG_ERROR("Tzif: malformed header"); - return false; - } + BufIter in = BufIterFromMemory((u8 *)data, len); + PcParserCtx cx = {.unix_seconds = unix_seconds}; + PcParserCtx *ctx = &cx; + i32 offset = 0; - bool ok; - i32 offset = 0; - if (h1.version >= '2') { - // Skip the v1 data block, then parse the v2+ header and its - // 8-byte-time data block (the authoritative one). - if (!IterMove(&it, (i64)tzif_v1_data_size(&h1))) { - LOG_ERROR("Tzif: truncated v1 data block"); - return false; - } - TzifHeader h2; - if (!tzif_read_header(&it, &h2)) { - LOG_ERROR("Tzif: malformed v2 header"); - return false; - } - ok = tzif_resolve(&it, 8, h2.timecnt, h2.typecnt, unix_seconds, &offset); - } else { - ok = tzif_resolve(&it, 4, h1.timecnt, h1.typecnt, unix_seconds, &offset); + if (!(PcRun(Tzif, &in, &offset) & PC_PARSER_STATUS_SUCCESS)) { + LOG_ERROR("Tzif: malformed or unresolvable TZif data"); + return false; } - - if (ok) - *out_offset_seconds = offset; - return ok; + *out_offset_seconds = offset; + return true; } bool TzifLocalOffsetSeconds(i64 unix_seconds, i32 *out_offset_seconds, Allocator *alloc) { diff --git a/meson.build b/meson.build index 04e3d87..3be52d0 100644 --- a/meson.build +++ b/meson.build @@ -213,6 +213,7 @@ misra_std_sources = files( 'Source/Misra/Std/Memory.c', 'Source/Misra/Std/Zstr.c', 'Source/Misra/Std/DateTime.c', + 'Source/Misra/ParserCombinator.c', 'Source/Misra/Std/Allocator.c', 'Source/Misra/Std/Allocator/_Os.c', 'Source/Misra/Std/Allocator/Page.c', From 4a2bebad23ee440fb905e77d0b82d458f3faf0ba Mon Sep 17 00:00:00 2001 From: Siddharth Mishra Date: Thu, 2 Jul 2026 17:32:05 -0700 Subject: [PATCH 09/11] docs(conventions): drop the intentional-bypass comment convention Removes the guidance requiring a `// intentional bypass:` comment on direct-field-write sites in corruption/Deadend tests, in both the "Accessor macros are read-only" and Deadend-fixtures sections. --- Conventions/CODING-CONVENTIONS.md | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/Conventions/CODING-CONVENTIONS.md b/Conventions/CODING-CONVENTIONS.md index 1056244..88898e6 100644 --- a/Conventions/CODING-CONVENTIONS.md +++ b/Conventions/CODING-CONVENTIONS.md @@ -188,13 +188,8 @@ of the codebase to see them in action. shouldn't be — adjusting state has invariants the mutators enforce. Intentional-corruption tests that need to bypass an invariant (to verify that a validator catches it) write the field directly, with - an inline comment opening with the canonical phrase - `// intentional bypass:` followed by *why* no public accessor / - mutator covers the case. The phrase is grep-able; a stray field - write without it is a review finding. See - `Tests/Std/Allocator.Heap.c`, `Tests/Std/BitVec.Convert.c`, - `Tests/Std/Graph.Init.c`, and the other `Tests/Std/*` Deadend - fixtures for the established shape. + an inline comment explaining why no public accessor or mutator + covers the case. - **Container key types ship `*_hash` and `*_compare`.** Any type meant to be a `Map` key (or `Vec`/`List` element with comparison semantics) must expose two snake_case helpers in the @@ -675,10 +670,7 @@ up-cast. exercise the validators by violating an invariant — bypassing capacity bookkeeping, scrambling a magic value, overrunning a buffer. These tests write fields directly because that's the whole point of - the test; open each such site with the canonical - `// intentional bypass:` comment (same phrase used by the - non-Deadend tests in *Accessor macros are read-only* above) so it - stays grep-able and doesn't get swept on the next pass. + the test. - **Fixture-local owned storage** (e.g. `Vec.Complex.c`'s `char *name` / `int *values` inside a `ComplexItem` that exercises `VecInitWithDeepCopy` callbacks) is fine as raw pointers because the From 31fef71853602ca8fa553efa65ca3b4ffa56156e Mon Sep 17 00:00:00 2001 From: Siddharth Mishra Date: Thu, 2 Jul 2026 22:51:50 -0700 Subject: [PATCH 10/11] feat(procmaps): own path as Str + (Zstr,len) open APIs ProcMaps now owns each entry's path as a NUL-terminated Str and drops the retained raw buffer after parsing -- no borrowed, non-terminated slices to reason about downstream. This dissolves the whole borrowed-slice hazard at its root: every consumer reads a real C-string. - File/Elf: add the (Zstr,len) fixed-length form as a fourth call shape alongside the existing Str* / Zstr / char* _Generic arms (char* is the MSVC string-literal synonym for Zstr -- see Zstr.h, kept intentionally). FileOpen, FileReadAndClose and ElfOpen gain a (Zstr,len) arity backed by file_open_n / file_read_and_close_*_n / elf_open_n, which copy the fixed-length view into a stack buffer only because open() needs a NUL-terminated C-string with no way to pass a bound. - SymbolResolver: cache owns its path Str copy; module_path borrows it. - Supporting DSL: PcCaptureUntil, PcFailIfNotEof, PcReportsRender, PcI. - Tests + CalC updated for the owned path and rendered diagnostics. --- Bin/CalC.c | 48 +--- Include/Misra/ParserCombinator.h | 45 +++ Include/Misra/Parsers/Elf.h | 17 +- Include/Misra/Parsers/Elf/Private.h | 13 +- Include/Misra/Parsers/ProcMaps.h | 26 +- Include/Misra/Std/File.h | 30 +- Include/Misra/Sys/SymbolResolver.h | 5 +- Source/Misra/ParserCombinator.c | 47 +++ Source/Misra/Parsers/Elf.c | 13 + Source/Misra/Parsers/ProcMaps.c | 330 ++++++++++++---------- Source/Misra/Std/File.c | 35 +++ Source/Misra/Sys/SymbolResolver.c | 32 +-- Tests/Parsers/ProcMaps.c | 142 +++++++--- Tests/Std/Io/Write.c | 4 +- Tests/Sys/SymbolResolver/Bias.c | 7 +- Tests/Sys/SymbolResolver/SymbolResolver.c | 7 +- 16 files changed, 504 insertions(+), 297 deletions(-) diff --git a/Bin/CalC.c b/Bin/CalC.c index d142c94..b10e794 100644 --- a/Bin/CalC.c +++ b/Bin/CalC.c @@ -86,16 +86,14 @@ static void binding_deinit(void *copy, const Allocator *alloc) { (void)alloc; StrDeinit(&((Binding *)copy)->name); } -typedef Vec(PcReport) Reports; - /// /// The grammar context threaded through every parser as `ctx`: the variable /// environment plus the diagnostics sink the `PcReport*` macros append to. Both /// are containers that carry their own allocator. /// typedef struct PcParserCtx { - Bindings vars; - Reports reports; + Bindings vars; + PcReports reports; } PcParserCtx; typedef struct { @@ -363,47 +361,17 @@ static void seed(Bindings *vars, HeapAllocator *heap, Zstr name, Num v) { VecPushBack(vars, b); } -static Zstr level_word(PcReportLevel level) { - switch (level) { - case PC_REPORT_ERROR : - return "error"; - case PC_REPORT_WARN : - return "warning"; - default : - return "note"; - } -} - -/// draw each report rustc-style: level + message, the source line, then carets -/// under its span. Parsers never touch this -- they only record a `PcReport`. -static void render(Str *src, Reports *reports) { - const char *bytes = StrBegin(src); - for (u64 r = 0; r < VecLen(reports); r++) { - PcReport rep = VecAt(reports, r); - u64 end = rep.end; - while (end > rep.start && (bytes[end - 1] == ' ' || bytes[end - 1] == '\t')) - end--; - WriteFmtLn("{}: {}", level_word(rep.level), rep.message); - WriteFmtLn(" {}", *src); - StrInitStack(caret, 512) { - char space = ' ', hat = '^'; - for (u64 c = 0; c < rep.start; c++) - StrPushBackR(&caret, space); - for (u64 c = rep.start; c < end; c++) - StrPushBackR(&caret, hat); - WriteFmtLn(" {}", caret); - } - } -} - static void eval_line(PcParserCtx *ctx, Str *line) { VecClear(&ctx->reports); StrIter in = StrIterFromStr(*line); Num out = {0}; PcParserStatus st = PcRun(Calc, &in, &out); - if (VecLen(&ctx->reports) > 0) - render(line, &ctx->reports); - else if ((st & PC_PARSER_STATUS_SUCCESS) && !StrIterRemainingLength(&in)) { + if (VecLen(&ctx->reports) > 0) { + StrInitStack(diag, 256) { + PcReportsRender(&diag, line, &ctx->reports); + WriteFmt("{}", diag); + } + } else if ((st & PC_PARSER_STATUS_SUCCESS) && !StrIterRemainingLength(&in)) { if (out.is_float) WriteFmtLn("{}", out.f); else diff --git a/Include/Misra/ParserCombinator.h b/Include/Misra/ParserCombinator.h index cc02583..d261477 100644 --- a/Include/Misra/ParserCombinator.h +++ b/Include/Misra/ParserCombinator.h @@ -85,6 +85,7 @@ #include #include #include +#include /// /// The cursor type. Text grammars use the default `StrIter` (`Iter(char)`); a byte-oriented @@ -157,6 +158,17 @@ typedef struct PcReport { Zstr message; ///< the parser's words; the specifics show under the caret } PcReport; +typedef Vec(PcReport) PcReports; + +/// +/// PcReportsRender: draw the recorded diagnostics rustc-style into `out`. For each report it appends +/// a `level: message` line, the source line the report spans (bounded by a newline or NUL on either +/// side, so it works on the whole multi-line input), and a caret run under the span. `src` is the +/// parsed input; a grammar only records `PcReport`s and never draws a caret. The caller owns `out` +/// and decides where it goes -- print it, log it, ... -- so the renderer is I/O-free. +/// +void PcReportsRender(Str *out, Str *src, PcReports *reports); + /// /// The parser context and its savepoint contract /// ============================================== @@ -376,6 +388,21 @@ typedef struct PcReport { /// #define PcReject() return PC_CONSUMED(pc_seq.start) | PC_PARSER_STATUS_FAILED +/// +/// PcFailIfNotEof: a `PcSeq` step that succeeds only at end of input. If any input remains, it +/// records `Msg` at the cursor and fails the rule (exactly as `PcReject`, carrying the sequence's +/// consumed bit) -- the "the whole input had to parse" assertion, so a grammar never inspects the +/// cursor by hand. `ctx` must carry a `Vec(PcReport) reports;` sink for the recorded cause. +/// +#define PcFailIfNotEof(Msg) \ + do { \ + char UNPL(pc_eof_) = 0; \ + if (IterPeekAt(in, 0, &UNPL(pc_eof_))) { \ + PcReportErrorHere(Msg); \ + return PC_CONSUMED(pc_seq.start) | PC_PARSER_STATUS_FAILED; \ + } \ + } while (0) + /// /// Record a diagnostic and KEEP GOING -- the greedy alternative to `PcReject`. Appends a /// `PcReport` (spanning what the current `PcSeq` frame has consumed so far) to `ctx->reports`, @@ -568,6 +595,24 @@ typedef struct PcReport { for (char Var = 0; IterPeekAt(in, 0, &Var) && !(IsSync);) \ IterMove(in, 1) +/// +/// PcCaptureUntil: capture the run of input from the cursor up to (not including) the first char +/// satisfying `IsStop`, or end of input. Binds `PtrOut` to a `Zstr` at the run's start and `LenOut` +/// (a `u64`) to its byte length -- a borrowed `(ptr, len)` view the rule does what it wants with: +/// parse it to a number, copy it into an owned `Str`, or store it as-is. The DSL owns the cursor the +/// whole time, so the rule never touches `in`. `Var` is the loop-scoped peeked char (as in +/// `PcRecover`). The cursor is left AT the stop char -- consume it separately if the grammar must +/// move past it. +/// +#define PcCaptureUntil(Var, IsStop, PtrOut, LenOut) \ + do { \ + u64 UNPL(pc_cap_) = IterIndex(in); \ + for (char Var = 0; IterPeekAt(in, 0, &Var) && !(IsStop);) \ + IterMove(in, 1); \ + *(PtrOut) = (Zstr)IterDataAt(in, UNPL(pc_cap_)); \ + *(LenOut) = IterIndex(in) - UNPL(pc_cap_); \ + } while (0) + /// /// Atoms -- the only place `StrIter` and `PcParserStatus` are handled directly. Every fundamental /// parser (a char class, a keyword, ...) is written on top of one of these, so it never pokes the diff --git a/Include/Misra/Parsers/Elf.h b/Include/Misra/Parsers/Elf.h index 50996e3..e24cf4d 100644 --- a/Include/Misra/Parsers/Elf.h +++ b/Include/Misra/Parsers/Elf.h @@ -243,10 +243,18 @@ typedef struct Elf { /// /// Open and parse an ELF file from disk. /// -/// out[out] : Populated on success. -/// path[in] : Filesystem path. Prefer `Str *`; `Zstr` (NUL-terminated) accepted. -/// alloc[in] : Allocator for the read-in byte buffer and the section / -/// symbol vectors. Must outlive the `Elf`. +/// Call shapes via `OVERLOAD` + `_Generic` on `path`: +/// `ElfOpen(out, path)` -- `path` is `Str *` or `Zstr`. +/// `ElfOpen(out, path, alloc)` -- same, explicit allocator. +/// `ElfOpen(out, path, path_len, alloc)`-- `path` is a fixed-length view +/// (`Zstr`, `size`); copied into a +/// stack buffer for the syscall. +/// +/// out[out] : Populated on success. +/// path[in] : Filesystem path. Prefer `Str *`; `Zstr` (NUL-terminated) accepted. +/// path_len[in] : Length of `path` for the fixed-length form. +/// alloc[in] : Allocator for the read-in byte buffer and the section / +/// symbol vectors. Must outlive the `Elf`. /// /// SUCCESS : Returns true; `out` owns the read-in buffer and will free /// it on `ElfDeinit`. @@ -270,6 +278,7 @@ typedef struct Elf { Zstr: elf_open((out), (Zstr)(path), ALLOCATOR_OF(alloc)), \ char *: elf_open((out), (Zstr)(path), ALLOCATOR_OF(alloc)) \ ) +#define ElfOpen_4(out, path, len, alloc) elf_open_n((out), (Zstr)(path), (len), ALLOCATOR_OF(alloc)) /// /// Parse an ELF object from an in-memory byte range -- **L-value / diff --git a/Include/Misra/Parsers/Elf/Private.h b/Include/Misra/Parsers/Elf/Private.h index c82859f..27a58ba 100644 --- a/Include/Misra/Parsers/Elf/Private.h +++ b/Include/Misra/Parsers/Elf/Private.h @@ -19,13 +19,14 @@ extern "C" { #endif -typedef struct Elf Elf; -typedef struct ElfSection ElfSection; + typedef struct Elf Elf; + typedef struct ElfSection ElfSection; -bool elf_open(Elf *out, Zstr path, Allocator *alloc); -bool elf_open_from_memory_copy(Elf *out, const u8 *data, size data_size, Allocator *alloc); -const ElfSection *elf_find_section_zstr(const Elf *self, Zstr name); -const ElfSection *elf_find_section_str(const Elf *self, const Str *name); + bool elf_open(Elf *out, Zstr path, Allocator *alloc); + bool elf_open_n(Elf *out, Zstr path, size len, Allocator *alloc); + bool elf_open_from_memory_copy(Elf *out, const u8 *data, size data_size, Allocator *alloc); + const ElfSection *elf_find_section_zstr(const Elf *self, Zstr name); + const ElfSection *elf_find_section_str(const Elf *self, const Str *name); #ifdef __cplusplus } diff --git a/Include/Misra/Parsers/ProcMaps.h b/Include/Misra/Parsers/ProcMaps.h index d2947b0..f02694d 100644 --- a/Include/Misra/Parsers/ProcMaps.h +++ b/Include/Misra/Parsers/ProcMaps.h @@ -32,33 +32,31 @@ typedef enum ProcMapPerms { } ProcMapPerms; /// -/// One line of `/proc/self/maps`. `path` is borrowed from the -/// `ProcMaps.raw` buffer and stays valid until `ProcMapsDeinit`. May -/// be empty for anonymous mappings (heap, stacks, vdso, etc.). +/// One line of `/proc/self/maps`. `path` is an owned, NUL-terminated copy of the +/// mapping's backing file, freed by `ProcMapsDeinit`. It is an empty `Str` +/// (`StrLen(&path) == 0`) for anonymous mappings (heap, stacks, vdso, etc.). /// typedef struct ProcMapEntry { - u64 start; // runtime virtual address (inclusive) - u64 end; // runtime virtual address (exclusive) - u32 perms; // bitmask of ProcMapPerms - u64 file_offset; // offset within the backing file - Zstr path; // backing file path, or "" if anonymous + u64 start; // runtime virtual address (inclusive) + u64 end; // runtime virtual address (exclusive) + u32 perms; // bitmask of ProcMapPerms + u64 file_offset; // offset within the backing file + Str path; // owned copy of the mapping path (empty for anonymous mappings) } ProcMapEntry; typedef Vec(ProcMapEntry) ProcMapEntries; typedef struct ProcMaps { - Str raw; // owns the raw /proc/self/maps bytes - ProcMapEntries entries; // pointers into `raw` + ProcMapEntries entries; // each entry owns its path copy u64 min_addr; // lowest `start` across all entries (0 if none) } ProcMaps; /// -/// Read and parse `/proc/self/maps`. The full file is held inside -/// `out->raw` for the lifetime of the ProcMaps so each entry's `path` -/// can borrow from it without a separate copy. +/// Read and parse `/proc/self/maps`. The file is parsed into `out->entries`, +/// each entry owning a copy of its path; the raw buffer is not retained. /// /// out[out] : Populated on success. -/// alloc[in] : Allocator for the raw buffer and entries vector. +/// alloc[in] : Allocator for the entries vector and each entry's path copy. /// /// SUCCESS : Returns true; `out->entries` is populated. /// FAILURE : Returns false; logs the failing step. `out` is left zeroed. diff --git a/Include/Misra/Std/File.h b/Include/Misra/Std/File.h index 57c99e4..1dca94c 100644 --- a/Include/Misra/Std/File.h +++ b/Include/Misra/Std/File.h @@ -66,9 +66,17 @@ typedef enum FileWhence { /// the `"b"` suffix is accepted but has no effect on the /// implementation. /// -/// path[in] : Path to open. Prefer `Str *` (carries length, can't -/// silently drop the NUL terminator). `Zstr` is accepted -/// as a literal / borrowed-buffer convenience. +/// Three call shapes via `OVERLOAD` + `_Generic` on `path`: +/// `FileOpen(path, mode)` -- `path` is `Str *` or `Zstr`. +/// `FileOpen(path, path_len, mode)` -- `path` is a fixed-length view +/// (`Zstr`, `size`); it is copied +/// into a stack buffer and +/// NUL-terminated for the syscall. +/// +/// path[in] : Path to open. Prefer `Str *` (carries length, can't +/// silently drop the NUL terminator). `Zstr` is accepted +/// as a literal / borrowed-buffer convenience. +/// path_len[in] : Length of `path` for the 3-arg fixed-length form. /// /// SUCCESS : Returns a File where `FileIsOpen(&out)` is true. /// FAILURE : Returns a File where `FileIsOpen(&out)` is false. @@ -76,13 +84,16 @@ typedef enum FileWhence { /// TAGS: File, Open, API /// File file_open(Zstr path, Zstr mode); -#define FileOpen(path, mode) \ +File file_open_n(Zstr path, size len, Zstr mode); +#define FileOpen(...) OVERLOAD(FileOpen, __VA_ARGS__) +#define FileOpen_2(path, mode) \ _Generic( \ (path), \ Str *: file_open((Zstr)StrBegin((Str *)(path)), (mode)), \ Zstr: file_open((Zstr)(path), (mode)), \ char *: file_open((Zstr)(path), (mode)) \ ) +#define FileOpen_3(path, len, mode) file_open_n((Zstr)(path), (len), (mode)) /// /// Borrow a POSIX fd into a `File`. The returned File has `owns = false` @@ -199,7 +210,10 @@ i64 file_read_to_buf(File *f, Buf *out); /// i64 file_read_and_close_to_buf(Zstr path, Buf *out); i64 file_read_and_close_to_str(Zstr path, Str *out); -#define FileReadAndClose(path, out) \ +i64 file_read_and_close_to_buf_n(Zstr path, size len, Buf *out); +i64 file_read_and_close_to_str_n(Zstr path, size len, Str *out); +#define FileReadAndClose(...) OVERLOAD(FileReadAndClose, __VA_ARGS__) +#define FileReadAndClose_2(path, out) \ _Generic( \ (out), \ Buf *: _Generic( \ @@ -215,6 +229,12 @@ i64 file_read_and_close_to_str(Zstr path, Str *out); char *: file_read_and_close_to_str((Zstr)(path), (Str *)(out)) \ ) \ ) +#define FileReadAndClose_3(path, len, out) \ + _Generic( \ + (out), \ + Buf *: file_read_and_close_to_buf_n((Zstr)(path), (len), (Buf *)(out)), \ + Str *: file_read_and_close_to_str_n((Zstr)(path), (len), (Str *)(out)) \ + ) // FileGetSize lives in `Sys/Dir.h` -- path-based size query that // goes straight to the kernel (open + lseek(SEEK_END) + close, or diff --git a/Include/Misra/Sys/SymbolResolver.h b/Include/Misra/Sys/SymbolResolver.h index 8a76f44..51fc6f6 100644 --- a/Include/Misra/Sys/SymbolResolver.h +++ b/Include/Misra/Sys/SymbolResolver.h @@ -30,6 +30,7 @@ # include #endif #include +#include #include #include #include @@ -73,8 +74,8 @@ typedef struct ResolvedSymbol { } ResolvedSymbol; typedef struct ResolverCacheEntry { - Zstr path; // borrowed from ProcMaps.raw - Elf elf; + Str path; // owned NUL-terminated copy of the mapping path (the ProcMaps slice is not terminated) + Elf elf; // Sidecar debug file found via .gnu_debuglink or .note.gnu.build-id. // Populated lazily for stripped binaries that have an installed // -dbg package or a debug file alongside them. When `has_sidecar` diff --git a/Source/Misra/ParserCombinator.c b/Source/Misra/ParserCombinator.c index b738510..b831b58 100644 --- a/Source/Misra/ParserCombinator.c +++ b/Source/Misra/ParserCombinator.c @@ -14,6 +14,8 @@ #include #include +#include +#include #define PC_DELIVER_READER(Name, Type, Reader) \ PcParserStatus pc_parser_##Name(BufIter *in, struct PcParserCtx *ctx, Type *value) { \ @@ -38,3 +40,48 @@ PC_DELIVER_READER(PcI64BE, i64, BufReadI64BE) PC_DELIVER_READER(PcI64LE, i64, BufReadI64LE) #undef PC_DELIVER_READER + +static Zstr pc_level_word(PcReportLevel level) { + switch (level) { + case PC_REPORT_ERROR : + return "error"; + case PC_REPORT_WARN : + return "warning"; + default : + return "note"; + } +} + +void PcReportsRender(Str *out, Str *src, PcReports *reports) { + const char *bytes = StrBegin(src); + u64 n = StrLen(src); + for (u64 r = 0; r < VecLen(reports); r++) { + PcReport rep = VecAt(reports, r); + + // The source line the span sits on, bounded by a newline -- or a NUL, since + // a parser may terminate a borrowed slice in place -- on either side. + u64 line_start = rep.start; + while (line_start > 0 && bytes[line_start - 1] != '\n' && bytes[line_start - 1] != '\0') + line_start--; + u64 line_end = rep.start; + while (line_end < n && bytes[line_end] != '\n' && bytes[line_end] != '\0') + line_end++; + u64 span_end = rep.end < line_end ? rep.end : line_end; + while (span_end > rep.start && (bytes[span_end - 1] == ' ' || bytes[span_end - 1] == '\t')) + span_end--; + + StrAppendFmt(out, "{}: {}\n", pc_level_word(rep.level), rep.message); + StrPushBackR(out, ' '); + StrPushBackR(out, ' '); + for (u64 c = line_start; c < line_end; c++) + StrPushBackR(out, bytes[c]); + StrPushBackR(out, '\n'); + StrPushBackR(out, ' '); + StrPushBackR(out, ' '); + for (u64 c = line_start; c < rep.start; c++) + StrPushBackR(out, ' '); + for (u64 c = rep.start; c < span_end; c++) + StrPushBackR(out, '^'); + StrPushBackR(out, '\n'); + } +} diff --git a/Source/Misra/Parsers/Elf.c b/Source/Misra/Parsers/Elf.c index d123c2a..a6af1a4 100644 --- a/Source/Misra/Parsers/Elf.c +++ b/Source/Misra/Parsers/Elf.c @@ -535,6 +535,19 @@ bool elf_open(Elf *out, Zstr path, Allocator *alloc) { return ElfOpenFromMemory(out, &data); } +bool elf_open_n(Elf *out, Zstr path, size len, Allocator *alloc) { + if (!out || !path || !alloc) { + LOG_FATAL("ElfOpen: NULL argument (contract violation)"); + } + Buf data = BufInit(alloc); + if (FileReadAndClose(path, len, &data) < 0) { + BufDeinit(&data); + LOG_ERROR("ElfOpen: failed to read {} path bytes", len); + return false; + } + return ElfOpenFromMemory(out, &data); +} + void ElfDeinit(Elf *self) { if (!self) return; diff --git a/Source/Misra/Parsers/ProcMaps.c b/Source/Misra/Parsers/ProcMaps.c index 3b591b7..871f760 100644 --- a/Source/Misra/Parsers/ProcMaps.c +++ b/Source/Misra/Parsers/ProcMaps.c @@ -15,8 +15,15 @@ /// inode : decimal (we ignore) /// path : optional; anonymous mappings have no path field /// -/// Paths can contain spaces — we treat everything after the inode -/// field's trailing whitespace as the path, up to the line terminator. +/// The whole file is a grammar on the parser-combinator DSL over a +/// `StrIter` (the default cursor -- no `#define PC_ITER`): each field is +/// its own rule, `ProcMapLine` a line, and `ProcMapLines` the file -- +/// which decodes each line into an entry while folding in `min_addr`, +/// and fails on the first line it cannot parse (naming the cause in the +/// diagnostics sink), so the loader just runs it. Paths can contain +/// spaces -- everything after the inode token runs to the line terminator +/// and is copied into the entry's owned `Str`, so the transient parse +/// buffer is dropped once loading returns. #include @@ -27,175 +34,175 @@ #include #include +#include + +// The context carries only the diagnostics sink the `PcReport*` macros append to: +// a malformed line is recorded here, not silently dropped. Nothing else is +// ambient -- every field is a return value. Reports are not rolled back on +// backtracking (a recorded error stays recorded), so the savepoint is a no-op. +typedef struct PcParserCtx { + PcReports reports; + Allocator *alloc; // copies each entry's path into an owned Str +} PcParserCtx; +typedef struct { + u8 unused; +} PcParserCtxMark; +static PcParserCtxMark PcParserCtxSnapshot(PcParserCtx *ctx) { + (void)ctx; + return (PcParserCtxMark) {0}; +} +static void PcParserCtxRollback(PcParserCtx *ctx, PcParserCtxMark mark) { + (void)ctx; + (void)mark; +} + // --------------------------------------------------------------------------- -// Field parsers +// Field grammar // --------------------------------------------------------------------------- -static int hex_digit_value(char c) { - if (c >= '0' && c <= '9') - return c - '0'; - if (c >= 'a' && c <= 'f') - return 10 + (c - 'a'); - if (c >= 'A' && c <= 'F') - return 10 + (c - 'A'); - return -1; +// One hex digit -> its 0..15 value (lower or upper case). +PcParser(HexDigit, u8) { + PcSatisfyChar(c, (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) { + *value = (u8)(c <= '9' ? c - '0' : (c | 0x20) - 'a' + 10); + } } -// Parse a hex run at the cursor. Advances the iter past the digits. -// Returns false if no digits are consumed. -static bool parse_hex_u64(StrIter *si, u64 *out) { - u64 v = 0; - int consumed = 0; - char c; - while (StrIterPeek(si, &c)) { - int d = hex_digit_value(c); - if (d < 0) - break; - v = (v << 4) | (u64)d; - StrIterMustNext(si); - ++consumed; +// A hex run -> u64. One-or-more, so an empty field (no digits) fails the rule. +PcParser(HexU64, u64) { + u8 d = 0; + u64 v = 0; + PcSeq() { + PcMatchOneOrMore(HexDigit, &d) { + v = (v << 4) | (u64)d; + } + *value = v; } - if (!consumed) - return false; - *out = v; - return true; } -static bool expect_char(StrIter *si, char want) { - char c; - if (!StrIterPeek(si, &c) || c != want) - return false; - StrIterMustNext(si); - return true; +// Inter-field whitespace (spaces/tabs), any amount including none. +PcRecognizer(SpaceCh) { + PcSatisfyChar(c, c == ' ' || c == '\t') {} +} +PcRecognizer(Spaces) { + PcRecognizeZeroOrMore(SpaceCh); } -static void skip_ws(StrIter *si) { - char c; - while (StrIterPeek(si, &c) && (c == ' ' || c == '\t')) { - StrIterMustNext(si); - } +// The '-' between start and end. +PcRecognizer(Dash) { + PcSatisfyChar(c, c == '-') {} } -// Read one "non-whitespace blob" (the dev/inode tokens). Just skip it. -static void skip_token(StrIter *si) { - char c; - while (StrIterPeek(si, &c) && c != ' ' && c != '\t' && c != '\n') { - StrIterMustNext(si); +// The four permission characters -> a ProcMapPerms bitmask. Position is +// significant: r/w/x/p set their bit, anything else (a '-' or 's') clears it. +PcParser(PermCh, char) { + PcSatisfyChar(c, c == 'r' || c == 'w' || c == 'x' || c == 'p' || c == 's' || c == '-') { + *value = c; } } - -// --------------------------------------------------------------------------- -// Line decode -// --------------------------------------------------------------------------- - -// Parses one `/proc/self/maps` line from `si`. On success the path is -// NUL-terminated in place (the trailing '\n' becomes '\0'), and -// `out->path` aliases the iter's underlying buffer. The iter is -// advanced past the line terminator so the next call resumes at the -// next line. -static bool parse_one_line(StrIter *si, ProcMapEntry *out) { - u64 start = 0, ende = 0, offset = 0; - if (!parse_hex_u64(si, &start)) - return false; - if (!expect_char(si, '-')) - return false; - if (!parse_hex_u64(si, &ende)) - return false; - skip_ws(si); - - // perms: 4 chars - if (StrIterRemainingLength(si) < 4) - return false; - u32 perms = 0; +PcParser(Perms, u32) { char p0 = 0, p1 = 0, p2 = 0, p3 = 0; - StrIterMustPeekAt(si, 0, &p0); - StrIterMustPeekAt(si, 1, &p1); - StrIterMustPeekAt(si, 2, &p2); - StrIterMustPeekAt(si, 3, &p3); - if (p0 == 'r') - perms |= PROC_MAP_PERM_READ; - if (p1 == 'w') - perms |= PROC_MAP_PERM_WRITE; - if (p2 == 'x') - perms |= PROC_MAP_PERM_EXEC; - if (p3 == 'p') - perms |= PROC_MAP_PERM_PRIVATE; - StrIterMustMove(si, 4); - skip_ws(si); - - if (!parse_hex_u64(si, &offset)) - return false; - skip_ws(si); + PcSeq() { + PcMatch(PermCh, &p0); + PcMatch(PermCh, &p1); + PcMatch(PermCh, &p2); + PcMatch(PermCh, &p3); + *value = (u32)((p0 == 'r' ? PROC_MAP_PERM_READ : 0u) | (p1 == 'w' ? PROC_MAP_PERM_WRITE : 0u) | + (p2 == 'x' ? PROC_MAP_PERM_EXEC : 0u) | (p3 == 'p' ? PROC_MAP_PERM_PRIVATE : 0u)); + } +} - // dev_major:dev_minor — we don't care, but the field must be there. - skip_token(si); - skip_ws(si); +// A whitespace-delimited token (the dev and inode fields, which we only skip). +PcRecognizer(TokenCh) { + PcSatisfyChar(c, c != ' ' && c != '\t' && c != '\n') {} +} +PcRecognizer(Token) { + PcRecognizeOneOrMore(TokenCh); +} - // inode — skip. - skip_token(si); - skip_ws(si); +// The line terminator, consumed after the path has been captured. +PcRecognizer(NewlineCh) { + PcSatisfyChar(c, c == '\n') {} +} - // path — optional, runs to end-of-line. We replace the newline - // with \0 in place so the path is a usable C string aliasing the - // iter's backing buffer. - size path_start_pos = StrIterIndex(si); - char c; - while (StrIterPeek(si, &c) && c != '\n') { - StrIterMustNext(si); +// One `/proc/self/maps` line's fields -> an entry. Any field failing fails the +// whole rule. +PcParser(ProcMapLine, ProcMapEntry) { + PcSeq() { + PcMatch(HexU64, &value->start); + PcExpect(Dash); + PcMatch(HexU64, &value->end); + PcRecognize(Spaces); + PcMatch(Perms, &value->perms); + PcRecognize(Spaces); + PcMatch(HexU64, &value->file_offset); + PcRecognize(Spaces); + PcExpect(Token); // dev_major:dev_minor + PcRecognize(Spaces); + PcExpect(Token); // inode + PcRecognize(Spaces); + // Path: the rest of the line. PcCaptureUntil hands back a (pointer, + // length) slice into the parse buffer; we copy it into an owned, + // NUL-terminated Str after the newline is consumed, so a line that fails + // to terminate never leaks a half-built path. + Zstr pc_path = NULL; + u64 pc_path_len = 0; + PcCaptureUntil(c, c == '\n', &pc_path, &pc_path_len); + PcRecognize(NewlineCh); + value->path = StrInitFromCstr(pc_path, pc_path_len, ctx->alloc); } - size line_terminator_pos = StrIterIndex(si); - - out->start = start; - out->end = ende; - out->perms = perms; - out->file_offset = offset; - out->path = (Zstr)StrIterDataAt(si, path_start_pos); // may be empty if anonymous +} - if (line_terminator_pos < StrIterLength(si) && *StrIterDataAt(si, line_terminator_pos) == '\n') { - // intentional bypass: in-place mutation of the iter's backing - // buffer to NUL-terminate the path slice we just exposed via - // `out->path`. Iter accessors are read-only; no public mutator - // covers single-byte writes to the underlying storage. - *StrIterDataAt(si, line_terminator_pos) = '\0'; - StrIterMustNext(si); +// The whole file: decode each line into `value->entries` (which the caller +// pre-initialized), folding `min_addr` in the same pass. This is not a compiler +// -- the first line the grammar cannot parse records its cause in the sink and +// faults (FAILED, carrying the consumed bit from wherever it got to), rather than +// skipping on; the caller fixes the input and reparses. +PcParser(ProcMapLines, ProcMaps) { + ProcMapEntry e = {0}; + PcSeq() { + PcMatchZeroOrMore(ProcMapLine, &e) { + if (!VecPushBackR(&value->entries, e)) { + StrDeinit(&e.path); + PcReject(); + } + if (VecLen(&value->entries) == 1 || e.start < value->min_addr) + value->min_addr = e.start; + } + // The zero-or-more stops at the first line it cannot parse; anything left + // means that line is malformed -- name the cause and fault. + PcFailIfNotEof("malformed /proc/self/maps line"); } - return true; } // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- -// Parse `out->raw` (already populated + NUL-terminated) into `entries` and -// `min_addr`. Shared by every loader. Deinits `out` on allocation failure. -static bool proc_maps_parse_raw(ProcMaps *out) { - // `StrResize`/`StrReserve` keep a NUL sentinel at index `length`, so path - // slices that alias the buffer are usable as `Zstr`. - StrIter si = StrIterFromStr(out->raw); - while (StrIterRemainingLength(&si)) { - ProcMapEntry e = {0}; - if (!parse_one_line(&si, &e)) { - // Skip past whatever line we couldn't parse. - char c; - while (StrIterPeek(&si, &c) && c != '\n') { - StrIterMustNext(&si); - } - if (StrIterRemainingLength(&si)) { - StrIterMustNext(&si); - } - continue; - } - if (!VecPushBackR(&out->entries, e)) { - ProcMapsDeinit(out); - return false; +// Parse `raw` (already populated + NUL-terminated) into `entries` and `min_addr` +// through the `ProcMapLines` grammar, each entry owning a copy of its path. A +// line the grammar cannot parse fails the whole load: render its cause under a +// caret to the log and return false (`out` left zeroed). An allocation failure +// fails the same way, without a recorded cause. `raw` is the caller's transient +// buffer -- it is not retained past this call. +static bool proc_maps_parse(ProcMaps *out, Str *raw) { + StrIter si = StrIterFromStr(*raw); + PcParserCtx cx; + cx.reports = VecInitT(cx.reports, VecAllocator(&out->entries)); + cx.alloc = VecAllocator(&out->entries); + PcParserCtx *ctx = &cx; + + bool ok = (PcRun(ProcMapLines, &si, out) & PC_PARSER_STATUS_SUCCESS); + if (!ok && VecLen(&cx.reports) > 0) { + StrInitStack(diag, 256) { + PcReportsRender(&diag, raw, &cx.reports); + LOG_ERROR("ProcMaps: {}", diag); } } + VecDeinit(&cx.reports); - // Cache the lowest mapped address so callers don't rescan the vector. - for (u64 i = 0; i < VecLen(&out->entries); ++i) { - const ProcMapEntry *e = VecPtrAt(&out->entries, i); - if (i == 0 || e->start < out->min_addr) - out->min_addr = e->start; + if (!ok) { + ProcMapsDeinit(out); + return false; } return true; } @@ -227,30 +234,35 @@ bool proc_maps_load(ProcMaps *out, Allocator *alloc) { LOG_FATAL("ProcMapsLoad: NULL argument"); } MemSet(out, 0, sizeof(*out)); - out->raw = StrInit(alloc); out->entries = VecInitT(out->entries, alloc); + Str raw = StrInit(alloc); // `/proc/self/maps` reports stat-size 0 because it's generated by the // kernel on read, so we loop-read into a growing buffer ourselves. File f = FileOpen("/proc/self/maps", "rb"); if (!FileIsOpen(&f)) { LOG_ERROR("ProcMapsLoad: FileOpen(/proc/self/maps) failed"); + StrDeinit(&raw); ProcMapsDeinit(out); return false; } - bool ok = proc_maps_read_all(&f, &out->raw); + bool ok = proc_maps_read_all(&f, &raw); FileClose(&f); if (!ok) { LOG_ERROR("ProcMapsLoad: FileRead failed"); + StrDeinit(&raw); ProcMapsDeinit(out); return false; } - if (StrLen(&out->raw) == 0) { + if (StrLen(&raw) == 0) { LOG_ERROR("ProcMapsLoad: /proc/self/maps was empty"); + StrDeinit(&raw); ProcMapsDeinit(out); return false; } - return proc_maps_parse_raw(out); + bool parsed = proc_maps_parse(out, &raw); + StrDeinit(&raw); + return parsed; } bool proc_maps_load_from_file(ProcMaps *out, File *f, Allocator *alloc) { @@ -258,19 +270,23 @@ bool proc_maps_load_from_file(ProcMaps *out, File *f, Allocator *alloc) { LOG_FATAL("ProcMapsLoadFrom: NULL argument"); } MemSet(out, 0, sizeof(*out)); - out->raw = StrInit(alloc); out->entries = VecInitT(out->entries, alloc); + Str raw = StrInit(alloc); if (!FileIsOpen(f)) { LOG_ERROR("ProcMapsLoadFrom: file is not open"); + StrDeinit(&raw); ProcMapsDeinit(out); return false; } - if (!proc_maps_read_all(f, &out->raw)) { + if (!proc_maps_read_all(f, &raw)) { LOG_ERROR("ProcMapsLoadFrom: FileRead failed"); + StrDeinit(&raw); ProcMapsDeinit(out); return false; } - return proc_maps_parse_raw(out); + bool parsed = proc_maps_parse(out, &raw); + StrDeinit(&raw); + return parsed; } bool proc_maps_load_from_bytes(ProcMaps *out, const u8 *bytes, u64 len, Allocator *alloc) { @@ -278,23 +294,29 @@ bool proc_maps_load_from_bytes(ProcMaps *out, const u8 *bytes, u64 len, Allocato LOG_FATAL("ProcMapsLoadFrom: NULL argument"); } MemSet(out, 0, sizeof(*out)); - out->raw = StrInit(alloc); out->entries = VecInitT(out->entries, alloc); + Str raw = StrInit(alloc); if (len) { - if (!StrReserve(&out->raw, len + 1)) { + if (!StrReserve(&raw, len + 1)) { + StrDeinit(&raw); ProcMapsDeinit(out); return false; } - MemCopy(StrEnd(&out->raw), bytes, len); - StrResize(&out->raw, len); + MemCopy(StrEnd(&raw), bytes, len); + StrResize(&raw, len); } - return proc_maps_parse_raw(out); + bool parsed = proc_maps_parse(out, &raw); + StrDeinit(&raw); + return parsed; } void ProcMapsDeinit(ProcMaps *self) { if (!self) return; - StrDeinit(&self->raw); + for (u64 i = 0; i < VecLen(&self->entries); ++i) { + ProcMapEntry *e = VecPtrAt(&self->entries, i); + StrDeinit(&e->path); + } VecDeinit(&self->entries); MemSet(self, 0, sizeof(*self)); } diff --git a/Source/Misra/Std/File.c b/Source/Misra/Std/File.c index 1c4703f..86bf625 100644 --- a/Source/Misra/Std/File.c +++ b/Source/Misra/Std/File.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -163,6 +164,23 @@ File file_open(Zstr path, Zstr mode) { #endif } +File file_open_n(Zstr path, size len, Zstr mode) { + // `path` is a fixed-length view; the OS open() needs a NUL-terminated + // C-string, so copy into a stack buffer bounded by the platform path cap + // (a path longer than this cannot be opened anyway). + enum { + PATH_CAP = 4096 + }; + char buf[PATH_CAP]; + if (!path || len >= sizeof(buf)) { + File f = {0}; + return f; + } + MemCopy(buf, path, len); + buf[len] = '\0'; + return file_open(buf, mode); +} + File FileFromFd(i32 fd) { File f = {0}; #if PLATFORM_WINDOWS @@ -494,6 +512,23 @@ i64 file_read_and_close_to_str(Zstr path, Str *out) { return file_read_and_close_to_buf(path, (Buf *)out); } +i64 file_read_and_close_to_buf_n(Zstr path, size len, Buf *out) { + if (!path || !out) { + LOG_FATAL("FileReadAndClose: NULL argument (contract violation)"); + } + File f = file_open_n(path, len, "rb"); + if (!FileIsOpen(&f)) { + return -1; + } + i64 got = file_read_to_buf(&f, out); + FileClose(&f); + return got; +} + +i64 file_read_and_close_to_str_n(Zstr path, size len, Str *out) { + return file_read_and_close_to_buf_n(path, len, (Buf *)out); +} + i64 file_write_and_close_from_bytes(Zstr path, const void *buf, u64 n) { if (!path || (!buf && n > 0)) { LOG_FATAL("FileWriteAndClose: NULL argument (contract violation)"); diff --git a/Source/Misra/Sys/SymbolResolver.c b/Source/Misra/Sys/SymbolResolver.c index 53a907b..dab0a8f 100644 --- a/Source/Misra/Sys/SymbolResolver.c +++ b/Source/Misra/Sys/SymbolResolver.c @@ -140,35 +140,34 @@ static bool try_open_sidecar(Zstr main_path, const Elf *main, Elf *out, Allocato // Cache management // --------------------------------------------------------------------------- -static ResolverCacheEntry *resolver_cache_find_or_open(SymbolResolver *self, Zstr path) { +static ResolverCacheEntry *resolver_cache_find_or_open(SymbolResolver *self, Zstr path, u64 path_len) { for (u64 i = 0; i < VecLen(&self->cache); ++i) { ResolverCacheEntry *e = VecPtrAt(&self->cache, i); - if (e->path == path) { - return e; - } - // Some `/proc/self/maps` lines share the same path string in - // different positions of the raw buffer if the kernel - // generated separate copies — fall back to string compare. - if (e->path && path && ZstrCompare(e->path, path) == 0) { + // The cache owns its path copy; match it against the caller's path. + if (StrCmp(&e->path, path, path_len) == 0) { return e; } } ResolverCacheEntry entry; MemSet(&entry, 0, sizeof(entry)); - entry.path = path; - if (!ElfOpen(&entry.elf, path, self->allocator)) { + // Keep the cache's own copy of the path so the entry (and the public + // module_path that borrows it) stay valid independent of the ProcMaps. + entry.path = StrInitFromCstr(path, path_len, self->allocator); + if (!ElfOpen(&entry.elf, &entry.path, self->allocator)) { + StrDeinit(&entry.path); return NULL; } // Best-effort sidecar lookup. Silent failure is fine — we'll just // resolve against whatever the main file has. - if (try_open_sidecar(path, &entry.elf, &entry.sidecar, self->allocator)) { + if (try_open_sidecar(StrBegin(&entry.path), &entry.elf, &entry.sidecar, self->allocator)) { entry.has_sidecar = true; } if (!VecPushBackR(&self->cache, entry)) { if (entry.has_sidecar) ElfDeinit(&entry.sidecar); ElfDeinit(&entry.elf); + StrDeinit(&entry.path); return NULL; } return VecPtrAt(&self->cache, VecLen(&self->cache) - 1); @@ -219,6 +218,7 @@ void SymbolResolverDeinit(SymbolResolver *self) { ElfDeinit(&e->sidecar); } ElfDeinit(&e->elf); + StrDeinit(&e->path); } VecDeinit(&self->cache); ProcMapsDeinit(&self->maps); @@ -265,10 +265,10 @@ bool SymbolResolverFindFde( u64 addr = (u64)runtime_addr; const ProcMapEntry *entry = ProcMapsFindByAddr(&self->maps, addr); - if (!entry || !entry->path || entry->path[0] == '\0') + if (!entry || StrEmpty(&entry->path)) return false; - ResolverCacheEntry *cache_entry = resolver_cache_find_or_open(self, entry->path); + ResolverCacheEntry *cache_entry = resolver_cache_find_or_open(self, StrBegin(&entry->path), StrLen(&entry->path)); if (!cache_entry) return false; // p_vaddr-space bias (not the file-offset shortcut) -- see resolver_load_bias. @@ -348,11 +348,11 @@ bool SymbolResolverResolve(SymbolResolver *self, void *runtime_addr, ResolvedSym u64 addr = (u64)runtime_addr; const ProcMapEntry *entry = ProcMapsFindByAddr(&self->maps, addr); - if (!entry || !entry->path || entry->path[0] == '\0') { + if (!entry || StrEmpty(&entry->path)) { return false; } - ResolverCacheEntry *cache_entry = resolver_cache_find_or_open(self, entry->path); + ResolverCacheEntry *cache_entry = resolver_cache_find_or_open(self, StrBegin(&entry->path), StrLen(&entry->path)); if (!cache_entry) { return false; } @@ -361,7 +361,7 @@ bool SymbolResolverResolve(SymbolResolver *self, void *runtime_addr, ResolvedSym // equals the mapping start, so the absolute base still falls out. u64 load_base = resolver_load_bias(&cache_entry->elf, entry->start, entry->file_offset, addr); - out->module_path = entry->path; + out->module_path = StrBegin(&cache_entry->path); out->module_base = load_base; u64 file_relative = addr - load_base; diff --git a/Tests/Parsers/ProcMaps.c b/Tests/Parsers/ProcMaps.c index 8d3740a..440c9e1 100644 --- a/Tests/Parsers/ProcMaps.c +++ b/Tests/Parsers/ProcMaps.c @@ -15,14 +15,19 @@ static int pm2_marker_fn(int x) { } // Parse crafted `/proc/self/maps`-format TEXT through the public LoadFrom -// seam. The bytes are copied into `m->raw`, so we can feed string literals -// and still assert exact parsed fields -- no live `/proc` involved. Returns -// the loader's own success flag (true even when a line is malformed: a bad -// line is skipped, not a hard failure). +// seam. The bytes are parsed through a transient buffer, so we can feed string +// literals and still assert exact parsed fields -- no live `/proc` involved. Returns +// the loader's own success flag -- false if any line is malformed, since the +// parse fails (naming the cause) on the first line it cannot decode. static bool pm_load_text(ProcMaps *m, Zstr text, DefaultAllocator *alloc) { return ProcMapsLoadFrom(m, text, ZstrLen(text), alloc); } +// `path` is an owned, NUL-terminated Str -- compare it against a literal. +static bool path_eq(const ProcMapEntry *e, Zstr want) { + return ZstrCompare(StrBegin(&e->path), want) == 0; +} + bool test_procmaps_load(void) { DefaultAllocator alloc = DefaultAllocatorInit(); ProcMaps maps; @@ -316,12 +321,14 @@ bool test_pm2_find_no_overrun_past_length(void) { DebugAllocator alloc = DebugAllocatorInit(); ProcMaps pm; MemSet(&pm, 0, sizeof(pm)); - pm.raw = StrInit(ALLOCATOR_OF(&alloc)); pm.entries = VecInitT(pm.entries, ALLOCATOR_OF(&alloc)); - ProcMapEntry e0 = {.start = 0x1000, .end = 0x2000, .perms = 0, .file_offset = 0, .path = ""}; - ProcMapEntry e1 = {.start = 0x3000, .end = 0x4000, .perms = 0, .file_offset = 0, .path = ""}; - ProcMapEntry e2 = {.start = 0x5000, .end = 0x6000, .perms = 0, .file_offset = 0, .path = ""}; + ProcMapEntry e0 = {.start = 0x1000, .end = 0x2000, .perms = 0, .file_offset = 0}; + ProcMapEntry e1 = {.start = 0x3000, .end = 0x4000, .perms = 0, .file_offset = 0}; + ProcMapEntry e2 = {.start = 0x5000, .end = 0x6000, .perms = 0, .file_offset = 0}; + e0.path = StrInit(ALLOCATOR_OF(&alloc)); + e1.path = StrInit(ALLOCATOR_OF(&alloc)); + e2.path = StrInit(ALLOCATOR_OF(&alloc)); bool pushed = VecPushBackR(&pm.entries, e0) && VecPushBackR(&pm.entries, e1) && VecPushBackR(&pm.entries, e2); if (!pushed) { @@ -348,13 +355,13 @@ bool test_pm2_find_no_overrun_past_length(void) { } // -------------------------------------------------------------------------- -// ProcMapsDeinit: actually frees the raw buffer + entries vector. +// ProcMapsDeinit: actually frees every entry's owned path + entries vector. // -------------------------------------------------------------------------- // Under a DebugAllocator, every allocation made by the load must be -// released by Deinit. If `StrDeinit(&self->raw)` is removed, the raw -// buffer (kilobytes) leaks and the live-allocation count stays above -// baseline. Asserting the count returns to baseline kills the +// released by Deinit. If `StrDeinit(&e->path)` is dropped from the per-entry +// loop, those owned path copies leak and the live-allocation count stays +// above baseline. Asserting the count returns to baseline kills the // removed-Deinit mutation observably. bool test_pm2_deinit_releases_all(void) { DebugAllocator dbg = DebugAllocatorInit(); @@ -436,8 +443,7 @@ bool test_pm_parse_full_line_fields(void) { if (ok) { const ProcMapEntry *e = VecPtrAt(&m.entries, 0); ok = e->start == 0x1000ULL && e->end == 0x2000ULL && e->file_offset == 0xdeadULL && - e->perms == (u32)(PROC_MAP_PERM_READ | PROC_MAP_PERM_EXEC | PROC_MAP_PERM_PRIVATE) && - ZstrCompare(e->path, "/x/y") == 0; + e->perms == (u32)(PROC_MAP_PERM_READ | PROC_MAP_PERM_EXEC | PROC_MAP_PERM_PRIVATE) && path_eq(e, "/x/y"); } ProcMapsDeinit(&m); @@ -491,23 +497,18 @@ bool test_pm_parse_addr_with_nine(void) { } // An address field with NO hex digits (here an empty start, the line opening -// straight on the '-') is malformed and the whole line is skipped. Pins the -// "no digits consumed -> reject" guard in the hex reader: a reader that -// reports success on an empty run would accept the line and emit a bogus -// zero-based entry, so asserting ZERO entries is the kill. +// straight on the '-') is malformed, so the load fails. Pins the "no digits +// consumed -> reject" guard in the hex reader: a reader that reports success on +// an empty run would accept the line, so asserting the load FAILS is the kill. bool test_pm_parse_rejects_empty_start_field(void) { DefaultAllocator alloc = DefaultAllocatorInit(); ProcMaps m; - if (!pm_load_text(&m, "-2000 r-xp 0 0:0 0 /x\n", &alloc)) { - DefaultAllocatorDeinit(&alloc); - return false; - } - - bool ok = VecLen(&m.entries) == 0; + bool failed = !pm_load_text(&m, "-2000 r-xp 0 0:0 0 /x\n", &alloc); - ProcMapsDeinit(&m); + if (!failed) // a failed load already freed + zeroed `m`; only clean up on success + ProcMapsDeinit(&m); DefaultAllocatorDeinit(&alloc); - return ok; + return failed; } // min_addr is the lowest start across ALL entries, regardless of file order. @@ -534,28 +535,74 @@ bool test_pm_parse_min_addr_descending(void) { return ok; } -// A line that fails to parse is skipped whole, and the NEXT valid line still -// decodes correctly from its true start. If the skip-to-newline scan stops -// short, the recovery resumes mid-garbage and the following entry comes out -// with the wrong start (e.g. a dropped leading digit) -- so pinning the -// recovered entry's exact start proves the bad line was consumed cleanly. -bool test_pm_parse_skips_malformed_line(void) { +// A line the grammar cannot parse fails the whole load -- this is not a +// compiler, so the first bad line stops the parse rather than being skipped over +// to reach a later good line. Feeding "garbage" ahead of a well-formed line and +// asserting the load FAILS pins that: a skip-and-continue reader would instead +// return the good entry. +bool test_pm_parse_fails_on_malformed_line(void) { DefaultAllocator alloc = DefaultAllocatorInit(); ProcMaps m; - if (!pm_load_text(&m, "garbage line here\n3000-4000 r-xp 0 0:0 0 /good\n", &alloc)) { - DefaultAllocatorDeinit(&alloc); - return false; - } + bool failed = !pm_load_text(&m, "garbage line here\n3000-4000 r-xp 0 0:0 0 /good\n", &alloc); - bool ok = VecLen(&m.entries) == 1; - if (ok) { - const ProcMapEntry *e = VecPtrAt(&m.entries, 0); - ok = e->start == 0x3000ULL && e->end == 0x4000ULL && ZstrCompare(e->path, "/good") == 0; - } + if (!failed) // a failed load already freed + zeroed `m`; only clean up on success + ProcMapsDeinit(&m); + DefaultAllocatorDeinit(&alloc); + return failed; +} - ProcMapsDeinit(&m); +// Field-level fault tolerance: a well-formed line up to a point, then one bad +// field. The grammar has no partial-line recovery -- a bad field fails the line +// and thus the load. Each of these logs a caret diagnostic (see the [ERROR] +// lines when the suite runs); the assertion is just that the load fails. + +// A non-hex character in the END address fails the `end` field. +bool test_pm_parse_fails_on_bad_end(void) { + DefaultAllocator alloc = DefaultAllocatorInit(); + ProcMaps m; + bool failed = !pm_load_text(&m, "1000-XYZ r-xp 0 0:0 0 /x\n", &alloc); + + if (!failed) + ProcMapsDeinit(&m); DefaultAllocatorDeinit(&alloc); - return ok; + return failed; +} + +// A character outside `rwxp-s` in the permission field fails `perms`. +bool test_pm_parse_fails_on_bad_perms(void) { + DefaultAllocator alloc = DefaultAllocatorInit(); + ProcMaps m; + bool failed = !pm_load_text(&m, "1000-2000 rZxp 0 0:0 0 /x\n", &alloc); + + if (!failed) + ProcMapsDeinit(&m); + DefaultAllocatorDeinit(&alloc); + return failed; +} + +// A non-hex character in the file-offset field fails `offset`. +bool test_pm_parse_fails_on_bad_offset(void) { + DefaultAllocator alloc = DefaultAllocatorInit(); + ProcMaps m; + bool failed = !pm_load_text(&m, "1000-2000 r-xp ZZZ 0:0 0 /x\n", &alloc); + + if (!failed) + ProcMapsDeinit(&m); + DefaultAllocatorDeinit(&alloc); + return failed; +} + +// A line that ends before its offset/dev/inode fields fails: the next field has +// nothing to parse. +bool test_pm_parse_fails_on_truncated_line(void) { + DefaultAllocator alloc = DefaultAllocatorInit(); + ProcMaps m; + bool failed = !pm_load_text(&m, "1000-2000 r-xp\n", &alloc); + + if (!failed) + ProcMapsDeinit(&m); + DefaultAllocatorDeinit(&alloc); + return failed; } // The File loader must read to TRUE EOF, not stop at the first chunk. We write @@ -612,8 +659,7 @@ bool test_pm_parse_large_file_reads_all_chunks(void) { // The trailing high mapping lives past the first read chunk; a loader // that stopped early would never have parsed it. const ProcMapEntry *e = ProcMapsFindByAddr(&m, 0xdeadbeef050ULL); - ok = e != NULL && e->start == 0xdeadbeef000ULL && e->end == 0xdeadbeef100ULL && - ZstrCompare(e->path, "/late") == 0; + ok = e != NULL && e->start == 0xdeadbeef000ULL && e->end == 0xdeadbeef100ULL && path_eq(e, "/late"); ProcMapsDeinit(&m); } @@ -682,7 +728,11 @@ int main(void) { test_pm_parse_addr_with_nine, test_pm_parse_rejects_empty_start_field, test_pm_parse_min_addr_descending, - test_pm_parse_skips_malformed_line, + test_pm_parse_fails_on_malformed_line, + test_pm_parse_fails_on_bad_end, + test_pm_parse_fails_on_bad_perms, + test_pm_parse_fails_on_bad_offset, + test_pm_parse_fails_on_truncated_line, test_pm_parse_large_file_reads_all_chunks, test_pm_load_from_file_read_fail_frees_raw, }; diff --git a/Tests/Std/Io/Write.c b/Tests/Std/Io/Write.c index 0dbaf6e..2489ec7 100644 --- a/Tests/Std/Io/Write.c +++ b/Tests/Std/Io/Write.c @@ -4993,7 +4993,7 @@ static bool test_fwrite_roundtrip(void) { if (ok) { ok = ok && FWriteFmtLn(&f, "n={}", LVAL((i32)42)); FileClose(&f); - File r = FileOpen(StrBegin(&path), "r"); + File r = FileOpen(&path, "r"); if (FileIsOpen(&r)) { Str back = StrInit(&alloc); FileRead(&r, &back); @@ -5024,7 +5024,7 @@ static bool test_fwrite_empty_skipped(void) { ok = ok && FWriteFmt(&f, ""); ok = ok && FWriteFmt(&f, "X"); FileClose(&f); - File r = FileOpen(StrBegin(&path), "r"); + File r = FileOpen(&path, "r"); if (FileIsOpen(&r)) { Str back = StrInit(&alloc); FileRead(&r, &back); diff --git a/Tests/Sys/SymbolResolver/Bias.c b/Tests/Sys/SymbolResolver/Bias.c index 4c8255b..36ac05d 100644 --- a/Tests/Sys/SymbolResolver/Bias.c +++ b/Tests/Sys/SymbolResolver/Bias.c @@ -32,10 +32,9 @@ static bool find_rw_mapping(Zstr path, u64 addr, u64 *map_start, u64 *map_file_o ProcMaps maps; bool got = false; if (ProcMapsLoad(&maps, ALLOCATOR_OF(&a))) { - for (u64 i = 0; i < VecLen(&maps.entries); ++i) { - const ProcMapEntry *m = VecPtrAt(&maps.entries, i); - if (m->path && ZstrCompare(m->path, path) == 0 && (m->perms & PROC_MAP_PERM_WRITE) && addr >= m->start && - addr < m->end) { + VecForeachPtr(&maps.entries, m) { + if (!StrEmpty(&m->path) && StrCmp(&m->path, path) == 0 && (m->perms & PROC_MAP_PERM_WRITE) && + addr >= m->start && addr < m->end) { *map_start = m->start; *map_file_offset = m->file_offset; got = true; diff --git a/Tests/Sys/SymbolResolver/SymbolResolver.c b/Tests/Sys/SymbolResolver/SymbolResolver.c index e2d98ce..c93e703 100644 --- a/Tests/Sys/SymbolResolver/SymbolResolver.c +++ b/Tests/Sys/SymbolResolver/SymbolResolver.c @@ -391,10 +391,9 @@ static bool find_other_module_addr(Zstr self_path, u64 *out_addr) { ProcMaps maps; bool got = false; if (ProcMapsLoad(&maps, ALLOCATOR_OF(&a))) { - for (u64 i = 0; i < VecLen(&maps.entries); ++i) { - const ProcMapEntry *m = VecPtrAt(&maps.entries, i); - if (m->path && m->path[0] == '/' && (m->perms & PROC_MAP_PERM_EXEC) && - ZstrCompare(m->path, self_path) != 0) { + VecForeachPtr(&maps.entries, m) { + if (!StrEmpty(&m->path) && StrBegin(&m->path)[0] == '/' && (m->perms & PROC_MAP_PERM_EXEC) && + StrCmp(&m->path, self_path) != 0) { *out_addr = m->start; got = true; break; From dffc807cf899fe0beeaa75ac39a66f5e93d19230 Mon Sep 17 00:00:00 2001 From: Siddharth Mishra Date: Fri, 3 Jul 2026 02:21:54 -0700 Subject: [PATCH 11/11] feat(dispatch): (Zstr,len) fourth branch for Sys/Parser string APIs Extend the string-input dispatch macros to the full four-branch StrCmp shape -- Str* / Zstr / char* (the MSVC literal synonym, kept) plus a (Zstr, len) fixed-length arity -- across: Dir, Dns, KvConfig, MachO, Http, Pe, Pdb, Socket. Each (Zstr, len) form routes to a `_cstr` worker that makes a transient copy ONLY when required: - byte compare/lookup (MachO/Pe/Http section+header keys) threads the bound via ZstrCompareN with an exact-length guard -- no copy; - Str-keyed lookups (KvConfig) use the existing StrInitFromCstr idiom (mirroring the _zstr sibling), not a hand-built Str view; - NUL-terminating syscalls (Dir open/mkdir/unlink, MachO/Pe/Pdb open, Socket connect, Http file) copy into a bounded stack buffer. Also fixes a latent prefix false-match in macho_find_section_cstr and keeps private snake_case workers in each module's Private.h. --- Include/Misra/Parsers/Http.h | 37 ++++++++-- Include/Misra/Parsers/Http/Private.h | 50 +++++++------ Include/Misra/Parsers/KvConfig.h | 24 +++++-- Include/Misra/Parsers/KvConfig/Private.h | 34 +++++---- Include/Misra/Parsers/MachO.h | 26 ++++++- Include/Misra/Parsers/MachO/Private.h | 13 ++-- Include/Misra/Parsers/Pdb.h | 10 +++ Include/Misra/Parsers/Pdb/Private.h | 7 +- Include/Misra/Parsers/Pe.h | 29 ++++++-- Include/Misra/Parsers/Pe/Private.h | 14 ++-- Include/Misra/Sys/Dir.h | 32 +++++++-- Include/Misra/Sys/Dns.h | 25 ++++++- Include/Misra/Sys/Socket.h | 32 +++++++-- Source/Misra/Parsers/Http.c | 53 +++++++++++++- Source/Misra/Parsers/KvConfig.c | 58 +++++++++++++++ Source/Misra/Parsers/MachO.c | 32 +++++++++ Source/Misra/Parsers/Pdb.c | 17 ++++- Source/Misra/Parsers/Pe.c | 25 +++++++ Source/Misra/Sys/Dir.c | 91 ++++++++++++++++++++++++ Source/Misra/Sys/Dns.c | 16 +++++ Source/Misra/Sys/Socket.c | 23 ++++++ 21 files changed, 563 insertions(+), 85 deletions(-) diff --git a/Include/Misra/Parsers/Http.h b/Include/Misra/Parsers/Http.h index 6276f31..c1d0daa 100644 --- a/Include/Misra/Parsers/Http.h +++ b/Include/Misra/Parsers/Http.h @@ -80,10 +80,16 @@ void HttpHeaderDeinit(HttpHeader *header); /// TAGS: Http, Deinit, Header, Init /// /// -/// Find a header by key (case-sensitive zero-terminated comparison). +/// Find a header by key (case-sensitive comparison). +/// +/// Two call shapes via `OVERLOAD` + `_Generic` on `key`: +/// `HttpHeadersFind(headers, key)` -- `key` is `Str *` / `Zstr`. +/// `HttpHeadersFind(headers, key, key_len)` -- `key` is a counted view +/// (`Zstr`, `size`). /// /// headers[in] : Caller's `Vec(HttpHeader)` to search. /// key[in] : Key to look up. +/// key_len[in] : Length of `key` for the 3-arg counted form. /// /// SUCCESS : Returns a pointer to the matching header inside the /// vector. The pointer is valid until `*headers` is mutated @@ -95,11 +101,14 @@ void HttpHeaderDeinit(HttpHeader *header); /// HttpHeader *http_headers_find_zstr(HttpHeaders *headers, Zstr key); HttpHeader *http_headers_find_str(HttpHeaders *headers, const Str *key); -#define HttpHeadersFind(headers, key) \ +HttpHeader *http_headers_find_cstr(HttpHeaders *headers, Zstr key, size key_len); +#define HttpHeadersFind(...) OVERLOAD(HttpHeadersFind, __VA_ARGS__) +#define HttpHeadersFind_2(headers, key) \ _Generic((key), Str *: http_headers_find_str, Zstr: http_headers_find_zstr, char *: http_headers_find_zstr)( \ (headers), \ (key) \ ) +#define HttpHeadersFind_3(headers, key, key_len) http_headers_find_cstr((headers), (Zstr)(key), (key_len)) typedef enum HttpResponseCode { HTTP_RESPONSE_CODE_INVALID = 0, @@ -254,19 +263,26 @@ typedef struct HttpRequest { /// be initialized with `HttpRequestInit(...)` so the parser has an /// allocator to write into. /// +/// Two call shapes via `OVERLOAD` + `_Generic` on `in`: +/// `HttpRequestParse(req, in)` -- `in` is `Str *` / `Zstr`. +/// `HttpRequestParse(req, in, in_len)` -- `in` is a counted view +/// (`Zstr`, `size`). +/// /// SUCCESS : Returns a pointer past the parsed request line + headers -/// (start of the body). +/// (start of the body), pointing into the caller's `in`. /// FAILURE : Returns `in` unchanged when the input is malformed. /// /// TAGS: Http, Parse, Request /// -#define HttpRequestParse(req, in) \ +#define HttpRequestParse(...) OVERLOAD(HttpRequestParse, __VA_ARGS__) +#define HttpRequestParse_2(req, in) \ _Generic( \ (in), \ Str *: http_request_parse_str((req), (const Str *)(in)), \ Zstr: http_request_parse_zstr((req), (Zstr)(in)), \ char *: http_request_parse_zstr((req), (Zstr)(in)) \ ) +#define HttpRequestParse_3(req, in, in_len) http_request_parse_cstr((req), (Zstr)(in), (in_len)) /// /// Release storage owned by `req` and zero the struct. Safe to call on @@ -312,7 +328,7 @@ typedef struct HttpResponse { ((HttpResponse) {.allocator = ALLOCATOR_OF(alloc_ptr), \ .content_type = HTTP_CONTENT_TYPE_INVALID, \ .status_code = HTTP_RESPONSE_CODE_INVALID, \ - .headers = VecInitWithDeepCopy_3(NULL, http_header_deinit, alloc_ptr), \ + .headers = VecInitWithDeepCopy_3(NULL, http_header_deinit, alloc_ptr), \ .body = StrInit_1(alloc_ptr)}) /// @@ -347,18 +363,27 @@ HttpResponse *HttpRespondWithHtml(HttpResponse *response, HttpResponseCode statu /// through `response->allocator`. Only available when the `file` /// feature is enabled. /// +/// Two call shapes via `OVERLOAD` + `_Generic` on `filepath`: +/// `HttpRespondWithFile(response, status, content_type, filepath)` +/// -- `filepath` is `Str *` / `Zstr`. +/// `HttpRespondWithFile(response, status, content_type, filepath, filepath_len)` +/// -- `filepath` is a counted view (`Zstr`, `size`). +/// /// SUCCESS : Returns `response` with body filled. /// FAILURE : Returns NULL on I/O or allocation failure. /// /// TAGS: Http, Respond, File /// -# define HttpRespondWithFile(response, status, content_type, filepath) \ +# define HttpRespondWithFile(...) OVERLOAD(HttpRespondWithFile, __VA_ARGS__) +# define HttpRespondWithFile_4(response, status, content_type, filepath) \ _Generic( \ (filepath), \ Str *: http_respond_with_file_str((response), (status), (content_type), (const Str *)(filepath)), \ Zstr: http_respond_with_file_zstr((response), (status), (content_type), (Zstr)(filepath)), \ char *: http_respond_with_file_zstr((response), (status), (content_type), (Zstr)(filepath)) \ ) +# define HttpRespondWithFile_5(response, status, content_type, filepath, filepath_len) \ + http_respond_with_file_cstr((response), (status), (content_type), (Zstr)(filepath), (filepath_len)) #endif /// diff --git a/Include/Misra/Parsers/Http/Private.h b/Include/Misra/Parsers/Http/Private.h index 9dd5c5d..e7b222d 100644 --- a/Include/Misra/Parsers/Http/Private.h +++ b/Include/Misra/Parsers/Http/Private.h @@ -21,30 +21,38 @@ extern "C" { #endif -typedef struct HttpRequest HttpRequest; -typedef struct HttpResponse HttpResponse; -typedef enum HttpResponseCode HttpResponseCode; -typedef enum HttpContentType HttpContentType; + typedef struct HttpRequest HttpRequest; + typedef struct HttpResponse HttpResponse; + typedef enum HttpResponseCode HttpResponseCode; + typedef enum HttpContentType HttpContentType; -void http_header_deinit(void *header, const Allocator *alloc); -bool http_header_init_copy(void *dst, const void *src, const Allocator *alloc); -Zstr http_request_parse_zstr(HttpRequest *req, Zstr in); -Zstr http_request_parse_str(HttpRequest *req, const Str *in); + void http_header_deinit(void *header, const Allocator *alloc); + bool http_header_init_copy(void *dst, const void *src, const Allocator *alloc); + Zstr http_request_parse_zstr(HttpRequest *req, Zstr in); + Zstr http_request_parse_str(HttpRequest *req, const Str *in); + Zstr http_request_parse_cstr(HttpRequest *req, Zstr in, size in_len); #if FEATURE_FILE -HttpResponse *http_respond_with_file_zstr( - HttpResponse *response, - HttpResponseCode status, - HttpContentType content_type, - Zstr filepath -); -HttpResponse *http_respond_with_file_str( - HttpResponse *response, - HttpResponseCode status, - HttpContentType content_type, - const Str *filepath -); + HttpResponse *http_respond_with_file_zstr( + HttpResponse *response, + HttpResponseCode status, + HttpContentType content_type, + Zstr filepath + ); + HttpResponse *http_respond_with_file_str( + HttpResponse *response, + HttpResponseCode status, + HttpContentType content_type, + const Str *filepath + ); + HttpResponse *http_respond_with_file_cstr( + HttpResponse *response, + HttpResponseCode status, + HttpContentType content_type, + Zstr filepath, + size filepath_len + ); #endif -Str http_response_serialize(const HttpResponse *response, Allocator *alloc); + Str http_response_serialize(const HttpResponse *response, Allocator *alloc); #ifdef __cplusplus } diff --git a/Include/Misra/Parsers/KvConfig.h b/Include/Misra/Parsers/KvConfig.h index 2c0586d..0cd5a97 100644 --- a/Include/Misra/Parsers/KvConfig.h +++ b/Include/Misra/Parsers/KvConfig.h @@ -216,8 +216,10 @@ StrIter KvConfigParse(StrIter si, KvConfig *cfg); /// /// TAGS: KvConfig, Get, API /// -#define KvConfigGet(cfg, key) \ +#define KvConfigGet(...) OVERLOAD(KvConfigGet, __VA_ARGS__) +#define KvConfigGet_2(cfg, key) \ _Generic((key), Str *: kvconfig_get_str, Zstr: kvconfig_get_zstr, char *: kvconfig_get_zstr)((cfg), (key)) +#define KvConfigGet_3(cfg, key, key_len) kvconfig_get_cstr((cfg), (Zstr)(key), (key_len)) /// /// Get stored value for `key` by internal reference. @@ -230,11 +232,13 @@ StrIter KvConfigParse(StrIter si, KvConfig *cfg); /// /// TAGS: KvConfig, Get, Pointer /// -#define KvConfigGetPtr(cfg, key) \ +#define KvConfigGetPtr(...) OVERLOAD(KvConfigGetPtr, __VA_ARGS__) +#define KvConfigGetPtr_2(cfg, key) \ _Generic((key), Str *: kvconfig_get_ptr_str, Zstr: kvconfig_get_ptr_zstr, char *: kvconfig_get_ptr_zstr)( \ (cfg), \ (key) \ ) +#define KvConfigGetPtr_3(cfg, key, key_len) kvconfig_get_ptr_cstr((cfg), (Zstr)(key), (key_len)) /// /// Check whether a key exists in config. @@ -247,11 +251,13 @@ StrIter KvConfigParse(StrIter si, KvConfig *cfg); /// /// TAGS: KvConfig, Contains, API /// -#define KvConfigContains(cfg, key) \ +#define KvConfigContains(...) OVERLOAD(KvConfigContains, __VA_ARGS__) +#define KvConfigContains_2(cfg, key) \ _Generic((key), Str *: kvconfig_contains_str, Zstr: kvconfig_contains_zstr, char *: kvconfig_contains_zstr)( \ (cfg), \ (key) \ ) +#define KvConfigContains_3(cfg, key, key_len) kvconfig_contains_cstr((cfg), (Zstr)(key), (key_len)) /// /// Parse and fetch a boolean config value. @@ -267,12 +273,14 @@ StrIter KvConfigParse(StrIter si, KvConfig *cfg); /// /// TAGS: KvConfig, Get, Bool /// -#define KvConfigGetBool(cfg, key, value) \ +#define KvConfigGetBool(...) OVERLOAD(KvConfigGetBool, __VA_ARGS__) +#define KvConfigGetBool_3(cfg, key, value) \ _Generic((key), Str *: kvconfig_get_bool_str, Zstr: kvconfig_get_bool_zstr, char *: kvconfig_get_bool_zstr)( \ (cfg), \ (key), \ (value) \ ) +#define KvConfigGetBool_4(cfg, key, key_len, value) kvconfig_get_bool_cstr((cfg), (Zstr)(key), (key_len), (value)) /// /// Parse and fetch a signed 64-bit integer config value. @@ -286,12 +294,14 @@ StrIter KvConfigParse(StrIter si, KvConfig *cfg); /// /// TAGS: KvConfig, Get, I64 /// -#define KvConfigGetI64(cfg, key, value) \ +#define KvConfigGetI64(...) OVERLOAD(KvConfigGetI64, __VA_ARGS__) +#define KvConfigGetI64_3(cfg, key, value) \ _Generic((key), Str *: kvconfig_get_i64_str, Zstr: kvconfig_get_i64_zstr, char *: kvconfig_get_i64_zstr)( \ (cfg), \ (key), \ (value) \ ) +#define KvConfigGetI64_4(cfg, key, key_len, value) kvconfig_get_i64_cstr((cfg), (Zstr)(key), (key_len), (value)) /// /// Parse and fetch a double-precision floating config value. @@ -305,12 +315,14 @@ StrIter KvConfigParse(StrIter si, KvConfig *cfg); /// /// TAGS: KvConfig, Get, F64 /// -#define KvConfigGetF64(cfg, key, value) \ +#define KvConfigGetF64(...) OVERLOAD(KvConfigGetF64, __VA_ARGS__) +#define KvConfigGetF64_3(cfg, key, value) \ _Generic((key), Str *: kvconfig_get_f64_str, Zstr: kvconfig_get_f64_zstr, char *: kvconfig_get_f64_zstr)( \ (cfg), \ (key), \ (value) \ ) +#define KvConfigGetF64_4(cfg, key, key_len, value) kvconfig_get_f64_cstr((cfg), (Zstr)(key), (key_len), (value)) // Snake_case backends live in , pulled // in from the top of this file so the _Generic dispatch arms above can diff --git a/Include/Misra/Parsers/KvConfig/Private.h b/Include/Misra/Parsers/KvConfig/Private.h index d0544cd..1d6e628 100644 --- a/Include/Misra/Parsers/KvConfig/Private.h +++ b/Include/Misra/Parsers/KvConfig/Private.h @@ -17,21 +17,27 @@ extern "C" { #endif -// `KvConfig` is `typedef Map(Str, Str) KvConfig` -- defined in the -// public header before that header pulls this one in. + // `KvConfig` is `typedef Map(Str, Str) KvConfig` -- defined in the + // public header before that header pulls this one in. -Str kvconfig_get_zstr(KvConfig *cfg, Zstr key); -Str kvconfig_get_str(KvConfig *cfg, const Str *key); -Str *kvconfig_get_ptr_zstr(KvConfig *cfg, Zstr key); -Str *kvconfig_get_ptr_str(KvConfig *cfg, const Str *key); -bool kvconfig_contains_zstr(KvConfig *cfg, Zstr key); -bool kvconfig_contains_str(KvConfig *cfg, const Str *key); -bool kvconfig_get_bool_zstr(KvConfig *cfg, Zstr key, bool *value); -bool kvconfig_get_bool_str(KvConfig *cfg, const Str *key, bool *value); -bool kvconfig_get_i64_zstr(KvConfig *cfg, Zstr key, i64 *value); -bool kvconfig_get_i64_str(KvConfig *cfg, const Str *key, i64 *value); -bool kvconfig_get_f64_zstr(KvConfig *cfg, Zstr key, f64 *value); -bool kvconfig_get_f64_str(KvConfig *cfg, const Str *key, f64 *value); + Str kvconfig_get_zstr(KvConfig *cfg, Zstr key); + Str kvconfig_get_str(KvConfig *cfg, const Str *key); + Str kvconfig_get_cstr(KvConfig *cfg, Zstr key, size len); + Str *kvconfig_get_ptr_zstr(KvConfig *cfg, Zstr key); + Str *kvconfig_get_ptr_str(KvConfig *cfg, const Str *key); + Str *kvconfig_get_ptr_cstr(KvConfig *cfg, Zstr key, size len); + bool kvconfig_contains_zstr(KvConfig *cfg, Zstr key); + bool kvconfig_contains_str(KvConfig *cfg, const Str *key); + bool kvconfig_contains_cstr(KvConfig *cfg, Zstr key, size len); + bool kvconfig_get_bool_zstr(KvConfig *cfg, Zstr key, bool *value); + bool kvconfig_get_bool_str(KvConfig *cfg, const Str *key, bool *value); + bool kvconfig_get_bool_cstr(KvConfig *cfg, Zstr key, size len, bool *value); + bool kvconfig_get_i64_zstr(KvConfig *cfg, Zstr key, i64 *value); + bool kvconfig_get_i64_str(KvConfig *cfg, const Str *key, i64 *value); + bool kvconfig_get_i64_cstr(KvConfig *cfg, Zstr key, size len, i64 *value); + bool kvconfig_get_f64_zstr(KvConfig *cfg, Zstr key, f64 *value); + bool kvconfig_get_f64_str(KvConfig *cfg, const Str *key, f64 *value); + bool kvconfig_get_f64_cstr(KvConfig *cfg, Zstr key, size len, f64 *value); #ifdef __cplusplus } diff --git a/Include/Misra/Parsers/MachO.h b/Include/Misra/Parsers/MachO.h index 332b42f..2fc8398 100644 --- a/Include/Misra/Parsers/MachO.h +++ b/Include/Misra/Parsers/MachO.h @@ -146,6 +146,19 @@ typedef struct Macho { /// /// Open and parse a Mach-O file from disk. /// +/// Four call shapes on `path`: +/// `MachoOpen(out, path)` -- `path` is `Str *` / `Zstr` / +/// `char *`; default allocator. +/// `MachoOpen(out, path, alloc)` -- same, explicit allocator. +/// `MachoOpen(out, path, len, alloc)` -- `path` is a fixed-length view +/// (`Zstr`, `size`); explicit +/// allocator. Arity 3 is already +/// the NUL-terminated + allocator +/// form, so the fixed-length form +/// takes its allocator explicitly. +/// The fixed-length path is copied into a NUL-terminated stack buffer +/// (bounded by the platform path cap) before it reaches `open`. +/// /// SUCCESS : Returns true; parser owns the read-in buffer. /// FAILURE : Returns false on read / magic / load-command parse error. /// Fat/universal headers (`CAFEBABE`) are rejected as @@ -168,6 +181,7 @@ typedef struct Macho { Zstr: macho_open((out), (Zstr)(path), ALLOCATOR_OF(alloc)), \ char *: macho_open((out), (Zstr)(path), ALLOCATOR_OF(alloc)) \ ) +#define MachoOpen_4(out, path, len, alloc) macho_open_n((out), (Zstr)(path), (len), ALLOCATOR_OF(alloc)) /// /// Parse a Mach-O image from an in-memory byte range -- **L-value / @@ -226,18 +240,28 @@ void MachoDeinit(Macho *self); /// /// Find a section by (segment, section) name pair. /// +/// Two arities on the name keys: +/// `MachoFindSection(self, segment, section)` +/// -- each key is `Str *` / `Zstr` / `char *`. +/// `MachoFindSection(self, segment, segment_len, section, section_len)` +/// -- each key is a fixed-length view (`Zstr`, `size`). The bounds +/// are threaded into the compare (exactly N bytes, no copy). +/// /// SUCCESS : Returns a pointer to the matching `MachoSection`, /// borrowed from `self` (valid until `MachoDeinit`). /// FAILURE : Returns NULL when no section matches. /// /// TAGS: Parser, MachO, Section, Query /// -#define MachoFindSection(self, segment, section) \ +#define MachoFindSection(...) OVERLOAD(MachoFindSection, __VA_ARGS__) +#define MachoFindSection_3(self, segment, section) \ macho_find_section( \ (self), \ _Generic((segment), Str *: (Zstr)StrBegin((Str *)(segment)), Zstr: (Zstr)(segment), char *: (Zstr)(segment)), \ _Generic((section), Str *: (Zstr)StrBegin((Str *)(section)), Zstr: (Zstr)(section), char *: (Zstr)(section)) \ ) +#define MachoFindSection_5(self, segment, segment_len, section, section_len) \ + macho_find_section_cstr((self), (Zstr)(segment), (segment_len), (Zstr)(section), (section_len)) /// /// Look up the symbol whose `value` is closest-not-greater than diff --git a/Include/Misra/Parsers/MachO/Private.h b/Include/Misra/Parsers/MachO/Private.h index 98d9f8e..9d6a9bc 100644 --- a/Include/Misra/Parsers/MachO/Private.h +++ b/Include/Misra/Parsers/MachO/Private.h @@ -17,12 +17,15 @@ extern "C" { #endif -typedef struct Macho Macho; -typedef struct MachoSection MachoSection; + typedef struct Macho Macho; + typedef struct MachoSection MachoSection; -bool macho_open(Macho *out, Zstr path, Allocator *alloc); -bool macho_open_from_memory_copy(Macho *out, const u8 *data, size data_size, Allocator *alloc); -const MachoSection *macho_find_section(const Macho *self, Zstr segment, Zstr section); + bool macho_open(Macho *out, Zstr path, Allocator *alloc); + bool macho_open_n(Macho *out, Zstr path, size len, Allocator *alloc); + bool macho_open_from_memory_copy(Macho *out, const u8 *data, size data_size, Allocator *alloc); + const MachoSection *macho_find_section(const Macho *self, Zstr segment, Zstr section); + const MachoSection * + macho_find_section_cstr(const Macho *self, Zstr segment, size segment_len, Zstr section, size section_len); #ifdef __cplusplus } diff --git a/Include/Misra/Parsers/Pdb.h b/Include/Misra/Parsers/Pdb.h index 5bb655a..833914c 100644 --- a/Include/Misra/Parsers/Pdb.h +++ b/Include/Misra/Parsers/Pdb.h @@ -122,6 +122,15 @@ typedef struct Pdb { /// /// Open and parse a PDB from disk. /// +/// Call shapes via `OVERLOAD` + `_Generic` on `path`: +/// `PdbOpen(out, path)` -- `path` is `Str *` / `Zstr`; +/// reads through `MisraScope`. +/// `PdbOpen(out, path, alloc)` -- `path` is `Str *` / `Zstr`; +/// reads through `alloc`. +/// `PdbOpen(out, path, len, alloc)` -- `path` is a fixed-length view +/// (`Zstr`, `size`); copied into a +/// stack buffer for the syscall. +/// /// SUCCESS : Returns true; parser owns the read-in buffer. /// FAILURE : Returns false; logs the failing step. `out` is left zeroed. /// @@ -142,6 +151,7 @@ typedef struct Pdb { Zstr: pdb_open((out), (Zstr)(path), ALLOCATOR_OF(alloc)), \ char *: pdb_open((out), (Zstr)(path), ALLOCATOR_OF(alloc)) \ ) +#define PdbOpen_4(out, path, len, alloc) pdb_open_n((out), (Zstr)(path), (len), ALLOCATOR_OF(alloc)) /// /// Open and parse a PDB from an in-memory byte range -- **L-value / diff --git a/Include/Misra/Parsers/Pdb/Private.h b/Include/Misra/Parsers/Pdb/Private.h index f0b3812..4751154 100644 --- a/Include/Misra/Parsers/Pdb/Private.h +++ b/Include/Misra/Parsers/Pdb/Private.h @@ -17,10 +17,11 @@ extern "C" { #endif -typedef struct Pdb Pdb; + typedef struct Pdb Pdb; -bool pdb_open(Pdb *out, Zstr path, Allocator *alloc); -bool pdb_open_from_memory_copy(Pdb *out, const u8 *data, size data_size, Allocator *alloc); + bool pdb_open(Pdb *out, Zstr path, Allocator *alloc); + bool pdb_open_n(Pdb *out, Zstr path, size len, Allocator *alloc); + bool pdb_open_from_memory_copy(Pdb *out, const u8 *data, size data_size, Allocator *alloc); #ifdef __cplusplus } diff --git a/Include/Misra/Parsers/Pe.h b/Include/Misra/Parsers/Pe.h index 2351abc..e674312 100644 --- a/Include/Misra/Parsers/Pe.h +++ b/Include/Misra/Parsers/Pe.h @@ -122,10 +122,19 @@ typedef struct Pe { /// /// Open and parse a PE file from disk. /// -/// out[out] : Populated on success. -/// path[in] : Filesystem path. `Str *` preferred; `Zstr ` accepted. -/// alloc[in] : Allocator for the read-in buffer and the sections -/// vector. Must outlive the `Pe`. +/// Call shapes via `OVERLOAD` + `_Generic` on `path`: +/// `PeOpen(out, path)` -- `path` is `Str *` or `Zstr`. +/// `PeOpen(out, path, alloc)` -- same, explicit allocator. +/// `PeOpen(out, path, path_len, alloc)`-- `path` is a fixed-length view +/// (`Zstr`, `size`); it is copied +/// into a stack buffer and +/// NUL-terminated for the open. +/// +/// out[out] : Populated on success. +/// path[in] : Filesystem path. `Str *` preferred; `Zstr ` accepted. +/// path_len[in] : Length of `path` for the fixed-length form. +/// alloc[in] : Allocator for the read-in buffer and the sections +/// vector. Must outlive the `Pe`. /// /// SUCCESS : Returns true; `out` owns the read-in buffer. /// FAILURE : Returns false; logs the failing step. `out` is left zeroed. @@ -147,6 +156,7 @@ typedef struct Pe { Zstr: pe_open((out), (Zstr)(path), ALLOCATOR_OF(alloc)), \ char *: pe_open((out), (Zstr)(path), ALLOCATOR_OF(alloc)) \ ) +#define PeOpen_4(out, path, len, alloc) pe_open_n((out), (Zstr)(path), (len), ALLOCATOR_OF(alloc)) /// /// Parse a PE image from an in-memory byte range -- **L-value / @@ -204,17 +214,26 @@ void PeDeinit(Pe *self); /// Find a section by name (first match; PE allows duplicates but /// they're vanishingly rare). /// +/// Call shapes via `OVERLOAD` + `_Generic` on `name`: +/// `PeFindSection(self, name)` -- `name` is `Str *` or `Zstr`. +/// `PeFindSection(self, name, name_len)` -- `name` is a fixed-length +/// view (`Zstr`, `size`); +/// matched over exactly +/// `name_len` bytes, no copy. +/// /// SUCCESS : Returns a pointer to the matching `PeSection`, borrowed /// from `self` (valid until `PeDeinit`). /// FAILURE : Returns NULL when no section matches. /// /// TAGS: Parser, PE, Section, Query /// -#define PeFindSection(self, name) \ +#define PeFindSection(...) OVERLOAD(PeFindSection, __VA_ARGS__) +#define PeFindSection_2(self, name) \ _Generic((name), Str *: pe_find_section_str, Zstr: pe_find_section_zstr, char *: pe_find_section_zstr)( \ (self), \ (name) \ ) +#define PeFindSection_3(self, name, name_len) pe_find_section_cstr((self), (Zstr)(name), (name_len)) /// /// Convert an RVA (offset from `ImageBase`) to a file offset by diff --git a/Include/Misra/Parsers/Pe/Private.h b/Include/Misra/Parsers/Pe/Private.h index fa04690..90dd07f 100644 --- a/Include/Misra/Parsers/Pe/Private.h +++ b/Include/Misra/Parsers/Pe/Private.h @@ -18,13 +18,15 @@ extern "C" { #endif -typedef struct Pe Pe; -typedef struct PeSection PeSection; + typedef struct Pe Pe; + typedef struct PeSection PeSection; -bool pe_open(Pe *out, Zstr path, Allocator *alloc); -bool pe_open_from_memory_copy(Pe *out, const u8 *data, size data_size, Allocator *alloc); -const PeSection *pe_find_section_zstr(const Pe *self, Zstr name); -const PeSection *pe_find_section_str(const Pe *self, const Str *name); + bool pe_open(Pe *out, Zstr path, Allocator *alloc); + bool pe_open_n(Pe *out, Zstr path, size len, Allocator *alloc); + bool pe_open_from_memory_copy(Pe *out, const u8 *data, size data_size, Allocator *alloc); + const PeSection *pe_find_section_zstr(const Pe *self, Zstr name); + const PeSection *pe_find_section_str(const Pe *self, const Str *name); + const PeSection *pe_find_section_cstr(const Pe *self, Zstr name, size name_len); #ifdef __cplusplus } diff --git a/Include/Misra/Sys/Dir.h b/Include/Misra/Sys/Dir.h index 5dca385..a73cd10 100644 --- a/Include/Misra/Sys/Dir.h +++ b/Include/Misra/Sys/Dir.h @@ -112,6 +112,7 @@ typedef Vec(DirEntry) DirContents; /// TAGS: System, FileSystem, Directory /// DirContents dir_get_contents(Zstr path, Allocator *alloc); +DirContents dir_get_contents_cstr(Zstr path, size len, Allocator *alloc); #define DirGetContents(...) OVERLOAD(DirGetContents, __VA_ARGS__) #define DirGetContents_1(path) \ _Generic( \ @@ -127,6 +128,7 @@ DirContents dir_get_contents(Zstr path, Allocator *alloc); Zstr: dir_get_contents((Zstr)(path), ALLOCATOR_OF(alloc)), \ char *: dir_get_contents((Zstr)(path), ALLOCATOR_OF(alloc)) \ ) +#define DirGetContents_3(path, len, alloc) dir_get_contents_cstr((Zstr)(path), (len), ALLOCATOR_OF(alloc)) /// /// Get size of file without opening it. @@ -139,13 +141,16 @@ DirContents dir_get_contents(Zstr path, Allocator *alloc); /// TAGS: System, File, Metadata /// i64 file_get_size(Zstr filename); -#define FileGetSize(path) \ +i64 file_get_size_cstr(Zstr filename, size len); +#define FileGetSize(...) OVERLOAD(FileGetSize, __VA_ARGS__) +#define FileGetSize_1(path) \ _Generic( \ (path), \ Str *: file_get_size((Zstr)StrBegin((Str *)(path))), \ Zstr: file_get_size((Zstr)(path)), \ char *: file_get_size((Zstr)(path)) \ ) +#define FileGetSize_2(path, len) file_get_size_cstr((Zstr)(path), (len)) /// /// Remove a regular file. Direct syscall (`unlink` on Linux x86_64 / @@ -167,13 +172,16 @@ i64 file_get_size(Zstr filename); /// TAGS: System, File, FileSystem /// i8 file_remove(Zstr path); -#define FileRemove(path) \ +i8 file_remove_cstr(Zstr path, size len); +#define FileRemove(...) OVERLOAD(FileRemove, __VA_ARGS__) +#define FileRemove_1(path) \ _Generic( \ (path), \ Str *: file_remove((Zstr)StrBegin((Str *)(path))), \ Zstr: file_remove((Zstr)(path)), \ char *: file_remove((Zstr)(path)) \ ) +#define FileRemove_2(path, len) file_remove_cstr((Zstr)(path), (len)) /// /// Remove an empty directory. Direct syscall (`rmdir` on Linux @@ -190,13 +198,16 @@ i8 file_remove(Zstr path); /// TAGS: System, Directory, FileSystem /// i8 dir_remove(Zstr path); -#define DirRemove(path) \ +i8 dir_remove_cstr(Zstr path, size len); +#define DirRemove(...) OVERLOAD(DirRemove, __VA_ARGS__) +#define DirRemove_1(path) \ _Generic( \ (path), \ Str *: dir_remove((Zstr)StrBegin((Str *)(path))), \ Zstr: dir_remove((Zstr)(path)), \ char *: dir_remove((Zstr)(path)) \ ) +#define DirRemove_2(path, len) dir_remove_cstr((Zstr)(path), (len)) /// /// Create a single directory. Direct syscall (`mkdir` on Linux @@ -213,13 +224,16 @@ i8 dir_remove(Zstr path); /// TAGS: System, Directory, FileSystem /// i8 dir_create(Zstr path); -#define DirCreate(path) \ +i8 dir_create_cstr(Zstr path, size len); +#define DirCreate(...) OVERLOAD(DirCreate, __VA_ARGS__) +#define DirCreate_1(path) \ _Generic( \ (path), \ Str *: dir_create((Zstr)StrBegin((Str *)(path))), \ Zstr: dir_create((Zstr)(path)), \ char *: dir_create((Zstr)(path)) \ ) +#define DirCreate_2(path, len) dir_create_cstr((Zstr)(path), (len)) /// /// Recursive `mkdir -p`. Creates all missing path components. @@ -235,13 +249,16 @@ i8 dir_create(Zstr path); /// TAGS: System, Directory, FileSystem /// i8 dir_create_all(Zstr path); -#define DirCreateAll(path) \ +i8 dir_create_all_cstr(Zstr path, size len); +#define DirCreateAll(...) OVERLOAD(DirCreateAll, __VA_ARGS__) +#define DirCreateAll_1(path) \ _Generic( \ (path), \ Str *: dir_create_all((Zstr)StrBegin((Str *)(path))), \ Zstr: dir_create_all((Zstr)(path)), \ char *: dir_create_all((Zstr)(path)) \ ) +#define DirCreateAll_2(path, len) dir_create_all_cstr((Zstr)(path), (len)) /// /// Recursive `rm -rf`. Removes a directory tree (regular files, @@ -256,12 +273,15 @@ i8 dir_create_all(Zstr path); /// TAGS: System, Directory, FileSystem /// i8 dir_remove_all(Zstr path); -#define DirRemoveAll(path) \ +i8 dir_remove_all_cstr(Zstr path, size len); +#define DirRemoveAll(...) OVERLOAD(DirRemoveAll, __VA_ARGS__) +#define DirRemoveAll_1(path) \ _Generic( \ (path), \ Str *: dir_remove_all((Zstr)StrBegin((Str *)(path))), \ Zstr: dir_remove_all((Zstr)(path)), \ char *: dir_remove_all((Zstr)(path)) \ ) +#define DirRemoveAll_2(path, len) dir_remove_all_cstr((Zstr)(path), (len)) #endif // MISRA_SYS_DIR_H diff --git a/Include/Misra/Sys/Dns.h b/Include/Misra/Sys/Dns.h index ebcfc5a..f0ddccc 100644 --- a/Include/Misra/Sys/Dns.h +++ b/Include/Misra/Sys/Dns.h @@ -178,7 +178,9 @@ extern "C" { /// strategy -- we always look up both A and AAAA). /// /// hostname[in] : Hostname to resolve. Prefer `Str *`; `Zstr` - /// accepted. Trailing dot tolerated. + /// accepted; a `(Zstr, len)` counted view is + /// accepted via the 6-arg form. Trailing dot + /// tolerated. /// port[in] : Port number to stamp on every returned address. /// kind[in] : `SOCKET_KIND_TCP` or `SOCKET_KIND_UDP`. /// out[in,out] : Vec to append results to. Stays untouched on failure. @@ -192,6 +194,14 @@ extern "C" { /// bool dns_resolve_5_zstr(DnsResolver *self, Zstr hostname, u16 port, SocketKind kind, DnsAddrs *out); bool dns_resolve_5_str(DnsResolver *self, const Str *hostname, u16 port, SocketKind kind, DnsAddrs *out); + bool dns_resolve_6_cstr( + DnsResolver *self, + Zstr hostname, + u64 hostname_len, + u16 port, + SocketKind kind, + DnsAddrs *out + ); #define DnsResolve_5(self, hostname, port, kind, out) \ _Generic( \ (hostname), \ @@ -199,6 +209,8 @@ extern "C" { Zstr: dns_resolve_5_zstr((self), (Zstr)(hostname), (port), (kind), (out)), \ char *: dns_resolve_5_zstr((self), (Zstr)(hostname), (port), (kind), (out)) \ ) +#define DnsResolve_6(self, hostname, hostname_len, port, kind, out) \ + dns_resolve_6_cstr((self), (Zstr)(hostname), (hostname_len), (port), (kind), (out)) /// /// Spec-based overload (vec form). Accepts a single `"host:port"` @@ -243,7 +255,16 @@ extern "C" { /// `DnsResolve` dispatches by argument count. The 4-arg form /// additionally dispatches on the `out` parameter type: /// `DnsAddrs *` selects the vec form, `SocketAddr *` selects the - /// single-addr form. + /// single-addr form. The 6-arg form is the counted-hostname + /// overload: `(self, hostname, hostname_len, port, kind, out)` + /// takes a `(Zstr, len)` view (the fourth string-input branch, + /// alongside `Str *` / `Zstr` / `char *` in the 5-arg form). + /// + /// The 4-arg spec form has no counted `(Zstr, len)` sibling: its + /// base+1 arity is 5, which is already the hostname form, so a + /// counted spec cannot be routed through this by-count OVERLOAD + /// without ambiguity. Callers with a non-terminated `host:port` + /// slice should terminate it (or wrap in a `Str`) themselves. /// /// TAGS: Dns, Resolve, API /// diff --git a/Include/Misra/Sys/Socket.h b/Include/Misra/Sys/Socket.h index cb995ca..cc1739e 100644 --- a/Include/Misra/Sys/Socket.h +++ b/Include/Misra/Sys/Socket.h @@ -146,7 +146,7 @@ bool socket_addr_parse_zstr(SocketAddr *out, Zstr spec, SocketKind kind); /// `Str` overload of `SocketAddrParse`. Identical contract to the Zstr /// arm above; provided so callers holding a `Str` don't have to reach /// into `.data`. The `_Generic` macro below routes through this for -/// `Str *` / `const Str *` inputs. +/// `Str *` inputs. /// /// SUCCESS : Returns true; `*out` populated. /// FAILURE : Returns false; `*out` zeroed. Silent (no log) — caller is @@ -155,13 +155,31 @@ bool socket_addr_parse_zstr(SocketAddr *out, Zstr spec, SocketKind kind); /// TAGS: Socket, Parse, Address /// bool socket_addr_parse_str(SocketAddr *out, const Str *spec, SocketKind kind); -#define SocketAddrParse(out, spec, kind) \ - _Generic( \ - (spec), \ - Str *: socket_addr_parse_str((out), (const Str *)(spec), (kind)), \ - Zstr: socket_addr_parse_zstr((out), (Zstr)(spec), (kind)), \ - char *: socket_addr_parse_zstr((out), (Zstr)(spec), (kind)) \ + +/// +/// Fixed-length (`Zstr`, `len`) overload of `SocketAddrParse`. `spec` +/// need not be NUL-terminated; exactly `len` bytes are considered. The +/// shared IP/port parsers are NUL-terminated scanners, so this copies +/// the view into a bounded stack buffer before delegating to the zstr +/// arm. Same contract as the other arms. +/// +/// SUCCESS : Returns true; `*out` populated. +/// FAILURE : Returns false; `*out` zeroed. Silent (no log) — caller is +/// expected to chain into DNS for the hostname case. A view +/// too long to name a valid host:port also returns false. +/// +/// TAGS: Socket, Parse, Address +/// +bool socket_addr_parse_cstr(SocketAddr *out, Zstr spec, size len, SocketKind kind); +#define SocketAddrParse(...) OVERLOAD(SocketAddrParse, __VA_ARGS__) +#define SocketAddrParse_3(out, spec, kind) \ + _Generic( \ + (spec), \ + Str *: socket_addr_parse_str((out), (const Str *)(spec), (kind)), \ + Zstr: socket_addr_parse_zstr((out), (Zstr)(spec), (kind)), \ + char *: socket_addr_parse_zstr((out), (Zstr)(spec), (kind)) \ ) +#define SocketAddrParse_4(out, spec, len, kind) socket_addr_parse_cstr((out), (Zstr)(spec), (len), (kind)) /// /// Render a `SocketAddr` back into a "ip:port" string. IPv6 addresses diff --git a/Source/Misra/Parsers/Http.c b/Source/Misra/Parsers/Http.c index c04e48b..15b7f74 100644 --- a/Source/Misra/Parsers/Http.c +++ b/Source/Misra/Parsers/Http.c @@ -76,6 +76,18 @@ HttpHeader *http_headers_find_str(HttpHeaders *headers, const Str *key) { return http_headers_find_zstr(headers, StrBegin(key)); } +HttpHeader *http_headers_find_cstr(HttpHeaders *headers, Zstr key, size key_len) { + if (!headers || !key) { + LOG_FATAL("invalid arguments"); + } + VecForeachPtr(headers, header) { + if (StrLen(&header->key) == key_len && 0 == ZstrCompareN(StrBegin(&header->key), key, key_len)) { + return header; + } + } + return NULL; +} + // --------------------------------------------------------------------------- // HttpRequest // --------------------------------------------------------------------------- @@ -141,7 +153,9 @@ Zstr http_request_parse_zstr(HttpRequest *req, Zstr in) { // RFC 7230 § 3.2.5 lets servers cap per-message header count; the // attacker-controlled stream otherwise grows req->headers without // bound. Match common reverse-proxy ceilings (nginx/haproxy ~100). - enum { HTTP_REQUEST_HEADERS_MAX = 100 }; + enum { + HTTP_REQUEST_HEADERS_MAX = 100 + }; while (true) { Zstr line_start = cursor; @@ -184,6 +198,23 @@ Zstr http_request_parse_str(HttpRequest *req, const Str *in) { return http_request_parse_zstr(req, StrBegin(in)); } +Zstr http_request_parse_cstr(HttpRequest *req, Zstr in, size in_len) { + if (!req || !req->allocator || !in) { + LOG_FATAL("invalid arguments"); + } + Str staged = StrInit(req->allocator); + if (!StrPushBackMany(&staged, in, in_len)) { + LOG_ERROR("failed to stage http request input"); + StrDeinit(&staged); + return in; + } + Zstr base = StrBegin(&staged); + Zstr cursor = http_request_parse_zstr(req, base); + Zstr result = (cursor == base) ? in : (in + (cursor - base)); + StrDeinit(&staged); + return result; +} + void HttpRequestDeinit(HttpRequest *req) { if (!req) { LOG_FATAL("invalid arguments"); @@ -448,6 +479,26 @@ HttpResponse *http_respond_with_file_str( } return http_respond_with_file_zstr(response, status, content_type, (Zstr)StrBegin(filepath)); } + +HttpResponse *http_respond_with_file_cstr( + HttpResponse *response, + HttpResponseCode status, + HttpContentType content_type, + Zstr filepath, + size filepath_len +) { + enum { + PATH_CAP = 4096 + }; + char buf[PATH_CAP]; + if (!filepath || filepath_len >= sizeof(buf)) { + LOG_ERROR("invalid or oversized file path"); + return NULL; + } + MemCopy(buf, filepath, filepath_len); + buf[filepath_len] = '\0'; + return http_respond_with_file_zstr(response, status, content_type, buf); +} #endif Str http_response_serialize(const HttpResponse *response, Allocator *alloc) { diff --git a/Source/Misra/Parsers/KvConfig.c b/Source/Misra/Parsers/KvConfig.c index 67d7c68..e70edd4 100644 --- a/Source/Misra/Parsers/KvConfig.c +++ b/Source/Misra/Parsers/KvConfig.c @@ -352,6 +352,20 @@ Str *kvconfig_get_ptr_zstr(KvConfig *cfg, Zstr key) { return value; } +Str *kvconfig_get_ptr_cstr(KvConfig *cfg, Zstr key, size len) { + Str lookup = {0}; + Str *value = NULL; + + if (!cfg || !key) { + return NULL; + } + + lookup = StrInitFromCstr(key, len, MapAllocator(cfg)); + value = kvconfig_get_ptr_str(cfg, &lookup); + StrDeinit(&lookup); + return value; +} + Str kvconfig_get_str(KvConfig *cfg, const Str *key) { Str *value = kvconfig_get_ptr_str(cfg, key); @@ -372,6 +386,16 @@ Str kvconfig_get_zstr(KvConfig *cfg, Zstr key) { return StrInitFromCstr(StrBegin(value), StrLen(value), MapAllocator(cfg)); } +Str kvconfig_get_cstr(KvConfig *cfg, Zstr key, size len) { + Str *value = kvconfig_get_ptr_cstr(cfg, key, len); + + if (!value) { + return StrInit(MapAllocator(cfg)); + } + + return StrInitFromCstr(StrBegin(value), StrLen(value), MapAllocator(cfg)); +} + bool kvconfig_contains_str(KvConfig *cfg, const Str *key) { return kvconfig_get_ptr_str(cfg, key) != NULL; } @@ -380,6 +404,10 @@ bool kvconfig_contains_zstr(KvConfig *cfg, Zstr key) { return kvconfig_get_ptr_zstr(cfg, key) != NULL; } +bool kvconfig_contains_cstr(KvConfig *cfg, Zstr key, size len) { + return kvconfig_get_ptr_cstr(cfg, key, len) != NULL; +} + bool kvconfig_get_bool_str(KvConfig *cfg, const Str *key, bool *value) { Str *str = kvconfig_get_ptr_str(cfg, key); @@ -400,6 +428,16 @@ bool kvconfig_get_bool_zstr(KvConfig *cfg, Zstr key, bool *value) { return kvconfig_parse_bool_value(str, value); } +bool kvconfig_get_bool_cstr(KvConfig *cfg, Zstr key, size len, bool *value) { + Str *str = kvconfig_get_ptr_cstr(cfg, key, len); + + if (!str) { + return false; + } + + return kvconfig_parse_bool_value(str, value); +} + bool kvconfig_get_i64_str(KvConfig *cfg, const Str *key, i64 *value) { Str *str = kvconfig_get_ptr_str(cfg, key); @@ -420,6 +458,16 @@ bool kvconfig_get_i64_zstr(KvConfig *cfg, Zstr key, i64 *value) { return kvconfig_parse_i64_value(str, value); } +bool kvconfig_get_i64_cstr(KvConfig *cfg, Zstr key, size len, i64 *value) { + Str *str = kvconfig_get_ptr_cstr(cfg, key, len); + + if (!str) { + return false; + } + + return kvconfig_parse_i64_value(str, value); +} + bool kvconfig_get_f64_str(KvConfig *cfg, const Str *key, f64 *value) { Str *str = kvconfig_get_ptr_str(cfg, key); @@ -439,3 +487,13 @@ bool kvconfig_get_f64_zstr(KvConfig *cfg, Zstr key, f64 *value) { return kvconfig_parse_f64_value(str, value); } + +bool kvconfig_get_f64_cstr(KvConfig *cfg, Zstr key, size len, f64 *value) { + Str *str = kvconfig_get_ptr_cstr(cfg, key, len); + + if (!str) { + return false; + } + + return kvconfig_parse_f64_value(str, value); +} diff --git a/Source/Misra/Parsers/MachO.c b/Source/Misra/Parsers/MachO.c index 745757d..5b3d6e9 100644 --- a/Source/Misra/Parsers/MachO.c +++ b/Source/Misra/Parsers/MachO.c @@ -471,6 +471,23 @@ bool macho_open(Macho *out, Zstr path, Allocator *alloc) { return MachoOpenFromMemory(out, &data); } +bool macho_open_n(Macho *out, Zstr path, size len, Allocator *alloc) { + if (!out || !path || !alloc) { + LOG_FATAL("MachoOpen: NULL argument (contract violation)"); + } + enum { + PATH_CAP = 4096 + }; + char buf[PATH_CAP]; + if (len >= sizeof(buf)) { + LOG_ERROR("MachoOpen: path length {} exceeds cap", (u64)len); + return false; + } + MemCopy(buf, path, len); + buf[len] = '\0'; + return macho_open(out, buf, alloc); +} + void MachoDeinit(Macho *self) { if (!self) return; @@ -493,6 +510,21 @@ const MachoSection *macho_find_section(const Macho *self, Zstr segment, Zstr sec return NULL; } +const MachoSection * + macho_find_section_cstr(const Macho *self, Zstr segment, size segment_len, Zstr section, size section_len) { + if (!self || !segment || !section) + return NULL; + for (size i = 0; i < VecLen(&self->sections); ++i) { + const MachoSection *s = VecPtrAt(&self->sections, i); + if (segment_len < sizeof(s->segment) && s->segment[segment_len] == '\0' && section_len < sizeof(s->section) && + s->section[section_len] == '\0' && ZstrCompareN(s->segment, segment, segment_len) == 0 && + ZstrCompareN(s->section, section, section_len) == 0) { + return s; + } + } + return NULL; +} + // Mach-O nlist_64 entries carry no size. Pick the symbol with the // largest `value <= vaddr`, then bound it by the next symbol in the // same section (or the section end). N_STAB entries are skipped: diff --git a/Source/Misra/Parsers/Pdb.c b/Source/Misra/Parsers/Pdb.c index 9be20c2..700dcd9 100644 --- a/Source/Misra/Parsers/Pdb.c +++ b/Source/Misra/Parsers/Pdb.c @@ -18,8 +18,8 @@ // --------------------------------------------------------------------------- static const u8 MSF_MAGIC_7[32] = {'M', 'i', 'c', 'r', 'o', 's', 'o', 'f', 't', ' ', 'C', - '/', 'C', '+', '+', ' ', 'M', 'S', 'F', ' ', '7', '.', - '0', '0', '\r', '\n', '\x1A', 'D', 'S', '\0', '\0', '\0'}; + '/', 'C', '+', '+', ' ', 'M', 'S', 'F', ' ', '7', '.', + '0', '0', '\r', '\n', '\x1A', 'D', 'S', '\0', '\0', '\0'}; enum { SUPERBLOCK_SIZE = 56, @@ -754,6 +754,19 @@ bool pdb_open(Pdb *out, Zstr path, Allocator *alloc) { return PdbOpenFromMemory(out, &data); } +bool pdb_open_n(Pdb *out, Zstr path, size len, Allocator *alloc) { + if (!out || !path || !alloc) { + LOG_FATAL("PdbOpen: NULL argument (contract violation)"); + } + Buf data = BufInit(alloc); + if (FileReadAndClose(path, len, &data) < 0) { + BufDeinit(&data); + LOG_ERROR("PdbOpen: failed to read {} path bytes", len); + return false; + } + return PdbOpenFromMemory(out, &data); +} + void PdbDeinit(Pdb *self) { if (!self) return; diff --git a/Source/Misra/Parsers/Pe.c b/Source/Misra/Parsers/Pe.c index 8073879..a69dc58 100644 --- a/Source/Misra/Parsers/Pe.c +++ b/Source/Misra/Parsers/Pe.c @@ -566,6 +566,19 @@ bool pe_open(Pe *out, Zstr path, Allocator *alloc) { return PeOpenFromMemory(out, &data); } +bool pe_open_n(Pe *out, Zstr path, size len, Allocator *alloc) { + if (!out || !path || !alloc) { + LOG_FATAL("PeOpen: NULL argument (contract violation)"); + } + Buf data = BufInit(alloc); + if (FileReadAndClose(path, len, &data) < 0) { + BufDeinit(&data); + LOG_ERROR("PeOpen: failed to read {} path bytes", len); + return false; + } + return PeOpenFromMemory(out, &data); +} + void PeDeinit(Pe *self) { if (!self) return; @@ -592,6 +605,18 @@ const PeSection *pe_find_section_str(const Pe *self, const Str *name) { return pe_find_section_zstr(self, StrBegin(name)); } +const PeSection *pe_find_section_cstr(const Pe *self, Zstr name, size name_len) { + if (!self || !name) + return NULL; + for (size i = 0; i < VecLen(&self->sections); ++i) { + const PeSection *s = VecPtrAt(&self->sections, i); + if (name_len < sizeof(s->name) && s->name[name_len] == '\0' && ZstrCompareN(s->name, name, name_len) == 0) { + return s; + } + } + return NULL; +} + bool PeRvaToOffset(const Pe *self, u32 rva, u64 *out_offset) { if (!self || !out_offset) return false; diff --git a/Source/Misra/Sys/Dir.c b/Source/Misra/Sys/Dir.c index 25fb561..43139ba 100644 --- a/Source/Misra/Sys/Dir.c +++ b/Source/Misra/Sys/Dir.c @@ -277,6 +277,19 @@ DirContents dir_get_contents(Zstr path, Allocator *alloc) { # error "dir_get_contents: unsupported platform/architecture (no direct-syscall path)" #endif +DirContents dir_get_contents_cstr(Zstr path, size len, Allocator *alloc) { + enum { + PATH_CAP = 4096 + }; + char buf[PATH_CAP]; + if (!path || len >= sizeof(buf)) { + return (DirContents)VecInit(alloc); + } + MemCopy(buf, path, len); + buf[len] = '\0'; + return dir_get_contents(buf, alloc); +} + // Cross-platform function to get file size i64 file_get_size(Zstr filename) { #if PLATFORM_WINDOWS @@ -329,6 +342,19 @@ i64 file_get_size(Zstr filename) { #endif } +i64 file_get_size_cstr(Zstr filename, size len) { + enum { + PATH_CAP = 4096 + }; + char buf[PATH_CAP]; + if (!filename || len >= sizeof(buf)) { + return -1; + } + MemCopy(buf, filename, len); + buf[len] = '\0'; + return file_get_size(buf); +} + // --------------------------------------------------------------------------- // FileRemove / DirRemove. Linux uses the direct-syscall path when // FEATURE_DIRECT_SYSCALL is set (x86_64 -> SYS_unlink/SYS_rmdir; @@ -366,6 +392,19 @@ i8 file_remove(Zstr path) { #endif } +i8 file_remove_cstr(Zstr path, size len) { + enum { + PATH_CAP = 4096 + }; + char buf[PATH_CAP]; + if (!path || len >= sizeof(buf)) { + return 0; + } + MemCopy(buf, path, len); + buf[len] = '\0'; + return file_remove(buf); +} + i8 dir_remove(Zstr path) { if (!path) { LOG_FATAL("DirRemove: NULL path"); @@ -396,6 +435,19 @@ i8 dir_remove(Zstr path) { #endif } +i8 dir_remove_cstr(Zstr path, size len) { + enum { + PATH_CAP = 4096 + }; + char buf[PATH_CAP]; + if (!path || len >= sizeof(buf)) { + return 0; + } + MemCopy(buf, path, len); + buf[len] = '\0'; + return dir_remove(buf); +} + // --------------------------------------------------------------------------- // DirCreate / DirCreateAll / DirRemoveAll. Linux x86_64 has SYS_mkdir; // aarch64 dropped it -- use SYS_mkdirat with AT_FDCWD. Darwin keeps @@ -435,6 +487,19 @@ i8 dir_create(Zstr path) { #endif } +i8 dir_create_cstr(Zstr path, size len) { + enum { + PATH_CAP = 4096 + }; + char buf[PATH_CAP]; + if (!path || len >= sizeof(buf)) { + return 0; + } + MemCopy(buf, path, len); + buf[len] = '\0'; + return dir_create(buf); +} + // Check whether the given path already exists as a directory. Used by // DirCreateAll to make EEXIST tolerant (idempotent). Avoids re-walking // the existing tree on the second invocation. @@ -508,6 +573,19 @@ i8 dir_create_all(Zstr path) { return ok; } +i8 dir_create_all_cstr(Zstr path, size len) { + enum { + PATH_CAP = 4096 + }; + char buf[PATH_CAP]; + if (!path || len >= sizeof(buf)) { + return 0; + } + MemCopy(buf, path, len); + buf[len] = '\0'; + return dir_create_all(buf); +} + // Per-entry "parent/child" path buffer cap for the recursive removal // loop. Kept well under 4 KiB on purpose: on macOS, Clang emits an // implicit `___chkstk_darwin` call in the prologue of any function @@ -575,3 +653,16 @@ i8 dir_remove_all(Zstr path) { } return DirRemove(path); } + +i8 dir_remove_all_cstr(Zstr path, size len) { + enum { + PATH_CAP = 4096 + }; + char buf[PATH_CAP]; + if (!path || len >= sizeof(buf)) { + return 0; + } + MemCopy(buf, path, len); + buf[len] = '\0'; + return dir_remove_all(buf); +} diff --git a/Source/Misra/Sys/Dns.c b/Source/Misra/Sys/Dns.c index 560d877..4766853 100644 --- a/Source/Misra/Sys/Dns.c +++ b/Source/Misra/Sys/Dns.c @@ -694,6 +694,22 @@ bool dns_resolve_5_str(DnsResolver *self, const Str *hostname, u16 port, SocketK return dns_resolve_5_zstr(self, StrBegin(hostname), port, kind, out); } +bool dns_resolve_6_cstr(DnsResolver *self, Zstr hostname, u64 hostname_len, u16 port, SocketKind kind, DnsAddrs *out) { + if (!self || !hostname || !out) { + return false; + } + if (hostname_len >= 256) { + LOG_ERROR("DnsResolve: hostname exceeds 255 bytes"); + return false; + } + bool ok = false; + StrInitStack(host, 256) { + StrPushBackMany(&host, hostname, hostname_len); + ok = dns_resolve_5_zstr(self, StrBegin(&host), port, kind, out); + } + return ok; +} + bool dns_resolve_4_vec_str(DnsResolver *self, const Str *spec, SocketKind kind, DnsAddrs *out) { if (!self || !spec || !out) { return false; diff --git a/Source/Misra/Sys/Socket.c b/Source/Misra/Sys/Socket.c index 9c6b145..6714950 100644 --- a/Source/Misra/Sys/Socket.c +++ b/Source/Misra/Sys/Socket.c @@ -455,6 +455,29 @@ bool socket_addr_parse_str(SocketAddr *out, const Str *spec, SocketKind kind) { return socket_addr_parse_zstr(out, StrBegin(spec), kind); } +bool socket_addr_parse_cstr(SocketAddr *out, Zstr spec, size len, SocketKind kind) { + if (!out) { + LOG_FATAL("SocketAddrParse: out is NULL"); + } + MemSet(out, 0, sizeof(*out)); + + // `spec` is a fixed-length view. The shared parsers (split_host_port, + // parse_ipv4/parse_ipv6, parse_port) all scan to a NUL terminator, so the + // view is copied into a bounded stack buffer and NUL-terminated before + // delegating to the zstr arm. A spec longer than this cannot name a valid + // host:port anyway. + enum { + SPEC_CAP = 256 + }; + char buf[SPEC_CAP]; + if (!spec || len >= sizeof(buf)) { + return false; + } + MemCopy(buf, spec, len); + buf[len] = '\0'; + return socket_addr_parse_zstr(out, buf, kind); +} + Str socket_addr_format(const SocketAddr *addr, Allocator *alloc) { Str out = StrInit(alloc); if (!addr || addr->length == 0) {