Travis Staloch 8af59d1f98 ComptimeStringMap: return a regular struct and optimize
this patch renames ComptimeStringMap to StaticStringMap, makes it
accept only a single type parameter, and return a known struct type
instead of an anonymous struct.  initial motivation for these changes
was to reduce the 'very long type names' issue described here
https://github.com/ziglang/zig/pull/19682.

this breaks the previous API.  users will now need to write:
`const map = std.StaticStringMap(T).initComptime(kvs_list);`

* move `kvs_list` param from type param to an `initComptime()` param
* new public methods
  * `keys()`, `values()` helpers
  * `init(allocator)`, `deinit(allocator)` for runtime data
  * `getLongestPrefix(str)`, `getLongestPrefixIndex(str)` - i'm not sure
     these belong but have left in for now incase they are deemed useful
* performance notes:
  * i posted some benchmarking results here:
    https://github.com/travisstaloch/comptime-string-map-revised/issues/1
  * i noticed a speedup reducing the size of the struct from 48 to 32
    bytes and thus use u32s instead of usize for all length fields
  * i noticed speedup storing KVs as a struct of arrays
  * latest benchmark shows these wall_time improvements for
    debug/safe/small/fast builds: -6.6% / -10.2% / -19.1% / -8.9%. full
    output in link above.
2024-04-22 15:31:41 -07:00

447 lines
13 KiB
Zig

