Zig 0.17.0-dev (Split by item)

This is an example of documentation generated by ZigDoc, an alternative to Zig's built-in Auto Doc feature. See also examples in other modes/formats. The project being documented here (as the example) is the Zig library itself.

IterativeStringParser

Valid escapes: "" -> " \a, \A => 0x08 (not 0x07 like in C) \n => 0x0A \r => 0x0D \t, \T => 0x09 \ =>
\nnn => byte with numeric value given by nnn interpreted as octal (wraps on overflow, number of digits can be 1-3 for ASCII strings and 1-7 for wide strings) \xhh => byte with numeric value given by hh interpreted as hex (number of digits can be 0-2 for ASCII strings and 0-4 for wide strings) <\r+> =>
<[\r\n\t ]+> => <nothing>

Special cases: <\t> => 1-8 spaces, dependent on columns in the source rc file itself <\r> => <nothing> <\n+><\w+?\n?> => <space><\n>

Special, especially weird case: "" => " NOTE: This leads to footguns because the preprocessor can start parsing things out-of-sync with the RC compiler, expanding macros within string literals, etc. This parse function handles this case the same as the Windows RC compiler, but " within a string literal is treated as an error by the lexer, so the relevant branches should never actually be hit during this function.

literals.IterativeStringParser
pub const IterativeStringParser = struct

File

Code

pub const IterativeStringParser = struct {
    source: []const u8,
    code_page: SupportedCodePage,
    /// The type of the string inferred by the prefix (L"" or "")
    /// This is what matters for things like the maximum digits in an
    /// escape sequence, whether or not invalid escape sequences are skipped, etc.
    declared_string_type: StringType,
    pending_codepoint: ?u21 = null,
    num_pending_spaces: u8 = 0,
    index: usize = 0,
    column: usize = 0,
    diagnostics: ?DiagnosticsContext = null,
    seen_tab: bool = false,

    const State = enum {
        normal,
        quote,
        newline,
        escaped,
        escaped_cr,
        escaped_newlines,
        escaped_octal,
        escaped_hex,
    };

    pub fn init(bytes: SourceBytes, options: StringParseOptions) IterativeStringParser {
        const declared_string_type: StringType = switch (bytes.slice[0]) {
            'L', 'l' => .wide,
            else => .ascii,
        };
        var source = bytes.slice[1 .. bytes.slice.len - 1]; // remove ""
        var column = options.start_column + 1; // for the removed "
        if (declared_string_type == .wide) {
            source = source[1..]; // remove L
            column += 1; // for the removed L
        }
        return .{
            .source = source,
            .code_page = bytes.code_page,
            .declared_string_type = declared_string_type,
            .column = column,
            .diagnostics = options.diagnostics,
        };
    }

    pub const ParsedCodepoint = struct {
        codepoint: u21,
        /// Note: If this is true, `codepoint` will have an effective maximum value
        /// of 0xFFFF, as `codepoint` is calculated using wrapping arithmetic on a u16.
        /// If the value needs to be truncated to a smaller integer (e.g. for ASCII string
        /// literals), then that must be done by the caller.
        from_escaped_integer: bool = false,
        /// Denotes that the codepoint is:
        /// - Escaped (has a \ in front of it), and
        /// - Has a value >= U+10000, meaning it would be encoded as a surrogate
        ///   pair in UTF-16, and
        /// - Is part of a wide string literal
        ///
        /// Normally in wide string literals, invalid escapes are omitted
        /// during parsing (the codepoints are not returned at all during
        /// the `next` call), but this is a special case in which the
        /// escape only applies to the high surrogate pair of the codepoint.
        ///
        /// TODO: Maybe just return the low surrogate codepoint by itself in this case.
        escaped_surrogate_pair: bool = false,
    };

    pub fn next(self: *IterativeStringParser) std.mem.Allocator.Error!?ParsedCodepoint {
        const result = try self.nextUnchecked();
        if (self.diagnostics != null and result != null and !result.?.from_escaped_integer) {
            switch (result.?.codepoint) {
                0x0900, 0x0A00, 0x0A0D, 0x2000, 0x0D00 => {
                    const err: ErrorDetails.Error = if (result.?.codepoint == 0xD00)
                        .rc_would_miscompile_codepoint_skip
                    else
                        .rc_would_miscompile_codepoint_whitespace;
                    try self.diagnostics.?.diagnostics.append(ErrorDetails{
                        .err = err,
                        .type = .warning,
                        .code_page = self.code_page,
                        .token = self.diagnostics.?.token,
                        .extra = .{ .number = result.?.codepoint },
                    });
                },
                0xFFFE, 0xFFFF => {
                    try self.diagnostics.?.diagnostics.append(ErrorDetails{
                        .err = .rc_would_miscompile_codepoint_bom,
                        .type = .warning,
                        .code_page = self.code_page,
                        .token = self.diagnostics.?.token,
                        .extra = .{ .number = result.?.codepoint },
                    });
                    try self.diagnostics.?.diagnostics.append(ErrorDetails{
                        .err = .rc_would_miscompile_codepoint_bom,
                        .type = .note,
                        .code_page = self.code_page,
                        .token = self.diagnostics.?.token,
                        .print_source_line = false,
                        .extra = .{ .number = result.?.codepoint },
                    });
                },
                else => {},
            }
        }
        return result;
    }

    pub fn nextUnchecked(self: *IterativeStringParser) std.mem.Allocator.Error!?ParsedCodepoint {
        if (self.num_pending_spaces > 0) {
            // Ensure that we don't get into this predicament so we can ensure that
            // the order of processing any pending stuff doesn't matter
            std.debug.assert(self.pending_codepoint == null);
            self.num_pending_spaces -= 1;
            return .{ .codepoint = ' ' };
        }
        if (self.pending_codepoint) |pending_codepoint| {
            self.pending_codepoint = null;
            return .{ .codepoint = pending_codepoint };
        }
        if (self.index >= self.source.len) return null;

        var state: State = .normal;
        var string_escape_n: u16 = 0;
        var string_escape_i: u8 = 0;
        const max_octal_escape_digits: u8 = switch (self.declared_string_type) {
            .ascii => 3,
            .wide => 7,
        };
        const max_hex_escape_digits: u8 = switch (self.declared_string_type) {
            .ascii => 2,
            .wide => 4,
        };

        var backtrack: bool = undefined;
        while (self.code_page.codepointAt(self.index, self.source)) |codepoint| : ({
            if (!backtrack) self.index += codepoint.byte_len;
        }) {
            backtrack = false;
            const c = codepoint.value;
            defer {
                if (!backtrack) {
                    if (c == '\t') {
                        self.column += columnsUntilTabStop(self.column, 8);
                    } else {
                        self.column += codepoint.byte_len;
                    }
                }
            }
            switch (state) {
                .normal => switch (c) {
                    '\\' => state = .escaped,
                    '"' => state = .quote,
                    '\r' => {},
                    '\n' => state = .newline,
                    '\t' => {
                        // Only warn about a tab getting converted to spaces once per string
                        if (self.diagnostics != null and !self.seen_tab) {
                            try self.diagnostics.?.diagnostics.append(ErrorDetails{
                                .err = .tab_converted_to_spaces,
                                .type = .warning,
                                .code_page = self.code_page,
                                .token = self.diagnostics.?.token,
                            });
                            try self.diagnostics.?.diagnostics.append(ErrorDetails{
                                .err = .tab_converted_to_spaces,
                                .type = .note,
                                .code_page = self.code_page,
                                .token = self.diagnostics.?.token,
                                .print_source_line = false,
                            });
                            self.seen_tab = true;
                        }
                        const cols = columnsUntilTabStop(self.column, 8);
                        self.num_pending_spaces = @intCast(cols - 1);
                        self.index += codepoint.byte_len;
                        return .{ .codepoint = ' ' };
                    },
                    else => {
                        self.index += codepoint.byte_len;
                        return .{ .codepoint = c };
                    },
                },
                .quote => switch (c) {
                    '"' => {
                        // "" => "
                        self.index += codepoint.byte_len;
                        return .{ .codepoint = '"' };
                    },
                    else => unreachable, // this is a bug in the lexer
                },
                .newline => switch (c) {
                    '\r', ' ', '\t', '\n', '\x0b', '\x0c', '\xa0' => {},
                    else => {
                        // we intentionally avoid incrementing self.index
                        // to handle the current char in the next call,
                        // and we set backtrack so column count is handled correctly
                        backtrack = true;

                        // <space><newline>
                        self.pending_codepoint = '\n';
                        return .{ .codepoint = ' ' };
                    },
                },
                .escaped => switch (c) {
                    '\r' => state = .escaped_cr,
                    '\n' => state = .escaped_newlines,
                    '0'...'7' => {
                        string_escape_n = std.fmt.charToDigit(@intCast(c), 8) catch unreachable;
                        string_escape_i = 1;
                        state = .escaped_octal;
                    },
                    'x', 'X' => {
                        string_escape_n = 0;
                        string_escape_i = 0;
                        state = .escaped_hex;
                    },
                    else => {
                        switch (c) {
                            'a', 'A' => {
                                self.index += codepoint.byte_len;
                                // might be a bug in RC, but matches its behavior
                                return .{ .codepoint = '\x08' };
                            },
                            'n' => {
                                self.index += codepoint.byte_len;
                                return .{ .codepoint = '\n' };
                            },
                            'r' => {
                                self.index += codepoint.byte_len;
                                return .{ .codepoint = '\r' };
                            },
                            't', 'T' => {
                                self.index += codepoint.byte_len;
                                return .{ .codepoint = '\t' };
                            },
                            '\\' => {
                                self.index += codepoint.byte_len;
                                return .{ .codepoint = '\\' };
                            },
                            '"' => {
                                // \" is a special case that doesn't get the \ included,
                                backtrack = true;
                            },
                            else => switch (self.declared_string_type) {
                                .wide => {
                                    // All invalid escape sequences are skipped in wide strings,
                                    // but there is a special case around \<tab> where the \
                                    // is skipped but the tab character is processed.
                                    // It's actually a bit weirder than that, though, since
                                    // the preprocessor is the one that does the <tab> -> spaces
                                    // conversion, so it goes something like this:
                                    //
                                    // Before preprocessing: L"\<tab>"
                                    // After preprocessing:  L"\     "
                                    //
                                    // So the parser only sees an escaped space character followed
                                    // by some other number of spaces >= 0.
                                    //
                                    // However, our preprocessor keeps tab characters intact, so we emulate
                                    // the above behavior by skipping the \ and then outputting one less
                                    // space than normal for the <tab> character.
                                    if (c == '\t') {
                                        // Only warn about a tab getting converted to spaces once per string
                                        if (self.diagnostics != null and !self.seen_tab) {
                                            try self.diagnostics.?.diagnostics.append(ErrorDetails{
                                                .err = .tab_converted_to_spaces,
                                                .type = .warning,
                                                .code_page = self.code_page,
                                                .token = self.diagnostics.?.token,
                                            });
                                            try self.diagnostics.?.diagnostics.append(ErrorDetails{
                                                .err = .tab_converted_to_spaces,
                                                .type = .note,
                                                .code_page = self.code_page,
                                                .token = self.diagnostics.?.token,
                                                .print_source_line = false,
                                            });
                                            self.seen_tab = true;
                                        }

                                        const cols = columnsUntilTabStop(self.column, 8);
                                        // If the tab character would only be converted to a single space,
                                        // then we can just skip both the \ and the <tab> and move on.
                                        if (cols > 1) {
                                            self.num_pending_spaces = @intCast(cols - 2);
                                            self.index += codepoint.byte_len;
                                            return .{ .codepoint = ' ' };
                                        }
                                    }
                                    // There's a second special case when the codepoint would be encoded
                                    // as a surrogate pair in UTF-16, as the escape 'applies' to the
                                    // high surrogate pair only in this instance. This is a side-effect
                                    // of the Win32 RC compiler preprocessor outputting UTF-16 and the
                                    // compiler itself seemingly working on code units instead of code points
                                    // in this particular instance.
                                    //
                                    // We emulate this behavior by emitting the codepoint, but with a marker
                                    // that indicates that it needs to be handled specially.
                                    if (c >= 0x10000 and c != code_pages.Codepoint.invalid) {
                                        self.index += codepoint.byte_len;
                                        return .{ .codepoint = c, .escaped_surrogate_pair = true };
                                    }
                                },
                                .ascii => {
                                    // we intentionally avoid incrementing self.index
                                    // to handle the current char in the next call,
                                    // and we set backtrack so column count is handled correctly
                                    backtrack = true;
                                    return .{ .codepoint = '\\' };
                                },
                            },
                        }
                        state = .normal;
                    },
                },
                .escaped_cr => switch (c) {
                    '\r' => {},
                    '\n' => state = .escaped_newlines,
                    else => {
                        // we intentionally avoid incrementing self.index
                        // to handle the current char in the next call,
                        // and we set backtrack so column count is handled correctly
                        backtrack = true;
                        return .{ .codepoint = '\\' };
                    },
                },
                .escaped_newlines => switch (c) {
                    '\r', '\n', '\t', ' ', '\x0b', '\x0c', '\xa0' => {},
                    else => {
                        // backtrack so that we handle the current char properly
                        backtrack = true;
                        state = .normal;
                    },
                },
                .escaped_octal => switch (c) {
                    '0'...'7' => {
                        // Note: We use wrapping arithmetic on a u16 here since there's been no observed
                        // string parsing scenario where an escaped integer with a value >= the u16
                        // max is interpreted as anything but the truncated u16 value.
                        string_escape_n *%= 8;
                        string_escape_n +%= std.fmt.charToDigit(@intCast(c), 8) catch unreachable;
                        string_escape_i += 1;
                        if (string_escape_i == max_octal_escape_digits) {
                            self.index += codepoint.byte_len;
                            return .{ .codepoint = string_escape_n, .from_escaped_integer = true };
                        }
                    },
                    else => {
                        // we intentionally avoid incrementing self.index
                        // to handle the current char in the next call,
                        // and we set backtrack so column count is handled correctly
                        backtrack = true;

                        // write out whatever byte we have parsed so far
                        return .{ .codepoint = string_escape_n, .from_escaped_integer = true };
                    },
                },
                .escaped_hex => switch (c) {
                    '0'...'9', 'a'...'f', 'A'...'F' => {
                        string_escape_n *= 16;
                        string_escape_n += std.fmt.charToDigit(@intCast(c), 16) catch unreachable;
                        string_escape_i += 1;
                        if (string_escape_i == max_hex_escape_digits) {
                            self.index += codepoint.byte_len;
                            return .{ .codepoint = string_escape_n, .from_escaped_integer = true };
                        }
                    },
                    else => {
                        // we intentionally avoid incrementing self.index
                        // to handle the current char in the next call,
                        // and we set backtrack so column count is handled correctly
                        backtrack = true;

                        // write out whatever byte we have parsed so far
                        // (even with 0 actual digits, \x alone parses to 0)
                        const escaped_value = string_escape_n;
                        return .{ .codepoint = escaped_value, .from_escaped_integer = true };
                    },
                },
            }
        }

        switch (state) {
            .normal, .escaped_newlines => {},
            .newline => {
                // <space><newline>
                self.pending_codepoint = '\n';
                return .{ .codepoint = ' ' };
            },
            .escaped, .escaped_cr => return .{ .codepoint = '\\' },
            .escaped_octal, .escaped_hex => {
                return .{ .codepoint = string_escape_n, .from_escaped_integer = true };
            },
            .quote => unreachable, // this is a bug in the lexer
        }

        return null;
    }
}