const std = @import("std");
const utils = @import("utils.zig");
const res = @import("res.zig");
const SourceBytes = @import("literals.zig").SourceBytes;
// https://learn.microsoft.com/en-us/windows/win32/menurc/about-resource-files
pub const Resource = enum {
accelerators,
bitmap,
cursor,
dialog,
dialogex,
/// As far as I can tell, this is undocumented; the most I could find was this:
/// https://www.betaarchive.com/wiki/index.php/Microsoft_KB_Archive/91697
dlginclude,
/// Undocumented, basically works exactly like RCDATA
dlginit,
font,
html,
icon,
menu,
menuex,
messagetable,
plugplay, // Obsolete
rcdata,
stringtable,
/// Undocumented
toolbar,
user_defined,
versioninfo,
vxd, // Obsolete
// Types that are treated as a user-defined type when encountered, but have
// special meaning without the Visual Studio GUI. We match the Win32 RC compiler
// behavior by acting as if these keyword don't exist when compiling the .rc
// (thereby treating them as user-defined).
//textinclude, // A special resource that is interpreted by Visual C++.
//typelib, // A special resource that is used with the /TLBID and /TLBOUT linker options
// Types that can only be specified by numbers, they don't have keywords
cursor_num,
icon_num,
string_num,
anicursor_num,
aniicon_num,
fontdir_num,
manifest_num,
const map = std.StaticStringMapWithEql(
Resource,
std.static_string_map.eqlAsciiIgnoreCase,
).initComptime(.{
.{ "ACCELERATORS", .accelerators },
.{ "BITMAP", .bitmap },
.{ "CURSOR", .cursor },
.{ "DIALOG", .dialog },
.{ "DIALOGEX", .dialogex },
.{ "DLGINCLUDE", .dlginclude },
.{ "DLGINIT", .dlginit },
.{ "FONT", .font },
.{ "HTML", .html },
.{ "ICON", .icon },
.{ "MENU", .menu },
.{ "MENUEX", .menuex },
.{ "MESSAGETABLE", .messagetable },
.{ "PLUGPLAY", .plugplay },
.{ "RCDATA", .rcdata },
.{ "STRINGTABLE", .stringtable },
.{ "TOOLBAR", .toolbar },
.{ "VERSIONINFO", .versioninfo },
.{ "VXD", .vxd },
});
pub fn fromString(bytes: SourceBytes) Resource {
const maybe_ordinal = res.NameOrOrdinal.maybeOrdinalFromString(bytes);
if (maybe_ordinal) |ordinal| {
if (ordinal.ordinal >= 256) return .user_defined;
return fromRT(@enumFromInt(ordinal.ordinal));
}
return map.get(bytes.slice) orelse .user_defined;
}
// TODO: Some comptime validation that RT <-> Resource conversion is synced?
pub fn fromRT(rt: res.RT) Resource {
return switch (rt) {
.ACCELERATOR => .accelerators,
.ANICURSOR => .anicursor_num,
.ANIICON => .aniicon_num,
.BITMAP => .bitmap,
.CURSOR => .cursor_num,
.DIALOG => .dialog,
.DLGINCLUDE => .dlginclude,
.DLGINIT => .dlginit,
.FONT => .font,
.FONTDIR => .fontdir_num,
.GROUP_CURSOR => .cursor,
.GROUP_ICON => .icon,
.HTML => .html,
.ICON => .icon_num,
.MANIFEST => .manifest_num,
.MENU => .menu,
.MESSAGETABLE => .messagetable,
.PLUGPLAY => .plugplay,
.RCDATA => .rcdata,
.STRING => .string_num,
.TOOLBAR => .toolbar,
.VERSION => .versioninfo,
.VXD => .vxd,
_ => .user_defined,
};
}
pub fn canUseRawData(resource: Resource) bool {
return switch (resource) {
.user_defined,
.html,
.plugplay, // Obsolete
.rcdata,
.vxd, // Obsolete
.manifest_num,
.dlginit,
=> true,
else => false,
};
}
pub fn nameForErrorDisplay(resource: Resource) []const u8 {
return switch (resource) {
// zig fmt: off
.accelerators, .bitmap, .cursor, .dialog, .dialogex, .dlginclude, .dlginit, .font,
.html, .icon, .menu, .menuex, .messagetable, .plugplay, .rcdata, .stringtable,
.toolbar, .versioninfo, .vxd => @tagName(resource),
// zig fmt: on
.user_defined => "user-defined",
.cursor_num => std.fmt.comptimePrint("{d} (cursor)", .{@intFromEnum(res.RT.CURSOR)}),
.icon_num => std.fmt.comptimePrint("{d} (icon)", .{@intFromEnum(res.RT.ICON)}),
.string_num => std.fmt.comptimePrint("{d} (string)", .{@intFromEnum(res.RT.STRING)}),
.anicursor_num => std.fmt.comptimePrint("{d} (anicursor)", .{@intFromEnum(res.RT.ANICURSOR)}),
.aniicon_num => std.fmt.comptimePrint("{d} (aniicon)", .{@intFromEnum(res.RT.ANIICON)}),
.fontdir_num => std.fmt.comptimePrint("{d} (fontdir)", .{@intFromEnum(res.RT.FONTDIR)}),
.manifest_num => std.fmt.comptimePrint("{d} (manifest)", .{@intFromEnum(res.RT.MANIFEST)}),
};
}
};
/// https://learn.microsoft.com/en-us/windows/win32/menurc/stringtable-resource#parameters
/// https://learn.microsoft.com/en-us/windows/win32/menurc/dialog-resource#parameters
/// https://learn.microsoft.com/en-us/windows/win32/menurc/dialogex-resource#parameters
pub const OptionalStatements = enum {
characteristics,
language,
version,
// DIALOG
caption,
class,
exstyle,
font,
menu,
style,
pub const map = std.StaticStringMapWithEql(
OptionalStatements,
std.static_string_map.eqlAsciiIgnoreCase,
).initComptime(.{
.{ "CHARACTERISTICS", .characteristics },
.{ "LANGUAGE", .language },
.{ "VERSION", .version },
});
pub const dialog_map = std.StaticStringMapWithEql(
OptionalStatements,
std.static_string_map.eqlAsciiIgnoreCase,
).initComptime(.{
.{ "CAPTION", .caption },
.{ "CLASS", .class },
.{ "EXSTYLE", .exstyle },
.{ "FONT", .font },
.{ "MENU", .menu },
.{ "STYLE", .style },
});
};
pub const Control = enum {
auto3state,
autocheckbox,
autoradiobutton,
checkbox,
combobox,
control,
ctext,
defpushbutton,
edittext,
hedit,
iedit,
groupbox,
icon,
listbox,
ltext,
pushbox,
pushbutton,
radiobutton,
rtext,
scrollbar,
state3,
userbutton,
pub const map = std.StaticStringMapWithEql(
Control,
std.static_string_map.eqlAsciiIgnoreCase,
).initComptime(.{
.{ "AUTO3STATE", .auto3state },
.{ "AUTOCHECKBOX", .autocheckbox },
.{ "AUTORADIOBUTTON", .autoradiobutton },
.{ "CHECKBOX", .checkbox },
.{ "COMBOBOX", .combobox },
.{ "CONTROL", .control },
.{ "CTEXT", .ctext },
.{ "DEFPUSHBUTTON", .defpushbutton },
.{ "EDITTEXT", .edittext },
.{ "HEDIT", .hedit },
.{ "IEDIT", .iedit },
.{ "GROUPBOX", .groupbox },
.{ "ICON", .icon },
.{ "LISTBOX", .listbox },
.{ "LTEXT", .ltext },
.{ "PUSHBOX", .pushbox },
.{ "PUSHBUTTON", .pushbutton },
.{ "RADIOBUTTON", .radiobutton },
.{ "RTEXT", .rtext },
.{ "SCROLLBAR", .scrollbar },
.{ "STATE3", .state3 },
.{ "USERBUTTON", .userbutton },
});
pub fn hasTextParam(control: Control) bool {
switch (control) {
.scrollbar, .listbox, .iedit, .hedit, .edittext, .combobox => return false,
else => return true,
}
}
};
pub const ControlClass = struct {
pub const map = std.StaticStringMapWithEql(
res.ControlClass,
std.static_string_map.eqlAsciiIgnoreCase,
).initComptime(.{
.{ "BUTTON", .button },
.{ "EDIT", .edit },
.{ "STATIC", .static },
.{ "LISTBOX", .listbox },
.{ "SCROLLBAR", .scrollbar },
.{ "COMBOBOX", .combobox },
});
/// Like `map.get` but works on WTF16 strings, for use with parsed
/// string literals ("BUTTON", or even "\x42UTTON")
pub fn fromWideString(str: []const u16) ?res.ControlClass {
const utf16Literal = std.unicode.utf8ToUtf16LeStringLiteral;
return if (ascii.eqlIgnoreCaseW(str, utf16Literal("BUTTON")))
.button
else if (ascii.eqlIgnoreCaseW(str, utf16Literal("EDIT")))
.edit
else if (ascii.eqlIgnoreCaseW(str, utf16Literal("STATIC")))
.static
else if (ascii.eqlIgnoreCaseW(str, utf16Literal("LISTBOX")))
.listbox
else if (ascii.eqlIgnoreCaseW(str, utf16Literal("SCROLLBAR")))
.scrollbar
else if (ascii.eqlIgnoreCaseW(str, utf16Literal("COMBOBOX")))
.combobox
else
null;
}
};
const ascii = struct {
/// Compares ASCII values case-insensitively, non-ASCII values are compared directly
pub fn eqlIgnoreCaseW(a: []const u16, b: []const u16) bool {
if (a.len != b.len) return false;
for (a, b) |a_c, b_c| {
if (a_c < 128) {
if (std.ascii.toLower(@intCast(a_c)) != std.ascii.toLower(@intCast(b_c))) return false;
} else {
if (a_c != b_c) return false;
}
}
return true;
}
};
pub const MenuItem = enum {
menuitem,
popup,
pub const map = std.StaticStringMapWithEql(
MenuItem,
std.static_string_map.eqlAsciiIgnoreCase,
).initComptime(.{
.{ "MENUITEM", .menuitem },
.{ "POPUP", .popup },
});
pub fn isSeparator(bytes: []const u8) bool {
return std.ascii.eqlIgnoreCase(bytes, "SEPARATOR");
}
pub const Option = enum {
checked,
grayed,
help,
inactive,
menubarbreak,
menubreak,
pub const map = std.StaticStringMapWithEql(
Option,
std.static_string_map.eqlAsciiIgnoreCase,
).initComptime(.{
.{ "CHECKED", .checked },
.{ "GRAYED", .grayed },
.{ "HELP", .help },
.{ "INACTIVE", .inactive },
.{ "MENUBARBREAK", .menubarbreak },
.{ "MENUBREAK", .menubreak },
});
};
};
pub const ToolbarButton = enum {
button,
separator,
pub const map = std.StaticStringMapWithEql(
ToolbarButton,
std.static_string_map.eqlAsciiIgnoreCase,
).initComptime(.{
.{ "BUTTON", .button },
.{ "SEPARATOR", .separator },
});
};
pub const VersionInfo = enum {
file_version,
product_version,
file_flags_mask,
file_flags,
file_os,
file_type,
file_subtype,
pub const map = std.StaticStringMapWithEql(
VersionInfo,
std.static_string_map.eqlAsciiIgnoreCase,
).initComptime(.{
.{ "FILEVERSION", .file_version },
.{ "PRODUCTVERSION", .product_version },
.{ "FILEFLAGSMASK", .file_flags_mask },
.{ "FILEFLAGS", .file_flags },
.{ "FILEOS", .file_os },
.{ "FILETYPE", .file_type },
.{ "FILESUBTYPE", .file_subtype },
});
};
pub const VersionBlock = enum {
block,
value,
pub const map = std.StaticStringMapWithEql(
VersionBlock,
std.static_string_map.eqlAsciiIgnoreCase,
).initComptime(.{
.{ "BLOCK", .block },
.{ "VALUE", .value },
});
};
/// Keywords that are be the first token in a statement and (if so) dictate how the rest
/// of the statement is parsed.
pub const TopLevelKeywords = enum {
language,
version,
characteristics,
stringtable,
pub const map = std.StaticStringMapWithEql(
TopLevelKeywords,
std.static_string_map.eqlAsciiIgnoreCase,
).initComptime(.{
.{ "LANGUAGE", .language },
.{ "VERSION", .version },
.{ "CHARACTERISTICS", .characteristics },
.{ "STRINGTABLE", .stringtable },
});
};
pub const CommonResourceAttributes = enum {
preload,
loadoncall,
fixed,
moveable,
discardable,
pure,
impure,
shared,
nonshared,
pub const map = std.StaticStringMapWithEql(
CommonResourceAttributes,
std.static_string_map.eqlAsciiIgnoreCase,
).initComptime(.{
.{ "PRELOAD", .preload },
.{ "LOADONCALL", .loadoncall },
.{ "FIXED", .fixed },
.{ "MOVEABLE", .moveable },
.{ "DISCARDABLE", .discardable },
.{ "PURE", .pure },
.{ "IMPURE", .impure },
.{ "SHARED", .shared },
.{ "NONSHARED", .nonshared },
});
};
pub const AcceleratorTypeAndOptions = enum {
virtkey,
ascii,
noinvert,
alt,
shift,
control,
pub const map = std.StaticStringMapWithEql(
AcceleratorTypeAndOptions,
std.static_string_map.eqlAsciiIgnoreCase,
).initComptime(.{
.{ "VIRTKEY", .virtkey },
.{ "ASCII", .ascii },
.{ "NOINVERT", .noinvert },
.{ "ALT", .alt },
.{ "SHIFT", .shift },
.{ "CONTROL", .control },
});
};