authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2025-01-17 19:07:46-08:00
committergravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2025-01-17 23:10:41-08:00
log289e9c3507d622bd42a6dee4df8a69dd71a7dfe6
treea7909fd15b7e4c2605f62b89bade74ada8d9b91b
parent4baa448335a8158d424a1e648c907fa7566f7060

resinator: Sync with upstream

Note: This mostly matches resinator v0.1.0 rather than the latest master version, since the latest master version focuses on adding support for .res -> .obj conversion which is not necessary for the future planned relationship of zig and resinator (resinator will likely be moved out of the compiler and into the build system, a la translate-c). So, ultimately the changes here consist mostly of bug fixes for obscure edge cases.

17 files changed, 2144 insertions(+), 948 deletions(-)

lib/compiler/resinator/ast.zig+49-49
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1const std = @import("std");1const std = @import("std");
2const Allocator = std.mem.Allocator;2const Allocator = std.mem.Allocator;
3const Token = @import("lex.zig").Token;3const Token = @import("lex.zig").Token;
4const CodePage = @import("code_pages.zig").CodePage;4const SupportedCodePage = @import("code_pages.zig").SupportedCodePage;
55
6pub const Tree = struct {6pub const Tree = struct {
7 node: *Node,7 node: *Node,
...@@ -28,11 +28,11 @@ pub const Tree = struct {...@@ -28,11 +28,11 @@ pub const Tree = struct {
28};28};
2929
30pub const CodePageLookup = struct {30pub const CodePageLookup = struct {
31 lookup: std.ArrayListUnmanaged(CodePage) = .empty,31 lookup: std.ArrayListUnmanaged(SupportedCodePage) = .empty,
32 allocator: Allocator,32 allocator: Allocator,
33 default_code_page: CodePage,33 default_code_page: SupportedCodePage,
3434
35 pub fn init(allocator: Allocator, default_code_page: CodePage) CodePageLookup {35 pub fn init(allocator: Allocator, default_code_page: SupportedCodePage) CodePageLookup {
36 return .{36 return .{
37 .allocator = allocator,37 .allocator = allocator,
38 .default_code_page = default_code_page,38 .default_code_page = default_code_page,
...@@ -44,7 +44,7 @@ pub const CodePageLookup = struct {...@@ -44,7 +44,7 @@ pub const CodePageLookup = struct {
44 }44 }
4545
46 /// line_num is 1-indexed46 /// line_num is 1-indexed
47 pub fn setForLineNum(self: *CodePageLookup, line_num: usize, code_page: CodePage) !void {47 pub fn setForLineNum(self: *CodePageLookup, line_num: usize, code_page: SupportedCodePage) !void {
48 const index = line_num - 1;48 const index = line_num - 1;
49 if (index >= self.lookup.items.len) {49 if (index >= self.lookup.items.len) {
50 const new_size = line_num;50 const new_size = line_num;
...@@ -66,16 +66,16 @@ pub const CodePageLookup = struct {...@@ -66,16 +66,16 @@ pub const CodePageLookup = struct {
66 self.lookup.items[index] = code_page;66 self.lookup.items[index] = code_page;
67 }67 }
6868
69 pub fn setForToken(self: *CodePageLookup, token: Token, code_page: CodePage) !void {69 pub fn setForToken(self: *CodePageLookup, token: Token, code_page: SupportedCodePage) !void {
70 return self.setForLineNum(token.line_number, code_page);70 return self.setForLineNum(token.line_number, code_page);
71 }71 }
7272
73 /// line_num is 1-indexed73 /// line_num is 1-indexed
74 pub fn getForLineNum(self: CodePageLookup, line_num: usize) CodePage {74 pub fn getForLineNum(self: CodePageLookup, line_num: usize) SupportedCodePage {
75 return self.lookup.items[line_num - 1];75 return self.lookup.items[line_num - 1];
76 }76 }
7777
78 pub fn getForToken(self: CodePageLookup, token: Token) CodePage {78 pub fn getForToken(self: CodePageLookup, token: Token) SupportedCodePage {
79 return self.getForLineNum(token.line_number);79 return self.getForLineNum(token.line_number);
80 }80 }
81};81};
...@@ -85,21 +85,21 @@ test "CodePageLookup" {...@@ -85,21 +85,21 @@ test "CodePageLookup" {
85 defer lookup.deinit();85 defer lookup.deinit();
8686
87 try lookup.setForLineNum(5, .utf8);87 try lookup.setForLineNum(5, .utf8);
88 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(1));88 try std.testing.expectEqual(SupportedCodePage.windows1252, lookup.getForLineNum(1));
89 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(2));89 try std.testing.expectEqual(SupportedCodePage.windows1252, lookup.getForLineNum(2));
90 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(3));90 try std.testing.expectEqual(SupportedCodePage.windows1252, lookup.getForLineNum(3));
91 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(4));91 try std.testing.expectEqual(SupportedCodePage.windows1252, lookup.getForLineNum(4));
92 try std.testing.expectEqual(CodePage.utf8, lookup.getForLineNum(5));92 try std.testing.expectEqual(SupportedCodePage.utf8, lookup.getForLineNum(5));
93 try std.testing.expectEqual(@as(usize, 5), lookup.lookup.items.len);93 try std.testing.expectEqual(@as(usize, 5), lookup.lookup.items.len);
9494
95 try lookup.setForLineNum(7, .windows1252);95 try lookup.setForLineNum(7, .windows1252);
96 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(1));96 try std.testing.expectEqual(SupportedCodePage.windows1252, lookup.getForLineNum(1));
97 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(2));97 try std.testing.expectEqual(SupportedCodePage.windows1252, lookup.getForLineNum(2));
98 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(3));98 try std.testing.expectEqual(SupportedCodePage.windows1252, lookup.getForLineNum(3));
99 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(4));99 try std.testing.expectEqual(SupportedCodePage.windows1252, lookup.getForLineNum(4));
100 try std.testing.expectEqual(CodePage.utf8, lookup.getForLineNum(5));100 try std.testing.expectEqual(SupportedCodePage.utf8, lookup.getForLineNum(5));
101 try std.testing.expectEqual(CodePage.utf8, lookup.getForLineNum(6));101 try std.testing.expectEqual(SupportedCodePage.utf8, lookup.getForLineNum(6));
102 try std.testing.expectEqual(CodePage.windows1252, lookup.getForLineNum(7));102 try std.testing.expectEqual(SupportedCodePage.windows1252, lookup.getForLineNum(7));
103 try std.testing.expectEqual(@as(usize, 7), lookup.lookup.items.len);103 try std.testing.expectEqual(@as(usize, 7), lookup.lookup.items.len);
104}104}
105105
...@@ -734,31 +734,31 @@ pub const Node = struct {...@@ -734,31 +734,31 @@ pub const Node = struct {
734 switch (node.id) {734 switch (node.id) {
735 .root => {735 .root => {
736 try writer.writeAll("\n");736 try writer.writeAll("\n");
737 const root: *Node.Root = @alignCast(@fieldParentPtr("base", node));737 const root: *const Node.Root = @alignCast(@fieldParentPtr("base", node));
738 for (root.body) |body_node| {738 for (root.body) |body_node| {
739 try body_node.dump(tree, writer, indent + 1);739 try body_node.dump(tree, writer, indent + 1);
740 }740 }
741 },741 },
742 .resource_external => {742 .resource_external => {
743 const resource: *Node.ResourceExternal = @alignCast(@fieldParentPtr("base", node));743 const resource: *const Node.ResourceExternal = @alignCast(@fieldParentPtr("base", node));
744 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ resource.id.slice(tree.source), resource.type.slice(tree.source), resource.common_resource_attributes.len });744 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ resource.id.slice(tree.source), resource.type.slice(tree.source), resource.common_resource_attributes.len });
745 try resource.filename.dump(tree, writer, indent + 1);745 try resource.filename.dump(tree, writer, indent + 1);
746 },746 },
747 .resource_raw_data => {747 .resource_raw_data => {
748 const resource: *Node.ResourceRawData = @alignCast(@fieldParentPtr("base", node));748 const resource: *const Node.ResourceRawData = @alignCast(@fieldParentPtr("base", node));
749 try writer.print(" {s} {s} [{d} common_resource_attributes] raw data: {}\n", .{ resource.id.slice(tree.source), resource.type.slice(tree.source), resource.common_resource_attributes.len, resource.raw_data.len });749 try writer.print(" {s} {s} [{d} common_resource_attributes] raw data: {}\n", .{ resource.id.slice(tree.source), resource.type.slice(tree.source), resource.common_resource_attributes.len, resource.raw_data.len });
750 for (resource.raw_data) |data_expression| {750 for (resource.raw_data) |data_expression| {
751 try data_expression.dump(tree, writer, indent + 1);751 try data_expression.dump(tree, writer, indent + 1);
752 }752 }
753 },753 },
754 .literal => {754 .literal => {
755 const literal: *Node.Literal = @alignCast(@fieldParentPtr("base", node));755 const literal: *const Node.Literal = @alignCast(@fieldParentPtr("base", node));
756 try writer.writeAll(" ");756 try writer.writeAll(" ");
757 try writer.writeAll(literal.token.slice(tree.source));757 try writer.writeAll(literal.token.slice(tree.source));
758 try writer.writeAll("\n");758 try writer.writeAll("\n");
759 },759 },
760 .binary_expression => {760 .binary_expression => {
761 const binary: *Node.BinaryExpression = @alignCast(@fieldParentPtr("base", node));761 const binary: *const Node.BinaryExpression = @alignCast(@fieldParentPtr("base", node));
762 try writer.writeAll(" ");762 try writer.writeAll(" ");
763 try writer.writeAll(binary.operator.slice(tree.source));763 try writer.writeAll(binary.operator.slice(tree.source));
764 try writer.writeAll("\n");764 try writer.writeAll("\n");
...@@ -766,7 +766,7 @@ pub const Node = struct {...@@ -766,7 +766,7 @@ pub const Node = struct {
766 try binary.right.dump(tree, writer, indent + 1);766 try binary.right.dump(tree, writer, indent + 1);
767 },767 },
768 .grouped_expression => {768 .grouped_expression => {
769 const grouped: *Node.GroupedExpression = @alignCast(@fieldParentPtr("base", node));769 const grouped: *const Node.GroupedExpression = @alignCast(@fieldParentPtr("base", node));
770 try writer.writeAll("\n");770 try writer.writeAll("\n");
771 try writer.writeByteNTimes(' ', indent);771 try writer.writeByteNTimes(' ', indent);
772 try writer.writeAll(grouped.open_token.slice(tree.source));772 try writer.writeAll(grouped.open_token.slice(tree.source));
...@@ -777,7 +777,7 @@ pub const Node = struct {...@@ -777,7 +777,7 @@ pub const Node = struct {
777 try writer.writeAll("\n");777 try writer.writeAll("\n");
778 },778 },
779 .not_expression => {779 .not_expression => {
780 const not: *Node.NotExpression = @alignCast(@fieldParentPtr("base", node));780 const not: *const Node.NotExpression = @alignCast(@fieldParentPtr("base", node));
781 try writer.writeAll(" ");781 try writer.writeAll(" ");
782 try writer.writeAll(not.not_token.slice(tree.source));782 try writer.writeAll(not.not_token.slice(tree.source));
783 try writer.writeAll(" ");783 try writer.writeAll(" ");
...@@ -785,7 +785,7 @@ pub const Node = struct {...@@ -785,7 +785,7 @@ pub const Node = struct {
785 try writer.writeAll("\n");785 try writer.writeAll("\n");
786 },786 },
787 .accelerators => {787 .accelerators => {
788 const accelerators: *Node.Accelerators = @alignCast(@fieldParentPtr("base", node));788 const accelerators: *const Node.Accelerators = @alignCast(@fieldParentPtr("base", node));
789 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ accelerators.id.slice(tree.source), accelerators.type.slice(tree.source), accelerators.common_resource_attributes.len });789 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ accelerators.id.slice(tree.source), accelerators.type.slice(tree.source), accelerators.common_resource_attributes.len });
790 for (accelerators.optional_statements) |statement| {790 for (accelerators.optional_statements) |statement| {
791 try statement.dump(tree, writer, indent + 1);791 try statement.dump(tree, writer, indent + 1);
...@@ -801,7 +801,7 @@ pub const Node = struct {...@@ -801,7 +801,7 @@ pub const Node = struct {
801 try writer.writeAll("\n");801 try writer.writeAll("\n");
802 },802 },
803 .accelerator => {803 .accelerator => {
804 const accelerator: *Node.Accelerator = @alignCast(@fieldParentPtr("base", node));804 const accelerator: *const Node.Accelerator = @alignCast(@fieldParentPtr("base", node));
805 for (accelerator.type_and_options, 0..) |option, i| {805 for (accelerator.type_and_options, 0..) |option, i| {
806 if (i != 0) try writer.writeAll(",");806 if (i != 0) try writer.writeAll(",");
807 try writer.writeByte(' ');807 try writer.writeByte(' ');
...@@ -812,7 +812,7 @@ pub const Node = struct {...@@ -812,7 +812,7 @@ pub const Node = struct {
812 try accelerator.idvalue.dump(tree, writer, indent + 1);812 try accelerator.idvalue.dump(tree, writer, indent + 1);
813 },813 },
814 .dialog => {814 .dialog => {
815 const dialog: *Node.Dialog = @alignCast(@fieldParentPtr("base", node));815 const dialog: *const Node.Dialog = @alignCast(@fieldParentPtr("base", node));
816 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ dialog.id.slice(tree.source), dialog.type.slice(tree.source), dialog.common_resource_attributes.len });816 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ dialog.id.slice(tree.source), dialog.type.slice(tree.source), dialog.common_resource_attributes.len });
817 inline for (.{ "x", "y", "width", "height" }) |arg| {817 inline for (.{ "x", "y", "width", "height" }) |arg| {
818 try writer.writeByteNTimes(' ', indent + 1);818 try writer.writeByteNTimes(' ', indent + 1);
...@@ -838,7 +838,7 @@ pub const Node = struct {...@@ -838,7 +838,7 @@ pub const Node = struct {
838 try writer.writeAll("\n");838 try writer.writeAll("\n");
839 },839 },
840 .control_statement => {840 .control_statement => {
841 const control: *Node.ControlStatement = @alignCast(@fieldParentPtr("base", node));841 const control: *const Node.ControlStatement = @alignCast(@fieldParentPtr("base", node));
842 try writer.print(" {s}", .{control.type.slice(tree.source)});842 try writer.print(" {s}", .{control.type.slice(tree.source)});
843 if (control.text) |text| {843 if (control.text) |text| {
844 try writer.print(" text: {s}", .{text.slice(tree.source)});844 try writer.print(" text: {s}", .{text.slice(tree.source)});
...@@ -874,7 +874,7 @@ pub const Node = struct {...@@ -874,7 +874,7 @@ pub const Node = struct {
874 }874 }
875 },875 },
876 .toolbar => {876 .toolbar => {
877 const toolbar: *Node.Toolbar = @alignCast(@fieldParentPtr("base", node));877 const toolbar: *const Node.Toolbar = @alignCast(@fieldParentPtr("base", node));
878 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ toolbar.id.slice(tree.source), toolbar.type.slice(tree.source), toolbar.common_resource_attributes.len });878 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ toolbar.id.slice(tree.source), toolbar.type.slice(tree.source), toolbar.common_resource_attributes.len });
879 inline for (.{ "button_width", "button_height" }) |arg| {879 inline for (.{ "button_width", "button_height" }) |arg| {
880 try writer.writeByteNTimes(' ', indent + 1);880 try writer.writeByteNTimes(' ', indent + 1);
...@@ -892,7 +892,7 @@ pub const Node = struct {...@@ -892,7 +892,7 @@ pub const Node = struct {
892 try writer.writeAll("\n");892 try writer.writeAll("\n");
893 },893 },
894 .menu => {894 .menu => {
895 const menu: *Node.Menu = @alignCast(@fieldParentPtr("base", node));895 const menu: *const Node.Menu = @alignCast(@fieldParentPtr("base", node));
896 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ menu.id.slice(tree.source), menu.type.slice(tree.source), menu.common_resource_attributes.len });896 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ menu.id.slice(tree.source), menu.type.slice(tree.source), menu.common_resource_attributes.len });
897 for (menu.optional_statements) |statement| {897 for (menu.optional_statements) |statement| {
898 try statement.dump(tree, writer, indent + 1);898 try statement.dump(tree, writer, indent + 1);
...@@ -913,16 +913,16 @@ pub const Node = struct {...@@ -913,16 +913,16 @@ pub const Node = struct {
913 try writer.writeAll("\n");913 try writer.writeAll("\n");
914 },914 },
915 .menu_item => {915 .menu_item => {
916 const menu_item: *Node.MenuItem = @alignCast(@fieldParentPtr("base", node));916 const menu_item: *const Node.MenuItem = @alignCast(@fieldParentPtr("base", node));
917 try writer.print(" {s} {s} [{d} options]\n", .{ menu_item.menuitem.slice(tree.source), menu_item.text.slice(tree.source), menu_item.option_list.len });917 try writer.print(" {s} {s} [{d} options]\n", .{ menu_item.menuitem.slice(tree.source), menu_item.text.slice(tree.source), menu_item.option_list.len });
918 try menu_item.result.dump(tree, writer, indent + 1);918 try menu_item.result.dump(tree, writer, indent + 1);
919 },919 },
920 .menu_item_separator => {920 .menu_item_separator => {
921 const menu_item: *Node.MenuItemSeparator = @alignCast(@fieldParentPtr("base", node));921 const menu_item: *const Node.MenuItemSeparator = @alignCast(@fieldParentPtr("base", node));
922 try writer.print(" {s} {s}\n", .{ menu_item.menuitem.slice(tree.source), menu_item.separator.slice(tree.source) });922 try writer.print(" {s} {s}\n", .{ menu_item.menuitem.slice(tree.source), menu_item.separator.slice(tree.source) });
923 },923 },
924 .menu_item_ex => {924 .menu_item_ex => {
925 const menu_item: *Node.MenuItemEx = @alignCast(@fieldParentPtr("base", node));925 const menu_item: *const Node.MenuItemEx = @alignCast(@fieldParentPtr("base", node));
926 try writer.print(" {s} {s}\n", .{ menu_item.menuitem.slice(tree.source), menu_item.text.slice(tree.source) });926 try writer.print(" {s} {s}\n", .{ menu_item.menuitem.slice(tree.source), menu_item.text.slice(tree.source) });
927 inline for (.{ "id", "type", "state" }) |arg| {927 inline for (.{ "id", "type", "state" }) |arg| {
928 if (@field(menu_item, arg)) |val_node| {928 if (@field(menu_item, arg)) |val_node| {
...@@ -933,7 +933,7 @@ pub const Node = struct {...@@ -933,7 +933,7 @@ pub const Node = struct {
933 }933 }
934 },934 },
935 .popup => {935 .popup => {
936 const popup: *Node.Popup = @alignCast(@fieldParentPtr("base", node));936 const popup: *const Node.Popup = @alignCast(@fieldParentPtr("base", node));
937 try writer.print(" {s} {s} [{d} options]\n", .{ popup.popup.slice(tree.source), popup.text.slice(tree.source), popup.option_list.len });937 try writer.print(" {s} {s} [{d} options]\n", .{ popup.popup.slice(tree.source), popup.text.slice(tree.source), popup.option_list.len });
938 try writer.writeByteNTimes(' ', indent);938 try writer.writeByteNTimes(' ', indent);
939 try writer.writeAll(popup.begin_token.slice(tree.source));939 try writer.writeAll(popup.begin_token.slice(tree.source));
...@@ -946,7 +946,7 @@ pub const Node = struct {...@@ -946,7 +946,7 @@ pub const Node = struct {
946 try writer.writeAll("\n");946 try writer.writeAll("\n");
947 },947 },
948 .popup_ex => {948 .popup_ex => {
949 const popup: *Node.PopupEx = @alignCast(@fieldParentPtr("base", node));949 const popup: *const Node.PopupEx = @alignCast(@fieldParentPtr("base", node));
950 try writer.print(" {s} {s}\n", .{ popup.popup.slice(tree.source), popup.text.slice(tree.source) });950 try writer.print(" {s} {s}\n", .{ popup.popup.slice(tree.source), popup.text.slice(tree.source) });
951 inline for (.{ "id", "type", "state", "help_id" }) |arg| {951 inline for (.{ "id", "type", "state", "help_id" }) |arg| {
952 if (@field(popup, arg)) |val_node| {952 if (@field(popup, arg)) |val_node| {
...@@ -966,7 +966,7 @@ pub const Node = struct {...@@ -966,7 +966,7 @@ pub const Node = struct {
966 try writer.writeAll("\n");966 try writer.writeAll("\n");
967 },967 },
968 .version_info => {968 .version_info => {
969 const version_info: *Node.VersionInfo = @alignCast(@fieldParentPtr("base", node));969 const version_info: *const Node.VersionInfo = @alignCast(@fieldParentPtr("base", node));
970 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ version_info.id.slice(tree.source), version_info.versioninfo.slice(tree.source), version_info.common_resource_attributes.len });970 try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ version_info.id.slice(tree.source), version_info.versioninfo.slice(tree.source), version_info.common_resource_attributes.len });
971 for (version_info.fixed_info) |fixed_info| {971 for (version_info.fixed_info) |fixed_info| {
972 try fixed_info.dump(tree, writer, indent + 1);972 try fixed_info.dump(tree, writer, indent + 1);
...@@ -982,14 +982,14 @@ pub const Node = struct {...@@ -982,14 +982,14 @@ pub const Node = struct {
982 try writer.writeAll("\n");982 try writer.writeAll("\n");
983 },983 },
984 .version_statement => {984 .version_statement => {
985 const version_statement: *Node.VersionStatement = @alignCast(@fieldParentPtr("base", node));985 const version_statement: *const Node.VersionStatement = @alignCast(@fieldParentPtr("base", node));
986 try writer.print(" {s}\n", .{version_statement.type.slice(tree.source)});986 try writer.print(" {s}\n", .{version_statement.type.slice(tree.source)});
987 for (version_statement.parts) |part| {987 for (version_statement.parts) |part| {
988 try part.dump(tree, writer, indent + 1);988 try part.dump(tree, writer, indent + 1);
989 }989 }
990 },990 },
991 .block => {991 .block => {
992 const block: *Node.Block = @alignCast(@fieldParentPtr("base", node));992 const block: *const Node.Block = @alignCast(@fieldParentPtr("base", node));
993 try writer.print(" {s} {s}\n", .{ block.identifier.slice(tree.source), block.key.slice(tree.source) });993 try writer.print(" {s} {s}\n", .{ block.identifier.slice(tree.source), block.key.slice(tree.source) });
994 for (block.values) |value| {994 for (block.values) |value| {
995 try value.dump(tree, writer, indent + 1);995 try value.dump(tree, writer, indent + 1);
...@@ -1005,14 +1005,14 @@ pub const Node = struct {...@@ -1005,14 +1005,14 @@ pub const Node = struct {
1005 try writer.writeAll("\n");1005 try writer.writeAll("\n");
1006 },1006 },
1007 .block_value => {1007 .block_value => {
1008 const block_value: *Node.BlockValue = @alignCast(@fieldParentPtr("base", node));1008 const block_value: *const Node.BlockValue = @alignCast(@fieldParentPtr("base", node));
1009 try writer.print(" {s} {s}\n", .{ block_value.identifier.slice(tree.source), block_value.key.slice(tree.source) });1009 try writer.print(" {s} {s}\n", .{ block_value.identifier.slice(tree.source), block_value.key.slice(tree.source) });
1010 for (block_value.values) |value| {1010 for (block_value.values) |value| {
1011 try value.dump(tree, writer, indent + 1);1011 try value.dump(tree, writer, indent + 1);
1012 }1012 }
1013 },1013 },
1014 .block_value_value => {1014 .block_value_value => {
1015 const block_value: *Node.BlockValueValue = @alignCast(@fieldParentPtr("base", node));1015 const block_value: *const Node.BlockValueValue = @alignCast(@fieldParentPtr("base", node));
1016 if (block_value.trailing_comma) {1016 if (block_value.trailing_comma) {
1017 try writer.writeAll(" ,");1017 try writer.writeAll(" ,");
1018 }1018 }
...@@ -1020,7 +1020,7 @@ pub const Node = struct {...@@ -1020,7 +1020,7 @@ pub const Node = struct {
1020 try block_value.expression.dump(tree, writer, indent + 1);1020 try block_value.expression.dump(tree, writer, indent + 1);
1021 },1021 },
1022 .string_table => {1022 .string_table => {
1023 const string_table: *Node.StringTable = @alignCast(@fieldParentPtr("base", node));1023 const string_table: *const Node.StringTable = @alignCast(@fieldParentPtr("base", node));
1024 try writer.print(" {s} [{d} common_resource_attributes]\n", .{ string_table.type.slice(tree.source), string_table.common_resource_attributes.len });1024 try writer.print(" {s} [{d} common_resource_attributes]\n", .{ string_table.type.slice(tree.source), string_table.common_resource_attributes.len });
1025 for (string_table.optional_statements) |statement| {1025 for (string_table.optional_statements) |statement| {
1026 try statement.dump(tree, writer, indent + 1);1026 try statement.dump(tree, writer, indent + 1);
...@@ -1037,19 +1037,19 @@ pub const Node = struct {...@@ -1037,19 +1037,19 @@ pub const Node = struct {
1037 },1037 },
1038 .string_table_string => {1038 .string_table_string => {
1039 try writer.writeAll("\n");1039 try writer.writeAll("\n");
1040 const string: *Node.StringTableString = @alignCast(@fieldParentPtr("base", node));1040 const string: *const Node.StringTableString = @alignCast(@fieldParentPtr("base", node));
1041 try string.id.dump(tree, writer, indent + 1);1041 try string.id.dump(tree, writer, indent + 1);
1042 try writer.writeByteNTimes(' ', indent + 1);1042 try writer.writeByteNTimes(' ', indent + 1);
1043 try writer.print("{s}\n", .{string.string.slice(tree.source)});1043 try writer.print("{s}\n", .{string.string.slice(tree.source)});
1044 },1044 },
1045 .language_statement => {1045 .language_statement => {
1046 const language: *Node.LanguageStatement = @alignCast(@fieldParentPtr("base", node));1046 const language: *const Node.LanguageStatement = @alignCast(@fieldParentPtr("base", node));
1047 try writer.print(" {s}\n", .{language.language_token.slice(tree.source)});1047 try writer.print(" {s}\n", .{language.language_token.slice(tree.source)});
1048 try language.primary_language_id.dump(tree, writer, indent + 1);1048 try language.primary_language_id.dump(tree, writer, indent + 1);
1049 try language.sublanguage_id.dump(tree, writer, indent + 1);1049 try language.sublanguage_id.dump(tree, writer, indent + 1);
1050 },1050 },
1051 .font_statement => {1051 .font_statement => {
1052 const font: *Node.FontStatement = @alignCast(@fieldParentPtr("base", node));1052 const font: *const Node.FontStatement = @alignCast(@fieldParentPtr("base", node));
1053 try writer.print(" {s} typeface: {s}\n", .{ font.identifier.slice(tree.source), font.typeface.slice(tree.source) });1053 try writer.print(" {s} typeface: {s}\n", .{ font.identifier.slice(tree.source), font.typeface.slice(tree.source) });
1054 try writer.writeByteNTimes(' ', indent + 1);1054 try writer.writeByteNTimes(' ', indent + 1);
1055 try writer.writeAll("point_size:\n");1055 try writer.writeAll("point_size:\n");
...@@ -1063,12 +1063,12 @@ pub const Node = struct {...@@ -1063,12 +1063,12 @@ pub const Node = struct {
1063 }1063 }
1064 },1064 },
1065 .simple_statement => {1065 .simple_statement => {
1066 const statement: *Node.SimpleStatement = @alignCast(@fieldParentPtr("base", node));1066 const statement: *const Node.SimpleStatement = @alignCast(@fieldParentPtr("base", node));
1067 try writer.print(" {s}\n", .{statement.identifier.slice(tree.source)});1067 try writer.print(" {s}\n", .{statement.identifier.slice(tree.source)});
1068 try statement.value.dump(tree, writer, indent + 1);1068 try statement.value.dump(tree, writer, indent + 1);
1069 },1069 },
1070 .invalid => {1070 .invalid => {
1071 const invalid: *Node.Invalid = @alignCast(@fieldParentPtr("base", node));1071 const invalid: *const Node.Invalid = @alignCast(@fieldParentPtr("base", node));
1072 try writer.print(" context.len: {}\n", .{invalid.context.len});1072 try writer.print(" context.len: {}\n", .{invalid.context.len});
1073 for (invalid.context) |context_token| {1073 for (invalid.context) |context_token| {
1074 try writer.writeByteNTimes(' ', indent + 1);1074 try writer.writeByteNTimes(' ', indent + 1);
lib/compiler/resinator/bmp.zig+10-3
...@@ -60,9 +60,16 @@ pub const BitmapInfo = struct {...@@ -60,9 +60,16 @@ pub const BitmapInfo = struct {
60 }60 }
6161
62 pub fn getBitmasksByteLen(self: *const BitmapInfo) u8 {62 pub fn getBitmasksByteLen(self: *const BitmapInfo) u8 {
63 return switch (self.compression) {63 // Only BITMAPINFOHEADER (3.1) has trailing bytes for the BITFIELDS
64 .BI_BITFIELDS => 12,64 // The 2.0 format doesn't have a compression field and 4.0+ has dedicated
65 .BI_ALPHABITFIELDS => 16,65 // fields for the masks in the header.
66 const dib_version = BitmapHeader.Version.get(self.dib_header_size);
67 return switch (dib_version) {
68 .@"nt3.1" => switch (self.compression) {
69 .BI_BITFIELDS => 12,
70 .BI_ALPHABITFIELDS => 16,
71 else => 0,
72 },
66 else => 0,73 else => 0,
67 };74 };
68 }75 }
lib/compiler/resinator/cli.zig+92-35
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1const std = @import("std");1const std = @import("std");
2const CodePage = @import("code_pages.zig").CodePage;2const code_pages = @import("code_pages.zig");
3const SupportedCodePage = code_pages.SupportedCodePage;
3const lang = @import("lang.zig");4const lang = @import("lang.zig");
4const res = @import("res.zig");5const res = @import("res.zig");
5const Allocator = std.mem.Allocator;6const Allocator = std.mem.Allocator;
...@@ -14,6 +15,8 @@ pub const usage_string_after_command_name =...@@ -14,6 +15,8 @@ pub const usage_string_after_command_name =
14 \\The sequence -- can be used to signify when to stop parsing options.15 \\The sequence -- can be used to signify when to stop parsing options.
15 \\This is necessary when the input path begins with a forward slash.16 \\This is necessary when the input path begins with a forward slash.
16 \\17 \\
18 \\Supported option prefixes are /, -, and --, so e.g. /h, -h, and --h all work.
19 \\
17 \\Supported Win32 RC Options:20 \\Supported Win32 RC Options:
18 \\ /?, /h Print this help and exit.21 \\ /?, /h Print this help and exit.
19 \\ /v Verbose (print progress messages).22 \\ /v Verbose (print progress messages).
...@@ -56,8 +59,6 @@ pub const usage_string_after_command_name =...@@ -56,8 +59,6 @@ pub const usage_string_after_command_name =
56 \\ the .rc includes or otherwise depends on.59 \\ the .rc includes or otherwise depends on.
57 \\ /:depfile-fmt <value> Output format of the depfile, if /:depfile is set.60 \\ /:depfile-fmt <value> Output format of the depfile, if /:depfile is set.
58 \\ json (default) A top-level JSON array of paths61 \\ json (default) A top-level JSON array of paths
59 \\ /:mingw-includes <path> Path to a directory containing MinGW include files. If
60 \\ not specified, bundled MinGW include files will be used.
61 \\62 \\
62 \\Note: For compatibility reasons, all custom options start with :63 \\Note: For compatibility reasons, all custom options start with :
63 \\64 \\
...@@ -136,7 +137,7 @@ pub const Options = struct {...@@ -136,7 +137,7 @@ pub const Options = struct {
136 ignore_include_env_var: bool = false,137 ignore_include_env_var: bool = false,
137 preprocess: Preprocess = .yes,138 preprocess: Preprocess = .yes,
138 default_language_id: ?u16 = null,139 default_language_id: ?u16 = null,
139 default_code_page: ?CodePage = null,140 default_code_page: ?SupportedCodePage = null,
140 verbose: bool = false,141 verbose: bool = false,
141 symbols: std.StringArrayHashMapUnmanaged(SymbolValue) = .empty,142 symbols: std.StringArrayHashMapUnmanaged(SymbolValue) = .empty,
142 null_terminate_string_table_strings: bool = false,143 null_terminate_string_table_strings: bool = false,
...@@ -148,7 +149,6 @@ pub const Options = struct {...@@ -148,7 +149,6 @@ pub const Options = struct {
148 auto_includes: AutoIncludes = .any,149 auto_includes: AutoIncludes = .any,
149 depfile_path: ?[]const u8 = null,150 depfile_path: ?[]const u8 = null,
150 depfile_fmt: DepfileFormat = .json,151 depfile_fmt: DepfileFormat = .json,
151 mingw_includes_dir: ?[]const u8 = null,
152152
153 pub const AutoIncludes = enum { any, msvc, gnu, none };153 pub const AutoIncludes = enum { any, msvc, gnu, none };
154 pub const DepfileFormat = enum { json };154 pub const DepfileFormat = enum { json };
...@@ -243,9 +243,6 @@ pub const Options = struct {...@@ -243,9 +243,6 @@ pub const Options = struct {
243 if (self.depfile_path) |depfile_path| {243 if (self.depfile_path) |depfile_path| {
244 self.allocator.free(depfile_path);244 self.allocator.free(depfile_path);
245 }245 }
246 if (self.mingw_includes_dir) |mingw_includes_dir| {
247 self.allocator.free(mingw_includes_dir);
248 }
249 }246 }
250247
251 pub fn dumpVerbose(self: *const Options, writer: anytype) !void {248 pub fn dumpVerbose(self: *const Options, writer: anytype) !void {
...@@ -358,6 +355,29 @@ pub const Arg = struct {...@@ -358,6 +355,29 @@ pub const Arg = struct {
358 };355 };
359 }356 }
360357
358 pub fn looksLikeFilepath(self: Arg) bool {
359 const meets_min_requirements = self.prefix == .slash and isSupportedInputExtension(std.fs.path.extension(self.full));
360 if (!meets_min_requirements) return false;
361
362 const could_be_fo_option = could_be_fo_option: {
363 var window_it = std.mem.window(u8, self.full[1..], 2, 1);
364 while (window_it.next()) |window| {
365 if (std.ascii.eqlIgnoreCase(window, "fo")) break :could_be_fo_option true;
366 // If we see '/' before "fo", then it's not possible for this to be a valid
367 // `/fo` option.
368 if (window[0] == '/') break;
369 }
370 break :could_be_fo_option false;
371 };
372 if (!could_be_fo_option) return true;
373
374 // It's still possible for a file path to look like a /fo option but not actually
375 // be one, e.g. `/foo/bar.rc`. As a last ditch effort to reduce false negatives,
376 // check if the file path exists and, if so, then we ignore the 'could be /fo option'-ness
377 std.fs.accessAbsolute(self.full, .{}) catch return false;
378 return true;
379 }
380
361 pub const Value = struct {381 pub const Value = struct {
362 slice: []const u8,382 slice: []const u8,
363 index_increment: u2 = 1,383 index_increment: u2 = 1,
...@@ -432,6 +452,16 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn...@@ -432,6 +452,16 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn
432 }452 }
433 }453 }
434454
455 const args_remaining = args.len - arg_i;
456 if (args_remaining <= 2 and arg.looksLikeFilepath()) {
457 var err_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = true, .arg_index = arg_i };
458 var msg_writer = err_details.msg.writer(allocator);
459 try msg_writer.writeAll("this argument was inferred to be a filepath, so argument parsing was terminated");
460 try diagnostics.append(err_details);
461
462 break;
463 }
464
435 while (arg.name().len > 0) {465 while (arg.name().len > 0) {
436 const arg_name = arg.name();466 const arg_name = arg.name();
437 // Note: These cases should be in order from longest to shortest, since467 // Note: These cases should be in order from longest to shortest, since
...@@ -440,24 +470,6 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn...@@ -440,24 +470,6 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn
440 if (std.ascii.startsWithIgnoreCase(arg_name, ":no-preprocess")) {470 if (std.ascii.startsWithIgnoreCase(arg_name, ":no-preprocess")) {
441 options.preprocess = .no;471 options.preprocess = .no;
442 arg.name_offset += ":no-preprocess".len;472 arg.name_offset += ":no-preprocess".len;
443 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":mingw-includes")) {
444 const value = arg.value(":mingw-includes".len, arg_i, args) catch {
445 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
446 var msg_writer = err_details.msg.writer(allocator);
447 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":mingw-includes".len) });
448 try diagnostics.append(err_details);
449 arg_i += 1;
450 break :next_arg;
451 };
452 if (options.mingw_includes_dir) |overwritten_path| {
453 allocator.free(overwritten_path);
454 options.mingw_includes_dir = null;
455 }
456 const path = try allocator.dupe(u8, value.slice);
457 errdefer allocator.free(path);
458 options.mingw_includes_dir = path;
459 arg_i += value.index_increment;
460 continue :next_arg;
461 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":auto-includes")) {473 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":auto-includes")) {
462 const value = arg.value(":auto-includes".len, arg_i, args) catch {474 const value = arg.value(":auto-includes".len, arg_i, args) catch {
463 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };475 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
...@@ -769,7 +781,7 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn...@@ -769,7 +781,7 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn
769 arg_i += value.index_increment;781 arg_i += value.index_increment;
770 continue :next_arg;782 continue :next_arg;
771 };783 };
772 options.default_code_page = CodePage.getByIdentifierEnsureSupported(code_page_id) catch |err| switch (err) {784 options.default_code_page = code_pages.getByIdentifierEnsureSupported(code_page_id) catch |err| switch (err) {
773 error.InvalidCodePage => {785 error.InvalidCodePage => {
774 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };786 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
775 var msg_writer = err_details.msg.writer(allocator);787 var msg_writer = err_details.msg.writer(allocator);
...@@ -782,7 +794,7 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn...@@ -782,7 +794,7 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn
782 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };794 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
783 var msg_writer = err_details.msg.writer(allocator);795 var msg_writer = err_details.msg.writer(allocator);
784 try msg_writer.print("unsupported code page: {s} (id={})", .{796 try msg_writer.print("unsupported code page: {s} (id={})", .{
785 @tagName(CodePage.getByIdentifier(code_page_id) catch unreachable),797 @tagName(code_pages.getByIdentifier(code_page_id) catch unreachable),
786 code_page_id,798 code_page_id,
787 });799 });
788 try diagnostics.append(err_details);800 try diagnostics.append(err_details);
...@@ -900,18 +912,20 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn...@@ -900,18 +912,20 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn
900912
901 const positionals = args[arg_i..];913 const positionals = args[arg_i..];
902914
903 if (positionals.len < 1) {915 if (positionals.len == 0) {
904 var err_details = Diagnostics.ErrorDetails{ .print_args = false, .arg_index = arg_i };916 var err_details = Diagnostics.ErrorDetails{ .print_args = false, .arg_index = arg_i };
905 var msg_writer = err_details.msg.writer(allocator);917 var msg_writer = err_details.msg.writer(allocator);
906 try msg_writer.writeAll("missing input filename");918 try msg_writer.writeAll("missing input filename");
907 try diagnostics.append(err_details);919 try diagnostics.append(err_details);
908920
909 const last_arg = args[args.len - 1];921 if (args.len > 0) {
910 if (arg_i > 0 and last_arg.len > 0 and last_arg[0] == '/' and std.ascii.endsWithIgnoreCase(last_arg, ".rc")) {922 const last_arg = args[args.len - 1];
911 var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = true, .arg_index = arg_i - 1 };923 if (arg_i > 0 and last_arg.len > 0 and last_arg[0] == '/' and std.ascii.endsWithIgnoreCase(last_arg, ".rc")) {
912 var note_writer = note_details.msg.writer(allocator);924 var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = true, .arg_index = arg_i - 1 };
913 try note_writer.writeAll("if this argument was intended to be the input filename, then -- should be specified in front of it to exclude it from option parsing");925 var note_writer = note_details.msg.writer(allocator);
914 try diagnostics.append(note_details);926 try note_writer.writeAll("if this argument was intended to be the input filename, then -- should be specified in front of it to exclude it from option parsing");
927 try diagnostics.append(note_details);
928 }
915 }929 }
916930
917 // This is a fatal enough problem to justify an early return, since931 // This is a fatal enough problem to justify an early return, since
...@@ -969,6 +983,12 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn...@@ -969,6 +983,12 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn
969 return options;983 return options;
970}984}
971985
986pub fn isSupportedInputExtension(ext: []const u8) bool {
987 if (std.ascii.eqlIgnoreCase(ext, ".rc")) return true;
988 if (std.ascii.eqlIgnoreCase(ext, ".rcpp")) return true;
989 return false;
990}
991
972/// Returns true if the str is a valid C identifier for use in a #define/#undef macro992/// Returns true if the str is a valid C identifier for use in a #define/#undef macro
973pub fn isValidIdentifier(str: []const u8) bool {993pub fn isValidIdentifier(str: []const u8) bool {
974 for (str, 0..) |c, i| switch (c) {994 for (str, 0..) |c, i| switch (c) {
...@@ -1271,6 +1291,43 @@ test "parse errors: basic" {...@@ -1271,6 +1291,43 @@ test "parse errors: basic" {
1271 );1291 );
1272}1292}
12731293
1294test "inferred absolute filepaths" {
1295 {
1296 var options = try testParseWarning(&.{ "/fo", "foo.res", "/home/absolute/path.rc" },
1297 \\<cli>: note: this argument was inferred to be a filepath, so argument parsing was terminated
1298 \\ ... /home/absolute/path.rc
1299 \\ ^~~~~~~~~~~~~~~~~~~~~~
1300 \\
1301 );
1302 defer options.deinit();
1303 }
1304 {
1305 var options = try testParseWarning(&.{ "/home/absolute/path.rc", "foo.res" },
1306 \\<cli>: note: this argument was inferred to be a filepath, so argument parsing was terminated
1307 \\ ... /home/absolute/path.rc ...
1308 \\ ^~~~~~~~~~~~~~~~~~~~~~
1309 \\
1310 );
1311 defer options.deinit();
1312 }
1313 {
1314 // Only the last two arguments are checked, so the /h is parsed as an option
1315 var options = try testParse(&.{ "/home/absolute/path.rc", "foo.rc", "foo.res" });
1316 defer options.deinit();
1317
1318 try std.testing.expect(options.print_help_and_exit);
1319 }
1320 {
1321 var options = try testParse(&.{ "/xvFO/some/absolute/path.res", "foo.rc" });
1322 defer options.deinit();
1323
1324 try std.testing.expectEqual(true, options.verbose);
1325 try std.testing.expectEqual(true, options.ignore_include_env_var);
1326 try std.testing.expectEqualStrings("foo.rc", options.input_source.filename);
1327 try std.testing.expectEqualStrings("/some/absolute/path.res", options.output_source.filename);
1328 }
1329}
1330
1274test "parse errors: /ln" {1331test "parse errors: /ln" {
1275 try testParseError(&.{ "/ln", "invalid", "foo.rc" },1332 try testParseError(&.{ "/ln", "invalid", "foo.rc" },
1276 \\<cli>: error: invalid language tag: invalid1333 \\<cli>: error: invalid language tag: invalid
lib/compiler/resinator/code_pages.zig+78-139
...@@ -1,86 +1,30 @@...@@ -1,86 +1,30 @@
1const std = @import("std");1const std = @import("std");
2const windows1252 = @import("windows1252.zig");2const windows1252 = @import("windows1252.zig");
33
4// TODO: Parts of this comment block may be more relevant to string/NameOrOrdinal parsing
5// than it is to the stuff in this file.
6//
7// ‰ representations for context:
8// Win-1252 89
9// UTF-8 E2 80 B0
10// UTF-16 20 30
11//
12// With code page 65001:
13// ‰ RCDATA { "‰" L"‰" }
14// File encoded as Windows-1252:
15// ‰ => <U+FFFD REPLACEMENT CHARACTER> as u16
16// "‰" => 0x3F ('?')
17// L"‰" => <U+FFFD REPLACEMENT CHARACTER> as u16
18// File encoded as UTF-8:
19// ‰ => <U+2030 ‰> as u16
20// "‰" => 0x89 ('‰' encoded as Windows-1252)
21// L"‰" => <U+2030 ‰> as u16
22//
23// With code page 1252:
24// ‰ RCDATA { "‰" L"‰" }
25// File encoded as Windows-1252:
26// ‰ => <U+2030 ‰> as u16
27// "‰" => 0x89 ('‰' encoded as Windows-1252)
28// L"‰" => <U+2030 ‰> as u16
29// File encoded as UTF-8:
30// ‰ => 0xE2 as u16, 0x20AC as u16, 0xB0 as u16
31// ^ first byte of utf8 representation
32// ^ second byte of UTF-8 representation (0x80), but interpretted as
33// Windows-1252 ('€') and then converted to UTF-16 (<U+20AC>)
34// ^ third byte of utf8 representation
35// "‰" => 0xE2, 0x80, 0xB0 (the bytes of the UTF-8 representation)
36// L"‰" => 0xE2 as u16, 0x20AC as u16, 0xB0 as u16 (see '‰ =>' explanation)
37//
38// With code page 1252:
39// <0x90> RCDATA { "<0x90>" L"<0x90>" }
40// File encoded as Windows-1252:
41// <0x90> => 0x90 as u16
42// "<0x90>" => 0x90
43// L"<0x90>" => 0x90 as u16
44// File encoded as UTF-8:
45// <0x90> => 0xC2 as u16, 0x90 as u16
46// "<0x90>" => 0xC2, 0x90 (the bytes of the UTF-8 representation of <U+0090>)
47// L"<0x90>" => 0xC2 as u16, 0x90 as u16
48//
49// Within a raw data block, file encoded as Windows-1252 (Â is <0xC2>):
50// "Âa" L"Âa" "\xC2ad" L"\xC2AD"
51// With code page 1252:
52// C2 61 C2 00 61 00 C2 61 64 AD C2
53// Â^ a^ Â~~~^ a~~~^ .^ a^ d^ ^~~~~\xC2AD
54// \xC2~`
55// With code page 65001:
56// 3F 61 FD FF 61 00 C2 61 64 AD C2
57// ^. a^ ^~~~. a~~~^ ^. a^ d^ ^~~~~\xC2AD
58// `. `. `~\xC2
59// `. `.~<0xC2>a is not well-formed UTF-8 (0xC2 expects a continutation byte after it).
60// `. Because 'a' is a valid first byte of a UTF-8 sequence, it is not included in the
61// `. invalid sequence so only the <0xC2> gets converted to <U+FFFD>.
62// `~Same as ^ but converted to '?' instead.
63//
64// Within a raw data block, file encoded as Windows-1252 (ð is <0xF0>, € is <0x80>):
65// "ð€a" L"ð€a"
66// With code page 1252:
67// F0 80 61 F0 00 AC 20 61 00
68// ð^ €^ a^ ð~~~^ €~~~^ a~~~^
69// With code page 65001:
70// 3F 61 FD FF 61 00
71// ^. a^ ^~~~. a~~~^
72// `. `.
73// `. `.~<0xF0><0x80> is not well-formed UTF-8, and <0x80> is not a valid first byte, so
74// `. both bytes are considered an invalid sequence and get converted to '<U+FFFD>'
75// `~Same as ^ but converted to '?' instead.
76
77/// https://learn.microsoft.com/en-us/windows/win32/intl/code-page-identifiers4/// https://learn.microsoft.com/en-us/windows/win32/intl/code-page-identifiers
78pub const CodePage = enum(u16) {5pub const SupportedCodePage = enum(u16) {
79 // supported
80 windows1252 = 1252, // windows-1252 ANSI Latin 1; Western European (Windows)6 windows1252 = 1252, // windows-1252 ANSI Latin 1; Western European (Windows)
81 utf8 = 65001, // utf-8 Unicode (UTF-8)7 utf8 = 65001, // utf-8 Unicode (UTF-8)
828
83 // unsupported but valid9 pub fn codepointAt(code_page: SupportedCodePage, index: usize, bytes: []const u8) ?Codepoint {
10 if (index >= bytes.len) return null;
11 switch (code_page) {
12 .windows1252 => {
13 // All byte values have a representation, so just convert the byte
14 return Codepoint{
15 .value = windows1252.toCodepoint(bytes[index]),
16 .byte_len = 1,
17 };
18 },
19 .utf8 => {
20 return Utf8.WellFormedDecoder.decode(bytes[index..]);
21 },
22 }
23 }
24};
25
26/// https://learn.microsoft.com/en-us/windows/win32/intl/code-page-identifiers
27pub const UnsupportedCodePage = enum(u16) {
84 ibm037 = 37, // IBM037 IBM EBCDIC US-Canada28 ibm037 = 37, // IBM037 IBM EBCDIC US-Canada
85 ibm437 = 437, // IBM437 OEM United States29 ibm437 = 437, // IBM437 OEM United States
86 ibm500 = 500, // IBM500 IBM EBCDIC International30 ibm500 = 500, // IBM500 IBM EBCDIC International
...@@ -231,50 +175,45 @@ pub const CodePage = enum(u16) {...@@ -231,50 +175,45 @@ pub const CodePage = enum(u16) {
231 x_iscii_gu = 57010, // x-iscii-gu ISCII Gujarati175 x_iscii_gu = 57010, // x-iscii-gu ISCII Gujarati
232 x_iscii_pa = 57011, // x-iscii-pa ISCII Punjabi176 x_iscii_pa = 57011, // x-iscii-pa ISCII Punjabi
233 utf7 = 65000, // utf-7 Unicode (UTF-7)177 utf7 = 65000, // utf-7 Unicode (UTF-7)
178};
234179
235 pub fn codepointAt(code_page: CodePage, index: usize, bytes: []const u8) ?Codepoint {180pub const CodePage = blk: {
236 if (index >= bytes.len) return null;181 const fields = @typeInfo(SupportedCodePage).@"enum".fields ++ @typeInfo(UnsupportedCodePage).@"enum".fields;
237 switch (code_page) {182 break :blk @Type(.{ .@"enum" = .{
238 .windows1252 => {183 .tag_type = u16,
239 // All byte values have a representation, so just convert the byte184 .decls = &.{},
240 return Codepoint{185 .fields = fields,
241 .value = windows1252.toCodepoint(bytes[index]),186 .is_exhaustive = true,
242 .byte_len = 1,187 } });
243 };188};
244 },
245 .utf8 => {
246 return Utf8.WellFormedDecoder.decode(bytes[index..]);
247 },
248 else => unreachable,
249 }
250 }
251
252 pub fn isSupported(code_page: CodePage) bool {
253 return switch (code_page) {
254 .windows1252, .utf8 => true,
255 else => false,
256 };
257 }
258189
259 pub fn getByIdentifier(identifier: u16) !CodePage {190pub fn isSupported(code_page: CodePage) bool {
260 // There's probably a more efficient way to do this (e.g. ComptimeHashMap?) but191 inline for (@typeInfo(SupportedCodePage).@"enum".fields) |enumField| {
261 // this should be fine, especially since this function likely won't be called much.192 if (@intFromEnum(code_page) == @intFromEnum(@field(SupportedCodePage, enumField.name))) {
262 inline for (@typeInfo(CodePage).@"enum".fields) |enumField| {193 return true;
263 if (identifier == enumField.value) {
264 return @field(CodePage, enumField.name);
265 }
266 }194 }
267 return error.InvalidCodePage;
268 }195 }
196 return false;
197}
269198
270 pub fn getByIdentifierEnsureSupported(identifier: u16) !CodePage {199pub fn getByIdentifier(identifier: u16) !CodePage {
271 const code_page = try getByIdentifier(identifier);200 // There's probably a more efficient way to do this (e.g. ComptimeHashMap?) but
272 switch (isSupported(code_page)) {201 // this should be fine, especially since this function likely won't be called much.
273 true => return code_page,202 inline for (@typeInfo(CodePage).@"enum".fields) |enumField| {
274 false => return error.UnsupportedCodePage,203 if (identifier == enumField.value) {
204 return @field(CodePage, enumField.name);
275 }205 }
276 }206 }
277};207 return error.InvalidCodePage;
208}
209
210pub fn getByIdentifierEnsureSupported(identifier: u16) !SupportedCodePage {
211 const code_page = try getByIdentifier(identifier);
212 return if (isSupported(code_page))
213 @enumFromInt(@intFromEnum(code_page))
214 else
215 error.UnsupportedCodePage;
216}
278217
279pub const Utf8 = struct {218pub const Utf8 = struct {
280 /// Implements decoding with rejection of ill-formed UTF-8 sequences based on section219 /// Implements decoding with rejection of ill-formed UTF-8 sequences based on section
...@@ -378,20 +317,20 @@ test "codepointAt invalid utf8" {...@@ -378,20 +317,20 @@ test "codepointAt invalid utf8" {
378 try std.testing.expectEqual(Codepoint{317 try std.testing.expectEqual(Codepoint{
379 .value = Codepoint.invalid,318 .value = Codepoint.invalid,
380 .byte_len = 1,319 .byte_len = 1,
381 }, CodePage.utf8.codepointAt(0, invalid_utf8).?);320 }, SupportedCodePage.utf8.codepointAt(0, invalid_utf8).?);
382 try std.testing.expectEqual(Codepoint{321 try std.testing.expectEqual(Codepoint{
383 .value = Codepoint.invalid,322 .value = Codepoint.invalid,
384 .byte_len = 2,323 .byte_len = 2,
385 }, CodePage.utf8.codepointAt(1, invalid_utf8).?);324 }, SupportedCodePage.utf8.codepointAt(1, invalid_utf8).?);
386 try std.testing.expectEqual(Codepoint{325 try std.testing.expectEqual(Codepoint{
387 .value = Codepoint.invalid,326 .value = Codepoint.invalid,
388 .byte_len = 1,327 .byte_len = 1,
389 }, CodePage.utf8.codepointAt(3, invalid_utf8).?);328 }, SupportedCodePage.utf8.codepointAt(3, invalid_utf8).?);
390 try std.testing.expectEqual(Codepoint{329 try std.testing.expectEqual(Codepoint{
391 .value = Codepoint.invalid,330 .value = Codepoint.invalid,
392 .byte_len = 1,331 .byte_len = 1,
393 }, CodePage.utf8.codepointAt(4, invalid_utf8).?);332 }, SupportedCodePage.utf8.codepointAt(4, invalid_utf8).?);
394 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(5, invalid_utf8));333 try std.testing.expectEqual(@as(?Codepoint, null), SupportedCodePage.utf8.codepointAt(5, invalid_utf8));
395 }334 }
396335
397 {336 {
...@@ -399,12 +338,12 @@ test "codepointAt invalid utf8" {...@@ -399,12 +338,12 @@ test "codepointAt invalid utf8" {
399 try std.testing.expectEqual(Codepoint{338 try std.testing.expectEqual(Codepoint{
400 .value = Codepoint.invalid,339 .value = Codepoint.invalid,
401 .byte_len = 2,340 .byte_len = 2,
402 }, CodePage.utf8.codepointAt(0, invalid_utf8).?);341 }, SupportedCodePage.utf8.codepointAt(0, invalid_utf8).?);
403 try std.testing.expectEqual(Codepoint{342 try std.testing.expectEqual(Codepoint{
404 .value = Codepoint.invalid,343 .value = Codepoint.invalid,
405 .byte_len = 1,344 .byte_len = 1,
406 }, CodePage.utf8.codepointAt(2, invalid_utf8).?);345 }, SupportedCodePage.utf8.codepointAt(2, invalid_utf8).?);
407 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(3, invalid_utf8));346 try std.testing.expectEqual(@as(?Codepoint, null), SupportedCodePage.utf8.codepointAt(3, invalid_utf8));
408 }347 }
409348
410 {349 {
...@@ -412,8 +351,8 @@ test "codepointAt invalid utf8" {...@@ -412,8 +351,8 @@ test "codepointAt invalid utf8" {
412 try std.testing.expectEqual(Codepoint{351 try std.testing.expectEqual(Codepoint{
413 .value = Codepoint.invalid,352 .value = Codepoint.invalid,
414 .byte_len = 1,353 .byte_len = 1,
415 }, CodePage.utf8.codepointAt(0, invalid_utf8).?);354 }, SupportedCodePage.utf8.codepointAt(0, invalid_utf8).?);
416 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(1, invalid_utf8));355 try std.testing.expectEqual(@as(?Codepoint, null), SupportedCodePage.utf8.codepointAt(1, invalid_utf8));
417 }356 }
418357
419 {358 {
...@@ -421,8 +360,8 @@ test "codepointAt invalid utf8" {...@@ -421,8 +360,8 @@ test "codepointAt invalid utf8" {
421 try std.testing.expectEqual(Codepoint{360 try std.testing.expectEqual(Codepoint{
422 .value = Codepoint.invalid,361 .value = Codepoint.invalid,
423 .byte_len = 2,362 .byte_len = 2,
424 }, CodePage.utf8.codepointAt(0, invalid_utf8).?);363 }, SupportedCodePage.utf8.codepointAt(0, invalid_utf8).?);
425 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(2, invalid_utf8));364 try std.testing.expectEqual(@as(?Codepoint, null), SupportedCodePage.utf8.codepointAt(2, invalid_utf8));
426 }365 }
427366
428 {367 {
...@@ -430,12 +369,12 @@ test "codepointAt invalid utf8" {...@@ -430,12 +369,12 @@ test "codepointAt invalid utf8" {
430 try std.testing.expectEqual(Codepoint{369 try std.testing.expectEqual(Codepoint{
431 .value = Codepoint.invalid,370 .value = Codepoint.invalid,
432 .byte_len = 1,371 .byte_len = 1,
433 }, CodePage.utf8.codepointAt(0, invalid_utf8).?);372 }, SupportedCodePage.utf8.codepointAt(0, invalid_utf8).?);
434 try std.testing.expectEqual(Codepoint{373 try std.testing.expectEqual(Codepoint{
435 .value = Codepoint.invalid,374 .value = Codepoint.invalid,
436 .byte_len = 1,375 .byte_len = 1,
437 }, CodePage.utf8.codepointAt(1, invalid_utf8).?);376 }, SupportedCodePage.utf8.codepointAt(1, invalid_utf8).?);
438 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(2, invalid_utf8));377 try std.testing.expectEqual(@as(?Codepoint, null), SupportedCodePage.utf8.codepointAt(2, invalid_utf8));
439 }378 }
440379
441 {380 {
...@@ -444,11 +383,11 @@ test "codepointAt invalid utf8" {...@@ -444,11 +383,11 @@ test "codepointAt invalid utf8" {
444 try std.testing.expectEqual(Codepoint{383 try std.testing.expectEqual(Codepoint{
445 .value = Codepoint.invalid,384 .value = Codepoint.invalid,
446 .byte_len = 2,385 .byte_len = 2,
447 }, CodePage.utf8.codepointAt(0, invalid_utf8).?);386 }, SupportedCodePage.utf8.codepointAt(0, invalid_utf8).?);
448 try std.testing.expectEqual(Codepoint{387 try std.testing.expectEqual(Codepoint{
449 .value = Codepoint.invalid,388 .value = Codepoint.invalid,
450 .byte_len = 1,389 .byte_len = 1,
451 }, CodePage.utf8.codepointAt(2, invalid_utf8).?);390 }, SupportedCodePage.utf8.codepointAt(2, invalid_utf8).?);
452 }391 }
453}392}
454393
...@@ -459,19 +398,19 @@ test "codepointAt utf8 encoded" {...@@ -459,19 +398,19 @@ test "codepointAt utf8 encoded" {
459 try std.testing.expectEqual(Codepoint{398 try std.testing.expectEqual(Codepoint{
460 .value = '²',399 .value = '²',
461 .byte_len = 2,400 .byte_len = 2,
462 }, CodePage.utf8.codepointAt(0, utf8_encoded).?);401 }, SupportedCodePage.utf8.codepointAt(0, utf8_encoded).?);
463 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.utf8.codepointAt(2, utf8_encoded));402 try std.testing.expectEqual(@as(?Codepoint, null), SupportedCodePage.utf8.codepointAt(2, utf8_encoded));
464403
465 // with code page windows1252404 // with code page windows1252
466 try std.testing.expectEqual(Codepoint{405 try std.testing.expectEqual(Codepoint{
467 .value = '\xC2',406 .value = '\xC2',
468 .byte_len = 1,407 .byte_len = 1,
469 }, CodePage.windows1252.codepointAt(0, utf8_encoded).?);408 }, SupportedCodePage.windows1252.codepointAt(0, utf8_encoded).?);
470 try std.testing.expectEqual(Codepoint{409 try std.testing.expectEqual(Codepoint{
471 .value = '\xB2',410 .value = '\xB2',
472 .byte_len = 1,411 .byte_len = 1,
473 }, CodePage.windows1252.codepointAt(1, utf8_encoded).?);412 }, SupportedCodePage.windows1252.codepointAt(1, utf8_encoded).?);
474 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(2, utf8_encoded));413 try std.testing.expectEqual(@as(?Codepoint, null), SupportedCodePage.windows1252.codepointAt(2, utf8_encoded));
475}414}
476415
477test "codepointAt windows1252 encoded" {416test "codepointAt windows1252 encoded" {
...@@ -481,15 +420,15 @@ test "codepointAt windows1252 encoded" {...@@ -481,15 +420,15 @@ test "codepointAt windows1252 encoded" {
481 try std.testing.expectEqual(Codepoint{420 try std.testing.expectEqual(Codepoint{
482 .value = Codepoint.invalid,421 .value = Codepoint.invalid,
483 .byte_len = 1,422 .byte_len = 1,
484 }, CodePage.utf8.codepointAt(0, windows1252_encoded).?);423 }, SupportedCodePage.utf8.codepointAt(0, windows1252_encoded).?);
485 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.utf8.codepointAt(2, windows1252_encoded));424 try std.testing.expectEqual(@as(?Codepoint, null), SupportedCodePage.utf8.codepointAt(2, windows1252_encoded));
486425
487 // with code page windows1252426 // with code page windows1252
488 try std.testing.expectEqual(Codepoint{427 try std.testing.expectEqual(Codepoint{
489 .value = '\xB2',428 .value = '\xB2',
490 .byte_len = 1,429 .byte_len = 1,
491 }, CodePage.windows1252.codepointAt(0, windows1252_encoded).?);430 }, SupportedCodePage.windows1252.codepointAt(0, windows1252_encoded).?);
492 try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(1, windows1252_encoded));431 try std.testing.expectEqual(@as(?Codepoint, null), SupportedCodePage.windows1252.codepointAt(1, windows1252_encoded));
493}432}
494433
495pub const Codepoint = struct {434pub const Codepoint = struct {
lib/compiler/resinator/comments.zig+25
...@@ -174,6 +174,21 @@ pub fn removeComments(source: []const u8, buf: []u8, source_mappings: ?*SourceMa...@@ -174,6 +174,21 @@ pub fn removeComments(source: []const u8, buf: []u8, source_mappings: ?*SourceMa
174 },174 },
175 },175 },
176 }176 }
177 } else {
178 switch (state) {
179 .start,
180 .line_comment,
181 .multiline_comment,
182 .multiline_comment_end,
183 .single_quoted,
184 .single_quoted_escape,
185 .double_quoted,
186 .double_quoted_escape,
187 => {},
188 .forward_slash => {
189 result.writeSlice(source[pending_start.?..index]);
190 },
191 }
177 }192 }
178 return result.getWritten();193 return result.getWritten();
179}194}
...@@ -334,6 +349,16 @@ test "comments appended to a line" {...@@ -334,6 +349,16 @@ test "comments appended to a line" {
334 );349 );
335}350}
336351
352test "forward slash only" {
353 try testRemoveComments(
354 \\ /
355 \\/
356 ,
357 \\ /
358 \\/
359 );
360}
361
337test "remove comments with mappings" {362test "remove comments with mappings" {
338 const allocator = std.testing.allocator;363 const allocator = std.testing.allocator;
339 var mut_source = "blah/*\rcommented line*\r/blah".*;364 var mut_source = "blah/*\rcommented line*\r/blah".*;
lib/compiler/resinator/compile.zig+165-147
...@@ -4,7 +4,7 @@ const Allocator = std.mem.Allocator;...@@ -4,7 +4,7 @@ const Allocator = std.mem.Allocator;
4const Node = @import("ast.zig").Node;4const Node = @import("ast.zig").Node;
5const lex = @import("lex.zig");5const lex = @import("lex.zig");
6const Parser = @import("parse.zig").Parser;6const Parser = @import("parse.zig").Parser;
7const Resource = @import("rc.zig").Resource;7const ResourceType = @import("rc.zig").ResourceType;
8const Token = @import("lex.zig").Token;8const Token = @import("lex.zig").Token;
9const literals = @import("literals.zig");9const literals = @import("literals.zig");
10const Number = literals.Number;10const Number = literals.Number;
...@@ -21,7 +21,7 @@ const WORD = std.os.windows.WORD;...@@ -21,7 +21,7 @@ const WORD = std.os.windows.WORD;
21const DWORD = std.os.windows.DWORD;21const DWORD = std.os.windows.DWORD;
22const utils = @import("utils.zig");22const utils = @import("utils.zig");
23const NameOrOrdinal = res.NameOrOrdinal;23const NameOrOrdinal = res.NameOrOrdinal;
24const CodePage = @import("code_pages.zig").CodePage;24const SupportedCodePage = @import("code_pages.zig").SupportedCodePage;
25const CodePageLookup = @import("ast.zig").CodePageLookup;25const CodePageLookup = @import("ast.zig").CodePageLookup;
26const SourceMappings = @import("source_mapping.zig").SourceMappings;26const SourceMappings = @import("source_mapping.zig").SourceMappings;
27const windows1252 = @import("windows1252.zig");27const windows1252 = @import("windows1252.zig");
...@@ -39,7 +39,10 @@ pub const CompileOptions = struct {...@@ -39,7 +39,10 @@ pub const CompileOptions = struct {
39 /// freed by the caller.39 /// freed by the caller.
40 /// TODO: Maybe a dedicated struct for this purpose so that it's a bit nicer to work with.40 /// TODO: Maybe a dedicated struct for this purpose so that it's a bit nicer to work with.
41 dependencies_list: ?*std.ArrayList([]const u8) = null,41 dependencies_list: ?*std.ArrayList([]const u8) = null,
42 default_code_page: CodePage = .windows1252,42 default_code_page: SupportedCodePage = .windows1252,
43 /// If true, the first #pragma code_page directive only sets the input code page, but not the output code page.
44 /// This check must be done before comments are removed from the file.
45 disjoint_code_page: bool = false,
43 ignore_include_env_var: bool = false,46 ignore_include_env_var: bool = false,
44 extra_include_paths: []const []const u8 = &.{},47 extra_include_paths: []const []const u8 = &.{},
45 /// This is just an API convenience to allow separately passing 'system' (i.e. those48 /// This is just an API convenience to allow separately passing 'system' (i.e. those
...@@ -66,6 +69,7 @@ pub fn compile(allocator: Allocator, source: []const u8, writer: anytype, option...@@ -66,6 +69,7 @@ pub fn compile(allocator: Allocator, source: []const u8, writer: anytype, option
66 });69 });
67 var parser = Parser.init(&lexer, .{70 var parser = Parser.init(&lexer, .{
68 .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page,71 .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page,
72 .disjoint_code_page = options.disjoint_code_page,
69 });73 });
70 var tree = try parser.parse(allocator, options.diagnostics);74 var tree = try parser.parse(allocator, options.diagnostics);
71 defer tree.deinit();75 defer tree.deinit();
...@@ -98,6 +102,7 @@ pub fn compile(allocator: Allocator, source: []const u8, writer: anytype, option...@@ -98,6 +102,7 @@ pub fn compile(allocator: Allocator, source: []const u8, writer: anytype, option
98 .end = 0,102 .end = 0,
99 .line_number = 1,103 .line_number = 1,
100 },104 },
105 .code_page = .utf8,
101 .print_source_line = false,106 .print_source_line = false,
102 .extra = .{ .file_open_error = .{107 .extra = .{ .file_open_error = .{
103 .err = ErrorDetails.FileOpenError.enumFromError(err),108 .err = ErrorDetails.FileOpenError.enumFromError(err),
...@@ -213,7 +218,12 @@ pub const Compiler = struct {...@@ -213,7 +218,12 @@ pub const Compiler = struct {
213 try self.addErrorDetails(.{218 try self.addErrorDetails(.{
214 .err = .result_contains_fontdir,219 .err = .result_contains_fontdir,
215 .type = .hint,220 .type = .hint,
216 .token = undefined,221 .token = .{
222 .id = .invalid,
223 .start = 0,
224 .end = 0,
225 .line_number = 1,
226 },
217 });227 });
218 }228 }
219 // once we've written every else out, we can write out the finalized STRINGTABLE resources229 // once we've written every else out, we can write out the finalized STRINGTABLE resources
...@@ -301,7 +311,10 @@ pub const Compiler = struct {...@@ -301,7 +311,10 @@ pub const Compiler = struct {
301 // UTF-8, we can parse either string type directly to UTF-8.311 // UTF-8, we can parse either string type directly to UTF-8.
302 var parser = literals.IterativeStringParser.init(bytes, .{312 var parser = literals.IterativeStringParser.init(bytes, .{
303 .start_column = column,313 .start_column = column,
304 .diagnostics = .{ .diagnostics = self.diagnostics, .token = literal_node.token },314 .diagnostics = self.errContext(literal_node.token),
315 // TODO: Re-evaluate this. It's not been tested whether or not using the actual
316 // output code page would make more sense.
317 .output_code_page = .windows1252,
305 });318 });
306319
307 while (try parser.nextUnchecked()) |parsed| {320 while (try parser.nextUnchecked()) |parsed| {
...@@ -401,56 +414,55 @@ pub const Compiler = struct {...@@ -401,56 +414,55 @@ pub const Compiler = struct {
401 return first_error orelse error.FileNotFound;414 return first_error orelse error.FileNotFound;
402 }415 }
403416
417 /// Returns a Windows-1252 encoded string regardless of the current output code page.
418 /// All codepoints are encoded as a maximum of 2 bytes, where unescaped codepoints
419 /// >= 0x10000 are encoded as `??` and everything else is encoded as 1 byte.
404 pub fn parseDlgIncludeString(self: *Compiler, token: Token) ![]u8 {420 pub fn parseDlgIncludeString(self: *Compiler, token: Token) ![]u8 {
405 // For the purposes of parsing, we want to strip the L prefix421 const bytes = self.sourceBytesForToken(token);
406 // if it exists since we want escaped integers to be limited to422 const output_code_page = self.output_code_pages.getForToken(token);
407 // their ascii string range.
408 //
409 // We keep track of whether or not there was an L prefix, though,
410 // since there's more weirdness to come.
411 var bytes = self.sourceBytesForToken(token);
412 var was_wide_string = false;
413 if (bytes.slice[0] == 'L' or bytes.slice[0] == 'l') {
414 was_wide_string = true;
415 bytes.slice = bytes.slice[1..];
416 }
417423
418 var buf = try std.ArrayList(u8).initCapacity(self.allocator, bytes.slice.len);424 var buf = try std.ArrayList(u8).initCapacity(self.allocator, bytes.slice.len);
419 errdefer buf.deinit();425 errdefer buf.deinit();
420426
421 var iterative_parser = literals.IterativeStringParser.init(bytes, .{427 var iterative_parser = literals.IterativeStringParser.init(bytes, .{
422 .start_column = token.calculateColumn(self.source, 8, null),428 .start_column = token.calculateColumn(self.source, 8, null),
423 .diagnostics = .{ .diagnostics = self.diagnostics, .token = token },429 .diagnostics = self.errContext(token),
430 // TODO: Potentially re-evaluate this, it's not been tested whether or not
431 // using the actual output code page would make more sense.
432 .output_code_page = .windows1252,
424 });433 });
425434
426 // No real idea what's going on here, but this matches the rc.exe behavior435 // This is similar to the logic in parseQuotedString, but ends up with everything
436 // encoded as Windows-1252. This effectively consolidates the two-step process
437 // of rc.exe into one step, since rc.exe's preprocessor converts to UTF-16 (this
438 // is when invalid sequences are replaced by the replacement character (U+FFFD)),
439 // and then that's run through the parser. Our preprocessor keeps things in their
440 // original encoding, meaning we emulate the <encoding> -> UTF-16 -> Windows-1252
441 // results all at once.
427 while (try iterative_parser.next()) |parsed| {442 while (try iterative_parser.next()) |parsed| {
428 const c = parsed.codepoint;443 const c = parsed.codepoint;
429 switch (was_wide_string) {444 switch (iterative_parser.declared_string_type) {
430 true => {445 .wide => {
431 switch (c) {446 if (windows1252.bestFitFromCodepoint(c)) |best_fit| {
432 0...0x7F, 0xA0...0xFF => try buf.append(@intCast(c)),447 try buf.append(best_fit);
433 0x80...0x9F => {448 } else if (c < 0x10000 or c == code_pages.Codepoint.invalid or parsed.escaped_surrogate_pair) {
434 if (windows1252.bestFitFromCodepoint(c)) |_| {449 try buf.append('?');
435 try buf.append(@intCast(c));450 } else {
436 } else {451 try buf.appendSlice("??");
437 try buf.append('?');
438 }
439 },
440 else => {
441 if (windows1252.bestFitFromCodepoint(c)) |best_fit| {
442 try buf.append(best_fit);
443 } else if (c < 0x10000 or c == code_pages.Codepoint.invalid) {
444 try buf.append('?');
445 } else {
446 try buf.appendSlice("??");
447 }
448 },
449 }452 }
450 },453 },
451 false => {454 .ascii => {
452 if (parsed.from_escaped_integer) {455 if (parsed.from_escaped_integer) {
453 try buf.append(@truncate(c));456 const truncated: u8 = @truncate(c);
457 switch (output_code_page) {
458 .utf8 => switch (truncated) {
459 0...0x7F => try buf.append(truncated),
460 else => try buf.append('?'),
461 },
462 .windows1252 => {
463 try buf.append(truncated);
464 },
465 }
454 } else {466 } else {
455 if (windows1252.bestFitFromCodepoint(c)) |best_fit| {467 if (windows1252.bestFitFromCodepoint(c)) |best_fit| {
456 try buf.append(best_fit);468 try buf.append(best_fit);
...@@ -484,8 +496,12 @@ pub const Compiler = struct {...@@ -484,8 +496,12 @@ pub const Compiler = struct {
484 const parsed_filename_terminated = std.mem.sliceTo(parsed_filename, 0);496 const parsed_filename_terminated = std.mem.sliceTo(parsed_filename, 0);
485497
486 header.applyMemoryFlags(node.common_resource_attributes, self.source);498 header.applyMemoryFlags(node.common_resource_attributes, self.source);
499 // This is effectively limited by `max_string_literal_codepoints` which is a u15.
500 // Each codepoint within a DLGINCLUDE string is encoded as a maximum of
501 // 2 bytes, which means that the maximum byte length of a DLGINCLUDE string is
502 // (including the NUL terminator): 32,767 * 2 + 1 = 65,535 or exactly the u16 max.
487 header.data_size = @intCast(parsed_filename_terminated.len + 1);503 header.data_size = @intCast(parsed_filename_terminated.len + 1);
488 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });504 try header.write(writer, self.errContext(node.id));
489 try writer.writeAll(parsed_filename_terminated);505 try writer.writeAll(parsed_filename_terminated);
490 try writer.writeByte(0);506 try writer.writeByte(0);
491 try writeDataPadding(writer, header.data_size);507 try writeDataPadding(writer, header.data_size);
...@@ -568,7 +584,7 @@ pub const Compiler = struct {...@@ -568,7 +584,7 @@ pub const Compiler = struct {
568 header.applyMemoryFlags(node.common_resource_attributes, self.source);584 header.applyMemoryFlags(node.common_resource_attributes, self.source);
569 header.data_size = @intCast(try file.getEndPos());585 header.data_size = @intCast(try file.getEndPos());
570586
571 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });587 try header.write(writer, self.errContext(node.id));
572 try file.seekTo(0);588 try file.seekTo(0);
573 try writeResourceData(writer, file.reader(), header.data_size);589 try writeResourceData(writer, file.reader(), header.data_size);
574 return;590 return;
...@@ -644,7 +660,7 @@ pub const Compiler = struct {...@@ -644,7 +660,7 @@ pub const Compiler = struct {
644 .version = self.state.version,660 .version = self.state.version,
645 .characteristics = self.state.characteristics,661 .characteristics = self.state.characteristics,
646 };662 };
647 try image_header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });663 try image_header.write(writer, self.errContext(node.id));
648664
649 // From https://learn.microsoft.com/en-us/windows/win32/menurc/localheader:665 // From https://learn.microsoft.com/en-us/windows/win32/menurc/localheader:
650 // > The LOCALHEADER structure is the first data written to the RT_CURSOR666 // > The LOCALHEADER structure is the first data written to the RT_CURSOR
...@@ -817,12 +833,26 @@ pub const Compiler = struct {...@@ -817,12 +833,26 @@ pub const Compiler = struct {
817833
818 header.data_size = icon_dir.getResDataSize();834 header.data_size = icon_dir.getResDataSize();
819835
820 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });836 try header.write(writer, self.errContext(node.id));
821 try icon_dir.writeResData(writer, first_icon_id);837 try icon_dir.writeResData(writer, first_icon_id);
822 try writeDataPadding(writer, header.data_size);838 try writeDataPadding(writer, header.data_size);
823 return;839 return;
824 },840 },
825 .RCDATA, .HTML, .MANIFEST, .MESSAGETABLE, .DLGINIT, .PLUGPLAY => {841 .RCDATA,
842 .HTML,
843 .MESSAGETABLE,
844 .DLGINIT,
845 .PLUGPLAY,
846 .VXD,
847 // Note: All of the below can only be specified by using a number
848 // as the resource type.
849 .MANIFEST,
850 .CURSOR,
851 .ICON,
852 .ANICURSOR,
853 .ANIICON,
854 .FONTDIR,
855 => {
826 header.applyMemoryFlags(node.common_resource_attributes, self.source);856 header.applyMemoryFlags(node.common_resource_attributes, self.source);
827 },857 },
828 .BITMAP => {858 .BITMAP => {
...@@ -855,47 +885,32 @@ pub const Compiler = struct {...@@ -855,47 +885,32 @@ pub const Compiler = struct {
855 } else if (bitmap_info.getActualPaletteByteLen() < bitmap_info.getExpectedPaletteByteLen()) {885 } else if (bitmap_info.getActualPaletteByteLen() < bitmap_info.getExpectedPaletteByteLen()) {
856 const num_padding_bytes = bitmap_info.getExpectedPaletteByteLen() - bitmap_info.getActualPaletteByteLen();886 const num_padding_bytes = bitmap_info.getExpectedPaletteByteLen() - bitmap_info.getActualPaletteByteLen();
857887
858 // TODO: Make this configurable (command line option)
859 const max_missing_bytes = 4096;
860 if (num_padding_bytes > max_missing_bytes) {
861 var numbers_as_bytes: [16]u8 = undefined;
862 std.mem.writeInt(u64, numbers_as_bytes[0..8], num_padding_bytes, native_endian);
863 std.mem.writeInt(u64, numbers_as_bytes[8..16], max_missing_bytes, native_endian);
864 const values_string_index = try self.diagnostics.putString(&numbers_as_bytes);
865 try self.addErrorDetails(.{
866 .err = .bmp_too_many_missing_palette_bytes,
867 .token = filename_token,
868 .extra = .{ .number = values_string_index },
869 });
870 return self.addErrorDetailsAndFail(.{
871 .err = .bmp_too_many_missing_palette_bytes,
872 .type = .note,
873 .print_source_line = false,
874 .token = filename_token,
875 });
876 }
877
878 var number_as_bytes: [8]u8 = undefined;888 var number_as_bytes: [8]u8 = undefined;
879 std.mem.writeInt(u64, &number_as_bytes, num_padding_bytes, native_endian);889 std.mem.writeInt(u64, &number_as_bytes, num_padding_bytes, native_endian);
880 const value_string_index = try self.diagnostics.putString(&number_as_bytes);890 const value_string_index = try self.diagnostics.putString(&number_as_bytes);
881 try self.addErrorDetails(.{891 try self.addErrorDetails(.{
882 .err = .bmp_missing_palette_bytes,892 .err = .bmp_missing_palette_bytes,
883 .type = .warning,893 .type = .err,
884 .token = filename_token,894 .token = filename_token,
885 .extra = .{ .number = value_string_index },895 .extra = .{ .number = value_string_index },
886 });896 });
887 const pixel_data_len = bitmap_info.getPixelDataLen(file_size);897 const pixel_data_len = bitmap_info.getPixelDataLen(file_size);
898 // TODO: This is a hack, but we know we have already added
899 // at least one entry to the diagnostics strings, so we can
900 // get away with using 0 to mean 'no string' here.
901 var miscompiled_bytes_string_index: u32 = 0;
888 if (pixel_data_len > 0) {902 if (pixel_data_len > 0) {
889 const miscompiled_bytes = @min(pixel_data_len, num_padding_bytes);903 const miscompiled_bytes = @min(pixel_data_len, num_padding_bytes);
890 std.mem.writeInt(u64, &number_as_bytes, miscompiled_bytes, native_endian);904 std.mem.writeInt(u64, &number_as_bytes, miscompiled_bytes, native_endian);
891 const miscompiled_bytes_string_index = try self.diagnostics.putString(&number_as_bytes);905 miscompiled_bytes_string_index = try self.diagnostics.putString(&number_as_bytes);
892 try self.addErrorDetails(.{
893 .err = .rc_would_miscompile_bmp_palette_padding,
894 .type = .warning,
895 .token = filename_token,
896 .extra = .{ .number = miscompiled_bytes_string_index },
897 });
898 }906 }
907 return self.addErrorDetailsAndFail(.{
908 .err = .rc_would_miscompile_bmp_palette_padding,
909 .type = .note,
910 .print_source_line = false,
911 .token = filename_token,
912 .extra = .{ .number = miscompiled_bytes_string_index },
913 });
899 }914 }
900915
901 // TODO: It might be possible that the calculation done in this function916 // TODO: It might be possible that the calculation done in this function
...@@ -905,7 +920,7 @@ pub const Compiler = struct {...@@ -905,7 +920,7 @@ pub const Compiler = struct {
905 const bmp_bytes_to_write: u32 = @intCast(bitmap_info.getExpectedByteLen(file_size));920 const bmp_bytes_to_write: u32 = @intCast(bitmap_info.getExpectedByteLen(file_size));
906921
907 header.data_size = bmp_bytes_to_write;922 header.data_size = bmp_bytes_to_write;
908 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });923 try header.write(writer, self.errContext(node.id));
909 try file.seekTo(bmp.file_header_len);924 try file.seekTo(bmp.file_header_len);
910 const file_reader = file.reader();925 const file_reader = file.reader();
911 try writeResourceDataNoPadding(writer, file_reader, bitmap_info.dib_header_size);926 try writeResourceDataNoPadding(writer, file_reader, bitmap_info.dib_header_size);
...@@ -914,12 +929,6 @@ pub const Compiler = struct {...@@ -914,12 +929,6 @@ pub const Compiler = struct {
914 }929 }
915 if (bitmap_info.getExpectedPaletteByteLen() > 0) {930 if (bitmap_info.getExpectedPaletteByteLen() > 0) {
916 try writeResourceDataNoPadding(writer, file_reader, @intCast(bitmap_info.getActualPaletteByteLen()));931 try writeResourceDataNoPadding(writer, file_reader, @intCast(bitmap_info.getActualPaletteByteLen()));
917 // We know that the number of missing palette bytes is <= 4096
918 // (see `bmp_too_many_missing_palette_bytes` error case above)
919 const padding_bytes: usize = @intCast(bitmap_info.getMissingPaletteByteLen());
920 if (padding_bytes > 0) {
921 try writer.writeByteNTimes(0, padding_bytes);
922 }
923 }932 }
924 try file.seekTo(bitmap_info.pixel_data_offset);933 try file.seekTo(bitmap_info.pixel_data_offset);
925 const pixel_bytes: u32 = @intCast(file_size - bitmap_info.pixel_data_offset);934 const pixel_bytes: u32 = @intCast(file_size - bitmap_info.pixel_data_offset);
...@@ -932,13 +941,13 @@ pub const Compiler = struct {...@@ -932,13 +941,13 @@ pub const Compiler = struct {
932 // Add warning and skip this resource941 // Add warning and skip this resource
933 // Note: The Win32 compiler prints this as an error but it doesn't fail the compilation942 // Note: The Win32 compiler prints this as an error but it doesn't fail the compilation
934 // and the duplicate resource is skipped.943 // and the duplicate resource is skipped.
935 try self.addErrorDetails(ErrorDetails{944 try self.addErrorDetails(.{
936 .err = .font_id_already_defined,945 .err = .font_id_already_defined,
937 .token = node.id,946 .token = node.id,
938 .type = .warning,947 .type = .warning,
939 .extra = .{ .number = header.name_value.ordinal },948 .extra = .{ .number = header.name_value.ordinal },
940 });949 });
941 try self.addErrorDetails(ErrorDetails{950 try self.addErrorDetails(.{
942 .err = .font_id_already_defined,951 .err = .font_id_already_defined,
943 .token = self.state.font_dir.ids.get(header.name_value.ordinal).?,952 .token = self.state.font_dir.ids.get(header.name_value.ordinal).?,
944 .type = .note,953 .type = .note,
...@@ -957,7 +966,7 @@ pub const Compiler = struct {...@@ -957,7 +966,7 @@ pub const Compiler = struct {
957966
958 // We now know that the data size will fit in a u32967 // We now know that the data size will fit in a u32
959 header.data_size = @intCast(file_size);968 header.data_size = @intCast(file_size);
960 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });969 try header.write(writer, self.errContext(node.id));
961970
962 var header_slurping_reader = headerSlurpingReader(148, file.reader());971 var header_slurping_reader = headerSlurpingReader(148, file.reader());
963 try writeResourceData(writer, header_slurping_reader.reader(), header.data_size);972 try writeResourceData(writer, header_slurping_reader.reader(), header.data_size);
...@@ -968,19 +977,13 @@ pub const Compiler = struct {...@@ -968,19 +977,13 @@ pub const Compiler = struct {
968 }, node.id);977 }, node.id);
969 return;978 return;
970 },979 },
971 .ACCELERATOR,980 .ACCELERATOR, // Cannot use an external file, enforced by the parser
972 .ANICURSOR,981 .DIALOG, // Cannot use an external file, enforced by the parser
973 .ANIICON,982 .DLGINCLUDE, // Handled specially above
974 .CURSOR,983 .MENU, // Cannot use an external file, enforced by the parser
975 .DIALOG,984 .STRING, // Parser error if this resource is specified as a number
976 .DLGINCLUDE,985 .TOOLBAR, // Cannot use an external file, enforced by the parser
977 .FONTDIR,986 .VERSION, // Cannot use an external file, enforced by the parser
978 .ICON,
979 .MENU,
980 .STRING,
981 .TOOLBAR,
982 .VERSION,
983 .VXD,
984 => unreachable,987 => unreachable,
985 _ => unreachable,988 _ => unreachable,
986 }989 }
...@@ -998,7 +1001,7 @@ pub const Compiler = struct {...@@ -998,7 +1001,7 @@ pub const Compiler = struct {
998 }1001 }
999 // We now know that the data size will fit in a u321002 // We now know that the data size will fit in a u32
1000 header.data_size = @intCast(data_size);1003 header.data_size = @intCast(data_size);
1001 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });1004 try header.write(writer, self.errContext(node.id));
1002 try writeResourceData(writer, file.reader(), header.data_size);1005 try writeResourceData(writer, file.reader(), header.data_size);
1003 }1006 }
10041007
...@@ -1188,7 +1191,7 @@ pub const Compiler = struct {...@@ -1188,7 +1191,7 @@ pub const Compiler = struct {
1188 };1191 };
1189 const parsed = try literals.parseQuotedAsciiString(self.allocator, bytes, .{1192 const parsed = try literals.parseQuotedAsciiString(self.allocator, bytes, .{
1190 .start_column = column,1193 .start_column = column,
1191 .diagnostics = .{ .diagnostics = self.diagnostics, .token = literal_node.token },1194 .diagnostics = self.errContext(literal_node.token),
1192 .output_code_page = self.output_code_pages.getForToken(literal_node.token),1195 .output_code_page = self.output_code_pages.getForToken(literal_node.token),
1193 });1196 });
1194 errdefer self.allocator.free(parsed);1197 errdefer self.allocator.free(parsed);
...@@ -1202,7 +1205,8 @@ pub const Compiler = struct {...@@ -1202,7 +1205,8 @@ pub const Compiler = struct {
1202 };1205 };
1203 const parsed_string = try literals.parseQuotedWideString(self.allocator, bytes, .{1206 const parsed_string = try literals.parseQuotedWideString(self.allocator, bytes, .{
1204 .start_column = column,1207 .start_column = column,
1205 .diagnostics = .{ .diagnostics = self.diagnostics, .token = literal_node.token },1208 .diagnostics = self.errContext(literal_node.token),
1209 .output_code_page = self.output_code_pages.getForToken(literal_node.token),
1206 });1210 });
1207 errdefer self.allocator.free(parsed_string);1211 errdefer self.allocator.free(parsed_string);
1208 return .{ .wide_string = parsed_string };1212 return .{ .wide_string = parsed_string };
...@@ -1259,7 +1263,7 @@ pub const Compiler = struct {...@@ -1259,7 +1263,7 @@ pub const Compiler = struct {
12591263
1260 header.applyMemoryFlags(common_resource_attributes, self.source);1264 header.applyMemoryFlags(common_resource_attributes, self.source);
12611265
1262 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = id_token });1266 try header.write(writer, self.errContext(id_token));
1263 }1267 }
12641268
1265 pub fn writeResourceDataNoPadding(writer: anytype, data_reader: anytype, data_size: u32) !void {1269 pub fn writeResourceDataNoPadding(writer: anytype, data_reader: anytype, data_size: u32) !void {
...@@ -1297,7 +1301,8 @@ pub const Compiler = struct {...@@ -1297,7 +1301,8 @@ pub const Compiler = struct {
1297 const column = literal.token.calculateColumn(self.source, 8, null);1301 const column = literal.token.calculateColumn(self.source, 8, null);
1298 return res.parseAcceleratorKeyString(bytes, is_virt, .{1302 return res.parseAcceleratorKeyString(bytes, is_virt, .{
1299 .start_column = column,1303 .start_column = column,
1300 .diagnostics = .{ .diagnostics = self.diagnostics, .token = literal.token },1304 .diagnostics = self.errContext(literal.token),
1305 .output_code_page = self.output_code_pages.getForToken(literal.token),
1301 });1306 });
1302 }1307 }
1303 }1308 }
...@@ -1332,7 +1337,7 @@ pub const Compiler = struct {...@@ -1332,7 +1337,7 @@ pub const Compiler = struct {
1332 header.applyMemoryFlags(node.common_resource_attributes, self.source);1337 header.applyMemoryFlags(node.common_resource_attributes, self.source);
1333 header.applyOptionalStatements(node.optional_statements, self.source, self.input_code_pages);1338 header.applyOptionalStatements(node.optional_statements, self.source, self.input_code_pages);
13341339
1335 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });1340 try header.write(writer, self.errContext(node.id));
13361341
1337 var data_fbs = std.io.fixedBufferStream(data_buffer.items);1342 var data_fbs = std.io.fixedBufferStream(data_buffer.items);
1338 try writeResourceData(writer, data_fbs.reader(), data_size);1343 try writeResourceData(writer, data_fbs.reader(), data_size);
...@@ -1348,6 +1353,16 @@ pub const Compiler = struct {...@@ -1348,6 +1353,16 @@ pub const Compiler = struct {
1348 const modifier = rc.AcceleratorTypeAndOptions.map.get(type_or_option.slice(self.source)).?;1353 const modifier = rc.AcceleratorTypeAndOptions.map.get(type_or_option.slice(self.source)).?;
1349 modifiers.apply(modifier);1354 modifiers.apply(modifier);
1350 }1355 }
1356 if ((modifiers.isSet(.control) or modifiers.isSet(.shift)) and !modifiers.isSet(.virtkey)) {
1357 try self.addErrorDetails(.{
1358 .err = .accelerator_shift_or_control_without_virtkey,
1359 .type = .warning,
1360 // We know that one of SHIFT or CONTROL was specified, so there's at least one item
1361 // in this list.
1362 .token = accelerator.type_and_options[0],
1363 .token_span_end = accelerator.type_and_options[accelerator.type_and_options.len - 1],
1364 });
1365 }
1351 if (accelerator.event.isNumberExpression() and !modifiers.explicit_ascii_or_virtkey) {1366 if (accelerator.event.isNumberExpression() and !modifiers.explicit_ascii_or_virtkey) {
1352 return self.addErrorDetailsAndFail(.{1367 return self.addErrorDetailsAndFail(.{
1353 .err = .accelerator_type_required,1368 .err = .accelerator_type_required,
...@@ -1399,7 +1414,7 @@ pub const Compiler = struct {...@@ -1399,7 +1414,7 @@ pub const Compiler = struct {
1399 var limited_writer = limitedWriter(data_buffer.writer(), std.math.maxInt(u32));1414 var limited_writer = limitedWriter(data_buffer.writer(), std.math.maxInt(u32));
1400 const data_writer = limited_writer.writer();1415 const data_writer = limited_writer.writer();
14011416
1402 const resource = Resource.fromString(.{1417 const resource = ResourceType.fromString(.{
1403 .slice = node.type.slice(self.source),1418 .slice = node.type.slice(self.source),
1404 .code_page = self.input_code_pages.getForToken(node.type),1419 .code_page = self.input_code_pages.getForToken(node.type),
1405 });1420 });
...@@ -1414,8 +1429,6 @@ pub const Compiler = struct {...@@ -1414,8 +1429,6 @@ pub const Compiler = struct {
1414 menu.deinit(self.allocator);1429 menu.deinit(self.allocator);
1415 }1430 }
1416 }1431 }
1417 var skipped_menu_or_classes = std.ArrayList(*Node.SimpleStatement).init(self.allocator);
1418 defer skipped_menu_or_classes.deinit();
1419 var last_menu: *Node.SimpleStatement = undefined;1432 var last_menu: *Node.SimpleStatement = undefined;
1420 var last_class: *Node.SimpleStatement = undefined;1433 var last_class: *Node.SimpleStatement = undefined;
1421 var last_menu_would_be_forced_ordinal = false;1434 var last_menu_would_be_forced_ordinal = false;
...@@ -1445,9 +1458,6 @@ pub const Compiler = struct {...@@ -1445,9 +1458,6 @@ pub const Compiler = struct {
1445 },1458 },
1446 .class => {1459 .class => {
1447 const is_duplicate = optional_statement_values.class != null;1460 const is_duplicate = optional_statement_values.class != null;
1448 if (is_duplicate) {
1449 try skipped_menu_or_classes.append(last_class);
1450 }
1451 const forced_ordinal = is_duplicate and optional_statement_values.class.? == .ordinal;1461 const forced_ordinal = is_duplicate and optional_statement_values.class.? == .ordinal;
1452 // In the Win32 RC compiler, if any CLASS values that are interpreted as1462 // In the Win32 RC compiler, if any CLASS values that are interpreted as
1453 // an ordinal exist, it affects all future CLASS statements and forces1463 // an ordinal exist, it affects all future CLASS statements and forces
...@@ -1475,9 +1485,6 @@ pub const Compiler = struct {...@@ -1475,9 +1485,6 @@ pub const Compiler = struct {
1475 },1485 },
1476 .menu => {1486 .menu => {
1477 const is_duplicate = optional_statement_values.menu != null;1487 const is_duplicate = optional_statement_values.menu != null;
1478 if (is_duplicate) {
1479 try skipped_menu_or_classes.append(last_menu);
1480 }
1481 const forced_ordinal = is_duplicate and optional_statement_values.menu.? == .ordinal;1488 const forced_ordinal = is_duplicate and optional_statement_values.menu.? == .ordinal;
1482 // In the Win32 RC compiler, if any MENU values that are interpreted as1489 // In the Win32 RC compiler, if any MENU values that are interpreted as
1483 // an ordinal exist, it affects all future MENU statements and forces1490 // an ordinal exist, it affects all future MENU statements and forces
...@@ -1561,22 +1568,6 @@ pub const Compiler = struct {...@@ -1561,22 +1568,6 @@ pub const Compiler = struct {
1561 }1568 }
1562 }1569 }
15631570
1564 for (skipped_menu_or_classes.items) |simple_statement| {
1565 const statement_identifier = simple_statement.identifier;
1566 const statement_type = rc.OptionalStatements.dialog_map.get(statement_identifier.slice(self.source)) orelse continue;
1567 try self.addErrorDetails(.{
1568 .err = .duplicate_menu_or_class_skipped,
1569 .type = .warning,
1570 .token = simple_statement.identifier,
1571 .token_span_start = simple_statement.base.getFirstToken(),
1572 .token_span_end = simple_statement.base.getLastToken(),
1573 .extra = .{ .menu_or_class = switch (statement_type) {
1574 .menu => .menu,
1575 .class => .class,
1576 else => unreachable,
1577 } },
1578 });
1579 }
1580 // The Win32 RC compiler miscompiles the value in the following scenario:1571 // The Win32 RC compiler miscompiles the value in the following scenario:
1581 // Multiple CLASS parameters are specified and any of them are treated as a number, then1572 // Multiple CLASS parameters are specified and any of them are treated as a number, then
1582 // the last CLASS is always treated as a number no matter what1573 // the last CLASS is always treated as a number no matter what
...@@ -1739,7 +1730,7 @@ pub const Compiler = struct {...@@ -1739,7 +1730,7 @@ pub const Compiler = struct {
1739 header.applyMemoryFlags(node.common_resource_attributes, self.source);1730 header.applyMemoryFlags(node.common_resource_attributes, self.source);
1740 header.applyOptionalStatements(node.optional_statements, self.source, self.input_code_pages);1731 header.applyOptionalStatements(node.optional_statements, self.source, self.input_code_pages);
17411732
1742 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });1733 try header.write(writer, self.errContext(node.id));
17431734
1744 var data_fbs = std.io.fixedBufferStream(data_buffer.items);1735 var data_fbs = std.io.fixedBufferStream(data_buffer.items);
1745 try writeResourceData(writer, data_fbs.reader(), data_size);1736 try writeResourceData(writer, data_fbs.reader(), data_size);
...@@ -1749,7 +1740,7 @@ pub const Compiler = struct {...@@ -1749,7 +1740,7 @@ pub const Compiler = struct {
1749 self: *Compiler,1740 self: *Compiler,
1750 node: *Node.Dialog,1741 node: *Node.Dialog,
1751 data_writer: anytype,1742 data_writer: anytype,
1752 resource: Resource,1743 resource: ResourceType,
1753 optional_statement_values: *const DialogOptionalStatementValues,1744 optional_statement_values: *const DialogOptionalStatementValues,
1754 x: Number,1745 x: Number,
1755 y: Number,1746 y: Number,
...@@ -1809,7 +1800,7 @@ pub const Compiler = struct {...@@ -1809,7 +1800,7 @@ pub const Compiler = struct {
1809 self: *Compiler,1800 self: *Compiler,
1810 control: *Node.ControlStatement,1801 control: *Node.ControlStatement,
1811 data_writer: anytype,1802 data_writer: anytype,
1812 resource: Resource,1803 resource: ResourceType,
1813 bytes_written_so_far: u32,1804 bytes_written_so_far: u32,
1814 controls_by_id: *std.AutoHashMap(u32, *const Node.ControlStatement),1805 controls_by_id: *std.AutoHashMap(u32, *const Node.ControlStatement),
1815 ) !void {1806 ) !void {
...@@ -2053,7 +2044,7 @@ pub const Compiler = struct {...@@ -2053,7 +2044,7 @@ pub const Compiler = struct {
20532044
2054 header.applyMemoryFlags(node.common_resource_attributes, self.source);2045 header.applyMemoryFlags(node.common_resource_attributes, self.source);
20552046
2056 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });2047 try header.write(writer, self.errContext(node.id));
20572048
2058 var data_fbs = std.io.fixedBufferStream(data_buffer.items);2049 var data_fbs = std.io.fixedBufferStream(data_buffer.items);
2059 try writeResourceData(writer, data_fbs.reader(), data_size);2050 try writeResourceData(writer, data_fbs.reader(), data_size);
...@@ -2067,7 +2058,7 @@ pub const Compiler = struct {...@@ -2067,7 +2058,7 @@ pub const Compiler = struct {
2067 node: *Node.FontStatement,2058 node: *Node.FontStatement,
2068 };2059 };
20692060
2070 pub fn writeDialogFont(self: *Compiler, resource: Resource, values: FontStatementValues, writer: anytype) !void {2061 pub fn writeDialogFont(self: *Compiler, resource: ResourceType, values: FontStatementValues, writer: anytype) !void {
2071 const node = values.node;2062 const node = values.node;
2072 const point_size = evaluateNumberExpression(node.point_size, self.source, self.input_code_pages);2063 const point_size = evaluateNumberExpression(node.point_size, self.source, self.input_code_pages);
2073 try writer.writeInt(u16, point_size.asWord(), .little);2064 try writer.writeInt(u16, point_size.asWord(), .little);
...@@ -2104,7 +2095,7 @@ pub const Compiler = struct {...@@ -2104,7 +2095,7 @@ pub const Compiler = struct {
2104 .slice = node.type.slice(self.source),2095 .slice = node.type.slice(self.source),
2105 .code_page = self.input_code_pages.getForToken(node.type),2096 .code_page = self.input_code_pages.getForToken(node.type),
2106 };2097 };
2107 const resource = Resource.fromString(type_bytes);2098 const resource = ResourceType.fromString(type_bytes);
2108 std.debug.assert(resource == .menu or resource == .menuex);2099 std.debug.assert(resource == .menu or resource == .menuex);
21092100
2110 self.writeMenuData(node, data_writer, resource) catch |err| switch (err) {2101 self.writeMenuData(node, data_writer, resource) catch |err| switch (err) {
...@@ -2128,7 +2119,7 @@ pub const Compiler = struct {...@@ -2128,7 +2119,7 @@ pub const Compiler = struct {
2128 header.applyMemoryFlags(node.common_resource_attributes, self.source);2119 header.applyMemoryFlags(node.common_resource_attributes, self.source);
2129 header.applyOptionalStatements(node.optional_statements, self.source, self.input_code_pages);2120 header.applyOptionalStatements(node.optional_statements, self.source, self.input_code_pages);
21302121
2131 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });2122 try header.write(writer, self.errContext(node.id));
21322123
2133 var data_fbs = std.io.fixedBufferStream(data_buffer.items);2124 var data_fbs = std.io.fixedBufferStream(data_buffer.items);
2134 try writeResourceData(writer, data_fbs.reader(), data_size);2125 try writeResourceData(writer, data_fbs.reader(), data_size);
...@@ -2136,7 +2127,7 @@ pub const Compiler = struct {...@@ -2136,7 +2127,7 @@ pub const Compiler = struct {
21362127
2137 /// Expects `data_writer` to be a LimitedWriter limited to u32, meaning all writes to2128 /// Expects `data_writer` to be a LimitedWriter limited to u32, meaning all writes to
2138 /// the writer within this function could return error.NoSpaceLeft2129 /// the writer within this function could return error.NoSpaceLeft
2139 pub fn writeMenuData(self: *Compiler, node: *Node.Menu, data_writer: anytype, resource: Resource) !void {2130 pub fn writeMenuData(self: *Compiler, node: *Node.Menu, data_writer: anytype, resource: ResourceType) !void {
2140 // menu header2131 // menu header
2141 const version: u16 = if (resource == .menu) 0 else 1;2132 const version: u16 = if (resource == .menu) 0 else 1;
2142 try data_writer.writeInt(u16, version, .little);2133 try data_writer.writeInt(u16, version, .little);
...@@ -2393,7 +2384,7 @@ pub const Compiler = struct {...@@ -2393,7 +2384,7 @@ pub const Compiler = struct {
23932384
2394 header.applyMemoryFlags(node.common_resource_attributes, self.source);2385 header.applyMemoryFlags(node.common_resource_attributes, self.source);
23952386
2396 try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });2387 try header.write(writer, self.errContext(node.id));
23972388
2398 var data_fbs = std.io.fixedBufferStream(data_buffer.items);2389 var data_fbs = std.io.fixedBufferStream(data_buffer.items);
2399 try writeResourceData(writer, data_fbs.reader(), data_size);2390 try writeResourceData(writer, data_fbs.reader(), data_size);
...@@ -2525,14 +2516,14 @@ pub const Compiler = struct {...@@ -2525,14 +2516,14 @@ pub const Compiler = struct {
2525 // It might be nice to have these errors point to the ids rather than the2516 // It might be nice to have these errors point to the ids rather than the
2526 // string tokens, but that would mean storing the id token of each string2517 // string tokens, but that would mean storing the id token of each string
2527 // which doesn't seem worth it just for slightly better error messages.2518 // which doesn't seem worth it just for slightly better error messages.
2528 try self.addErrorDetails(ErrorDetails{2519 try self.addErrorDetails(.{
2529 .err = .string_already_defined,2520 .err = .string_already_defined,
2530 .token = string.string,2521 .token = string.string,
2531 .extra = .{ .string_and_language = .{ .id = string_id, .language = language } },2522 .extra = .{ .string_and_language = .{ .id = string_id, .language = language } },
2532 });2523 });
2533 const existing_def_table = self.state.string_tables.tables.getPtr(language).?;2524 const existing_def_table = self.state.string_tables.tables.getPtr(language).?;
2534 const existing_definition = existing_def_table.get(string_id).?;2525 const existing_definition = existing_def_table.get(string_id).?;
2535 return self.addErrorDetailsAndFail(ErrorDetails{2526 return self.addErrorDetailsAndFail(.{
2536 .err = .string_already_defined,2527 .err = .string_already_defined,
2537 .type = .note,2528 .type = .note,
2538 .token = existing_definition,2529 .token = existing_definition,
...@@ -2628,7 +2619,7 @@ pub const Compiler = struct {...@@ -2628,7 +2619,7 @@ pub const Compiler = struct {
26282619
2629 pub fn init(allocator: Allocator, id_bytes: SourceBytes, type_bytes: SourceBytes, data_size: DWORD, language: res.Language, version: DWORD, characteristics: DWORD) InitError!ResourceHeader {2620 pub fn init(allocator: Allocator, id_bytes: SourceBytes, type_bytes: SourceBytes, data_size: DWORD, language: res.Language, version: DWORD, characteristics: DWORD) InitError!ResourceHeader {
2630 const type_value = type: {2621 const type_value = type: {
2631 const resource_type = Resource.fromString(type_bytes);2622 const resource_type = ResourceType.fromString(type_bytes);
2632 if (res.RT.fromResource(resource_type)) |rt_constant| {2623 if (res.RT.fromResource(resource_type)) |rt_constant| {
2633 break :type NameOrOrdinal{ .ordinal = @intFromEnum(rt_constant) };2624 break :type NameOrOrdinal{ .ordinal = @intFromEnum(rt_constant) };
2634 } else {2625 } else {
...@@ -2673,7 +2664,7 @@ pub const Compiler = struct {...@@ -2673,7 +2664,7 @@ pub const Compiler = struct {
2673 padding_after_name: u2,2664 padding_after_name: u2,
2674 };2665 };
26752666
2676 fn calcSize(self: ResourceHeader) error{Overflow}!SizeInfo {2667 pub fn calcSize(self: ResourceHeader) error{Overflow}!SizeInfo {
2677 var header_size: u32 = 8;2668 var header_size: u32 = 8;
2678 header_size = try std.math.add(2669 header_size = try std.math.add(
2679 u32,2670 u32,
...@@ -2699,6 +2690,7 @@ pub const Compiler = struct {...@@ -2699,6 +2690,7 @@ pub const Compiler = struct {
2699 const size_info = self.calcSize() catch {2690 const size_info = self.calcSize() catch {
2700 try err_ctx.diagnostics.append(.{2691 try err_ctx.diagnostics.append(.{
2701 .err = .resource_data_size_exceeds_max,2692 .err = .resource_data_size_exceeds_max,
2693 .code_page = err_ctx.code_page,
2702 .token = err_ctx.token,2694 .token = err_ctx.token,
2703 });2695 });
2704 return error.CompileError;2696 return error.CompileError;
...@@ -2706,7 +2698,7 @@ pub const Compiler = struct {...@@ -2706,7 +2698,7 @@ pub const Compiler = struct {
2706 return self.writeSizeInfo(writer, size_info);2698 return self.writeSizeInfo(writer, size_info);
2707 }2699 }
27082700
2709 fn writeSizeInfo(self: ResourceHeader, writer: anytype, size_info: SizeInfo) !void {2701 pub fn writeSizeInfo(self: ResourceHeader, writer: anytype, size_info: SizeInfo) !void {
2710 try writer.writeInt(DWORD, self.data_size, .little); // DataSize2702 try writer.writeInt(DWORD, self.data_size, .little); // DataSize
2711 try writer.writeInt(DWORD, size_info.bytes, .little); // HeaderSize2703 try writer.writeInt(DWORD, size_info.bytes, .little); // HeaderSize
2712 try self.type_value.write(writer); // TYPE2704 try self.type_value.write(writer); // TYPE
...@@ -2863,19 +2855,44 @@ pub const Compiler = struct {...@@ -2863,19 +2855,44 @@ pub const Compiler = struct {
2863 self.sourceBytesForToken(token),2855 self.sourceBytesForToken(token),
2864 .{2856 .{
2865 .start_column = token.calculateColumn(self.source, 8, null),2857 .start_column = token.calculateColumn(self.source, 8, null),
2866 .diagnostics = .{ .diagnostics = self.diagnostics, .token = token },2858 .diagnostics = self.errContext(token),
2859 .output_code_page = self.output_code_pages.getForToken(token),
2867 },2860 },
2868 );2861 );
2869 }2862 }
28702863
2871 fn addErrorDetails(self: *Compiler, details: ErrorDetails) Allocator.Error!void {2864 fn addErrorDetailsWithCodePage(self: *Compiler, details: ErrorDetails) Allocator.Error!void {
2872 try self.diagnostics.append(details);2865 try self.diagnostics.append(details);
2873 }2866 }
28742867
2875 fn addErrorDetailsAndFail(self: *Compiler, details: ErrorDetails) error{ CompileError, OutOfMemory } {2868 /// Code page is looked up in input_code_pages using the token
2876 try self.addErrorDetails(details);2869 fn addErrorDetails(self: *Compiler, details_without_code_page: errors.ErrorDetailsWithoutCodePage) Allocator.Error!void {
2870 const details = ErrorDetails{
2871 .err = details_without_code_page.err,
2872 .code_page = self.input_code_pages.getForToken(details_without_code_page.token),
2873 .token = details_without_code_page.token,
2874 .token_span_start = details_without_code_page.token_span_start,
2875 .token_span_end = details_without_code_page.token_span_end,
2876 .type = details_without_code_page.type,
2877 .print_source_line = details_without_code_page.print_source_line,
2878 .extra = details_without_code_page.extra,
2879 };
2880 try self.addErrorDetailsWithCodePage(details);
2881 }
2882
2883 /// Code page is looked up in input_code_pages using the token
2884 fn addErrorDetailsAndFail(self: *Compiler, details_without_code_page: errors.ErrorDetailsWithoutCodePage) error{ CompileError, OutOfMemory } {
2885 try self.addErrorDetails(details_without_code_page);
2877 return error.CompileError;2886 return error.CompileError;
2878 }2887 }
2888
2889 fn errContext(self: *Compiler, token: Token) errors.DiagnosticsContext {
2890 return .{
2891 .diagnostics = self.diagnostics,
2892 .token = token,
2893 .code_page = self.input_code_pages.getForToken(token),
2894 };
2895 }
2879};2896};
28802897
2881pub const OpenSearchPathError = std.fs.Dir.OpenError;2898pub const OpenSearchPathError = std.fs.Dir.OpenError;
...@@ -3247,7 +3264,8 @@ pub const StringTable = struct {...@@ -3247,7 +3264,8 @@ pub const StringTable = struct {
3247 const bytes = SourceBytes{ .slice = slice, .code_page = code_page };3264 const bytes = SourceBytes{ .slice = slice, .code_page = code_page };
3248 const utf16_string = try literals.parseQuotedStringAsWideString(compiler.allocator, bytes, .{3265 const utf16_string = try literals.parseQuotedStringAsWideString(compiler.allocator, bytes, .{
3249 .start_column = column,3266 .start_column = column,
3250 .diagnostics = .{ .diagnostics = compiler.diagnostics, .token = string_token },3267 .diagnostics = compiler.errContext(string_token),
3268 .output_code_page = compiler.output_code_pages.getForToken(string_token),
3251 });3269 });
3252 defer compiler.allocator.free(utf16_string);3270 defer compiler.allocator.free(utf16_string);
32533271
lib/compiler/resinator/disjoint_code_page.zig created+99
...@@ -0,0 +1,99 @@
1const std = @import("std");
2const lex = @import("lex.zig");
3const SourceMappings = @import("source_mapping.zig").SourceMappings;
4const SupportedCodePage = @import("code_pages.zig").SupportedCodePage;
5
6pub fn hasDisjointCodePage(source: []const u8, source_mappings: ?*const SourceMappings, default_code_page: SupportedCodePage) bool {
7 var line_handler = lex.LineHandler{ .buffer = source };
8 var i: usize = 0;
9 while (i < source.len) {
10 const codepoint = default_code_page.codepointAt(i, source) orelse break;
11 const c = codepoint.value;
12 switch (c) {
13 '\r', '\n' => {
14 _ = line_handler.incrementLineNumber(i);
15 // Any lines that are not from the root file interrupt the disjoint code page
16 if (source_mappings != null and !source_mappings.?.isRootFile(line_handler.line_number)) return false;
17 },
18 // whitespace is ignored
19 ' ',
20 '\t',
21 // NBSP, this should technically be in the TODO below, but it is treated as whitespace
22 // due to a (misguided) special casing in the lexer, see the TODO in lex.zig
23 '\u{A0}',
24 => {},
25
26 // TODO: All of the below are treated as whitespace by the Win32 RC preprocessor, which also
27 // means they are trimmed from the file during preprocessing. This means that these characters
28 // should be treated like ' ', '\t' above, but since the resinator preprocessor does not treat
29 // them as whitespace *or* trim whitespace, files with these characters are likely going to
30 // error. So, in the future some sort of emulation of/rejection of the Win32 behavior might
31 // make handling these codepoints specially make sense, but for now it doesn't really matter
32 // so they are not handled specially for simplicity's sake.
33 //'\u{1680}',
34 //'\u{180E}',
35 //'\u{2001}',
36 //'\u{2002}',
37 //'\u{2003}',
38 //'\u{2004}',
39 //'\u{2005}',
40 //'\u{2006}',
41 //'\u{2007}',
42 //'\u{2008}',
43 //'\u{2009}',
44 //'\u{200A}',
45 //'\u{2028}',
46 //'\u{2029}',
47 //'\u{202F}',
48 //'\u{205F}',
49 //'\u{3000}',
50
51 '#' => {
52 if (source_mappings != null and !source_mappings.?.isRootFile(line_handler.line_number)) {
53 return false;
54 }
55 const start_i = i;
56 while (i < source.len and source[i] != '\r' and source[i] != '\n') : (i += 1) {}
57 const line = source[start_i..i];
58 _ = (lex.parsePragmaCodePage(line) catch |err| switch (err) {
59 error.NotPragma => return false,
60 error.NotCodePagePragma => continue,
61 error.CodePagePragmaUnsupportedCodePage => continue,
62 else => continue,
63 }) orelse return false; // DEFAULT interrupts disjoint code page
64
65 // If we got a code page, then it is a disjoint code page pragma
66 return true;
67 },
68 else => {
69 // Any other character interrupts the disjoint code page
70 return false;
71 },
72 }
73
74 i += codepoint.byte_len;
75 }
76 return false;
77}
78
79test hasDisjointCodePage {
80 try std.testing.expect(hasDisjointCodePage("#pragma code_page(65001)\n", null, .windows1252));
81 // NBSP is a special case
82 try std.testing.expect(hasDisjointCodePage("\xA0\n#pragma code_page(65001)\n", null, .windows1252));
83 try std.testing.expect(hasDisjointCodePage("\u{A0}\n#pragma code_page(1252)\n", null, .utf8));
84 // other preprocessor commands don't interrupt
85 try std.testing.expect(hasDisjointCodePage("#pragma foo\n#pragma code_page(65001)\n", null, .windows1252));
86 // invalid code page doesn't interrupt
87 try std.testing.expect(hasDisjointCodePage("#pragma code_page(1234567)\n#pragma code_page(65001)\n", null, .windows1252));
88
89 try std.testing.expect(!hasDisjointCodePage("#if 1\n#endif\n#pragma code_page(65001)", null, .windows1252));
90 try std.testing.expect(!hasDisjointCodePage("// comment\n#pragma code_page(65001)", null, .windows1252));
91 try std.testing.expect(!hasDisjointCodePage("/* comment */\n#pragma code_page(65001)", null, .windows1252));
92}
93
94test "multiline comment edge case" {
95 // TODO
96 if (true) return error.SkipZigTest;
97
98 try std.testing.expect(hasDisjointCodePage("/* comment */#pragma code_page(65001)", null, .windows1252));
99}
lib/compiler/resinator/errors.zig+391-204
...@@ -8,7 +8,8 @@ const ico = @import("ico.zig");...@@ -8,7 +8,8 @@ const ico = @import("ico.zig");
8const bmp = @import("bmp.zig");8const bmp = @import("bmp.zig");
9const parse = @import("parse.zig");9const parse = @import("parse.zig");
10const lang = @import("lang.zig");10const lang = @import("lang.zig");
11const CodePage = @import("code_pages.zig").CodePage;11const code_pages = @import("code_pages.zig");
12const SupportedCodePage = code_pages.SupportedCodePage;
12const builtin = @import("builtin");13const builtin = @import("builtin");
13const native_endian = builtin.cpu.arch.endian();14const native_endian = builtin.cpu.arch.endian();
1415
...@@ -64,7 +65,7 @@ pub const Diagnostics = struct {...@@ -64,7 +65,7 @@ pub const Diagnostics = struct {
64 defer std.debug.unlockStdErr();65 defer std.debug.unlockStdErr();
65 const stderr = std.io.getStdErr().writer();66 const stderr = std.io.getStdErr().writer();
66 for (self.errors.items) |err_details| {67 for (self.errors.items) |err_details| {
67 renderErrorMessage(self.allocator, stderr, tty_config, cwd, err_details, source, self.strings.items, source_mappings) catch return;68 renderErrorMessage(stderr, tty_config, cwd, err_details, source, self.strings.items, source_mappings) catch return;
68 }69 }
69 }70 }
7071
...@@ -94,32 +95,22 @@ pub const Diagnostics = struct {...@@ -94,32 +95,22 @@ pub const Diagnostics = struct {
94pub const DiagnosticsContext = struct {95pub const DiagnosticsContext = struct {
95 diagnostics: *Diagnostics,96 diagnostics: *Diagnostics,
96 token: Token,97 token: Token,
98 /// Code page of the source file at the token location
99 code_page: SupportedCodePage,
97};100};
98101
99pub const ErrorDetails = struct {102pub const ErrorDetails = struct {
100 err: Error,103 err: Error,
101 token: Token,104 token: Token,
105 /// Code page of the source file at the token location
106 code_page: SupportedCodePage,
102 /// If non-null, should be before `token`. If null, `token` is assumed to be the start.107 /// If non-null, should be before `token`. If null, `token` is assumed to be the start.
103 token_span_start: ?Token = null,108 token_span_start: ?Token = null,
104 /// If non-null, should be after `token`. If null, `token` is assumed to be the end.109 /// If non-null, should be after `token`. If null, `token` is assumed to be the end.
105 token_span_end: ?Token = null,110 token_span_end: ?Token = null,
106 type: Type = .err,111 type: Type = .err,
107 print_source_line: bool = true,112 print_source_line: bool = true,
108 extra: union {113 extra: Extra = .{ .none = {} },
109 none: void,
110 expected: Token.Id,
111 number: u32,
112 expected_types: ExpectedTypes,
113 resource: rc.Resource,
114 string_and_language: StringAndLanguage,
115 file_open_error: FileOpenError,
116 icon_read_error: IconReadError,
117 icon_dir: IconDirContext,
118 bmp_read_error: BitmapReadError,
119 accelerator_error: AcceleratorError,
120 statement_with_u16_param: StatementWithU16Param,
121 menu_or_class: enum { class, menu },
122 } = .{ .none = {} },
123114
124 pub const Type = enum {115 pub const Type = enum {
125 /// Fatal error, stops compilation116 /// Fatal error, stops compilation
...@@ -137,9 +128,25 @@ pub const ErrorDetails = struct {...@@ -137,9 +128,25 @@ pub const ErrorDetails = struct {
137 hint,128 hint,
138 };129 };
139130
131 pub const Extra = union {
132 none: void,
133 expected: Token.Id,
134 number: u32,
135 expected_types: ExpectedTypes,
136 resource: rc.ResourceType,
137 string_and_language: StringAndLanguage,
138 file_open_error: FileOpenError,
139 icon_read_error: IconReadError,
140 icon_dir: IconDirContext,
141 bmp_read_error: BitmapReadError,
142 accelerator_error: AcceleratorError,
143 statement_with_u16_param: StatementWithU16Param,
144 menu_or_class: enum { class, menu },
145 };
146
140 comptime {147 comptime {
141 // all fields in the extra union should be 32 bits or less148 // all fields in the extra union should be 32 bits or less
142 for (std.meta.fields(std.meta.fieldInfo(ErrorDetails, .extra).type)) |field| {149 for (std.meta.fields(Extra)) |field| {
143 std.debug.assert(@bitSizeOf(field.type) <= 32);150 std.debug.assert(@bitSizeOf(field.type) <= 32);
144 }151 }
145 }152 }
...@@ -321,6 +328,8 @@ pub const ErrorDetails = struct {...@@ -321,6 +328,8 @@ pub const ErrorDetails = struct {
321 close_paren_expression,328 close_paren_expression,
322 unary_plus_expression,329 unary_plus_expression,
323 rc_could_miscompile_control_params,330 rc_could_miscompile_control_params,
331 dangling_literal_at_eof,
332 disjoint_code_page,
324333
325 // Compiler334 // Compiler
326 /// `string_and_language` is populated335 /// `string_and_language` is populated
...@@ -331,6 +340,7 @@ pub const ErrorDetails = struct {...@@ -331,6 +340,7 @@ pub const ErrorDetails = struct {
331 /// `accelerator_error` is populated340 /// `accelerator_error` is populated
332 invalid_accelerator_key,341 invalid_accelerator_key,
333 accelerator_type_required,342 accelerator_type_required,
343 accelerator_shift_or_control_without_virtkey,
334 rc_would_miscompile_control_padding,344 rc_would_miscompile_control_padding,
335 rc_would_miscompile_control_class_ordinal,345 rc_would_miscompile_control_class_ordinal,
336 /// `icon_dir` is populated346 /// `icon_dir` is populated
...@@ -356,11 +366,6 @@ pub const ErrorDetails = struct {...@@ -356,11 +366,6 @@ pub const ErrorDetails = struct {
356 /// `number` is populated and contains a string index for which the string contains366 /// `number` is populated and contains a string index for which the string contains
357 /// the bytes of a `u64` (native endian). The `u64` contains the number of miscompiled bytes.367 /// the bytes of a `u64` (native endian). The `u64` contains the number of miscompiled bytes.
358 rc_would_miscompile_bmp_palette_padding,368 rc_would_miscompile_bmp_palette_padding,
359 /// `number` is populated and contains a string index for which the string contains
360 /// the bytes of two `u64`s (native endian). The first contains the number of missing
361 /// palette bytes and the second contains the max number of missing palette bytes.
362 /// If type is `.note`, then `extra` is `none`.
363 bmp_too_many_missing_palette_bytes,
364 resource_header_size_exceeds_max,369 resource_header_size_exceeds_max,
365 resource_data_size_exceeds_max,370 resource_data_size_exceeds_max,
366 control_extra_data_size_exceeds_max,371 control_extra_data_size_exceeds_max,
...@@ -383,15 +388,16 @@ pub const ErrorDetails = struct {...@@ -383,15 +388,16 @@ pub const ErrorDetails = struct {
383 rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal,388 rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal,
384 rc_would_miscompile_dialog_menu_id_starts_with_digit,389 rc_would_miscompile_dialog_menu_id_starts_with_digit,
385 dialog_menu_id_was_uppercased,390 dialog_menu_id_was_uppercased,
386 /// `menu_or_class` is populated and contains the type of the parameter statement391 duplicate_optional_statement_skipped,
387 duplicate_menu_or_class_skipped,
388 invalid_digit_character_in_ordinal,392 invalid_digit_character_in_ordinal,
389393
390 // Literals394 // Literals
391 /// `number` is populated395 /// `number` is populated
392 rc_would_miscompile_codepoint_byte_swap,396 rc_would_miscompile_codepoint_whitespace,
393 /// `number` is populated397 /// `number` is populated
394 rc_would_miscompile_codepoint_skip,398 rc_would_miscompile_codepoint_skip,
399 /// `number` is populated
400 rc_would_miscompile_codepoint_bom,
395 tab_converted_to_spaces,401 tab_converted_to_spaces,
396402
397 // General (used in various places)403 // General (used in various places)
...@@ -403,10 +409,50 @@ pub const ErrorDetails = struct {...@@ -403,10 +409,50 @@ pub const ErrorDetails = struct {
403 failed_to_open_cwd,409 failed_to_open_cwd,
404 };410 };
405411
412 fn formatToken(
413 ctx: TokenFormatContext,
414 comptime fmt: []const u8,
415 options: std.fmt.FormatOptions,
416 writer: anytype,
417 ) !void {
418 _ = fmt;
419 _ = options;
420
421 switch (ctx.token.id) {
422 .eof => return writer.writeAll(ctx.token.id.nameForErrorDisplay()),
423 else => {},
424 }
425
426 const slice = ctx.token.slice(ctx.source);
427 var src_i: usize = 0;
428 while (src_i < slice.len) {
429 const codepoint = ctx.code_page.codepointAt(src_i, slice) orelse break;
430 defer src_i += codepoint.byte_len;
431 const display_codepoint = codepointForDisplay(codepoint) orelse continue;
432 var buf: [4]u8 = undefined;
433 const utf8_len = std.unicode.utf8Encode(display_codepoint, &buf) catch unreachable;
434 try writer.writeAll(buf[0..utf8_len]);
435 }
436 }
437
438 const TokenFormatContext = struct {
439 token: Token,
440 source: []const u8,
441 code_page: SupportedCodePage,
442 };
443
444 fn fmtToken(self: ErrorDetails, source: []const u8) std.fmt.Formatter(formatToken) {
445 return .{ .data = .{
446 .token = self.token,
447 .code_page = self.code_page,
448 .source = source,
449 } };
450 }
451
406 pub fn render(self: ErrorDetails, writer: anytype, source: []const u8, strings: []const []const u8) !void {452 pub fn render(self: ErrorDetails, writer: anytype, source: []const u8, strings: []const []const u8) !void {
407 switch (self.err) {453 switch (self.err) {
408 .unfinished_string_literal => {454 .unfinished_string_literal => {
409 return writer.print("unfinished string literal at '{s}', expected closing '\"'", .{self.token.nameForErrorDisplay(source)});455 return writer.print("unfinished string literal at '{s}', expected closing '\"'", .{self.fmtToken(source)});
410 },456 },
411 .string_literal_too_long => {457 .string_literal_too_long => {
412 return writer.print("string literal too long (max is currently {} characters)", .{self.extra.number});458 return writer.print("string literal too long (max is currently {} characters)", .{self.extra.number});
...@@ -474,33 +520,33 @@ pub const ErrorDetails = struct {...@@ -474,33 +520,33 @@ pub const ErrorDetails = struct {
474 number_slice.len += 1;520 number_slice.len += 1;
475 }521 }
476 const number = std.fmt.parseUnsigned(u16, number_slice, 10) catch unreachable;522 const number = std.fmt.parseUnsigned(u16, number_slice, 10) catch unreachable;
477 const code_page = CodePage.getByIdentifier(number) catch unreachable;523 const code_page = code_pages.getByIdentifier(number) catch unreachable;
478 // TODO: Improve or maybe add a note making it more clear that the code page524 // TODO: Improve or maybe add a note making it more clear that the code page
479 // is valid and that the code page is unsupported purely due to a limitation525 // is valid and that the code page is unsupported purely due to a limitation
480 // in this compiler.526 // in this compiler.
481 return writer.print("unsupported code page '{s} (id={})' in #pragma code_page", .{ @tagName(code_page), number });527 return writer.print("unsupported code page '{s} (id={})' in #pragma code_page", .{ @tagName(code_page), number });
482 },528 },
483 .unfinished_raw_data_block => {529 .unfinished_raw_data_block => {
484 return writer.print("unfinished raw data block at '{s}', expected closing '}}' or 'END'", .{self.token.nameForErrorDisplay(source)});530 return writer.print("unfinished raw data block at '{s}', expected closing '}}' or 'END'", .{self.fmtToken(source)});
485 },531 },
486 .unfinished_string_table_block => {532 .unfinished_string_table_block => {
487 return writer.print("unfinished STRINGTABLE block at '{s}', expected closing '}}' or 'END'", .{self.token.nameForErrorDisplay(source)});533 return writer.print("unfinished STRINGTABLE block at '{s}', expected closing '}}' or 'END'", .{self.fmtToken(source)});
488 },534 },
489 .expected_token => {535 .expected_token => {
490 return writer.print("expected '{s}', got '{s}'", .{ self.extra.expected.nameForErrorDisplay(), self.token.nameForErrorDisplay(source) });536 return writer.print("expected '{s}', got '{s}'", .{ self.extra.expected.nameForErrorDisplay(), self.fmtToken(source) });
491 },537 },
492 .expected_something_else => {538 .expected_something_else => {
493 try writer.writeAll("expected ");539 try writer.writeAll("expected ");
494 try self.extra.expected_types.writeCommaSeparated(writer);540 try self.extra.expected_types.writeCommaSeparated(writer);
495 return writer.print("; got '{s}'", .{self.token.nameForErrorDisplay(source)});541 return writer.print("; got '{s}'", .{self.fmtToken(source)});
496 },542 },
497 .resource_type_cant_use_raw_data => switch (self.type) {543 .resource_type_cant_use_raw_data => switch (self.type) {
498 .err, .warning => try writer.print("expected '<filename>', found '{s}' (resource type '{s}' can't use raw data)", .{ self.token.nameForErrorDisplay(source), self.extra.resource.nameForErrorDisplay() }),544 .err, .warning => try writer.print("expected '<filename>', found '{s}' (resource type '{s}' can't use raw data)", .{ self.fmtToken(source), self.extra.resource.nameForErrorDisplay() }),
499 .note => try writer.print("if '{s}' is intended to be a filename, it must be specified as a quoted string literal", .{self.token.nameForErrorDisplay(source)}),545 .note => try writer.print("if '{s}' is intended to be a filename, it must be specified as a quoted string literal", .{self.fmtToken(source)}),
500 .hint => return,546 .hint => return,
501 },547 },
502 .id_must_be_ordinal => {548 .id_must_be_ordinal => {
503 try writer.print("id of resource type '{s}' must be an ordinal (u16), got '{s}'", .{ self.extra.resource.nameForErrorDisplay(), self.token.nameForErrorDisplay(source) });549 try writer.print("id of resource type '{s}' must be an ordinal (u16), got '{s}'", .{ self.extra.resource.nameForErrorDisplay(), self.fmtToken(source) });
504 },550 },
505 .name_or_id_not_allowed => {551 .name_or_id_not_allowed => {
506 try writer.print("name or id is not allowed for resource type '{s}'", .{self.extra.resource.nameForErrorDisplay()});552 try writer.print("name or id is not allowed for resource type '{s}'", .{self.extra.resource.nameForErrorDisplay()});
...@@ -516,7 +562,7 @@ pub const ErrorDetails = struct {...@@ -516,7 +562,7 @@ pub const ErrorDetails = struct {
516 try writer.writeAll("ASCII character not equivalent to virtual key code");562 try writer.writeAll("ASCII character not equivalent to virtual key code");
517 },563 },
518 .empty_menu_not_allowed => {564 .empty_menu_not_allowed => {
519 try writer.print("empty menu of type '{s}' not allowed", .{self.token.nameForErrorDisplay(source)});565 try writer.print("empty menu of type '{s}' not allowed", .{self.fmtToken(source)});
520 },566 },
521 .rc_would_miscompile_version_value_padding => switch (self.type) {567 .rc_would_miscompile_version_value_padding => switch (self.type) {
522 .err, .warning => return writer.print("the padding before this quoted string value would be miscompiled by the Win32 RC compiler", .{}),568 .err, .warning => return writer.print("the padding before this quoted string value would be miscompiled by the Win32 RC compiler", .{}),
...@@ -570,19 +616,18 @@ pub const ErrorDetails = struct {...@@ -570,19 +616,18 @@ pub const ErrorDetails = struct {
570 .note => return writer.print("to avoid the potential miscompilation, consider adding a comma after the style parameter", .{}),616 .note => return writer.print("to avoid the potential miscompilation, consider adding a comma after the style parameter", .{}),
571 .hint => return,617 .hint => return,
572 },618 },
619 .dangling_literal_at_eof => {
620 try writer.writeAll("dangling literal at end-of-file; this is not a problem, but it is likely a mistake");
621 },
622 .disjoint_code_page => switch (self.type) {
623 .err, .warning => return writer.print("#pragma code_page as the first thing in the .rc script can cause the input and output code pages to become out-of-sync", .{}),
624 .note => return writer.print("to avoid unexpected behavior, add a comment (or anything else) above the #pragma code_page line", .{}),
625 .hint => return,
626 },
573 .string_already_defined => switch (self.type) {627 .string_already_defined => switch (self.type) {
574 .err, .warning => {628 .err, .warning => {
575 const language_id = self.extra.string_and_language.language.asInt();629 const language = self.extra.string_and_language.language;
576 const language_name = language_name: {630 return writer.print("string with id {d} (0x{X}) already defined for language {}", .{ self.extra.string_and_language.id, self.extra.string_and_language.id, language });
577 if (std.meta.intToEnum(lang.LanguageId, language_id)) |lang_enum_val| {
578 break :language_name @tagName(lang_enum_val);
579 } else |_| {}
580 if (language_id == lang.LOCALE_CUSTOM_UNSPECIFIED) {
581 break :language_name "LOCALE_CUSTOM_UNSPECIFIED";
582 }
583 break :language_name "<UNKNOWN>";
584 };
585 return writer.print("string with id {d} (0x{X}) already defined for language {s} (0x{X})", .{ self.extra.string_and_language.id, self.extra.string_and_language.id, language_name, language_id });
586 },631 },
587 .note => return writer.print("previous definition of string with id {d} (0x{X}) here", .{ self.extra.string_and_language.id, self.extra.string_and_language.id }),632 .note => return writer.print("previous definition of string with id {d} (0x{X}) here", .{ self.extra.string_and_language.id, self.extra.string_and_language.id }),
588 .hint => return,633 .hint => return,
...@@ -597,14 +642,17 @@ pub const ErrorDetails = struct {...@@ -597,14 +642,17 @@ pub const ErrorDetails = struct {
597 try writer.print("unable to open file '{s}': {s}", .{ strings[self.extra.file_open_error.filename_string_index], @tagName(self.extra.file_open_error.err) });642 try writer.print("unable to open file '{s}': {s}", .{ strings[self.extra.file_open_error.filename_string_index], @tagName(self.extra.file_open_error.err) });
598 },643 },
599 .invalid_accelerator_key => {644 .invalid_accelerator_key => {
600 try writer.print("invalid accelerator key '{s}': {s}", .{ self.token.nameForErrorDisplay(source), @tagName(self.extra.accelerator_error.err) });645 try writer.print("invalid accelerator key '{s}': {s}", .{ self.fmtToken(source), @tagName(self.extra.accelerator_error.err) });
601 },646 },
602 .accelerator_type_required => {647 .accelerator_type_required => {
603 try writer.print("accelerator type [ASCII or VIRTKEY] required when key is an integer", .{});648 try writer.writeAll("accelerator type [ASCII or VIRTKEY] required when key is an integer");
649 },
650 .accelerator_shift_or_control_without_virtkey => {
651 try writer.writeAll("SHIFT or CONTROL used without VIRTKEY");
604 },652 },
605 .rc_would_miscompile_control_padding => switch (self.type) {653 .rc_would_miscompile_control_padding => switch (self.type) {
606 .err, .warning => return writer.print("the padding before this control would be miscompiled by the Win32 RC compiler (it would insert 2 extra bytes of padding)", .{}),654 .err, .warning => return writer.print("the padding before this control would be miscompiled by the Win32 RC compiler (it would insert 2 extra bytes of padding)", .{}),
607 .note => return writer.print("to avoid the potential miscompilation, consider removing any 'control data' blocks from the controls in this dialog", .{}),655 .note => return writer.print("to avoid the potential miscompilation, consider adding one more byte to the control data of the control preceding this one", .{}),
608 .hint => return,656 .hint => return,
609 },657 },
610 .rc_would_miscompile_control_class_ordinal => switch (self.type) {658 .rc_would_miscompile_control_class_ordinal => switch (self.type) {
...@@ -625,7 +673,7 @@ pub const ErrorDetails = struct {...@@ -625,7 +673,7 @@ pub const ErrorDetails = struct {
625 try writer.print("resource with format '{s}' (at index {}) is not allowed in {s} resource groups", .{ @tagName(self.extra.icon_dir.icon_format), self.extra.icon_dir.index, @tagName(self.extra.icon_dir.icon_type) });673 try writer.print("resource with format '{s}' (at index {}) is not allowed in {s} resource groups", .{ @tagName(self.extra.icon_dir.icon_format), self.extra.icon_dir.index, @tagName(self.extra.icon_dir.icon_type) });
626 },674 },
627 .icon_dir_and_resource_type_mismatch => {675 .icon_dir_and_resource_type_mismatch => {
628 const unexpected_type: rc.Resource = if (self.extra.resource == .icon) .cursor else .icon;676 const unexpected_type: rc.ResourceType = if (self.extra.resource == .icon) .cursor else .icon;
629 // TODO: Better wording677 // TODO: Better wording
630 try writer.print("resource type '{s}' does not match type '{s}' specified in the file", .{ self.extra.resource.nameForErrorDisplay(), unexpected_type.nameForErrorDisplay() });678 try writer.print("resource type '{s}' does not match type '{s}' specified in the file", .{ self.extra.resource.nameForErrorDisplay(), unexpected_type.nameForErrorDisplay() });
631 },679 },
...@@ -663,23 +711,15 @@ pub const ErrorDetails = struct {...@@ -663,23 +711,15 @@ pub const ErrorDetails = struct {
663 .bmp_missing_palette_bytes => {711 .bmp_missing_palette_bytes => {
664 const bytes = strings[self.extra.number];712 const bytes = strings[self.extra.number];
665 const missing_bytes = std.mem.readInt(u64, bytes[0..8], native_endian);713 const missing_bytes = std.mem.readInt(u64, bytes[0..8], native_endian);
666 try writer.print("bitmap has {d} missing color palette bytes which will be padded with zeroes", .{missing_bytes});714 try writer.print("bitmap has {d} missing color palette bytes", .{missing_bytes});
667 },715 },
668 .rc_would_miscompile_bmp_palette_padding => {716 .rc_would_miscompile_bmp_palette_padding => {
669 const bytes = strings[self.extra.number];717 try writer.writeAll("the Win32 RC compiler would erroneously pad out the missing bytes");
670 const miscompiled_bytes = std.mem.readInt(u64, bytes[0..8], native_endian);718 if (self.extra.number != 0) {
671 try writer.print("the missing color palette bytes would be miscompiled by the Win32 RC compiler (the added padding bytes would include {d} bytes of the pixel data)", .{miscompiled_bytes});
672 },
673 .bmp_too_many_missing_palette_bytes => switch (self.type) {
674 .err, .warning => {
675 const bytes = strings[self.extra.number];719 const bytes = strings[self.extra.number];
676 const missing_bytes = std.mem.readInt(u64, bytes[0..8], native_endian);720 const miscompiled_bytes = std.mem.readInt(u64, bytes[0..8], native_endian);
677 const max_missing_bytes = std.mem.readInt(u64, bytes[8..16], native_endian);721 try writer.print(" (and the added padding bytes would include {d} bytes of the pixel data)", .{miscompiled_bytes});
678 try writer.print("bitmap has {} missing color palette bytes which exceeds the maximum of {}", .{ missing_bytes, max_missing_bytes });722 }
679 },
680 // TODO: command line option
681 .note => try writer.writeAll("the maximum number of missing color palette bytes is configurable via <<TODO command line option>>"),
682 .hint => return,
683 },723 },
684 .resource_header_size_exceeds_max => {724 .resource_header_size_exceeds_max => {
685 try writer.print("resource's header length exceeds maximum of {} bytes", .{std.math.maxInt(u32)});725 try writer.print("resource's header length exceeds maximum of {} bytes", .{std.math.maxInt(u32)});
...@@ -749,23 +789,22 @@ pub const ErrorDetails = struct {...@@ -749,23 +789,22 @@ pub const ErrorDetails = struct {
749 .hint => return,789 .hint => return,
750 },790 },
751 .dialog_menu_id_was_uppercased => return,791 .dialog_menu_id_was_uppercased => return,
752 .duplicate_menu_or_class_skipped => {792 .duplicate_optional_statement_skipped => {
753 return writer.print("this {s} was ignored; when multiple {s} statements are specified, only the last takes precedence", .{793 return writer.writeAll("this statement was ignored; when multiple statements of the same type are specified, only the last takes precedence");
754 @tagName(self.extra.menu_or_class),
755 @tagName(self.extra.menu_or_class),
756 });
757 },794 },
758 .invalid_digit_character_in_ordinal => {795 .invalid_digit_character_in_ordinal => {
759 return writer.writeAll("non-ASCII digit characters are not allowed in ordinal (number) values");796 return writer.writeAll("non-ASCII digit characters are not allowed in ordinal (number) values");
760 },797 },
761 .rc_would_miscompile_codepoint_byte_swap => switch (self.type) {798 .rc_would_miscompile_codepoint_whitespace => {
762 .err, .warning => return writer.print("codepoint U+{X} within a string literal would be miscompiled by the Win32 RC compiler (the bytes of the UTF-16 code unit would be swapped)", .{self.extra.number}),799 const treated_as = self.extra.number >> 8;
763 .note => return writer.print("to avoid the potential miscompilation, an integer escape sequence in a wide string literal could be used instead: L\"\\x{X}\"", .{self.extra.number}),800 return writer.print("codepoint U+{X:0>4} within a string literal would be miscompiled by the Win32 RC compiler (it would get treated as U+{X:0>4})", .{ self.extra.number, treated_as });
764 .hint => return,
765 },801 },
766 .rc_would_miscompile_codepoint_skip => switch (self.type) {802 .rc_would_miscompile_codepoint_skip => {
767 .err, .warning => return writer.print("codepoint U+{X} within a string literal would be miscompiled by the Win32 RC compiler (the codepoint would be missing from the compiled resource)", .{self.extra.number}),803 return writer.print("codepoint U+{X:0>4} within a string literal would be miscompiled by the Win32 RC compiler (the codepoint would be missing from the compiled resource)", .{self.extra.number});
768 .note => return writer.print("to avoid the potential miscompilation, an integer escape sequence in a wide string literal could be used instead: L\"\\x{X}\"", .{self.extra.number}),804 },
805 .rc_would_miscompile_codepoint_bom => switch (self.type) {
806 .err, .warning => return writer.print("codepoint U+{X:0>4} within a string literal would cause the entire file to be miscompiled by the Win32 RC compiler", .{self.extra.number}),
807 .note => return writer.writeAll("the presence of this codepoint causes all non-ASCII codepoints to be byteswapped by the Win32 RC preprocessor"),
769 .hint => return,808 .hint => return,
770 },809 },
771 .tab_converted_to_spaces => switch (self.type) {810 .tab_converted_to_spaces => switch (self.type) {
...@@ -790,14 +829,7 @@ pub const ErrorDetails = struct {...@@ -790,14 +829,7 @@ pub const ErrorDetails = struct {
790 after_len: usize,829 after_len: usize,
791 };830 };
792831
793 pub fn visualTokenInfo(self: ErrorDetails, source_line_start: usize, source_line_end: usize) VisualTokenInfo {832 pub fn visualTokenInfo(self: ErrorDetails, source_line_start: usize, source_line_end: usize, source: []const u8) VisualTokenInfo {
794 // Note: A perfect solution here would involve full grapheme cluster
795 // awareness, but oh well. This will give incorrect offsets
796 // if there are any multibyte codepoints within the relevant span,
797 // and even more inflated for grapheme clusters.
798 //
799 // We mitigate this slightly when we know we'll be pointing at
800 // something that displays as 1 character.
801 return switch (self.err) {833 return switch (self.err) {
802 // These can technically be more than 1 byte depending on encoding,834 // These can technically be more than 1 byte depending on encoding,
803 // but they always refer to one visual character/grapheme.835 // but they always refer to one visual character/grapheme.
...@@ -808,27 +840,65 @@ pub const ErrorDetails = struct {...@@ -808,27 +840,65 @@ pub const ErrorDetails = struct {
808 .illegal_private_use_character,840 .illegal_private_use_character,
809 => .{841 => .{
810 .before_len = 0,842 .before_len = 0,
811 .point_offset = self.token.start - source_line_start,843 .point_offset = cellCount(self.code_page, source, source_line_start, self.token.start),
812 .after_len = 0,844 .after_len = 0,
813 },845 },
814 else => .{846 else => .{
815 .before_len = before: {847 .before_len = before: {
816 const start = @max(source_line_start, if (self.token_span_start) |span_start| span_start.start else self.token.start);848 const start = @max(source_line_start, if (self.token_span_start) |span_start| span_start.start else self.token.start);
817 break :before self.token.start - start;849 break :before cellCount(self.code_page, source, start, self.token.start);
818 },850 },
819 .point_offset = self.token.start - source_line_start,851 .point_offset = cellCount(self.code_page, source, source_line_start, self.token.start),
820 .after_len = after: {852 .after_len = after: {
821 const end = @min(source_line_end, if (self.token_span_end) |span_end| span_end.end else self.token.end);853 const end = @min(source_line_end, if (self.token_span_end) |span_end| span_end.end else self.token.end);
822 // end may be less than start when pointing to EOF854 // end may be less than start when pointing to EOF
823 if (end <= self.token.start) break :after 0;855 if (end <= self.token.start) break :after 0;
824 break :after end - self.token.start - 1;856 break :after cellCount(self.code_page, source, self.token.start, end) - 1;
825 },857 },
826 },858 },
827 };859 };
828 }860 }
829};861};
830862
831pub fn renderErrorMessage(allocator: std.mem.Allocator, writer: anytype, tty_config: std.io.tty.Config, cwd: std.fs.Dir, err_details: ErrorDetails, source: []const u8, strings: []const []const u8, source_mappings: ?SourceMappings) !void {863/// Convenience struct only useful when the code page can be inferred from the token
864pub const ErrorDetailsWithoutCodePage = blk: {
865 const details_info = @typeInfo(ErrorDetails);
866 const fields = details_info.@"struct".fields;
867 var fields_without_codepage: [fields.len - 1]std.builtin.Type.StructField = undefined;
868 var i: usize = 0;
869 for (fields) |field| {
870 if (std.mem.eql(u8, field.name, "code_page")) continue;
871 fields_without_codepage[i] = field;
872 i += 1;
873 }
874 std.debug.assert(i == fields_without_codepage.len);
875 break :blk @Type(.{ .@"struct" = .{
876 .layout = .auto,
877 .fields = &fields_without_codepage,
878 .decls = &.{},
879 .is_tuple = false,
880 } });
881};
882
883fn cellCount(code_page: SupportedCodePage, source: []const u8, start_index: usize, end_index: usize) usize {
884 // Note: This is an imperfect solution. A proper implementation here would
885 // involve full grapheme cluster awareness + grapheme width data, but oh well.
886 var codepoint_count: usize = 0;
887 var index: usize = start_index;
888 while (index < end_index) {
889 const codepoint = code_page.codepointAt(index, source) orelse break;
890 defer index += codepoint.byte_len;
891 _ = codepointForDisplay(codepoint) orelse continue;
892 codepoint_count += 1;
893 // no need to count more than we will display
894 if (codepoint_count >= max_source_line_codepoints + truncated_str.len) break;
895 }
896 return codepoint_count;
897}
898
899const truncated_str = "<...truncated...>";
900
901pub fn renderErrorMessage(writer: anytype, tty_config: std.io.tty.Config, cwd: std.fs.Dir, err_details: ErrorDetails, source: []const u8, strings: []const []const u8, source_mappings: ?SourceMappings) !void {
832 if (err_details.type == .hint) return;902 if (err_details.type == .hint) return;
833903
834 const source_line_start = err_details.token.getLineStartForErrorDisplay(source);904 const source_line_start = err_details.token.getLineStartForErrorDisplay(source);
...@@ -884,45 +954,61 @@ pub fn renderErrorMessage(allocator: std.mem.Allocator, writer: anytype, tty_con...@@ -884,45 +954,61 @@ pub fn renderErrorMessage(allocator: std.mem.Allocator, writer: anytype, tty_con
884 }954 }
885955
886 const source_line = err_details.token.getLineForErrorDisplay(source, source_line_start);956 const source_line = err_details.token.getLineForErrorDisplay(source, source_line_start);
887 const visual_info = err_details.visualTokenInfo(source_line_start, source_line_start + source_line.len);957 const visual_info = err_details.visualTokenInfo(source_line_start, source_line_start + source_line.len, source);
958 const truncated_visual_info = ErrorDetails.VisualTokenInfo{
959 .before_len = if (visual_info.point_offset > max_source_line_codepoints and visual_info.before_len > 0)
960 (visual_info.before_len + 1) -| (visual_info.point_offset - max_source_line_codepoints)
961 else
962 visual_info.before_len,
963 .point_offset = @min(max_source_line_codepoints + 1, visual_info.point_offset),
964 .after_len = if (visual_info.point_offset > max_source_line_codepoints)
965 @min(truncated_str.len - 3, visual_info.after_len)
966 else
967 @min(max_source_line_codepoints - visual_info.point_offset + (truncated_str.len - 2), visual_info.after_len),
968 };
888969
889 // Need this to determine if the 'line originated from' note is worth printing970 // Need this to determine if the 'line originated from' note is worth printing
890 var source_line_for_display_buf = try std.ArrayList(u8).initCapacity(allocator, source_line.len);971 var source_line_for_display_buf: [max_source_line_bytes]u8 = undefined;
891 defer source_line_for_display_buf.deinit();972 const source_line_for_display = writeSourceSlice(&source_line_for_display_buf, source_line, err_details.code_page);
892 try writeSourceSlice(source_line_for_display_buf.writer(), source_line);973
893974 try writer.writeAll(source_line_for_display.line);
894 // TODO: General handling of long lines, not tied to this specific error975 if (source_line_for_display.truncated) {
895 if (err_details.err == .string_literal_too_long) {
896 const before_slice = source_line[0..@min(source_line.len, visual_info.point_offset + 16)];
897 try writeSourceSlice(writer, before_slice);
898 try tty_config.setColor(writer, .dim);976 try tty_config.setColor(writer, .dim);
899 try writer.writeAll("<...truncated...>");977 try writer.writeAll(truncated_str);
900 try tty_config.setColor(writer, .reset);978 try tty_config.setColor(writer, .reset);
901 } else {
902 try writer.writeAll(source_line_for_display_buf.items);
903 }979 }
904 try writer.writeByte('\n');980 try writer.writeByte('\n');
905981
906 try tty_config.setColor(writer, .green);982 try tty_config.setColor(writer, .green);
907 const num_spaces = visual_info.point_offset - visual_info.before_len;983 const num_spaces = truncated_visual_info.point_offset - truncated_visual_info.before_len;
908 try writer.writeByteNTimes(' ', num_spaces);984 try writer.writeByteNTimes(' ', num_spaces);
909 try writer.writeByteNTimes('~', visual_info.before_len);985 try writer.writeByteNTimes('~', truncated_visual_info.before_len);
910 try writer.writeByte('^');986 try writer.writeByte('^');
911 if (visual_info.after_len > 0) {987 try writer.writeByteNTimes('~', truncated_visual_info.after_len);
912 var num_squiggles = visual_info.after_len;
913 if (err_details.err == .string_literal_too_long) {
914 num_squiggles = @min(num_squiggles, 15);
915 }
916 try writer.writeByteNTimes('~', num_squiggles);
917 }
918 try writer.writeByte('\n');988 try writer.writeByte('\n');
919 try tty_config.setColor(writer, .reset);989 try tty_config.setColor(writer, .reset);
920990
921 if (corresponding_span != null and corresponding_file != null) {991 if (corresponding_span != null and corresponding_file != null) {
922 var corresponding_lines = try CorrespondingLines.init(allocator, cwd, err_details, source_line_for_display_buf.items, corresponding_span.?, corresponding_file.?);992 var worth_printing_lines: bool = true;
923 defer corresponding_lines.deinit(allocator);993 var initial_lines_err: ?anyerror = null;
924994 var corresponding_lines: ?CorrespondingLines = CorrespondingLines.init(
925 if (!corresponding_lines.worth_printing_note) return;995 cwd,
996 err_details,
997 source_line_for_display.line,
998 corresponding_span.?,
999 corresponding_file.?,
1000 ) catch |err| switch (err) {
1001 error.NotWorthPrintingLines => blk: {
1002 worth_printing_lines = false;
1003 break :blk null;
1004 },
1005 error.NotWorthPrintingNote => return,
1006 else => |e| blk: {
1007 initial_lines_err = e;
1008 break :blk null;
1009 },
1010 };
1011 defer if (corresponding_lines) |*cl| cl.deinit();
9261012
927 try tty_config.setColor(writer, .bold);1013 try tty_config.setColor(writer, .bold);
928 if (corresponding_file) |file| {1014 if (corresponding_file) |file| {
...@@ -947,85 +1033,222 @@ pub fn renderErrorMessage(allocator: std.mem.Allocator, writer: anytype, tty_con...@@ -947,85 +1033,222 @@ pub fn renderErrorMessage(allocator: std.mem.Allocator, writer: anytype, tty_con
947 try writer.print(" of file '{s}'\n", .{corresponding_file.?});1033 try writer.print(" of file '{s}'\n", .{corresponding_file.?});
948 try tty_config.setColor(writer, .reset);1034 try tty_config.setColor(writer, .reset);
9491035
950 if (!corresponding_lines.worth_printing_lines) return;1036 if (!worth_printing_lines) return;
9511037
952 if (corresponding_lines.lines_is_error_message) {1038 const write_lines_err: ?anyerror = write_lines: {
1039 if (initial_lines_err) |err| break :write_lines err;
1040 while (corresponding_lines.?.next() catch |err| {
1041 break :write_lines err;
1042 }) |display_line| {
1043 try writer.writeAll(display_line.line);
1044 if (display_line.truncated) {
1045 try tty_config.setColor(writer, .dim);
1046 try writer.writeAll(truncated_str);
1047 try tty_config.setColor(writer, .reset);
1048 }
1049 try writer.writeByte('\n');
1050 }
1051 break :write_lines null;
1052 };
1053 if (write_lines_err) |err| {
953 try tty_config.setColor(writer, .red);1054 try tty_config.setColor(writer, .red);
954 try writer.writeAll(" | ");1055 try writer.writeAll(" | ");
955 try tty_config.setColor(writer, .reset);1056 try tty_config.setColor(writer, .reset);
956 try tty_config.setColor(writer, .dim);1057 try tty_config.setColor(writer, .dim);
957 try writer.writeAll(corresponding_lines.lines.items);1058 try writer.print("unable to print line(s) from file: {s}\n", .{@errorName(err)});
958 try tty_config.setColor(writer, .reset);1059 try tty_config.setColor(writer, .reset);
959 try writer.writeAll("\n\n");
960 return;
961 }1060 }
9621061 try writer.writeByte('\n');
963 try writer.writeAll(corresponding_lines.lines.items);
964 try writer.writeAll("\n\n");
965 }1062 }
966}1063}
9671064
968const CorrespondingLines = struct {1065const VisualLine = struct {
969 worth_printing_note: bool = true,1066 line: []u8,
970 worth_printing_lines: bool = true,1067 truncated: bool,
971 lines: std.ArrayListUnmanaged(u8) = .empty,1068};
972 lines_is_error_message: bool = false,
973
974 pub fn init(allocator: std.mem.Allocator, cwd: std.fs.Dir, err_details: ErrorDetails, lines_for_comparison: []const u8, corresponding_span: SourceMappings.CorrespondingSpan, corresponding_file: []const u8) !CorrespondingLines {
975 var corresponding_lines = CorrespondingLines{};
9761069
1070const CorrespondingLines = struct {
1071 // enough room for one more codepoint, just so that we don't have to keep
1072 // track of this being truncated, since the extra codepoint will ensure
1073 // the visual line will need to truncate in that case.
1074 line_buf: [max_source_line_bytes + 4]u8 = undefined,
1075 line_len: usize = 0,
1076 visual_line_buf: [max_source_line_bytes]u8 = undefined,
1077 visual_line_len: usize = 0,
1078 truncated: bool = false,
1079 line_num: usize = 1,
1080 initial_line: bool = true,
1081 last_byte: u8 = 0,
1082 at_eof: bool = false,
1083 span: SourceMappings.CorrespondingSpan,
1084 file: std.fs.File,
1085 buffered_reader: BufferedReaderType,
1086 code_page: SupportedCodePage,
1087
1088 const BufferedReaderType = std.io.BufferedReader(512, std.fs.File.Reader);
1089
1090 pub fn init(cwd: std.fs.Dir, err_details: ErrorDetails, line_for_comparison: []const u8, corresponding_span: SourceMappings.CorrespondingSpan, corresponding_file: []const u8) !CorrespondingLines {
977 // We don't do line comparison for this error, so don't print the note if the line1091 // We don't do line comparison for this error, so don't print the note if the line
978 // number is different1092 // number is different
979 if (err_details.err == .string_literal_too_long and err_details.token.line_number == corresponding_span.start_line) {1093 if (err_details.err == .string_literal_too_long and err_details.token.line_number != corresponding_span.start_line) {
980 corresponding_lines.worth_printing_note = false;1094 return error.NotWorthPrintingNote;
981 return corresponding_lines;
982 }1095 }
9831096
984 // Don't print the originating line for this error, we know it's really long1097 // Don't print the originating line for this error, we know it's really long
985 if (err_details.err == .string_literal_too_long) {1098 if (err_details.err == .string_literal_too_long) {
986 corresponding_lines.worth_printing_lines = false;1099 return error.NotWorthPrintingLines;
987 return corresponding_lines;
988 }1100 }
9891101
990 var writer = corresponding_lines.lines.writer(allocator);1102 var corresponding_lines = CorrespondingLines{
991 if (utils.openFileNotDir(cwd, corresponding_file, .{})) |file| {1103 .span = corresponding_span,
992 defer file.close();1104 .file = try utils.openFileNotDir(cwd, corresponding_file, .{}),
993 var buffered_reader = std.io.bufferedReader(file.reader());1105 .buffered_reader = undefined,
994 writeLinesFromStream(writer, buffered_reader.reader(), corresponding_span.start_line, corresponding_span.end_line) catch |err| switch (err) {1106 .code_page = err_details.code_page,
995 error.LinesNotFound => {1107 };
996 corresponding_lines.lines.clearRetainingCapacity();1108 corresponding_lines.buffered_reader = BufferedReaderType{
997 try writer.print("unable to print line(s) from file: {s}", .{@errorName(err)});1109 .unbuffered_reader = corresponding_lines.file.reader(),
998 corresponding_lines.lines_is_error_message = true;1110 };
999 return corresponding_lines;1111 errdefer corresponding_lines.deinit();
1000 },1112
1001 else => |e| return e,1113 var fbs = std.io.fixedBufferStream(&corresponding_lines.line_buf);
1002 };1114 const writer = fbs.writer();
1003 } else |err| {1115
1004 corresponding_lines.lines.clearRetainingCapacity();1116 try corresponding_lines.writeLineFromStreamVerbatim(
1005 try writer.print("unable to print line(s) from file: {s}", .{@errorName(err)});1117 writer,
1006 corresponding_lines.lines_is_error_message = true;1118 corresponding_lines.buffered_reader.reader(),
1007 return corresponding_lines;1119 corresponding_span.start_line,
1008 }1120 );
1121
1122 const visual_line = writeSourceSlice(
1123 &corresponding_lines.visual_line_buf,
1124 corresponding_lines.line_buf[0..corresponding_lines.line_len],
1125 err_details.code_page,
1126 );
1127 corresponding_lines.visual_line_len = visual_line.line.len;
1128 corresponding_lines.truncated = visual_line.truncated;
10091129
1010 // If the lines are the same as they were before preprocessing, skip printing the note entirely1130 // If the lines are the same as they were before preprocessing, skip printing the note entirely
1011 if (std.mem.eql(u8, lines_for_comparison, corresponding_lines.lines.items)) {1131 if (corresponding_span.start_line == corresponding_span.end_line and std.mem.eql(
1012 corresponding_lines.worth_printing_note = false;1132 u8,
1133 line_for_comparison,
1134 corresponding_lines.visual_line_buf[0..corresponding_lines.visual_line_len],
1135 )) {
1136 return error.NotWorthPrintingNote;
1013 }1137 }
1138
1014 return corresponding_lines;1139 return corresponding_lines;
1015 }1140 }
10161141
1017 pub fn deinit(self: *CorrespondingLines, allocator: std.mem.Allocator) void {1142 pub fn next(self: *CorrespondingLines) !?VisualLine {
1018 self.lines.deinit(allocator);1143 if (self.initial_line) {
1144 self.initial_line = false;
1145 return .{
1146 .line = self.visual_line_buf[0..self.visual_line_len],
1147 .truncated = self.truncated,
1148 };
1149 }
1150 if (self.line_num > self.span.end_line) return null;
1151 if (self.at_eof) return error.LinesNotFound;
1152
1153 self.line_len = 0;
1154 self.visual_line_len = 0;
1155
1156 var fbs = std.io.fixedBufferStream(&self.line_buf);
1157 const writer = fbs.writer();
1158
1159 try self.writeLineFromStreamVerbatim(
1160 writer,
1161 self.buffered_reader.reader(),
1162 self.line_num,
1163 );
1164
1165 const visual_line = writeSourceSlice(
1166 &self.visual_line_buf,
1167 self.line_buf[0..self.line_len],
1168 self.code_page,
1169 );
1170 self.visual_line_len = visual_line.line.len;
1171
1172 return visual_line;
1173 }
1174
1175 fn writeLineFromStreamVerbatim(self: *CorrespondingLines, writer: anytype, input: anytype, line_num: usize) !void {
1176 while (try readByteOrEof(input)) |byte| {
1177 switch (byte) {
1178 '\n', '\r' => {
1179 if (!utils.isLineEndingPair(self.last_byte, byte)) {
1180 const line_complete = self.line_num == line_num;
1181 self.line_num += 1;
1182 if (line_complete) {
1183 self.last_byte = byte;
1184 return;
1185 }
1186 } else {
1187 // reset last_byte to a non-line ending so that
1188 // consecutive CRLF pairs don't get treated as one
1189 // long line ending 'pair'
1190 self.last_byte = 0;
1191 continue;
1192 }
1193 },
1194 else => {
1195 if (self.line_num == line_num) {
1196 if (writer.writeByte(byte)) {
1197 self.line_len += 1;
1198 } else |err| switch (err) {
1199 error.NoSpaceLeft => {},
1200 else => |e| return e,
1201 }
1202 }
1203 },
1204 }
1205 self.last_byte = byte;
1206 }
1207 self.at_eof = true;
1208 // hacky way to get next to return null
1209 self.line_num += 1;
1210 }
1211
1212 fn readByteOrEof(reader: anytype) !?u8 {
1213 return reader.readByte() catch |err| switch (err) {
1214 error.EndOfStream => return null,
1215 else => |e| return e,
1216 };
1217 }
1218
1219 pub fn deinit(self: *CorrespondingLines) void {
1220 self.file.close();
1019 }1221 }
1020};1222};
10211223
1022fn writeSourceSlice(writer: anytype, slice: []const u8) !void {1224const max_source_line_codepoints = 120;
1023 for (slice) |c| try writeSourceByte(writer, c);1225const max_source_line_bytes = max_source_line_codepoints * 4;
1226
1227fn writeSourceSlice(buf: []u8, slice: []const u8, code_page: SupportedCodePage) VisualLine {
1228 var src_i: usize = 0;
1229 var dest_i: usize = 0;
1230 var codepoint_count: usize = 0;
1231 while (src_i < slice.len) {
1232 const codepoint = code_page.codepointAt(src_i, slice) orelse break;
1233 defer src_i += codepoint.byte_len;
1234 const display_codepoint = codepointForDisplay(codepoint) orelse continue;
1235 codepoint_count += 1;
1236 if (codepoint_count > max_source_line_codepoints) {
1237 return .{ .line = buf[0..dest_i], .truncated = true };
1238 }
1239 const utf8_len = std.unicode.utf8Encode(display_codepoint, buf[dest_i..]) catch unreachable;
1240 dest_i += utf8_len;
1241 }
1242 return .{ .line = buf[0..dest_i], .truncated = false };
1024}1243}
10251244
1026inline fn writeSourceByte(writer: anytype, byte: u8) !void {1245fn codepointForDisplay(codepoint: code_pages.Codepoint) ?u21 {
1027 switch (byte) {1246 return switch (codepoint.value) {
1028 '\x00'...'\x08', '\x0E'...'\x1F', '\x7F' => try writer.writeAll("�"),1247 '\x00'...'\x08',
1248 '\x0E'...'\x1F',
1249 '\x7F',
1250 code_pages.Codepoint.invalid,
1251 => '�',
1029 // \r is seemingly ignored by the RC compiler so skipping it when printing source lines1252 // \r is seemingly ignored by the RC compiler so skipping it when printing source lines
1030 // could help avoid confusing output (e.g. RC\rDATA if printed verbatim would show up1253 // could help avoid confusing output (e.g. RC\rDATA if printed verbatim would show up
1031 // in the console as DATA but the compiler reads it as RCDATA)1254 // in the console as DATA but the compiler reads it as RCDATA)
...@@ -1033,44 +1256,8 @@ inline fn writeSourceByte(writer: anytype, byte: u8) !void {...@@ -1033,44 +1256,8 @@ inline fn writeSourceByte(writer: anytype, byte: u8) !void {
1033 // NOTE: This is irrelevant when using the clang preprocessor, because unpaired \r1256 // NOTE: This is irrelevant when using the clang preprocessor, because unpaired \r
1034 // characters get converted to \n, but may become relevant if another1257 // characters get converted to \n, but may become relevant if another
1035 // preprocessor is used instead.1258 // preprocessor is used instead.
1036 '\r' => {},1259 '\r' => null,
1037 '\t', '\x0B', '\x0C' => try writer.writeByte(' '),1260 '\t', '\x0B', '\x0C' => ' ',
1038 else => try writer.writeByte(byte),1261 else => |v| v,
1039 }
1040}
1041
1042pub fn writeLinesFromStream(writer: anytype, input: anytype, start_line: usize, end_line: usize) !void {
1043 var line_num: usize = 1;
1044 var last_byte: u8 = 0;
1045 while (try readByteOrEof(input)) |byte| {
1046 switch (byte) {
1047 '\n', '\r' => {
1048 if (!utils.isLineEndingPair(last_byte, byte)) {
1049 if (line_num == end_line) return;
1050 if (line_num >= start_line) try writeSourceByte(writer, byte);
1051 line_num += 1;
1052 } else {
1053 // reset last_byte to a non-line ending so that
1054 // consecutive CRLF pairs don't get treated as one
1055 // long line ending 'pair'
1056 last_byte = 0;
1057 continue;
1058 }
1059 },
1060 else => {
1061 if (line_num >= start_line) try writeSourceByte(writer, byte);
1062 },
1063 }
1064 last_byte = byte;
1065 }
1066 if (line_num != end_line) {
1067 return error.LinesNotFound;
1068 }
1069}
1070
1071pub fn readByteOrEof(reader: anytype) !?u8 {
1072 return reader.readByte() catch |err| switch (err) {
1073 error.EndOfStream => return null,
1074 else => |e| return e,
1075 };1262 };
1076}1263}
lib/compiler/resinator/lang.zig+1-1
...@@ -119,7 +119,7 @@ test tagToId {...@@ -119,7 +119,7 @@ test tagToId {
119}119}
120120
121test "exhaustive tagToId" {121test "exhaustive tagToId" {
122 inline for (@typeInfo(LanguageId).Enum.fields) |field| {122 inline for (@typeInfo(LanguageId).@"enum".fields) |field| {
123 const id = tagToId(field.name) catch |err| {123 const id = tagToId(field.name) catch |err| {
124 std.debug.print("tag: {s}\n", .{field.name});124 std.debug.print("tag: {s}\n", .{field.name});
125 return err;125 return err;
lib/compiler/resinator/lex.zig+134-113
...@@ -8,7 +8,7 @@ const std = @import("std");...@@ -8,7 +8,7 @@ const std = @import("std");
8const ErrorDetails = @import("errors.zig").ErrorDetails;8const ErrorDetails = @import("errors.zig").ErrorDetails;
9const columnWidth = @import("literals.zig").columnWidth;9const columnWidth = @import("literals.zig").columnWidth;
10const code_pages = @import("code_pages.zig");10const code_pages = @import("code_pages.zig");
11const CodePage = code_pages.CodePage;11const SupportedCodePage = code_pages.SupportedCodePage;
12const SourceMappings = @import("source_mapping.zig").SourceMappings;12const SourceMappings = @import("source_mapping.zig").SourceMappings;
13const isNonAsciiDigit = @import("utils.zig").isNonAsciiDigit;13const isNonAsciiDigit = @import("utils.zig").isNonAsciiDigit;
1414
...@@ -62,13 +62,6 @@ pub const Token = struct {...@@ -62,13 +62,6 @@ pub const Token = struct {
62 return buffer[self.start..self.end];62 return buffer[self.start..self.end];
63 }63 }
6464
65 pub fn nameForErrorDisplay(self: Token, buffer: []const u8) []const u8 {
66 return switch (self.id) {
67 .eof => self.id.nameForErrorDisplay(),
68 else => self.slice(buffer),
69 };
70 }
71
72 /// Returns 0-based column65 /// Returns 0-based column
73 pub fn calculateColumn(token: Token, source: []const u8, tab_columns: usize, maybe_line_start: ?usize) usize {66 pub fn calculateColumn(token: Token, source: []const u8, tab_columns: usize, maybe_line_start: ?usize) usize {
74 const line_start = maybe_line_start orelse token.getLineStartForColumnCalc(source);67 const line_start = maybe_line_start orelse token.getLineStartForColumnCalc(source);
...@@ -214,18 +207,19 @@ pub const Lexer = struct {...@@ -214,18 +207,19 @@ pub const Lexer = struct {
214 line_handler: LineHandler,207 line_handler: LineHandler,
215 at_start_of_line: bool = true,208 at_start_of_line: bool = true,
216 error_context_token: ?Token = null,209 error_context_token: ?Token = null,
217 current_code_page: CodePage,210 current_code_page: SupportedCodePage,
218 default_code_page: CodePage,211 default_code_page: SupportedCodePage,
219 source_mappings: ?*SourceMappings,212 source_mappings: ?*SourceMappings,
220 max_string_literal_codepoints: u15,213 max_string_literal_codepoints: u15,
221 /// Needed to determine whether or not the output code page should214 /// Needed to determine whether or not the output code page should
222 /// be set in the parser.215 /// be set in the parser.
223 seen_pragma_code_pages: u2 = 0,216 seen_pragma_code_pages: u2 = 0,
217 last_pragma_code_page_token: ?Token = null,
224218
225 pub const Error = LexError;219 pub const Error = LexError;
226220
227 pub const LexerOptions = struct {221 pub const LexerOptions = struct {
228 default_code_page: CodePage = .windows1252,222 default_code_page: SupportedCodePage = .windows1252,
229 source_mappings: ?*SourceMappings = null,223 source_mappings: ?*SourceMappings = null,
230 max_string_literal_codepoints: u15 = default_max_string_literal_codepoints,224 max_string_literal_codepoints: u15 = default_max_string_literal_codepoints,
231 };225 };
...@@ -291,6 +285,8 @@ pub const Lexer = struct {...@@ -291,6 +285,8 @@ pub const Lexer = struct {
291 },285 },
292 // NBSP only counts as whitespace at the start of a line (but286 // NBSP only counts as whitespace at the start of a line (but
293 // can be intermixed with other whitespace). Who knows why.287 // can be intermixed with other whitespace). Who knows why.
288 // TODO: This should either be removed, or it should also include
289 // the codepoints listed in disjoint_code_page.zig
294 '\xA0' => if (self.at_start_of_line) {290 '\xA0' => if (self.at_start_of_line) {
295 result.start = self.index + codepoint.byte_len;291 result.start = self.index + codepoint.byte_len;
296 } else {292 } else {
...@@ -305,12 +301,8 @@ pub const Lexer = struct {...@@ -305,12 +301,8 @@ pub const Lexer = struct {
305 }301 }
306 self.at_start_of_line = false;302 self.at_start_of_line = false;
307 },303 },
308 // Semi-colon acts as a line-terminator, but in this lexing mode
309 // that's only true if it's at the start of a line.
310 ';' => {304 ';' => {
311 if (self.at_start_of_line) {305 state = .semicolon;
312 state = .semicolon;
313 }
314 self.at_start_of_line = false;306 self.at_start_of_line = false;
315 },307 },
316 else => {308 else => {
...@@ -345,7 +337,11 @@ pub const Lexer = struct {...@@ -345,7 +337,11 @@ pub const Lexer = struct {
345 }337 }
346 } else { // got EOF338 } else { // got EOF
347 switch (state) {339 switch (state) {
348 .start, .semicolon => {},340 .start => {},
341 .semicolon => {
342 // Skip past everything up to the EOF
343 result.start = self.index;
344 },
349 .literal => {345 .literal => {
350 result.id = .literal;346 result.id = .literal;
351 },347 },
...@@ -357,6 +353,10 @@ pub const Lexer = struct {...@@ -357,6 +353,10 @@ pub const Lexer = struct {
357 }353 }
358354
359 result.end = self.index;355 result.end = self.index;
356
357 // EOF tokens must have their start index match the end index
358 std.debug.assert(result.id != .eof or result.start == result.end);
359
360 return result;360 return result;
361 }361 }
362362
...@@ -796,7 +796,11 @@ pub const Lexer = struct {...@@ -796,7 +796,11 @@ pub const Lexer = struct {
796 }796 }
797 } else { // got EOF797 } else { // got EOF
798 switch (state) {798 switch (state) {
799 .start, .semicolon => {},799 .start => {},
800 .semicolon => {
801 // Skip past everything up to the EOF
802 result.start = self.index;
803 },
800 .literal_or_quoted_wide_string, .literal, .e, .en, .b, .be, .beg, .begi => {804 .literal_or_quoted_wide_string, .literal, .e, .en, .b, .be, .beg, .begi => {
801 result.id = .literal;805 result.id = .literal;
802 },806 },
...@@ -835,6 +839,9 @@ pub const Lexer = struct {...@@ -835,6 +839,9 @@ pub const Lexer = struct {
835 }839 }
836 }840 }
837841
842 // EOF tokens must have their start index match the end index
843 std.debug.assert(result.id != .eof or result.start == result.end);
844
838 return result;845 return result;
839 }846 }
840847
...@@ -878,7 +885,7 @@ pub const Lexer = struct {...@@ -878,7 +885,7 @@ pub const Lexer = struct {
878 // and miscompilations when used within string literals. We avoid the miscompilation885 // and miscompilations when used within string literals. We avoid the miscompilation
879 // within string literals and emit a warning, but outside of string literals it makes886 // within string literals and emit a warning, but outside of string literals it makes
880 // more sense to just disallow these codepoints.887 // more sense to just disallow these codepoints.
881 0x900, 0xA00, 0xA0D, 0x2000, 0xFFFE, 0xD00 => if (!in_string_literal) error.IllegalCodepointOutsideStringLiterals else return,888 0x900, 0xA00, 0xA0D, 0x2000, 0xD00, 0xFFFE, 0xFFFF => if (!in_string_literal) error.IllegalCodepointOutsideStringLiterals else return,
882 else => return,889 else => return,
883 };890 };
884 self.error_context_token = .{891 self.error_context_token = .{
...@@ -899,90 +906,11 @@ pub const Lexer = struct {...@@ -899,90 +906,11 @@ pub const Lexer = struct {
899 };906 };
900 errdefer self.error_context_token = token;907 errdefer self.error_context_token = token;
901 const full_command = self.buffer[start..end];908 const full_command = self.buffer[start..end];
902 var command = full_command;
903
904 // Anything besides exactly this is ignored by the Windows RC implementation
905 const expected_directive = "#pragma";
906 if (!std.mem.startsWith(u8, command, expected_directive)) return;
907 command = command[expected_directive.len..];
908
909 if (command.len == 0 or !std.ascii.isWhitespace(command[0])) return;
910 while (command.len > 0 and std.ascii.isWhitespace(command[0])) {
911 command = command[1..];
912 }
913
914 // Note: CoDe_PaGeZ is also treated as "code_page" by the Windows RC implementation,
915 // and it will error with 'Missing left parenthesis in code_page #pragma'
916 const expected_extension = "code_page";
917 if (!std.ascii.startsWithIgnoreCase(command, expected_extension)) return;
918 command = command[expected_extension.len..];
919
920 while (command.len > 0 and std.ascii.isWhitespace(command[0])) {
921 command = command[1..];
922 }
923
924 if (command.len == 0 or command[0] != '(') {
925 return error.CodePagePragmaMissingLeftParen;
926 }
927 command = command[1..];
928909
929 while (command.len > 0 and std.ascii.isWhitespace(command[0])) {910 const code_page = (parsePragmaCodePage(full_command) catch |err| switch (err) {
930 command = command[1..];911 error.NotPragma, error.NotCodePagePragma => return,
931 }912 else => |e| return e,
932913 }) orelse self.default_code_page;
933 var num_str: []u8 = command[0..0];
934 while (command.len > 0 and (command[0] != ')' and !std.ascii.isWhitespace(command[0]))) {
935 command = command[1..];
936 num_str.len += 1;
937 }
938
939 if (num_str.len == 0) {
940 return error.CodePagePragmaNotInteger;
941 }
942
943 while (command.len > 0 and std.ascii.isWhitespace(command[0])) {
944 command = command[1..];
945 }
946
947 if (command.len == 0 or command[0] != ')') {
948 return error.CodePagePragmaMissingRightParen;
949 }
950
951 const code_page = code_page: {
952 if (std.ascii.eqlIgnoreCase("DEFAULT", num_str)) {
953 break :code_page self.default_code_page;
954 }
955
956 // The Win32 compiler behaves fairly strangely around maxInt(u32):
957 // - If the overflowed u32 wraps and becomes a known code page ID, then
958 // it will error/warn with "Codepage not valid: ignored" (depending on /w)
959 // - If the overflowed u32 wraps and does not become a known code page ID,
960 // then it will error with 'constant too big' and 'Codepage not integer'
961 //
962 // Instead of that, we just have a separate error specifically for overflow.
963 const num = parseCodePageNum(num_str) catch |err| switch (err) {
964 error.InvalidCharacter => return error.CodePagePragmaNotInteger,
965 error.Overflow => return error.CodePagePragmaOverflow,
966 };
967
968 // Anything that starts with 0 but does not resolve to 0 is treated as invalid, e.g. 01252
969 if (num_str[0] == '0' and num != 0) {
970 return error.CodePagePragmaInvalidCodePage;
971 }
972 // Anything that resolves to 0 is treated as 'not an integer' by the Win32 implementation.
973 else if (num == 0) {
974 return error.CodePagePragmaNotInteger;
975 }
976 // Anything above u16 max is not going to be found since our CodePage enum is backed by a u16.
977 if (num > std.math.maxInt(u16)) {
978 return error.CodePagePragmaInvalidCodePage;
979 }
980
981 break :code_page code_pages.CodePage.getByIdentifierEnsureSupported(@intCast(num)) catch |err| switch (err) {
982 error.InvalidCodePage => return error.CodePagePragmaInvalidCodePage,
983 error.UnsupportedCodePage => return error.CodePagePragmaUnsupportedCodePage,
984 };
985 };
986914
987 // https://learn.microsoft.com/en-us/windows/win32/menurc/pragma-directives915 // https://learn.microsoft.com/en-us/windows/win32/menurc/pragma-directives
988 // > This pragma is not supported in an included resource file (.rc)916 // > This pragma is not supported in an included resource file (.rc)
...@@ -998,24 +926,16 @@ pub const Lexer = struct {...@@ -998,24 +926,16 @@ pub const Lexer = struct {
998 }926 }
999927
1000 self.seen_pragma_code_pages +|= 1;928 self.seen_pragma_code_pages +|= 1;
929 self.last_pragma_code_page_token = token;
1001 self.current_code_page = code_page;930 self.current_code_page = code_page;
1002 }931 }
1003932
1004 fn parseCodePageNum(str: []const u8) !u32 {
1005 var x: u32 = 0;
1006 for (str) |c| {
1007 const digit = try std.fmt.charToDigit(c, 10);
1008 if (x != 0) x = try std.math.mul(u32, x, 10);
1009 x = try std.math.add(u32, x, digit);
1010 }
1011 return x;
1012 }
1013
1014 pub fn getErrorDetails(self: Self, lex_err: LexError) ErrorDetails {933 pub fn getErrorDetails(self: Self, lex_err: LexError) ErrorDetails {
1015 const err = switch (lex_err) {934 const err = switch (lex_err) {
1016 error.UnfinishedStringLiteral => ErrorDetails.Error.unfinished_string_literal,935 error.UnfinishedStringLiteral => ErrorDetails.Error.unfinished_string_literal,
1017 error.StringLiteralTooLong => return .{936 error.StringLiteralTooLong => return .{
1018 .err = .string_literal_too_long,937 .err = .string_literal_too_long,
938 .code_page = self.current_code_page,
1019 .token = self.error_context_token.?,939 .token = self.error_context_token.?,
1020 .extra = .{ .number = self.max_string_literal_codepoints },940 .extra = .{ .number = self.max_string_literal_codepoints },
1021 },941 },
...@@ -1037,11 +957,112 @@ pub const Lexer = struct {...@@ -1037,11 +957,112 @@ pub const Lexer = struct {
1037 };957 };
1038 return .{958 return .{
1039 .err = err,959 .err = err,
960 .code_page = self.current_code_page,
1040 .token = self.error_context_token.?,961 .token = self.error_context_token.?,
1041 };962 };
1042 }963 }
1043};964};
1044965
966fn parseCodePageNum(str: []const u8) !u32 {
967 var x: u32 = 0;
968 for (str) |c| {
969 const digit = try std.fmt.charToDigit(c, 10);
970 if (x != 0) x = try std.math.mul(u32, x, 10);
971 x = try std.math.add(u32, x, digit);
972 }
973 return x;
974}
975
976/// Returns `null` when the code_page is set to DEFAULT
977pub fn parsePragmaCodePage(full_command: []const u8) !?SupportedCodePage {
978 var command = full_command;
979
980 // Anything besides exactly this is ignored by the Windows RC implementation
981 const expected_directive = "#pragma";
982 if (!std.mem.startsWith(u8, command, expected_directive)) return error.NotPragma;
983 command = command[expected_directive.len..];
984
985 if (command.len == 0 or !std.ascii.isWhitespace(command[0])) return error.NotCodePagePragma;
986 while (command.len > 0 and std.ascii.isWhitespace(command[0])) {
987 command = command[1..];
988 }
989
990 // Note: CoDe_PaGeZ is also treated as "code_page" by the Windows RC implementation,
991 // and it will error with 'Missing left parenthesis in code_page #pragma'
992 const expected_extension = "code_page";
993 if (!std.ascii.startsWithIgnoreCase(command, expected_extension)) return error.NotCodePagePragma;
994 command = command[expected_extension.len..];
995
996 while (command.len > 0 and std.ascii.isWhitespace(command[0])) {
997 command = command[1..];
998 }
999
1000 if (command.len == 0 or command[0] != '(') {
1001 return error.CodePagePragmaMissingLeftParen;
1002 }
1003 command = command[1..];
1004
1005 while (command.len > 0 and std.ascii.isWhitespace(command[0])) {
1006 command = command[1..];
1007 }
1008
1009 var num_str: []u8 = command[0..0];
1010 while (command.len > 0 and (command[0] != ')' and !std.ascii.isWhitespace(command[0]))) {
1011 command = command[1..];
1012 num_str.len += 1;
1013 }
1014
1015 if (num_str.len == 0) {
1016 return error.CodePagePragmaNotInteger;
1017 }
1018
1019 while (command.len > 0 and std.ascii.isWhitespace(command[0])) {
1020 command = command[1..];
1021 }
1022
1023 if (command.len == 0 or command[0] != ')') {
1024 return error.CodePagePragmaMissingRightParen;
1025 }
1026
1027 const code_page: ?SupportedCodePage = code_page: {
1028 if (std.ascii.eqlIgnoreCase("DEFAULT", num_str)) {
1029 break :code_page null;
1030 }
1031
1032 // The Win32 compiler behaves fairly strangely around maxInt(u32):
1033 // - If the overflowed u32 wraps and becomes a known code page ID, then
1034 // it will error/warn with "Codepage not valid: ignored" (depending on /w)
1035 // - If the overflowed u32 wraps and does not become a known code page ID,
1036 // then it will error with 'constant too big' and 'Codepage not integer'
1037 //
1038 // Instead of that, we just have a separate error specifically for overflow.
1039 const num = parseCodePageNum(num_str) catch |err| switch (err) {
1040 error.InvalidCharacter => return error.CodePagePragmaNotInteger,
1041 error.Overflow => return error.CodePagePragmaOverflow,
1042 };
1043
1044 // Anything that starts with 0 but does not resolve to 0 is treated as invalid, e.g. 01252
1045 if (num_str[0] == '0' and num != 0) {
1046 return error.CodePagePragmaInvalidCodePage;
1047 }
1048 // Anything that resolves to 0 is treated as 'not an integer' by the Win32 implementation.
1049 else if (num == 0) {
1050 return error.CodePagePragmaNotInteger;
1051 }
1052 // Anything above u16 max is not going to be found since our CodePage enum is backed by a u16.
1053 if (num > std.math.maxInt(u16)) {
1054 return error.CodePagePragmaInvalidCodePage;
1055 }
1056
1057 break :code_page code_pages.getByIdentifierEnsureSupported(@intCast(num)) catch |err| switch (err) {
1058 error.InvalidCodePage => return error.CodePagePragmaInvalidCodePage,
1059 error.UnsupportedCodePage => return error.CodePagePragmaUnsupportedCodePage,
1060 };
1061 };
1062
1063 return code_page;
1064}
1065
1045fn testLexNormal(source: []const u8, expected_tokens: []const Token.Id) !void {1066fn testLexNormal(source: []const u8, expected_tokens: []const Token.Id) !void {
1046 var lexer = Lexer.init(source, .{});1067 var lexer = Lexer.init(source, .{});
1047 if (dumpTokensDuringTests) std.debug.print("\n----------------------\n{s}\n----------------------\n", .{lexer.buffer});1068 if (dumpTokensDuringTests) std.debug.print("\n----------------------\n{s}\n----------------------\n", .{lexer.buffer});
...@@ -1074,7 +1095,7 @@ test "normal: string literals" {...@@ -1074,7 +1095,7 @@ test "normal: string literals" {
10741095
1075test "superscript chars and code pages" {1096test "superscript chars and code pages" {
1076 const firstToken = struct {1097 const firstToken = struct {
1077 pub fn firstToken(source: []const u8, default_code_page: CodePage, comptime lex_method: Lexer.LexMethod) LexError!Token {1098 pub fn firstToken(source: []const u8, default_code_page: SupportedCodePage, comptime lex_method: Lexer.LexMethod) LexError!Token {
1078 var lexer = Lexer.init(source, .{ .default_code_page = default_code_page });1099 var lexer = Lexer.init(source, .{ .default_code_page = default_code_page });
1079 return lexer.next(lex_method);1100 return lexer.next(lex_method);
1080 }1101 }
lib/compiler/resinator/literals.zig+280-93
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1const std = @import("std");1const std = @import("std");
2const code_pages = @import("code_pages.zig");2const code_pages = @import("code_pages.zig");
3const CodePage = code_pages.CodePage;3const SupportedCodePage = code_pages.SupportedCodePage;
4const windows1252 = @import("windows1252.zig");4const windows1252 = @import("windows1252.zig");
5const ErrorDetails = @import("errors.zig").ErrorDetails;5const ErrorDetails = @import("errors.zig").ErrorDetails;
6const DiagnosticsContext = @import("errors.zig").DiagnosticsContext;6const DiagnosticsContext = @import("errors.zig").DiagnosticsContext;
...@@ -18,7 +18,7 @@ pub fn isValidNumberDataLiteral(str: []const u8) bool {...@@ -18,7 +18,7 @@ pub fn isValidNumberDataLiteral(str: []const u8) bool {
1818
19pub const SourceBytes = struct {19pub const SourceBytes = struct {
20 slice: []const u8,20 slice: []const u8,
21 code_page: CodePage,21 code_page: SupportedCodePage,
22};22};
2323
24pub const StringType = enum { ascii, wide };24pub const StringType = enum { ascii, wide };
...@@ -53,7 +53,7 @@ pub const StringType = enum { ascii, wide };...@@ -53,7 +53,7 @@ pub const StringType = enum { ascii, wide };
53/// branches should never actually be hit during this function.53/// branches should never actually be hit during this function.
54pub const IterativeStringParser = struct {54pub const IterativeStringParser = struct {
55 source: []const u8,55 source: []const u8,
56 code_page: CodePage,56 code_page: SupportedCodePage,
57 /// The type of the string inferred by the prefix (L"" or "")57 /// The type of the string inferred by the prefix (L"" or "")
58 /// This is what matters for things like the maximum digits in an58 /// This is what matters for things like the maximum digits in an
59 /// escape sequence, whether or not invalid escape sequences are skipped, etc.59 /// escape sequence, whether or not invalid escape sequences are skipped, etc.
...@@ -98,32 +98,55 @@ pub const IterativeStringParser = struct {...@@ -98,32 +98,55 @@ pub const IterativeStringParser = struct {
9898
99 pub const ParsedCodepoint = struct {99 pub const ParsedCodepoint = struct {
100 codepoint: u21,100 codepoint: u21,
101 /// Note: If this is true, `codepoint` will be a value with a max of maxInt(u16).101 /// Note: If this is true, `codepoint` will have an effective maximum value
102 /// This is enforced by using saturating arithmetic, so in e.g. a wide string literal the102 /// of 0xFFFF, as `codepoint` is calculated using wrapping arithmetic on a u16.
103 /// octal escape sequence \7777777 (2,097,151) will be parsed into the value 0xFFFF (65,535).103 /// If the value needs to be truncated to a smaller integer (e.g. for ASCII string
104 /// If the value needs to be truncated to a smaller integer (for ASCII string literals), then that104 /// literals), then that must be done by the caller.
105 /// must be done by the caller.
106 from_escaped_integer: bool = false,105 from_escaped_integer: bool = false,
106 /// Denotes that the codepoint is:
107 /// - Escaped (has a \ in front of it), and
108 /// - Has a value >= U+10000, meaning it would be encoded as a surrogate
109 /// pair in UTF-16, and
110 /// - Is part of a wide string literal
111 ///
112 /// Normally in wide string literals, invalid escapes are omitted
113 /// during parsing (the codepoints are not returned at all during
114 /// the `next` call), but this is a special case in which the
115 /// escape only applies to the high surrogate pair of the codepoint.
116 ///
117 /// TODO: Maybe just return the low surrogate codepoint by itself in this case.
118 escaped_surrogate_pair: bool = false,
107 };119 };
108120
109 pub fn next(self: *IterativeStringParser) std.mem.Allocator.Error!?ParsedCodepoint {121 pub fn next(self: *IterativeStringParser) std.mem.Allocator.Error!?ParsedCodepoint {
110 const result = try self.nextUnchecked();122 const result = try self.nextUnchecked();
111 if (self.diagnostics != null and result != null and !result.?.from_escaped_integer) {123 if (self.diagnostics != null and result != null and !result.?.from_escaped_integer) {
112 switch (result.?.codepoint) {124 switch (result.?.codepoint) {
113 0x900, 0xA00, 0xA0D, 0x2000, 0xFFFE, 0xD00 => {125 0x0900, 0x0A00, 0x0A0D, 0x2000, 0x0D00 => {
114 const err: ErrorDetails.Error = if (result.?.codepoint == 0xD00)126 const err: ErrorDetails.Error = if (result.?.codepoint == 0xD00)
115 .rc_would_miscompile_codepoint_skip127 .rc_would_miscompile_codepoint_skip
116 else128 else
117 .rc_would_miscompile_codepoint_byte_swap;129 .rc_would_miscompile_codepoint_whitespace;
118 try self.diagnostics.?.diagnostics.append(ErrorDetails{130 try self.diagnostics.?.diagnostics.append(ErrorDetails{
119 .err = err,131 .err = err,
120 .type = .warning,132 .type = .warning,
133 .code_page = self.code_page,
121 .token = self.diagnostics.?.token,134 .token = self.diagnostics.?.token,
122 .extra = .{ .number = result.?.codepoint },135 .extra = .{ .number = result.?.codepoint },
123 });136 });
137 },
138 0xFFFE, 0xFFFF => {
124 try self.diagnostics.?.diagnostics.append(ErrorDetails{139 try self.diagnostics.?.diagnostics.append(ErrorDetails{
125 .err = err,140 .err = .rc_would_miscompile_codepoint_bom,
141 .type = .warning,
142 .code_page = self.code_page,
143 .token = self.diagnostics.?.token,
144 .extra = .{ .number = result.?.codepoint },
145 });
146 try self.diagnostics.?.diagnostics.append(ErrorDetails{
147 .err = .rc_would_miscompile_codepoint_bom,
126 .type = .note,148 .type = .note,
149 .code_page = self.code_page,
127 .token = self.diagnostics.?.token,150 .token = self.diagnostics.?.token,
128 .print_source_line = false,151 .print_source_line = false,
129 .extra = .{ .number = result.?.codepoint },152 .extra = .{ .number = result.?.codepoint },
...@@ -188,11 +211,13 @@ pub const IterativeStringParser = struct {...@@ -188,11 +211,13 @@ pub const IterativeStringParser = struct {
188 try self.diagnostics.?.diagnostics.append(ErrorDetails{211 try self.diagnostics.?.diagnostics.append(ErrorDetails{
189 .err = .tab_converted_to_spaces,212 .err = .tab_converted_to_spaces,
190 .type = .warning,213 .type = .warning,
214 .code_page = self.code_page,
191 .token = self.diagnostics.?.token,215 .token = self.diagnostics.?.token,
192 });216 });
193 try self.diagnostics.?.diagnostics.append(ErrorDetails{217 try self.diagnostics.?.diagnostics.append(ErrorDetails{
194 .err = .tab_converted_to_spaces,218 .err = .tab_converted_to_spaces,
195 .type = .note,219 .type = .note,
220 .code_page = self.code_page,
196 .token = self.diagnostics.?.token,221 .token = self.diagnostics.?.token,
197 .print_source_line = false,222 .print_source_line = false,
198 });223 });
...@@ -246,8 +271,9 @@ pub const IterativeStringParser = struct {...@@ -246,8 +271,9 @@ pub const IterativeStringParser = struct {
246 switch (c) {271 switch (c) {
247 'a', 'A' => {272 'a', 'A' => {
248 self.index += codepoint.byte_len;273 self.index += codepoint.byte_len;
274 // might be a bug in RC, but matches its behavior
249 return .{ .codepoint = '\x08' };275 return .{ .codepoint = '\x08' };
250 }, // might be a bug in RC, but matches its behavior276 },
251 'n' => {277 'n' => {
252 self.index += codepoint.byte_len;278 self.index += codepoint.byte_len;
253 return .{ .codepoint = '\n' };279 return .{ .codepoint = '\n' };
...@@ -269,7 +295,65 @@ pub const IterativeStringParser = struct {...@@ -269,7 +295,65 @@ pub const IterativeStringParser = struct {
269 backtrack = true;295 backtrack = true;
270 },296 },
271 else => switch (self.declared_string_type) {297 else => switch (self.declared_string_type) {
272 .wide => {}, // invalid escape sequences are skipped in wide strings298 .wide => {
299 // All invalid escape sequences are skipped in wide strings,
300 // but there is a special case around \<tab> where the \
301 // is skipped but the tab character is processed.
302 // It's actually a bit weirder than that, though, since
303 // the preprocessor is the one that does the <tab> -> spaces
304 // conversion, so it goes something like this:
305 //
306 // Before preprocessing: L"\<tab>"
307 // After preprocessing: L"\ "
308 //
309 // So the parser only sees an escaped space character followed
310 // by some other number of spaces >= 0.
311 //
312 // However, our preprocessor keeps tab characters intact, so we emulate
313 // the above behavior by skipping the \ and then outputting one less
314 // space than normal for the <tab> character.
315 if (c == '\t') {
316 // Only warn about a tab getting converted to spaces once per string
317 if (self.diagnostics != null and !self.seen_tab) {
318 try self.diagnostics.?.diagnostics.append(ErrorDetails{
319 .err = .tab_converted_to_spaces,
320 .type = .warning,
321 .code_page = self.code_page,
322 .token = self.diagnostics.?.token,
323 });
324 try self.diagnostics.?.diagnostics.append(ErrorDetails{
325 .err = .tab_converted_to_spaces,
326 .type = .note,
327 .code_page = self.code_page,
328 .token = self.diagnostics.?.token,
329 .print_source_line = false,
330 });
331 self.seen_tab = true;
332 }
333
334 const cols = columnsUntilTabStop(self.column, 8);
335 // If the tab character would only be converted to a single space,
336 // then we can just skip both the \ and the <tab> and move on.
337 if (cols > 1) {
338 self.num_pending_spaces = @intCast(cols - 2);
339 self.index += codepoint.byte_len;
340 return .{ .codepoint = ' ' };
341 }
342 }
343 // There's a second special case when the codepoint would be encoded
344 // as a surrogate pair in UTF-16, as the escape 'applies' to the
345 // high surrogate pair only in this instance. This is a side-effect
346 // of the Win32 RC compiler preprocessor outputting UTF-16 and the
347 // compiler itself seemingly working on code units instead of code points
348 // in this particular instance.
349 //
350 // We emulate this behavior by emitting the codepoint, but with a marker
351 // that indicates that it needs to be handled specially.
352 if (c >= 0x10000 and c != code_pages.Codepoint.invalid) {
353 self.index += codepoint.byte_len;
354 return .{ .codepoint = c, .escaped_surrogate_pair = true };
355 }
356 },
273 .ascii => {357 .ascii => {
274 // we intentionally avoid incrementing self.index358 // we intentionally avoid incrementing self.index
275 // to handle the current char in the next call,359 // to handle the current char in the next call,
...@@ -303,6 +387,9 @@ pub const IterativeStringParser = struct {...@@ -303,6 +387,9 @@ pub const IterativeStringParser = struct {
303 },387 },
304 .escaped_octal => switch (c) {388 .escaped_octal => switch (c) {
305 '0'...'7' => {389 '0'...'7' => {
390 // Note: We use wrapping arithmetic on a u16 here since there's been no observed
391 // string parsing scenario where an escaped integer with a value >= the u16
392 // max is interpreted as anything but the truncated u16 value.
306 string_escape_n *%= 8;393 string_escape_n *%= 8;
307 string_escape_n +%= std.fmt.charToDigit(@intCast(c), 8) catch unreachable;394 string_escape_n +%= std.fmt.charToDigit(@intCast(c), 8) catch unreachable;
308 string_escape_i += 1;395 string_escape_i += 1;
...@@ -367,7 +454,7 @@ pub const IterativeStringParser = struct {...@@ -367,7 +454,7 @@ pub const IterativeStringParser = struct {
367pub const StringParseOptions = struct {454pub const StringParseOptions = struct {
368 start_column: usize = 0,455 start_column: usize = 0,
369 diagnostics: ?DiagnosticsContext = null,456 diagnostics: ?DiagnosticsContext = null,
370 output_code_page: CodePage = .windows1252,457 output_code_page: SupportedCodePage,
371};458};
372459
373pub fn parseQuotedString(460pub fn parseQuotedString(
...@@ -389,46 +476,52 @@ pub fn parseQuotedString(...@@ -389,46 +476,52 @@ pub fn parseQuotedString(
389476
390 while (try iterative_parser.next()) |parsed| {477 while (try iterative_parser.next()) |parsed| {
391 const c = parsed.codepoint;478 const c = parsed.codepoint;
392 if (parsed.from_escaped_integer) {479 switch (literal_type) {
393 // We truncate here to get the correct behavior for ascii strings480 .ascii => switch (options.output_code_page) {
394 try buf.append(std.mem.nativeToLittle(T, @truncate(c)));481 .windows1252 => {
395 } else {482 if (parsed.from_escaped_integer) {
396 switch (literal_type) {483 try buf.append(@truncate(c));
397 .ascii => switch (options.output_code_page) {484 } else if (windows1252.bestFitFromCodepoint(c)) |best_fit| {
398 .windows1252 => {485 try buf.append(best_fit);
399 if (windows1252.bestFitFromCodepoint(c)) |best_fit| {486 } else if (c < 0x10000 or c == code_pages.Codepoint.invalid) {
400 try buf.append(best_fit);487 try buf.append('?');
401 } else if (c < 0x10000 or c == code_pages.Codepoint.invalid) {
402 try buf.append('?');
403 } else {
404 try buf.appendSlice("??");
405 }
406 },
407 .utf8 => {
408 var codepoint_to_encode = c;
409 if (c == code_pages.Codepoint.invalid) {
410 codepoint_to_encode = '�';
411 }
412 var utf8_buf: [4]u8 = undefined;
413 const utf8_len = std.unicode.utf8Encode(codepoint_to_encode, &utf8_buf) catch unreachable;
414 try buf.appendSlice(utf8_buf[0..utf8_len]);
415 },
416 else => unreachable, // Unsupported code page
417 },
418 .wide => {
419 if (c == code_pages.Codepoint.invalid) {
420 try buf.append(std.mem.nativeToLittle(u16, '�'));
421 } else if (c < 0x10000) {
422 const short: u16 = @intCast(c);
423 try buf.append(std.mem.nativeToLittle(u16, short));
424 } else {488 } else {
489 try buf.appendSlice("??");
490 }
491 },
492 .utf8 => {
493 var codepoint_to_encode = c;
494 if (parsed.from_escaped_integer) {
495 codepoint_to_encode = @as(T, @truncate(c));
496 }
497 const escaped_integer_outside_ascii_range = parsed.from_escaped_integer and codepoint_to_encode > 0x7F;
498 if (escaped_integer_outside_ascii_range or c == code_pages.Codepoint.invalid) {
499 codepoint_to_encode = '�';
500 }
501 var utf8_buf: [4]u8 = undefined;
502 const utf8_len = std.unicode.utf8Encode(codepoint_to_encode, &utf8_buf) catch unreachable;
503 try buf.appendSlice(utf8_buf[0..utf8_len]);
504 },
505 },
506 .wide => {
507 // Parsing any string type as a wide string is handled separately, see parseQuotedStringAsWideString
508 std.debug.assert(iterative_parser.declared_string_type == .wide);
509 if (parsed.from_escaped_integer) {
510 try buf.append(std.mem.nativeToLittle(u16, @truncate(c)));
511 } else if (c == code_pages.Codepoint.invalid) {
512 try buf.append(std.mem.nativeToLittle(u16, '�'));
513 } else if (c < 0x10000) {
514 const short: u16 = @intCast(c);
515 try buf.append(std.mem.nativeToLittle(u16, short));
516 } else {
517 if (!parsed.escaped_surrogate_pair) {
425 const high = @as(u16, @intCast((c - 0x10000) >> 10)) + 0xD800;518 const high = @as(u16, @intCast((c - 0x10000) >> 10)) + 0xD800;
426 try buf.append(std.mem.nativeToLittle(u16, high));519 try buf.append(std.mem.nativeToLittle(u16, high));
427 const low = @as(u16, @intCast(c & 0x3FF)) + 0xDC00;
428 try buf.append(std.mem.nativeToLittle(u16, low));
429 }520 }
430 },521 const low = @as(u16, @intCast(c & 0x3FF)) + 0xDC00;
431 }522 try buf.append(std.mem.nativeToLittle(u16, low));
523 }
524 },
432 }525 }
433 }526 }
434527
...@@ -449,9 +542,59 @@ pub fn parseQuotedWideString(allocator: std.mem.Allocator, bytes: SourceBytes, o...@@ -449,9 +542,59 @@ pub fn parseQuotedWideString(allocator: std.mem.Allocator, bytes: SourceBytes, o
449 return parseQuotedString(.wide, allocator, bytes, options);542 return parseQuotedString(.wide, allocator, bytes, options);
450}543}
451544
545/// Parses any string type into a wide string.
546/// If the string is declared as a wide string (L""), then it is handled normally.
547/// Otherwise, things are fairly normal with the exception of escaped integers.
548/// Escaped integers are handled by:
549/// - Truncating the escape to a u8
550/// - Reinterpeting the u8 as a byte from the *output* code page
551/// - Outputting the codepoint that corresponds to the interpreted byte, or � if no such
552/// interpretation is possible
553/// For example, if the code page is UTF-8, then while \x80 is a valid start byte, it's
554/// interpreted as a single byte, so it ends up being seen as invalid and � is outputted.
555/// If the code page is Windows-1252, then \x80 is interpreted to be € which has the
556/// codepoint U+20AC, so the UTF-16 encoding of U+20AC is outputted.
452pub fn parseQuotedStringAsWideString(allocator: std.mem.Allocator, bytes: SourceBytes, options: StringParseOptions) ![:0]u16 {557pub fn parseQuotedStringAsWideString(allocator: std.mem.Allocator, bytes: SourceBytes, options: StringParseOptions) ![:0]u16 {
453 std.debug.assert(bytes.slice.len >= 2); // ""558 std.debug.assert(bytes.slice.len >= 2); // ""
454 return parseQuotedString(.wide, allocator, bytes, options);559
560 if (bytes.slice[0] == 'l' or bytes.slice[0] == 'L') {
561 return parseQuotedWideString(allocator, bytes, options);
562 }
563
564 // Note: We're only handling the case of parsing an ASCII string into a wide string from here on out.
565 // TODO: The logic below is similar to that in AcceleratorKeyCodepointTranslator, might be worth merging the two
566
567 var buf = try std.ArrayList(u16).initCapacity(allocator, bytes.slice.len);
568 errdefer buf.deinit();
569
570 var iterative_parser = IterativeStringParser.init(bytes, options);
571
572 while (try iterative_parser.next()) |parsed| {
573 const c = parsed.codepoint;
574 if (parsed.from_escaped_integer) {
575 std.debug.assert(c != code_pages.Codepoint.invalid);
576 const byte_to_interpret: u8 = @truncate(c);
577 const code_unit_to_encode: u16 = switch (options.output_code_page) {
578 .windows1252 => windows1252.toCodepoint(byte_to_interpret),
579 .utf8 => if (byte_to_interpret > 0x7F) '�' else byte_to_interpret,
580 };
581 try buf.append(std.mem.nativeToLittle(u16, code_unit_to_encode));
582 } else if (c == code_pages.Codepoint.invalid) {
583 try buf.append(std.mem.nativeToLittle(u16, '�'));
584 } else if (c < 0x10000) {
585 const short: u16 = @intCast(c);
586 try buf.append(std.mem.nativeToLittle(u16, short));
587 } else {
588 if (!parsed.escaped_surrogate_pair) {
589 const high = @as(u16, @intCast((c - 0x10000) >> 10)) + 0xD800;
590 try buf.append(std.mem.nativeToLittle(u16, high));
591 }
592 const low = @as(u16, @intCast(c & 0x3FF)) + 0xDC00;
593 try buf.append(std.mem.nativeToLittle(u16, low));
594 }
595 }
596
597 return buf.toOwnedSliceSentinel(0);
455}598}
456599
457test "parse quoted ascii string" {600test "parse quoted ascii string" {
...@@ -464,133 +607,155 @@ test "parse quoted ascii string" {...@@ -464,133 +607,155 @@ test "parse quoted ascii string" {
464 \\"hello"607 \\"hello"
465 ,608 ,
466 .code_page = .windows1252,609 .code_page = .windows1252,
467 }, .{}));610 }, .{
611 .output_code_page = .windows1252,
612 }));
468 // hex with 0 digits613 // hex with 0 digits
469 try std.testing.expectEqualSlices(u8, "\x00", try parseQuotedAsciiString(arena, .{614 try std.testing.expectEqualSlices(u8, "\x00", try parseQuotedAsciiString(arena, .{
470 .slice =615 .slice =
471 \\"\x"616 \\"\x"
472 ,617 ,
473 .code_page = .windows1252,618 .code_page = .windows1252,
474 }, .{}));619 }, .{
620 .output_code_page = .windows1252,
621 }));
475 // hex max of 2 digits622 // hex max of 2 digits
476 try std.testing.expectEqualSlices(u8, "\xFFf", try parseQuotedAsciiString(arena, .{623 try std.testing.expectEqualSlices(u8, "\xFFf", try parseQuotedAsciiString(arena, .{
477 .slice =624 .slice =
478 \\"\XfFf"625 \\"\XfFf"
479 ,626 ,
480 .code_page = .windows1252,627 .code_page = .windows1252,
481 }, .{}));628 }, .{
629 .output_code_page = .windows1252,
630 }));
482 // octal with invalid octal digit631 // octal with invalid octal digit
483 try std.testing.expectEqualSlices(u8, "\x019", try parseQuotedAsciiString(arena, .{632 try std.testing.expectEqualSlices(u8, "\x019", try parseQuotedAsciiString(arena, .{
484 .slice =633 .slice =
485 \\"\19"634 \\"\19"
486 ,635 ,
487 .code_page = .windows1252,636 .code_page = .windows1252,
488 }, .{}));637 }, .{
638 .output_code_page = .windows1252,
639 }));
489 // escaped quotes640 // escaped quotes
490 try std.testing.expectEqualSlices(u8, " \" ", try parseQuotedAsciiString(arena, .{641 try std.testing.expectEqualSlices(u8, " \" ", try parseQuotedAsciiString(arena, .{
491 .slice =642 .slice =
492 \\" "" "643 \\" "" "
493 ,644 ,
494 .code_page = .windows1252,645 .code_page = .windows1252,
495 }, .{}));646 }, .{
647 .output_code_page = .windows1252,
648 }));
496 // backslash right before escaped quotes649 // backslash right before escaped quotes
497 try std.testing.expectEqualSlices(u8, "\"", try parseQuotedAsciiString(arena, .{650 try std.testing.expectEqualSlices(u8, "\"", try parseQuotedAsciiString(arena, .{
498 .slice =651 .slice =
499 \\"\"""652 \\"\"""
500 ,653 ,
501 .code_page = .windows1252,654 .code_page = .windows1252,
502 }, .{}));655 }, .{
656 .output_code_page = .windows1252,
657 }));
503 // octal overflow658 // octal overflow
504 try std.testing.expectEqualSlices(u8, "\x01", try parseQuotedAsciiString(arena, .{659 try std.testing.expectEqualSlices(u8, "\x01", try parseQuotedAsciiString(arena, .{
505 .slice =660 .slice =
506 \\"\401"661 \\"\401"
507 ,662 ,
508 .code_page = .windows1252,663 .code_page = .windows1252,
509 }, .{}));664 }, .{
665 .output_code_page = .windows1252,
666 }));
510 // escapes667 // escapes
511 try std.testing.expectEqualSlices(u8, "\x08\n\r\t\\", try parseQuotedAsciiString(arena, .{668 try std.testing.expectEqualSlices(u8, "\x08\n\r\t\\", try parseQuotedAsciiString(arena, .{
512 .slice =669 .slice =
513 \\"\a\n\r\t\\"670 \\"\a\n\r\t\\"
514 ,671 ,
515 .code_page = .windows1252,672 .code_page = .windows1252,
516 }, .{}));673 }, .{
674 .output_code_page = .windows1252,
675 }));
517 // uppercase escapes676 // uppercase escapes
518 try std.testing.expectEqualSlices(u8, "\x08\\N\\R\t\\", try parseQuotedAsciiString(arena, .{677 try std.testing.expectEqualSlices(u8, "\x08\\N\\R\t\\", try parseQuotedAsciiString(arena, .{
519 .slice =678 .slice =
520 \\"\A\N\R\T\\"679 \\"\A\N\R\T\\"
521 ,680 ,
522 .code_page = .windows1252,681 .code_page = .windows1252,
523 }, .{}));682 }, .{
683 .output_code_page = .windows1252,
684 }));
524 // backslash on its own685 // backslash on its own
525 try std.testing.expectEqualSlices(u8, "\\", try parseQuotedAsciiString(arena, .{686 try std.testing.expectEqualSlices(u8, "\\", try parseQuotedAsciiString(arena, .{
526 .slice =687 .slice =
527 \\"\"688 \\"\"
528 ,689 ,
529 .code_page = .windows1252,690 .code_page = .windows1252,
530 }, .{}));691 }, .{
692 .output_code_page = .windows1252,
693 }));
531 // unrecognized escapes694 // unrecognized escapes
532 try std.testing.expectEqualSlices(u8, "\\b", try parseQuotedAsciiString(arena, .{695 try std.testing.expectEqualSlices(u8, "\\b", try parseQuotedAsciiString(arena, .{
533 .slice =696 .slice =
534 \\"\b"697 \\"\b"
535 ,698 ,
536 .code_page = .windows1252,699 .code_page = .windows1252,
537 }, .{}));700 }, .{
701 .output_code_page = .windows1252,
702 }));
538 // escaped carriage returns703 // escaped carriage returns
539 try std.testing.expectEqualSlices(u8, "\\", try parseQuotedAsciiString(704 try std.testing.expectEqualSlices(u8, "\\", try parseQuotedAsciiString(
540 arena,705 arena,
541 .{ .slice = "\"\\\r\r\r\r\r\"", .code_page = .windows1252 },706 .{ .slice = "\"\\\r\r\r\r\r\"", .code_page = .windows1252 },
542 .{},707 .{ .output_code_page = .windows1252 },
543 ));708 ));
544 // escaped newlines709 // escaped newlines
545 try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString(710 try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString(
546 arena,711 arena,
547 .{ .slice = "\"\\\n\n\n\n\n\"", .code_page = .windows1252 },712 .{ .slice = "\"\\\n\n\n\n\n\"", .code_page = .windows1252 },
548 .{},713 .{ .output_code_page = .windows1252 },
549 ));714 ));
550 // escaped CRLF pairs715 // escaped CRLF pairs
551 try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString(716 try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString(
552 arena,717 arena,
553 .{ .slice = "\"\\\r\n\r\n\r\n\r\n\r\n\"", .code_page = .windows1252 },718 .{ .slice = "\"\\\r\n\r\n\r\n\r\n\r\n\"", .code_page = .windows1252 },
554 .{},719 .{ .output_code_page = .windows1252 },
555 ));720 ));
556 // escaped newlines with other whitespace721 // escaped newlines with other whitespace
557 try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString(722 try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString(
558 arena,723 arena,
559 .{ .slice = "\"\\\n \t\r\n \r\t\n \t\"", .code_page = .windows1252 },724 .{ .slice = "\"\\\n \t\r\n \r\t\n \t\"", .code_page = .windows1252 },
560 .{},725 .{ .output_code_page = .windows1252 },
561 ));726 ));
562 // literal tab characters get converted to spaces (dependent on source file columns)727 // literal tab characters get converted to spaces (dependent on source file columns)
563 try std.testing.expectEqualSlices(u8, " ", try parseQuotedAsciiString(728 try std.testing.expectEqualSlices(u8, " ", try parseQuotedAsciiString(
564 arena,729 arena,
565 .{ .slice = "\"\t\"", .code_page = .windows1252 },730 .{ .slice = "\"\t\"", .code_page = .windows1252 },
566 .{},731 .{ .output_code_page = .windows1252 },
567 ));732 ));
568 try std.testing.expectEqualSlices(u8, "abc ", try parseQuotedAsciiString(733 try std.testing.expectEqualSlices(u8, "abc ", try parseQuotedAsciiString(
569 arena,734 arena,
570 .{ .slice = "\"abc\t\"", .code_page = .windows1252 },735 .{ .slice = "\"abc\t\"", .code_page = .windows1252 },
571 .{},736 .{ .output_code_page = .windows1252 },
572 ));737 ));
573 try std.testing.expectEqualSlices(u8, "abcdefg ", try parseQuotedAsciiString(738 try std.testing.expectEqualSlices(u8, "abcdefg ", try parseQuotedAsciiString(
574 arena,739 arena,
575 .{ .slice = "\"abcdefg\t\"", .code_page = .windows1252 },740 .{ .slice = "\"abcdefg\t\"", .code_page = .windows1252 },
576 .{},741 .{ .output_code_page = .windows1252 },
577 ));742 ));
578 try std.testing.expectEqualSlices(u8, "\\ ", try parseQuotedAsciiString(743 try std.testing.expectEqualSlices(u8, "\\ ", try parseQuotedAsciiString(
579 arena,744 arena,
580 .{ .slice = "\"\\\t\"", .code_page = .windows1252 },745 .{ .slice = "\"\\\t\"", .code_page = .windows1252 },
581 .{},746 .{ .output_code_page = .windows1252 },
582 ));747 ));
583 // literal CR's get dropped748 // literal CR's get dropped
584 try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString(749 try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString(
585 arena,750 arena,
586 .{ .slice = "\"\r\r\r\r\r\"", .code_page = .windows1252 },751 .{ .slice = "\"\r\r\r\r\r\"", .code_page = .windows1252 },
587 .{},752 .{ .output_code_page = .windows1252 },
588 ));753 ));
589 // contiguous newlines and whitespace get collapsed to <space><newline>754 // contiguous newlines and whitespace get collapsed to <space><newline>
590 try std.testing.expectEqualSlices(u8, " \n", try parseQuotedAsciiString(755 try std.testing.expectEqualSlices(u8, " \n", try parseQuotedAsciiString(
591 arena,756 arena,
592 .{ .slice = "\"\n\r\r \r\n \t \"", .code_page = .windows1252 },757 .{ .slice = "\"\n\r\r \r\n \t \"", .code_page = .windows1252 },
593 .{},758 .{ .output_code_page = .windows1252 },
594 ));759 ));
595}760}
596761
...@@ -602,32 +767,32 @@ test "parse quoted ascii string with utf8 code page" {...@@ -602,32 +767,32 @@ test "parse quoted ascii string with utf8 code page" {
602 try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString(767 try std.testing.expectEqualSlices(u8, "", try parseQuotedAsciiString(
603 arena,768 arena,
604 .{ .slice = "\"\"", .code_page = .utf8 },769 .{ .slice = "\"\"", .code_page = .utf8 },
605 .{},770 .{ .output_code_page = .windows1252 },
606 ));771 ));
607 // Codepoints that don't have a Windows-1252 representation get converted to ?772 // Codepoints that don't have a Windows-1252 representation get converted to ?
608 try std.testing.expectEqualSlices(u8, "?????????", try parseQuotedAsciiString(773 try std.testing.expectEqualSlices(u8, "?????????", try parseQuotedAsciiString(
609 arena,774 arena,
610 .{ .slice = "\"кириллица\"", .code_page = .utf8 },775 .{ .slice = "\"кириллица\"", .code_page = .utf8 },
611 .{},776 .{ .output_code_page = .windows1252 },
612 ));777 ));
613 // Codepoints that have a best fit mapping get converted accordingly,778 // Codepoints that have a best fit mapping get converted accordingly,
614 // these are box drawing codepoints779 // these are box drawing codepoints
615 try std.testing.expectEqualSlices(u8, "\x2b\x2d\x2b", try parseQuotedAsciiString(780 try std.testing.expectEqualSlices(u8, "\x2b\x2d\x2b", try parseQuotedAsciiString(
616 arena,781 arena,
617 .{ .slice = "\"┌─┐\"", .code_page = .utf8 },782 .{ .slice = "\"┌─┐\"", .code_page = .utf8 },
618 .{},783 .{ .output_code_page = .windows1252 },
619 ));784 ));
620 // Invalid UTF-8 gets converted to ? depending on well-formedness785 // Invalid UTF-8 gets converted to ? depending on well-formedness
621 try std.testing.expectEqualSlices(u8, "????", try parseQuotedAsciiString(786 try std.testing.expectEqualSlices(u8, "????", try parseQuotedAsciiString(
622 arena,787 arena,
623 .{ .slice = "\"\xf0\xf0\x80\x80\x80\"", .code_page = .utf8 },788 .{ .slice = "\"\xf0\xf0\x80\x80\x80\"", .code_page = .utf8 },
624 .{},789 .{ .output_code_page = .windows1252 },
625 ));790 ));
626 // Codepoints that would require a UTF-16 surrogate pair get converted to ??791 // Codepoints that would require a UTF-16 surrogate pair get converted to ??
627 try std.testing.expectEqualSlices(u8, "??", try parseQuotedAsciiString(792 try std.testing.expectEqualSlices(u8, "??", try parseQuotedAsciiString(
628 arena,793 arena,
629 .{ .slice = "\"\xF2\xAF\xBA\xB4\"", .code_page = .utf8 },794 .{ .slice = "\"\xF2\xAF\xBA\xB4\"", .code_page = .utf8 },
630 .{},795 .{ .output_code_page = .windows1252 },
631 ));796 ));
632797
633 // Output code page changes how invalid UTF-8 gets converted, since it798 // Output code page changes how invalid UTF-8 gets converted, since it
...@@ -652,6 +817,18 @@ test "parse quoted ascii string with utf8 code page" {...@@ -652,6 +817,18 @@ test "parse quoted ascii string with utf8 code page" {
652 ));817 ));
653}818}
654819
820test "parse quoted string with different input/output code pages" {
821 var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator);
822 defer arena_allocator.deinit();
823 const arena = arena_allocator.allocator();
824
825 try std.testing.expectEqualSlices(u8, "€���\x60\x7F", try parseQuotedAsciiString(
826 arena,
827 .{ .slice = "\"\x80\\x8a\\600\\612\\540\\577\"", .code_page = .windows1252 },
828 .{ .output_code_page = .utf8 },
829 ));
830}
831
655test "parse quoted wide string" {832test "parse quoted wide string" {
656 var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator);833 var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator);
657 defer arena_allocator.deinit();834 defer arena_allocator.deinit();
...@@ -662,52 +839,62 @@ test "parse quoted wide string" {...@@ -662,52 +839,62 @@ test "parse quoted wide string" {
662 \\L"hello"839 \\L"hello"
663 ,840 ,
664 .code_page = .windows1252,841 .code_page = .windows1252,
665 }, .{}));842 }, .{
843 .output_code_page = .windows1252,
844 }));
666 // hex with 0 digits845 // hex with 0 digits
667 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{0x0}, try parseQuotedWideString(arena, .{846 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{0x0}, try parseQuotedWideString(arena, .{
668 .slice =847 .slice =
669 \\L"\x"848 \\L"\x"
670 ,849 ,
671 .code_page = .windows1252,850 .code_page = .windows1252,
672 }, .{}));851 }, .{
852 .output_code_page = .windows1252,
853 }));
673 // hex max of 4 digits854 // hex max of 4 digits
674 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{ std.mem.nativeToLittle(u16, 0xFFFF), std.mem.nativeToLittle(u16, 'f') }, try parseQuotedWideString(arena, .{855 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{ std.mem.nativeToLittle(u16, 0xFFFF), std.mem.nativeToLittle(u16, 'f') }, try parseQuotedWideString(arena, .{
675 .slice =856 .slice =
676 \\L"\XfFfFf"857 \\L"\XfFfFf"
677 ,858 ,
678 .code_page = .windows1252,859 .code_page = .windows1252,
679 }, .{}));860 }, .{
861 .output_code_page = .windows1252,
862 }));
680 // octal max of 7 digits863 // octal max of 7 digits
681 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{ std.mem.nativeToLittle(u16, 0x9493), std.mem.nativeToLittle(u16, '3'), std.mem.nativeToLittle(u16, '3') }, try parseQuotedWideString(arena, .{864 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{ std.mem.nativeToLittle(u16, 0x9493), std.mem.nativeToLittle(u16, '3'), std.mem.nativeToLittle(u16, '3') }, try parseQuotedWideString(arena, .{
682 .slice =865 .slice =
683 \\L"\111222333"866 \\L"\111222333"
684 ,867 ,
685 .code_page = .windows1252,868 .code_page = .windows1252,
686 }, .{}));869 }, .{
870 .output_code_page = .windows1252,
871 }));
687 // octal overflow872 // octal overflow
688 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{std.mem.nativeToLittle(u16, 0xFF01)}, try parseQuotedWideString(arena, .{873 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{std.mem.nativeToLittle(u16, 0xFF01)}, try parseQuotedWideString(arena, .{
689 .slice =874 .slice =
690 \\L"\777401"875 \\L"\777401"
691 ,876 ,
692 .code_page = .windows1252,877 .code_page = .windows1252,
693 }, .{}));878 }, .{
879 .output_code_page = .windows1252,
880 }));
694 // literal tab characters get converted to spaces (dependent on source file columns)881 // literal tab characters get converted to spaces (dependent on source file columns)
695 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("abcdefg "), try parseQuotedWideString(882 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("abcdefg "), try parseQuotedWideString(
696 arena,883 arena,
697 .{ .slice = "L\"abcdefg\t\"", .code_page = .windows1252 },884 .{ .slice = "L\"abcdefg\t\"", .code_page = .windows1252 },
698 .{},885 .{ .output_code_page = .windows1252 },
699 ));886 ));
700 // Windows-1252 conversion887 // Windows-1252 conversion
701 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("ðð€€€"), try parseQuotedWideString(888 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("ðð€€€"), try parseQuotedWideString(
702 arena,889 arena,
703 .{ .slice = "L\"\xf0\xf0\x80\x80\x80\"", .code_page = .windows1252 },890 .{ .slice = "L\"\xf0\xf0\x80\x80\x80\"", .code_page = .windows1252 },
704 .{},891 .{ .output_code_page = .windows1252 },
705 ));892 ));
706 // Invalid escape sequences are skipped893 // Invalid escape sequences are skipped
707 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral(""), try parseQuotedWideString(894 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral(""), try parseQuotedWideString(
708 arena,895 arena,
709 .{ .slice = "L\"\\H\"", .code_page = .windows1252 },896 .{ .slice = "L\"\\H\"", .code_page = .windows1252 },
710 .{},897 .{ .output_code_page = .windows1252 },
711 ));898 ));
712}899}
713900
...@@ -719,18 +906,18 @@ test "parse quoted wide string with utf8 code page" {...@@ -719,18 +906,18 @@ test "parse quoted wide string with utf8 code page" {
719 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{}, try parseQuotedWideString(906 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{}, try parseQuotedWideString(
720 arena,907 arena,
721 .{ .slice = "L\"\"", .code_page = .utf8 },908 .{ .slice = "L\"\"", .code_page = .utf8 },
722 .{},909 .{ .output_code_page = .windows1252 },
723 ));910 ));
724 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("кириллица"), try parseQuotedWideString(911 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("кириллица"), try parseQuotedWideString(
725 arena,912 arena,
726 .{ .slice = "L\"кириллица\"", .code_page = .utf8 },913 .{ .slice = "L\"кириллица\"", .code_page = .utf8 },
727 .{},914 .{ .output_code_page = .windows1252 },
728 ));915 ));
729 // Invalid UTF-8 gets converted to � depending on well-formedness916 // Invalid UTF-8 gets converted to � depending on well-formedness
730 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("����"), try parseQuotedWideString(917 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("����"), try parseQuotedWideString(
731 arena,918 arena,
732 .{ .slice = "L\"\xf0\xf0\x80\x80\x80\"", .code_page = .utf8 },919 .{ .slice = "L\"\xf0\xf0\x80\x80\x80\"", .code_page = .utf8 },
733 .{},920 .{ .output_code_page = .windows1252 },
734 ));921 ));
735}922}
736923
...@@ -742,29 +929,29 @@ test "parse quoted ascii string as wide string" {...@@ -742,29 +929,29 @@ test "parse quoted ascii string as wide string" {
742 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("кириллица"), try parseQuotedStringAsWideString(929 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("кириллица"), try parseQuotedStringAsWideString(
743 arena,930 arena,
744 .{ .slice = "\"кириллица\"", .code_page = .utf8 },931 .{ .slice = "\"кириллица\"", .code_page = .utf8 },
745 .{},932 .{ .output_code_page = .windows1252 },
746 ));933 ));
747 // Whether or not invalid escapes are skipped is still determined by the L prefix934 // Whether or not invalid escapes are skipped is still determined by the L prefix
748 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("\\H"), try parseQuotedStringAsWideString(935 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral("\\H"), try parseQuotedStringAsWideString(
749 arena,936 arena,
750 .{ .slice = "\"\\H\"", .code_page = .windows1252 },937 .{ .slice = "\"\\H\"", .code_page = .windows1252 },
751 .{},938 .{ .output_code_page = .windows1252 },
752 ));939 ));
753 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral(""), try parseQuotedStringAsWideString(940 try std.testing.expectEqualSentinel(u16, 0, std.unicode.utf8ToUtf16LeStringLiteral(""), try parseQuotedStringAsWideString(
754 arena,941 arena,
755 .{ .slice = "L\"\\H\"", .code_page = .windows1252 },942 .{ .slice = "L\"\\H\"", .code_page = .windows1252 },
756 .{},943 .{ .output_code_page = .windows1252 },
757 ));944 ));
758 // Maximum escape sequence value is also determined by the L prefix945 // Maximum escape sequence value is also determined by the L prefix
759 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{ std.mem.nativeToLittle(u16, 0x12), std.mem.nativeToLittle(u16, '3'), std.mem.nativeToLittle(u16, '4') }, try parseQuotedStringAsWideString(946 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{ std.mem.nativeToLittle(u16, 0x12), std.mem.nativeToLittle(u16, '3'), std.mem.nativeToLittle(u16, '4') }, try parseQuotedStringAsWideString(
760 arena,947 arena,
761 .{ .slice = "\"\\x1234\"", .code_page = .windows1252 },948 .{ .slice = "\"\\x1234\"", .code_page = .windows1252 },
762 .{},949 .{ .output_code_page = .windows1252 },
763 ));950 ));
764 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{std.mem.nativeToLittle(u16, 0x1234)}, try parseQuotedStringAsWideString(951 try std.testing.expectEqualSentinel(u16, 0, &[_:0]u16{std.mem.nativeToLittle(u16, 0x1234)}, try parseQuotedStringAsWideString(
765 arena,952 arena,
766 .{ .slice = "L\"\\x1234\"", .code_page = .windows1252 },953 .{ .slice = "L\"\\x1234\"", .code_page = .windows1252 },
767 .{},954 .{ .output_code_page = .windows1252 },
768 ));955 ));
769}956}
770957
lib/compiler/resinator/main.zig+25-9
...@@ -7,6 +7,7 @@ const Diagnostics = @import("errors.zig").Diagnostics;...@@ -7,6 +7,7 @@ const Diagnostics = @import("errors.zig").Diagnostics;
7const cli = @import("cli.zig");7const cli = @import("cli.zig");
8const preprocess = @import("preprocess.zig");8const preprocess = @import("preprocess.zig");
9const renderErrorMessage = @import("utils.zig").renderErrorMessage;9const renderErrorMessage = @import("utils.zig").renderErrorMessage;
10const hasDisjointCodePage = @import("disjoint_code_page.zig").hasDisjointCodePage;
10const aro = @import("aro");11const aro = @import("aro");
1112
12pub fn main() !void {13pub fn main() !void {
...@@ -179,16 +180,30 @@ pub fn main() !void {...@@ -179,16 +180,30 @@ pub fn main() !void {
179 // Note: We still want to run this when no-preprocess is set because:180 // Note: We still want to run this when no-preprocess is set because:
180 // 1. We want to print accurate line numbers after removing multiline comments181 // 1. We want to print accurate line numbers after removing multiline comments
181 // 2. We want to be able to handle an already-preprocessed input with #line commands in it182 // 2. We want to be able to handle an already-preprocessed input with #line commands in it
182 var mapping_results = try parseAndRemoveLineCommands(allocator, full_input, full_input, .{ .initial_filename = options.input_filename });183 var mapping_results = parseAndRemoveLineCommands(allocator, full_input, full_input, .{ .initial_filename = options.input_filename }) catch |err| switch (err) {
183 defer mapping_results.mappings.deinit(allocator);184 error.InvalidLineCommand => {
184185 // TODO: Maybe output the invalid line command
185 const final_input = removeComments(mapping_results.result, mapping_results.result, &mapping_results.mappings) catch |err| switch (err) {186 try renderErrorMessage(stderr.writer(), stderr_config, .err, "invalid line command in the preprocessed source", .{});
186 error.InvalidSourceMappingCollapse => {187 if (options.preprocess == .no) {
187 try error_handler.emitMessage(allocator, .err, "failed during comment removal; this is a known bug", .{});188 try renderErrorMessage(stderr.writer(), stderr_config, .note, "line commands must be of the format: #line <num> \"<path>\"", .{});
189 } else {
190 try renderErrorMessage(stderr.writer(), stderr_config, .note, "this is likely to be a bug, please report it", .{});
191 }
188 std.process.exit(1);192 std.process.exit(1);
189 },193 },
190 else => |e| return e,194 error.LineNumberOverflow => {
195 // TODO: Better error message
196 try renderErrorMessage(stderr.writer(), stderr_config, .err, "line number count exceeded maximum of {}", .{std.math.maxInt(usize)});
197 std.process.exit(1);
198 },
199 error.OutOfMemory => |e| return e,
191 };200 };
201 defer mapping_results.mappings.deinit(allocator);
202
203 const default_code_page = options.default_code_page orelse .windows1252;
204 const has_disjoint_code_page = hasDisjointCodePage(mapping_results.result, &mapping_results.mappings, default_code_page);
205
206 const final_input = try removeComments(mapping_results.result, mapping_results.result, &mapping_results.mappings);
192207
193 var output_file = std.fs.cwd().createFile(options.output_filename, .{}) catch |err| {208 var output_file = std.fs.cwd().createFile(options.output_filename, .{}) catch |err| {
194 try error_handler.emitMessage(allocator, .err, "unable to create output file '{s}': {s}", .{ options.output_filename, @errorName(err) });209 try error_handler.emitMessage(allocator, .err, "unable to create output file '{s}': {s}", .{ options.output_filename, @errorName(err) });
...@@ -211,7 +226,8 @@ pub fn main() !void {...@@ -211,7 +226,8 @@ pub fn main() !void {
211 .extra_include_paths = options.extra_include_paths.items,226 .extra_include_paths = options.extra_include_paths.items,
212 .system_include_paths = include_paths,227 .system_include_paths = include_paths,
213 .default_language_id = options.default_language_id,228 .default_language_id = options.default_language_id,
214 .default_code_page = options.default_code_page orelse .windows1252,229 .default_code_page = default_code_page,
230 .disjoint_code_page = has_disjoint_code_page,
215 .verbose = options.verbose,231 .verbose = options.verbose,
216 .null_terminate_string_table_strings = options.null_terminate_string_table_strings,232 .null_terminate_string_table_strings = options.null_terminate_string_table_strings,
217 .max_string_literal_codepoints = options.max_string_literal_codepoints,233 .max_string_literal_codepoints = options.max_string_literal_codepoints,
...@@ -513,7 +529,7 @@ fn diagnosticsToErrorBundle(...@@ -513,7 +529,7 @@ fn diagnosticsToErrorBundle(
513 };529 };
514 if (err_details.print_source_line) {530 if (err_details.print_source_line) {
515 const source_line = err_details.token.getLineForErrorDisplay(source, source_line_start);531 const source_line = err_details.token.getLineForErrorDisplay(source, source_line_start);
516 const visual_info = err_details.visualTokenInfo(source_line_start, source_line_start + source_line.len);532 const visual_info = err_details.visualTokenInfo(source_line_start, source_line_start + source_line.len, source);
517 src_loc.span_start = @intCast(visual_info.point_offset - visual_info.before_len);533 src_loc.span_start = @intCast(visual_info.point_offset - visual_info.before_len);
518 src_loc.span_main = @intCast(visual_info.point_offset);534 src_loc.span_main = @intCast(visual_info.point_offset);
519 src_loc.span_end = @intCast(visual_info.point_offset + 1 + visual_info.after_len);535 src_loc.span_end = @intCast(visual_info.point_offset + 1 + visual_info.after_len);
lib/compiler/resinator/parse.zig+174-69
...@@ -4,9 +4,10 @@ const Token = @import("lex.zig").Token;...@@ -4,9 +4,10 @@ const Token = @import("lex.zig").Token;
4const Node = @import("ast.zig").Node;4const Node = @import("ast.zig").Node;
5const Tree = @import("ast.zig").Tree;5const Tree = @import("ast.zig").Tree;
6const CodePageLookup = @import("ast.zig").CodePageLookup;6const CodePageLookup = @import("ast.zig").CodePageLookup;
7const Resource = @import("rc.zig").Resource;7const ResourceType = @import("rc.zig").ResourceType;
8const Allocator = std.mem.Allocator;8const Allocator = std.mem.Allocator;
9const ErrorDetails = @import("errors.zig").ErrorDetails;9const ErrorDetails = @import("errors.zig").ErrorDetails;
10const ErrorDetailsWithoutCodePage = @import("errors.zig").ErrorDetailsWithoutCodePage;
10const Diagnostics = @import("errors.zig").Diagnostics;11const Diagnostics = @import("errors.zig").Diagnostics;
11const SourceBytes = @import("literals.zig").SourceBytes;12const SourceBytes = @import("literals.zig").SourceBytes;
12const Compiler = @import("compile.zig").Compiler;13const Compiler = @import("compile.zig").Compiler;
...@@ -30,6 +31,7 @@ pub const Parser = struct {...@@ -30,6 +31,7 @@ pub const Parser = struct {
3031
31 pub const Options = struct {32 pub const Options = struct {
32 warn_instead_of_error_on_invalid_code_page: bool = false,33 warn_instead_of_error_on_invalid_code_page: bool = false,
34 disjoint_code_page: bool = false,
33 };35 };
3436
35 pub fn init(lexer: *Lexer, options: Options) Parser {37 pub fn init(lexer: *Lexer, options: Options) Parser {
...@@ -47,6 +49,7 @@ pub const Parser = struct {...@@ -47,6 +49,7 @@ pub const Parser = struct {
47 diagnostics: *Diagnostics,49 diagnostics: *Diagnostics,
48 input_code_page_lookup: CodePageLookup,50 input_code_page_lookup: CodePageLookup,
49 output_code_page_lookup: CodePageLookup,51 output_code_page_lookup: CodePageLookup,
52 warned_about_disjoint_code_page: bool,
50 };53 };
5154
52 pub fn parse(self: *Self, allocator: Allocator, diagnostics: *Diagnostics) Error!*Tree {55 pub fn parse(self: *Self, allocator: Allocator, diagnostics: *Diagnostics) Error!*Tree {
...@@ -61,6 +64,7 @@ pub const Parser = struct {...@@ -61,6 +64,7 @@ pub const Parser = struct {
61 .diagnostics = diagnostics,64 .diagnostics = diagnostics,
62 .input_code_page_lookup = CodePageLookup.init(arena.allocator(), self.lexer.default_code_page),65 .input_code_page_lookup = CodePageLookup.init(arena.allocator(), self.lexer.default_code_page),
63 .output_code_page_lookup = CodePageLookup.init(arena.allocator(), self.lexer.default_code_page),66 .output_code_page_lookup = CodePageLookup.init(arena.allocator(), self.lexer.default_code_page),
67 .warned_about_disjoint_code_page = false,
64 };68 };
6569
66 const parsed_root = try self.parseRoot();70 const parsed_root = try self.parseRoot();
...@@ -116,7 +120,7 @@ pub const Parser = struct {...@@ -116,7 +120,7 @@ pub const Parser = struct {
116 const maybe_common_resource_attribute = try self.lookaheadToken(.normal);120 const maybe_common_resource_attribute = try self.lookaheadToken(.normal);
117 if (maybe_common_resource_attribute.id == .literal and rc.CommonResourceAttributes.map.has(maybe_common_resource_attribute.slice(self.lexer.buffer))) {121 if (maybe_common_resource_attribute.id == .literal and rc.CommonResourceAttributes.map.has(maybe_common_resource_attribute.slice(self.lexer.buffer))) {
118 try common_resource_attributes.append(self.state.arena, maybe_common_resource_attribute);122 try common_resource_attributes.append(self.state.arena, maybe_common_resource_attribute);
119 self.nextToken(.normal) catch unreachable;123 try self.nextToken(.normal);
120 } else {124 } else {
121 break;125 break;
122 }126 }
...@@ -130,8 +134,13 @@ pub const Parser = struct {...@@ -130,8 +134,13 @@ pub const Parser = struct {
130 /// optional statements (if any). If there are no optional statements, the134 /// optional statements (if any). If there are no optional statements, the
131 /// current token is unchanged.135 /// current token is unchanged.
132 /// The returned slice is allocated by the parser's arena136 /// The returned slice is allocated by the parser's arena
133 fn parseOptionalStatements(self: *Self, resource: Resource) ![]*Node {137 fn parseOptionalStatements(self: *Self, resource: ResourceType) ![]*Node {
134 var optional_statements: std.ArrayListUnmanaged(*Node) = .empty;138 var optional_statements: std.ArrayListUnmanaged(*Node) = .empty;
139
140 const num_statement_types = @typeInfo(rc.OptionalStatements).@"enum".fields.len;
141 var statement_type_has_duplicates = [_]bool{false} ** num_statement_types;
142 var last_statement_per_type = [_]?*Node{null} ** num_statement_types;
143
135 while (true) {144 while (true) {
136 const lookahead_token = try self.lookaheadToken(.normal);145 const lookahead_token = try self.lookaheadToken(.normal);
137 if (lookahead_token.id != .literal) break;146 if (lookahead_token.id != .literal) break;
...@@ -140,7 +149,13 @@ pub const Parser = struct {...@@ -140,7 +149,13 @@ pub const Parser = struct {
140 .dialog, .dialogex => rc.OptionalStatements.dialog_map.get(slice) orelse break,149 .dialog, .dialogex => rc.OptionalStatements.dialog_map.get(slice) orelse break,
141 else => break,150 else => break,
142 };151 };
143 self.nextToken(.normal) catch unreachable;152 try self.nextToken(.normal);
153
154 const type_i = @intFromEnum(optional_statement_type);
155 if (last_statement_per_type[type_i] != null) {
156 statement_type_has_duplicates[type_i] = true;
157 }
158
144 switch (optional_statement_type) {159 switch (optional_statement_type) {
145 .language => {160 .language => {
146 const language = try self.parseLanguageStatement();161 const language = try self.parseLanguageStatement();
...@@ -166,7 +181,7 @@ pub const Parser = struct {...@@ -166,7 +181,7 @@ pub const Parser = struct {
166 try self.nextToken(.normal);181 try self.nextToken(.normal);
167 const value = self.state.token;182 const value = self.state.token;
168 if (!value.isStringLiteral()) {183 if (!value.isStringLiteral()) {
169 return self.addErrorDetailsAndFail(ErrorDetails{184 return self.addErrorDetailsAndFail(.{
170 .err = .expected_something_else,185 .err = .expected_something_else,
171 .token = value,186 .token = value,
172 .extra = .{ .expected_types = .{187 .extra = .{ .expected_types = .{
...@@ -223,7 +238,7 @@ pub const Parser = struct {...@@ -223,7 +238,7 @@ pub const Parser = struct {
223 try self.nextToken(.normal);238 try self.nextToken(.normal);
224 const typeface = self.state.token;239 const typeface = self.state.token;
225 if (!typeface.isStringLiteral()) {240 if (!typeface.isStringLiteral()) {
226 return self.addErrorDetailsAndFail(ErrorDetails{241 return self.addErrorDetailsAndFail(.{
227 .err = .expected_something_else,242 .err = .expected_something_else,
228 .token = typeface,243 .token = typeface,
229 .extra = .{ .expected_types = .{244 .extra = .{ .expected_types = .{
...@@ -272,7 +287,42 @@ pub const Parser = struct {...@@ -272,7 +287,42 @@ pub const Parser = struct {
272 try optional_statements.append(self.state.arena, &node.base);287 try optional_statements.append(self.state.arena, &node.base);
273 },288 },
274 }289 }
290
291 last_statement_per_type[type_i] = optional_statements.items[optional_statements.items.len - 1];
275 }292 }
293
294 for (optional_statements.items) |optional_statement| {
295 const type_i = type_i: {
296 switch (optional_statement.id) {
297 .simple_statement => {
298 const simple_statement: *Node.SimpleStatement = @alignCast(@fieldParentPtr("base", optional_statement));
299 const statement_identifier = simple_statement.identifier;
300 const slice = statement_identifier.slice(self.lexer.buffer);
301 const optional_statement_type = rc.OptionalStatements.map.get(slice) orelse
302 rc.OptionalStatements.dialog_map.get(slice).?;
303 break :type_i @intFromEnum(optional_statement_type);
304 },
305 .font_statement => {
306 break :type_i @intFromEnum(rc.OptionalStatements.font);
307 },
308 .language_statement => {
309 break :type_i @intFromEnum(rc.OptionalStatements.language);
310 },
311 else => unreachable,
312 }
313 };
314 if (!statement_type_has_duplicates[type_i]) continue;
315 if (optional_statement == last_statement_per_type[type_i].?) continue;
316
317 try self.addErrorDetails(.{
318 .err = .duplicate_optional_statement_skipped,
319 .type = .warning,
320 .token = optional_statement.getFirstToken(),
321 .token_span_start = optional_statement.getFirstToken(),
322 .token_span_end = optional_statement.getLastToken(),
323 });
324 }
325
276 return optional_statements.toOwnedSlice(self.state.arena);326 return optional_statements.toOwnedSlice(self.state.arena);
277 }327 }
278328
...@@ -311,12 +361,13 @@ pub const Parser = struct {...@@ -311,12 +361,13 @@ pub const Parser = struct {
311 const maybe_end_token = try self.lookaheadToken(.normal);361 const maybe_end_token = try self.lookaheadToken(.normal);
312 switch (maybe_end_token.id) {362 switch (maybe_end_token.id) {
313 .end => {363 .end => {
314 self.nextToken(.normal) catch unreachable;364 try self.nextToken(.normal);
315 break;365 break;
316 },366 },
317 .eof => {367 .eof => {
318 return self.addErrorDetailsAndFail(ErrorDetails{368 return self.addErrorDetailsWithCodePageAndFail(.{
319 .err = .unfinished_string_table_block,369 .err = .unfinished_string_table_block,
370 .code_page = self.lexer.current_code_page,
320 .token = maybe_end_token,371 .token = maybe_end_token,
321 });372 });
322 },373 },
...@@ -328,7 +379,7 @@ pub const Parser = struct {...@@ -328,7 +379,7 @@ pub const Parser = struct {
328379
329 try self.nextToken(.normal);380 try self.nextToken(.normal);
330 if (self.state.token.id != .quoted_ascii_string and self.state.token.id != .quoted_wide_string) {381 if (self.state.token.id != .quoted_ascii_string and self.state.token.id != .quoted_wide_string) {
331 return self.addErrorDetailsAndFail(ErrorDetails{382 return self.addErrorDetailsAndFail(.{
332 .err = .expected_something_else,383 .err = .expected_something_else,
333 .token = self.state.token,384 .token = self.state.token,
334 .extra = .{ .expected_types = .{ .string_literal = true } },385 .extra = .{ .expected_types = .{ .string_literal = true } },
...@@ -345,7 +396,7 @@ pub const Parser = struct {...@@ -345,7 +396,7 @@ pub const Parser = struct {
345 }396 }
346397
347 if (strings.items.len == 0) {398 if (strings.items.len == 0) {
348 return self.addErrorDetailsAndFail(ErrorDetails{399 return self.addErrorDetailsAndFail(.{
349 .err = .expected_token, // TODO: probably a more specific error message400 .err = .expected_token, // TODO: probably a more specific error message
350 .token = self.state.token,401 .token = self.state.token,
351 .extra = .{ .expected = .number },402 .extra = .{ .expected = .number },
...@@ -374,7 +425,12 @@ pub const Parser = struct {...@@ -374,7 +425,12 @@ pub const Parser = struct {
374 // of projects. So, we have special compatibility for this particular case.425 // of projects. So, we have special compatibility for this particular case.
375 const maybe_eof = try self.lookaheadToken(.whitespace_delimiter_only);426 const maybe_eof = try self.lookaheadToken(.whitespace_delimiter_only);
376 if (maybe_eof.id == .eof) {427 if (maybe_eof.id == .eof) {
377 // TODO: emit warning428 try self.addErrorDetails(.{
429 .err = .dangling_literal_at_eof,
430 .type = .warning,
431 .token = first_token,
432 });
433
378 var context = try self.state.arena.alloc(Token, 2);434 var context = try self.state.arena.alloc(Token, 2);
379 context[0] = first_token;435 context[0] = first_token;
380 context[1] = maybe_eof;436 context[1] = maybe_eof;
...@@ -413,12 +469,12 @@ pub const Parser = struct {...@@ -413,12 +469,12 @@ pub const Parser = struct {
413 if (maybe_ordinal == null) {469 if (maybe_ordinal == null) {
414 const would_be_win32_rc_ordinal = res.NameOrOrdinal.maybeNonAsciiOrdinalFromString(id_bytes);470 const would_be_win32_rc_ordinal = res.NameOrOrdinal.maybeNonAsciiOrdinalFromString(id_bytes);
415 if (would_be_win32_rc_ordinal) |win32_rc_ordinal| {471 if (would_be_win32_rc_ordinal) |win32_rc_ordinal| {
416 try self.addErrorDetails(ErrorDetails{472 try self.addErrorDetails(.{
417 .err = .id_must_be_ordinal,473 .err = .id_must_be_ordinal,
418 .token = id_token,474 .token = id_token,
419 .extra = .{ .resource = resource },475 .extra = .{ .resource = resource },
420 });476 });
421 return self.addErrorDetailsAndFail(ErrorDetails{477 return self.addErrorDetailsAndFail(.{
422 .err = .win32_non_ascii_ordinal,478 .err = .win32_non_ascii_ordinal,
423 .token = id_token,479 .token = id_token,
424 .type = .note,480 .type = .note,
...@@ -426,7 +482,7 @@ pub const Parser = struct {...@@ -426,7 +482,7 @@ pub const Parser = struct {
426 .extra = .{ .number = win32_rc_ordinal.ordinal },482 .extra = .{ .number = win32_rc_ordinal.ordinal },
427 });483 });
428 } else {484 } else {
429 return self.addErrorDetailsAndFail(ErrorDetails{485 return self.addErrorDetailsAndFail(.{
430 .err = .id_must_be_ordinal,486 .err = .id_must_be_ordinal,
431 .token = id_token,487 .token = id_token,
432 .extra = .{ .resource = resource },488 .extra = .{ .resource = resource },
...@@ -451,7 +507,7 @@ pub const Parser = struct {...@@ -451,7 +507,7 @@ pub const Parser = struct {
451 const lookahead = try self.lookaheadToken(.normal);507 const lookahead = try self.lookaheadToken(.normal);
452 switch (lookahead.id) {508 switch (lookahead.id) {
453 .end, .eof => {509 .end, .eof => {
454 self.nextToken(.normal) catch unreachable;510 try self.nextToken(.normal);
455 break;511 break;
456 },512 },
457 else => {},513 else => {},
...@@ -739,19 +795,19 @@ pub const Parser = struct {...@@ -739,19 +795,19 @@ pub const Parser = struct {
739795
740 const maybe_begin = try self.lookaheadToken(.normal);796 const maybe_begin = try self.lookaheadToken(.normal);
741 if (maybe_begin.id == .begin) {797 if (maybe_begin.id == .begin) {
742 self.nextToken(.normal) catch unreachable;798 try self.nextToken(.normal);
743799
744 if (!resource.canUseRawData()) {800 if (!resource.canUseRawData()) {
745 try self.addErrorDetails(ErrorDetails{801 try self.addErrorDetails(.{
746 .err = .resource_type_cant_use_raw_data,802 .err = .resource_type_cant_use_raw_data,
747 .token = maybe_begin,803 .token = self.state.token,
748 .extra = .{ .resource = resource },804 .extra = .{ .resource = resource },
749 });805 });
750 return self.addErrorDetailsAndFail(ErrorDetails{806 return self.addErrorDetailsAndFail(.{
751 .err = .resource_type_cant_use_raw_data,807 .err = .resource_type_cant_use_raw_data,
752 .type = .note,808 .type = .note,
753 .print_source_line = false,809 .print_source_line = false,
754 .token = maybe_begin,810 .token = self.state.token,
755 });811 });
756 }812 }
757813
...@@ -802,11 +858,12 @@ pub const Parser = struct {...@@ -802,11 +858,12 @@ pub const Parser = struct {
802 const maybe_end_token = try self.lookaheadToken(.normal);858 const maybe_end_token = try self.lookaheadToken(.normal);
803 switch (maybe_end_token.id) {859 switch (maybe_end_token.id) {
804 .comma => {860 .comma => {
861 try self.nextToken(.normal);
805 // comma as the first token in a raw data block is an error862 // comma as the first token in a raw data block is an error
806 if (raw_data.items.len == 0) {863 if (raw_data.items.len == 0) {
807 return self.addErrorDetailsAndFail(ErrorDetails{864 return self.addErrorDetailsAndFail(.{
808 .err = .expected_something_else,865 .err = .expected_something_else,
809 .token = maybe_end_token,866 .token = self.state.token,
810 .extra = .{ .expected_types = .{867 .extra = .{ .expected_types = .{
811 .number = true,868 .number = true,
812 .number_expression = true,869 .number_expression = true,
...@@ -815,16 +872,16 @@ pub const Parser = struct {...@@ -815,16 +872,16 @@ pub const Parser = struct {
815 });872 });
816 }873 }
817 // otherwise just skip over commas874 // otherwise just skip over commas
818 self.nextToken(.normal) catch unreachable;
819 continue;875 continue;
820 },876 },
821 .end => {877 .end => {
822 self.nextToken(.normal) catch unreachable;878 try self.nextToken(.normal);
823 break;879 break;
824 },880 },
825 .eof => {881 .eof => {
826 return self.addErrorDetailsAndFail(ErrorDetails{882 return self.addErrorDetailsWithCodePageAndFail(.{
827 .err = .unfinished_raw_data_block,883 .err = .unfinished_raw_data_block,
884 .code_page = self.lexer.current_code_page,
828 .token = maybe_end_token,885 .token = maybe_end_token,
829 });886 });
830 },887 },
...@@ -836,10 +893,12 @@ pub const Parser = struct {...@@ -836,10 +893,12 @@ pub const Parser = struct {
836 if (expression.isNumberExpression()) {893 if (expression.isNumberExpression()) {
837 const maybe_close_paren = try self.lookaheadToken(.normal);894 const maybe_close_paren = try self.lookaheadToken(.normal);
838 if (maybe_close_paren.id == .close_paren) {895 if (maybe_close_paren.id == .close_paren) {
896 // advance to ensure that the code page lookup is populated for this token
897 try self.nextToken(.normal);
839 // <number expression>) is an error898 // <number expression>) is an error
840 return self.addErrorDetailsAndFail(ErrorDetails{899 return self.addErrorDetailsAndFail(.{
841 .err = .expected_token,900 .err = .expected_token,
842 .token = maybe_close_paren,901 .token = self.state.token,
843 .extra = .{ .expected = .operator },902 .extra = .{ .expected = .operator },
844 });903 });
845 }904 }
...@@ -852,10 +911,10 @@ pub const Parser = struct {...@@ -852,10 +911,10 @@ pub const Parser = struct {
852 /// begin on the next token.911 /// begin on the next token.
853 /// After return, the current token will be the token immediately before the end of the912 /// After return, the current token will be the token immediately before the end of the
854 /// control statement (or unchanged if the function returns null).913 /// control statement (or unchanged if the function returns null).
855 fn parseControlStatement(self: *Self, resource: Resource) Error!?*Node {914 fn parseControlStatement(self: *Self, resource: ResourceType) Error!?*Node {
856 const control_token = try self.lookaheadToken(.normal);915 const control_token = try self.lookaheadToken(.normal);
857 const control = rc.Control.map.get(control_token.slice(self.lexer.buffer)) orelse return null;916 const control = rc.Control.map.get(control_token.slice(self.lexer.buffer)) orelse return null;
858 self.nextToken(.normal) catch unreachable;917 try self.nextToken(.normal);
859918
860 try self.skipAnyCommas();919 try self.skipAnyCommas();
861920
...@@ -867,7 +926,7 @@ pub const Parser = struct {...@@ -867,7 +926,7 @@ pub const Parser = struct {
867 text = self.state.token;926 text = self.state.token;
868 },927 },
869 else => {928 else => {
870 return self.addErrorDetailsAndFail(ErrorDetails{929 return self.addErrorDetailsAndFail(.{
871 .err = .expected_something_else,930 .err = .expected_something_else,
872 .token = self.state.token,931 .token = self.state.token,
873 .extra = .{ .expected_types = .{932 .extra = .{ .expected_types = .{
...@@ -920,14 +979,16 @@ pub const Parser = struct {...@@ -920,14 +979,16 @@ pub const Parser = struct {
920 // the style parameter.979 // the style parameter.
921 const lookahead_token = try self.lookaheadToken(.normal);980 const lookahead_token = try self.lookaheadToken(.normal);
922 if (lookahead_token.id != .comma and lookahead_token.id != .eof) {981 if (lookahead_token.id != .comma and lookahead_token.id != .eof) {
923 try self.addErrorDetails(.{982 try self.addErrorDetailsWithCodePage(.{
924 .err = .rc_could_miscompile_control_params,983 .err = .rc_could_miscompile_control_params,
925 .type = .warning,984 .type = .warning,
985 .code_page = self.lexer.current_code_page,
926 .token = lookahead_token,986 .token = lookahead_token,
927 });987 });
928 try self.addErrorDetails(.{988 try self.addErrorDetailsWithCodePage(.{
929 .err = .rc_could_miscompile_control_params,989 .err = .rc_could_miscompile_control_params,
930 .type = .note,990 .type = .note,
991 .code_page = self.lexer.current_code_page,
931 .token = style.?.getFirstToken(),992 .token = style.?.getFirstToken(),
932 .token_span_end = style.?.getLastToken(),993 .token_span_end = style.?.getLastToken(),
933 });994 });
...@@ -987,7 +1048,7 @@ pub const Parser = struct {...@@ -987,7 +1048,7 @@ pub const Parser = struct {
987 fn parseToolbarButtonStatement(self: *Self) Error!?*Node {1048 fn parseToolbarButtonStatement(self: *Self) Error!?*Node {
988 const keyword_token = try self.lookaheadToken(.normal);1049 const keyword_token = try self.lookaheadToken(.normal);
989 const button_type = rc.ToolbarButton.map.get(keyword_token.slice(self.lexer.buffer)) orelse return null;1050 const button_type = rc.ToolbarButton.map.get(keyword_token.slice(self.lexer.buffer)) orelse return null;
990 self.nextToken(.normal) catch unreachable;1051 try self.nextToken(.normal);
9911052
992 switch (button_type) {1053 switch (button_type) {
993 .separator => {1054 .separator => {
...@@ -1014,10 +1075,10 @@ pub const Parser = struct {...@@ -1014,10 +1075,10 @@ pub const Parser = struct {
1014 /// begin on the next token.1075 /// begin on the next token.
1015 /// After return, the current token will be the token immediately before the end of the1076 /// After return, the current token will be the token immediately before the end of the
1016 /// menuitem statement (or unchanged if the function returns null).1077 /// menuitem statement (or unchanged if the function returns null).
1017 fn parseMenuItemStatement(self: *Self, resource: Resource, top_level_menu_id_token: Token, nesting_level: u32) Error!?*Node {1078 fn parseMenuItemStatement(self: *Self, resource: ResourceType, top_level_menu_id_token: Token, nesting_level: u32) Error!?*Node {
1018 const menuitem_token = try self.lookaheadToken(.normal);1079 const menuitem_token = try self.lookaheadToken(.normal);
1019 const menuitem = rc.MenuItem.map.get(menuitem_token.slice(self.lexer.buffer)) orelse return null;1080 const menuitem = rc.MenuItem.map.get(menuitem_token.slice(self.lexer.buffer)) orelse return null;
1020 self.nextToken(.normal) catch unreachable;1081 try self.nextToken(.normal);
10211082
1022 if (nesting_level > max_nested_menu_level) {1083 if (nesting_level > max_nested_menu_level) {
1023 try self.addErrorDetails(.{1084 try self.addErrorDetails(.{
...@@ -1050,7 +1111,7 @@ pub const Parser = struct {...@@ -1050,7 +1111,7 @@ pub const Parser = struct {
1050 } else {1111 } else {
1051 const text = self.state.token;1112 const text = self.state.token;
1052 if (!text.isStringLiteral()) {1113 if (!text.isStringLiteral()) {
1053 return self.addErrorDetailsAndFail(ErrorDetails{1114 return self.addErrorDetailsAndFail(.{
1054 .err = .expected_something_else,1115 .err = .expected_something_else,
1055 .token = text,1116 .token = text,
1056 .extra = .{ .expected_types = .{1117 .extra = .{ .expected_types = .{
...@@ -1070,7 +1131,7 @@ pub const Parser = struct {...@@ -1070,7 +1131,7 @@ pub const Parser = struct {
1070 if (!rc.MenuItem.Option.map.has(option_token.slice(self.lexer.buffer))) {1131 if (!rc.MenuItem.Option.map.has(option_token.slice(self.lexer.buffer))) {
1071 break;1132 break;
1072 }1133 }
1073 self.nextToken(.normal) catch unreachable;1134 try self.nextToken(.normal);
1074 try options.append(self.state.arena, option_token);1135 try options.append(self.state.arena, option_token);
1075 try self.skipAnyCommas();1136 try self.skipAnyCommas();
1076 }1137 }
...@@ -1089,7 +1150,7 @@ pub const Parser = struct {...@@ -1089,7 +1150,7 @@ pub const Parser = struct {
1089 try self.nextToken(.normal);1150 try self.nextToken(.normal);
1090 const text = self.state.token;1151 const text = self.state.token;
1091 if (!text.isStringLiteral()) {1152 if (!text.isStringLiteral()) {
1092 return self.addErrorDetailsAndFail(ErrorDetails{1153 return self.addErrorDetailsAndFail(.{
1093 .err = .expected_something_else,1154 .err = .expected_something_else,
1094 .token = text,1155 .token = text,
1095 .extra = .{ .expected_types = .{1156 .extra = .{ .expected_types = .{
...@@ -1105,7 +1166,7 @@ pub const Parser = struct {...@@ -1105,7 +1166,7 @@ pub const Parser = struct {
1105 if (!rc.MenuItem.Option.map.has(option_token.slice(self.lexer.buffer))) {1166 if (!rc.MenuItem.Option.map.has(option_token.slice(self.lexer.buffer))) {
1106 break;1167 break;
1107 }1168 }
1108 self.nextToken(.normal) catch unreachable;1169 try self.nextToken(.normal);
1109 try options.append(self.state.arena, option_token);1170 try options.append(self.state.arena, option_token);
1110 try self.skipAnyCommas();1171 try self.skipAnyCommas();
1111 }1172 }
...@@ -1146,7 +1207,7 @@ pub const Parser = struct {...@@ -1146,7 +1207,7 @@ pub const Parser = struct {
1146 try self.nextToken(.normal);1207 try self.nextToken(.normal);
1147 const text = self.state.token;1208 const text = self.state.token;
1148 if (!text.isStringLiteral()) {1209 if (!text.isStringLiteral()) {
1149 return self.addErrorDetailsAndFail(ErrorDetails{1210 return self.addErrorDetailsAndFail(.{
1150 .err = .expected_something_else,1211 .err = .expected_something_else,
1151 .token = text,1212 .token = text,
1152 .extra = .{ .expected_types = .{1213 .extra = .{ .expected_types = .{
...@@ -1257,7 +1318,7 @@ pub const Parser = struct {...@@ -1257,7 +1318,7 @@ pub const Parser = struct {
1257 fn parseVersionStatement(self: *Self) Error!?*Node {1318 fn parseVersionStatement(self: *Self) Error!?*Node {
1258 const type_token = try self.lookaheadToken(.normal);1319 const type_token = try self.lookaheadToken(.normal);
1259 const statement_type = rc.VersionInfo.map.get(type_token.slice(self.lexer.buffer)) orelse return null;1320 const statement_type = rc.VersionInfo.map.get(type_token.slice(self.lexer.buffer)) orelse return null;
1260 self.nextToken(.normal) catch unreachable;1321 try self.nextToken(.normal);
1261 switch (statement_type) {1322 switch (statement_type) {
1262 .file_version, .product_version => {1323 .file_version, .product_version => {
1263 var parts_buffer: [4]*Node = undefined;1324 var parts_buffer: [4]*Node = undefined;
...@@ -1301,7 +1362,7 @@ pub const Parser = struct {...@@ -1301,7 +1362,7 @@ pub const Parser = struct {
1301 fn parseVersionBlockOrValue(self: *Self, top_level_version_id_token: Token, nesting_level: u32) Error!?*Node {1362 fn parseVersionBlockOrValue(self: *Self, top_level_version_id_token: Token, nesting_level: u32) Error!?*Node {
1302 const keyword_token = try self.lookaheadToken(.normal);1363 const keyword_token = try self.lookaheadToken(.normal);
1303 const keyword = rc.VersionBlock.map.get(keyword_token.slice(self.lexer.buffer)) orelse return null;1364 const keyword = rc.VersionBlock.map.get(keyword_token.slice(self.lexer.buffer)) orelse return null;
1304 self.nextToken(.normal) catch unreachable;1365 try self.nextToken(.normal);
13051366
1306 if (nesting_level > max_nested_version_level) {1367 if (nesting_level > max_nested_version_level) {
1307 try self.addErrorDetails(.{1368 try self.addErrorDetails(.{
...@@ -1541,7 +1602,7 @@ pub const Parser = struct {...@@ -1541,7 +1602,7 @@ pub const Parser = struct {
1541 }1602 }
1542 };1603 };
15431604
1544 pub fn toErrorDetails(options: ParseExpressionOptions, token: Token) ErrorDetails {1605 pub fn toErrorDetails(options: ParseExpressionOptions, token: Token) ErrorDetailsWithoutCodePage {
1545 // TODO: expected_types_override interaction with is_known_to_be_number_expression?1606 // TODO: expected_types_override interaction with is_known_to_be_number_expression?
1546 const expected_types = options.expected_types_override orelse ErrorDetails.ExpectedTypes{1607 const expected_types = options.expected_types_override orelse ErrorDetails.ExpectedTypes{
1547 .number = options.allowed_types.number,1608 .number = options.allowed_types.number,
...@@ -1549,7 +1610,7 @@ pub const Parser = struct {...@@ -1549,7 +1610,7 @@ pub const Parser = struct {
1549 .string_literal = options.allowed_types.string and !options.is_known_to_be_number_expression,1610 .string_literal = options.allowed_types.string and !options.is_known_to_be_number_expression,
1550 .literal = options.allowed_types.literal and !options.is_known_to_be_number_expression,1611 .literal = options.allowed_types.literal and !options.is_known_to_be_number_expression,
1551 };1612 };
1552 return ErrorDetails{1613 return .{
1553 .err = .expected_something_else,1614 .err = .expected_something_else,
1554 .token = token,1615 .token = token,
1555 .extra = .{ .expected_types = expected_types },1616 .extra = .{ .expected_types = expected_types },
...@@ -1690,7 +1751,7 @@ pub const Parser = struct {...@@ -1690,7 +1751,7 @@ pub const Parser = struct {
16901751
1691 try self.addErrorDetails(options.toErrorDetails(self.state.token));1752 try self.addErrorDetails(options.toErrorDetails(self.state.token));
1692 if (is_close_paren_expression) {1753 if (is_close_paren_expression) {
1693 try self.addErrorDetails(ErrorDetails{1754 try self.addErrorDetails(.{
1694 .err = .close_paren_expression,1755 .err = .close_paren_expression,
1695 .type = .note,1756 .type = .note,
1696 .token = self.state.token,1757 .token = self.state.token,
...@@ -1698,7 +1759,7 @@ pub const Parser = struct {...@@ -1698,7 +1759,7 @@ pub const Parser = struct {
1698 });1759 });
1699 }1760 }
1700 if (is_unary_plus_expression) {1761 if (is_unary_plus_expression) {
1701 try self.addErrorDetails(ErrorDetails{1762 try self.addErrorDetails(.{
1702 .err = .unary_plus_expression,1763 .err = .unary_plus_expression,
1703 .type = .note,1764 .type = .note,
1704 .token = self.state.token,1765 .token = self.state.token,
...@@ -1739,7 +1800,7 @@ pub const Parser = struct {...@@ -1739,7 +1800,7 @@ pub const Parser = struct {
1739 });1800 });
17401801
1741 if (!rhs_node.isNumberExpression()) {1802 if (!rhs_node.isNumberExpression()) {
1742 return self.addErrorDetailsAndFail(ErrorDetails{1803 return self.addErrorDetailsAndFail(.{
1743 .err = .expected_something_else,1804 .err = .expected_something_else,
1744 .token = rhs_node.getFirstToken(),1805 .token = rhs_node.getFirstToken(),
1745 .token_span_end = rhs_node.getLastToken(),1806 .token_span_end = rhs_node.getLastToken(),
...@@ -1781,16 +1842,39 @@ pub const Parser = struct {...@@ -1781,16 +1842,39 @@ pub const Parser = struct {
1781 fn parseOptionalTokenAdvanced(self: *Self, id: Token.Id, comptime method: Lexer.LexMethod) Error!bool {1842 fn parseOptionalTokenAdvanced(self: *Self, id: Token.Id, comptime method: Lexer.LexMethod) Error!bool {
1782 const maybe_token = try self.lookaheadToken(method);1843 const maybe_token = try self.lookaheadToken(method);
1783 if (maybe_token.id != id) return false;1844 if (maybe_token.id != id) return false;
1784 self.nextToken(method) catch unreachable;1845 try self.nextToken(method);
1785 return true;1846 return true;
1786 }1847 }
17871848
1788 fn addErrorDetails(self: *Self, details: ErrorDetails) Allocator.Error!void {1849 fn addErrorDetailsWithCodePage(self: *Self, details: ErrorDetails) Allocator.Error!void {
1789 try self.state.diagnostics.append(details);1850 try self.state.diagnostics.append(details);
1790 }1851 }
17911852
1792 fn addErrorDetailsAndFail(self: *Self, details: ErrorDetails) Error {1853 fn addErrorDetailsWithCodePageAndFail(self: *Self, details: ErrorDetails) Error {
1793 try self.addErrorDetails(details);1854 try self.addErrorDetailsWithCodePage(details);
1855 return error.ParseError;
1856 }
1857
1858 /// Code page is looked up in input_code_page_lookup using the token, meaning the token
1859 /// must come from nextToken (i.e. it can't be a lookahead token).
1860 fn addErrorDetails(self: *Self, details_without_code_page: ErrorDetailsWithoutCodePage) Allocator.Error!void {
1861 const details = ErrorDetails{
1862 .err = details_without_code_page.err,
1863 .code_page = self.state.input_code_page_lookup.getForToken(details_without_code_page.token),
1864 .token = details_without_code_page.token,
1865 .token_span_start = details_without_code_page.token_span_start,
1866 .token_span_end = details_without_code_page.token_span_end,
1867 .type = details_without_code_page.type,
1868 .print_source_line = details_without_code_page.print_source_line,
1869 .extra = details_without_code_page.extra,
1870 };
1871 try self.addErrorDetailsWithCodePage(details);
1872 }
1873
1874 /// Code page is looked up in input_code_page_lookup using the token, meaning the token
1875 /// must come from nextToken (i.e. it can't be a lookahead token).
1876 fn addErrorDetailsAndFail(self: *Self, details_without_code_page: ErrorDetailsWithoutCodePage) Error {
1877 try self.addErrorDetails(details_without_code_page);
1794 return error.ParseError;1878 return error.ParseError;
1795 }1879 }
17961880
...@@ -1798,35 +1882,34 @@ pub const Parser = struct {...@@ -1798,35 +1882,34 @@ pub const Parser = struct {
1798 self.state.token = token: while (true) {1882 self.state.token = token: while (true) {
1799 const token = self.lexer.next(method) catch |err| switch (err) {1883 const token = self.lexer.next(method) catch |err| switch (err) {
1800 error.CodePagePragmaInIncludedFile => {1884 error.CodePagePragmaInIncludedFile => {
1801 // The Win32 RC compiler silently ignores such `#pragma code_point` directives,1885 // The Win32 RC compiler silently ignores such `#pragma code_page` directives,
1802 // but we want to both ignore them *and* emit a warning1886 // but we want to both ignore them *and* emit a warning
1803 try self.addErrorDetails(.{1887 var details = self.lexer.getErrorDetails(err);
1804 .err = .code_page_pragma_in_included_file,1888 details.type = .warning;
1805 .type = .warning,1889 try self.addErrorDetailsWithCodePage(details);
1806 .token = self.lexer.error_context_token.?,
1807 });
1808 continue;1890 continue;
1809 },1891 },
1810 error.CodePagePragmaInvalidCodePage => {1892 error.CodePagePragmaInvalidCodePage => {
1811 var details = self.lexer.getErrorDetails(err);1893 var details = self.lexer.getErrorDetails(err);
1812 if (!self.options.warn_instead_of_error_on_invalid_code_page) {1894 if (!self.options.warn_instead_of_error_on_invalid_code_page) {
1813 return self.addErrorDetailsAndFail(details);1895 return self.addErrorDetailsWithCodePageAndFail(details);
1814 }1896 }
1815 details.type = .warning;1897 details.type = .warning;
1816 try self.addErrorDetails(details);1898 try self.addErrorDetailsWithCodePage(details);
1817 continue;1899 continue;
1818 },1900 },
1819 error.InvalidDigitCharacterInNumberLiteral => {1901 error.InvalidDigitCharacterInNumberLiteral => {
1820 const details = self.lexer.getErrorDetails(err);1902 const details = self.lexer.getErrorDetails(err);
1821 try self.addErrorDetails(details);1903 try self.addErrorDetailsWithCodePage(details);
1822 return self.addErrorDetailsAndFail(.{1904 return self.addErrorDetailsWithCodePageAndFail(.{
1823 .err = details.err,1905 .err = details.err,
1824 .type = .note,1906 .type = .note,
1907 .code_page = self.lexer.current_code_page,
1825 .token = details.token,1908 .token = details.token,
1826 .print_source_line = false,1909 .print_source_line = false,
1827 });1910 });
1828 },1911 },
1829 else => return self.addErrorDetailsAndFail(self.lexer.getErrorDetails(err)),1912 else => return self.addErrorDetailsWithCodePageAndFail(self.lexer.getErrorDetails(err)),
1830 };1913 };
1831 break :token token;1914 break :token token;
1832 };1915 };
...@@ -1835,7 +1918,29 @@ pub const Parser = struct {...@@ -1835,7 +1918,29 @@ pub const Parser = struct {
1835 // But only set the output code page to the current code page if we are past the first code_page pragma in the file.1918 // But only set the output code page to the current code page if we are past the first code_page pragma in the file.
1836 // Otherwise, we want to fill the lookup using the default code page so that lookups still work for lines that1919 // Otherwise, we want to fill the lookup using the default code page so that lookups still work for lines that
1837 // don't have an explicit output code page set.1920 // don't have an explicit output code page set.
1838 const output_code_page = if (self.lexer.seen_pragma_code_pages > 1) self.lexer.current_code_page else self.state.output_code_page_lookup.default_code_page;1921 const is_disjoint_code_page = self.options.disjoint_code_page and self.lexer.seen_pragma_code_pages == 1;
1922 const output_code_page = if (is_disjoint_code_page)
1923 self.state.output_code_page_lookup.default_code_page
1924 else
1925 self.lexer.current_code_page;
1926
1927 if (is_disjoint_code_page and !self.state.warned_about_disjoint_code_page) {
1928 try self.addErrorDetailsWithCodePage(.{
1929 .err = .disjoint_code_page,
1930 .type = .warning,
1931 .code_page = self.state.input_code_page_lookup.getForLineNum(self.lexer.last_pragma_code_page_token.?.line_number),
1932 .token = self.lexer.last_pragma_code_page_token.?,
1933 });
1934 try self.addErrorDetailsWithCodePage(.{
1935 .err = .disjoint_code_page,
1936 .type = .note,
1937 .code_page = self.state.input_code_page_lookup.getForLineNum(self.lexer.last_pragma_code_page_token.?.line_number),
1938 .token = self.lexer.last_pragma_code_page_token.?,
1939 .print_source_line = false,
1940 });
1941 self.state.warned_about_disjoint_code_page = true;
1942 }
1943
1839 try self.state.output_code_page_lookup.setForToken(self.state.token, output_code_page);1944 try self.state.output_code_page_lookup.setForToken(self.state.token, output_code_page);
1840 }1945 }
18411946
...@@ -1846,7 +1951,7 @@ pub const Parser = struct {...@@ -1846,7 +1951,7 @@ pub const Parser = struct {
1846 // Ignore this error and get the next valid token, we'll deal with this1951 // Ignore this error and get the next valid token, we'll deal with this
1847 // properly when getting the token for real1952 // properly when getting the token for real
1848 error.CodePagePragmaInIncludedFile => continue,1953 error.CodePagePragmaInIncludedFile => continue,
1849 else => return self.addErrorDetailsAndFail(self.state.lookahead_lexer.getErrorDetails(err)),1954 else => return self.addErrorDetailsWithCodePageAndFail(self.state.lookahead_lexer.getErrorDetails(err)),
1850 };1955 };
1851 };1956 };
1852 }1957 }
...@@ -1860,7 +1965,7 @@ pub const Parser = struct {...@@ -1860,7 +1965,7 @@ pub const Parser = struct {
1860 switch (self.state.token.id) {1965 switch (self.state.token.id) {
1861 .literal => {},1966 .literal => {},
1862 else => {1967 else => {
1863 return self.addErrorDetailsAndFail(ErrorDetails{1968 return self.addErrorDetailsAndFail(.{
1864 .err = .expected_token,1969 .err = .expected_token,
1865 .token = self.state.token,1970 .token = self.state.token,
1866 .extra = .{ .expected = .literal },1971 .extra = .{ .expected = .literal },
...@@ -1871,7 +1976,7 @@ pub const Parser = struct {...@@ -1871,7 +1976,7 @@ pub const Parser = struct {
18711976
1872 fn check(self: *Self, expected_token_id: Token.Id) !void {1977 fn check(self: *Self, expected_token_id: Token.Id) !void {
1873 if (self.state.token.id != expected_token_id) {1978 if (self.state.token.id != expected_token_id) {
1874 return self.addErrorDetailsAndFail(ErrorDetails{1979 return self.addErrorDetailsAndFail(.{
1875 .err = .expected_token,1980 .err = .expected_token,
1876 .token = self.state.token,1981 .token = self.state.token,
1877 .extra = .{ .expected = expected_token_id },1982 .extra = .{ .expected = expected_token_id },
...@@ -1879,14 +1984,14 @@ pub const Parser = struct {...@@ -1879,14 +1984,14 @@ pub const Parser = struct {
1879 }1984 }
1880 }1985 }
18811986
1882 fn checkResource(self: *Self) !Resource {1987 fn checkResource(self: *Self) !ResourceType {
1883 switch (self.state.token.id) {1988 switch (self.state.token.id) {
1884 .literal => return Resource.fromString(.{1989 .literal => return ResourceType.fromString(.{
1885 .slice = self.state.token.slice(self.lexer.buffer),1990 .slice = self.state.token.slice(self.lexer.buffer),
1886 .code_page = self.lexer.current_code_page,1991 .code_page = self.lexer.current_code_page,
1887 }),1992 }),
1888 else => {1993 else => {
1889 return self.addErrorDetailsAndFail(ErrorDetails{1994 return self.addErrorDetailsAndFail(.{
1890 .err = .expected_token,1995 .err = .expected_token,
1891 .token = self.state.token,1996 .token = self.state.token,
1892 .extra = .{ .expected = .literal },1997 .extra = .{ .expected = .literal },
lib/compiler/resinator/preprocess.zig+1
...@@ -96,6 +96,7 @@ pub fn appendAroArgs(arena: Allocator, argv: *std.ArrayList([]const u8), options...@@ -96,6 +96,7 @@ pub fn appendAroArgs(arena: Allocator, argv: *std.ArrayList([]const u8), options
96 "--emulate=msvc",96 "--emulate=msvc",
97 "-nostdinc",97 "-nostdinc",
98 "-DRC_INVOKED",98 "-DRC_INVOKED",
99 "-D_WIN32", // undocumented, but defined by default
99 });100 });
100 for (options.extra_include_paths.items) |extra_include_path| {101 for (options.extra_include_paths.items) |extra_include_path| {
101 try argv.append("-I");102 try argv.append("-I");
lib/compiler/resinator/rc.zig+7-7
...@@ -5,7 +5,7 @@ const SourceBytes = @import("literals.zig").SourceBytes;...@@ -5,7 +5,7 @@ const SourceBytes = @import("literals.zig").SourceBytes;
55
6// https://learn.microsoft.com/en-us/windows/win32/menurc/about-resource-files6// https://learn.microsoft.com/en-us/windows/win32/menurc/about-resource-files
77
8pub const Resource = enum {8pub const ResourceType = enum {
9 accelerators,9 accelerators,
10 bitmap,10 bitmap,
11 cursor,11 cursor,
...@@ -48,7 +48,7 @@ pub const Resource = enum {...@@ -48,7 +48,7 @@ pub const Resource = enum {
48 manifest_num,48 manifest_num,
4949
50 const map = std.StaticStringMapWithEql(50 const map = std.StaticStringMapWithEql(
51 Resource,51 ResourceType,
52 std.static_string_map.eqlAsciiIgnoreCase,52 std.static_string_map.eqlAsciiIgnoreCase,
53 ).initComptime(.{53 ).initComptime(.{
54 .{ "ACCELERATORS", .accelerators },54 .{ "ACCELERATORS", .accelerators },
...@@ -72,7 +72,7 @@ pub const Resource = enum {...@@ -72,7 +72,7 @@ pub const Resource = enum {
72 .{ "VXD", .vxd },72 .{ "VXD", .vxd },
73 });73 });
7474
75 pub fn fromString(bytes: SourceBytes) Resource {75 pub fn fromString(bytes: SourceBytes) ResourceType {
76 const maybe_ordinal = res.NameOrOrdinal.maybeOrdinalFromString(bytes);76 const maybe_ordinal = res.NameOrOrdinal.maybeOrdinalFromString(bytes);
77 if (maybe_ordinal) |ordinal| {77 if (maybe_ordinal) |ordinal| {
78 if (ordinal.ordinal >= 256) return .user_defined;78 if (ordinal.ordinal >= 256) return .user_defined;
...@@ -81,8 +81,8 @@ pub const Resource = enum {...@@ -81,8 +81,8 @@ pub const Resource = enum {
81 return map.get(bytes.slice) orelse .user_defined;81 return map.get(bytes.slice) orelse .user_defined;
82 }82 }
8383
84 // TODO: Some comptime validation that RT <-> Resource conversion is synced?84 // TODO: Some comptime validation that RT <-> ResourceType conversion is synced?
85 pub fn fromRT(rt: res.RT) Resource {85 pub fn fromRT(rt: res.RT) ResourceType {
86 return switch (rt) {86 return switch (rt) {
87 .ACCELERATOR => .accelerators,87 .ACCELERATOR => .accelerators,
88 .ANICURSOR => .anicursor_num,88 .ANICURSOR => .anicursor_num,
...@@ -111,7 +111,7 @@ pub const Resource = enum {...@@ -111,7 +111,7 @@ pub const Resource = enum {
111 };111 };
112 }112 }
113113
114 pub fn canUseRawData(resource: Resource) bool {114 pub fn canUseRawData(resource: ResourceType) bool {
115 return switch (resource) {115 return switch (resource) {
116 .user_defined,116 .user_defined,
117 .html,117 .html,
...@@ -125,7 +125,7 @@ pub const Resource = enum {...@@ -125,7 +125,7 @@ pub const Resource = enum {
125 };125 };
126 }126 }
127127
128 pub fn nameForErrorDisplay(resource: Resource) []const u8 {128 pub fn nameForErrorDisplay(resource: ResourceType) []const u8 {
129 return switch (resource) {129 return switch (resource) {
130 // zig fmt: off130 // zig fmt: off
131 .accelerators, .bitmap, .cursor, .dialog, .dialogex, .dlginclude, .dlginit, .font,131 .accelerators, .bitmap, .cursor, .dialog, .dialogex, .dlginclude, .dlginit, .font,
lib/compiler/resinator/res.zig+175-38
...@@ -1,10 +1,10 @@...@@ -1,10 +1,10 @@
1const std = @import("std");1const std = @import("std");
2const rc = @import("rc.zig");2const rc = @import("rc.zig");
3const Resource = rc.Resource;3const ResourceType = rc.ResourceType;
4const CommonResourceAttributes = rc.CommonResourceAttributes;4const CommonResourceAttributes = rc.CommonResourceAttributes;
5const Allocator = std.mem.Allocator;5const Allocator = std.mem.Allocator;
6const windows1252 = @import("windows1252.zig");6const windows1252 = @import("windows1252.zig");
7const CodePage = @import("code_pages.zig").CodePage;7const SupportedCodePage = @import("code_pages.zig").SupportedCodePage;
8const literals = @import("literals.zig");8const literals = @import("literals.zig");
9const SourceBytes = literals.SourceBytes;9const SourceBytes = literals.SourceBytes;
10const Codepoint = @import("code_pages.zig").Codepoint;10const Codepoint = @import("code_pages.zig").Codepoint;
...@@ -40,7 +40,7 @@ pub const RT = enum(u8) {...@@ -40,7 +40,7 @@ pub const RT = enum(u8) {
4040
41 /// Returns null if the resource type is user-defined41 /// Returns null if the resource type is user-defined
42 /// Asserts that the resource is not `stringtable`42 /// Asserts that the resource is not `stringtable`
43 pub fn fromResource(resource: Resource) ?RT {43 pub fn fromResource(resource: ResourceType) ?RT {
44 return switch (resource) {44 return switch (resource) {
45 .accelerators => .ACCELERATOR,45 .accelerators => .ACCELERATOR,
46 .bitmap => .BITMAP,46 .bitmap => .BITMAP,
...@@ -162,6 +162,27 @@ pub const Language = packed struct(u16) {...@@ -162,6 +162,27 @@ pub const Language = packed struct(u16) {
162 pub fn asInt(self: Language) u16 {162 pub fn asInt(self: Language) u16 {
163 return @bitCast(self);163 return @bitCast(self);
164 }164 }
165
166 pub fn format(
167 language: Language,
168 comptime fmt: []const u8,
169 options: std.fmt.FormatOptions,
170 out_stream: anytype,
171 ) !void {
172 _ = fmt;
173 _ = options;
174 const language_id = language.asInt();
175 const language_name = language_name: {
176 if (std.meta.intToEnum(lang.LanguageId, language_id)) |lang_enum_val| {
177 break :language_name @tagName(lang_enum_val);
178 } else |_| {}
179 if (language_id == lang.LOCALE_CUSTOM_UNSPECIFIED) {
180 break :language_name "LOCALE_CUSTOM_UNSPECIFIED";
181 }
182 break :language_name "<UNKNOWN>";
183 };
184 try out_stream.print("{s} (0x{X})", .{ language_name, language_id });
185 }
165};186};
166187
167/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-dlgitemtemplate#remarks188/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-dlgitemtemplate#remarks
...@@ -423,6 +444,50 @@ pub const NameOrOrdinal = union(enum) {...@@ -423,6 +444,50 @@ pub const NameOrOrdinal = union(enum) {
423 .name => return null,444 .name => return null,
424 }445 }
425 }446 }
447
448 pub fn format(
449 self: NameOrOrdinal,
450 comptime fmt: []const u8,
451 options: std.fmt.FormatOptions,
452 out_stream: anytype,
453 ) !void {
454 _ = fmt;
455 _ = options;
456 switch (self) {
457 .name => |name| {
458 try out_stream.print("{s}", .{std.unicode.fmtUtf16Le(name)});
459 },
460 .ordinal => |ordinal| {
461 try out_stream.print("{d}", .{ordinal});
462 },
463 }
464 }
465
466 fn formatResourceType(
467 self: NameOrOrdinal,
468 comptime fmt: []const u8,
469 options: std.fmt.FormatOptions,
470 out_stream: anytype,
471 ) !void {
472 _ = fmt;
473 _ = options;
474 switch (self) {
475 .name => |name| {
476 try out_stream.print("{s}", .{std.unicode.fmtUtf16Le(name)});
477 },
478 .ordinal => |ordinal| {
479 if (std.enums.tagName(RT, @enumFromInt(ordinal))) |predefined_type_name| {
480 try out_stream.print("{s}", .{predefined_type_name});
481 } else {
482 try out_stream.print("{d}", .{ordinal});
483 }
484 },
485 }
486 }
487
488 pub fn fmtResourceType(type_value: NameOrOrdinal) std.fmt.Formatter(formatResourceType) {
489 return .{ .data = type_value };
490 }
426};491};
427492
428fn expectNameOrOrdinal(expected: NameOrOrdinal, actual: NameOrOrdinal) !void {493fn expectNameOrOrdinal(expected: NameOrOrdinal, actual: NameOrOrdinal) !void {
...@@ -603,12 +668,33 @@ pub const AcceleratorModifiers = struct {...@@ -603,12 +668,33 @@ pub const AcceleratorModifiers = struct {
603668
604const AcceleratorKeyCodepointTranslator = struct {669const AcceleratorKeyCodepointTranslator = struct {
605 string_type: literals.StringType,670 string_type: literals.StringType,
671 output_code_page: SupportedCodePage,
606672
607 pub fn translate(self: @This(), maybe_parsed: ?literals.IterativeStringParser.ParsedCodepoint) ?u21 {673 pub fn translate(self: @This(), maybe_parsed: ?literals.IterativeStringParser.ParsedCodepoint) ?u21 {
608 const parsed = maybe_parsed orelse return null;674 const parsed = maybe_parsed orelse return null;
609 if (parsed.codepoint == Codepoint.invalid) return 0xFFFD;675 if (parsed.codepoint == Codepoint.invalid) return 0xFFFD;
610 if (parsed.from_escaped_integer and self.string_type == .ascii) {676 if (parsed.from_escaped_integer) {
611 return windows1252.toCodepoint(@truncate(parsed.codepoint));677 switch (self.string_type) {
678 .ascii => {
679 const truncated: u8 = @truncate(parsed.codepoint);
680 switch (self.output_code_page) {
681 .utf8 => switch (truncated) {
682 0...0x7F => return truncated,
683 else => return 0xFFFD,
684 },
685 .windows1252 => return windows1252.toCodepoint(truncated),
686 }
687 },
688 .wide => {
689 const truncated: u16 = @truncate(parsed.codepoint);
690 return truncated;
691 },
692 }
693 }
694 if (parsed.escaped_surrogate_pair) {
695 // The codepoint of only the low surrogate
696 const low = @as(u16, @intCast(parsed.codepoint & 0x3FF)) + 0xDC00;
697 return low;
612 }698 }
613 return parsed.codepoint;699 return parsed.codepoint;
614 }700 }
...@@ -623,14 +709,17 @@ pub fn parseAcceleratorKeyString(bytes: SourceBytes, is_virt: bool, options: lit...@@ -623,14 +709,17 @@ pub fn parseAcceleratorKeyString(bytes: SourceBytes, is_virt: bool, options: lit
623 }709 }
624710
625 var parser = literals.IterativeStringParser.init(bytes, options);711 var parser = literals.IterativeStringParser.init(bytes, options);
626 var translator = AcceleratorKeyCodepointTranslator{ .string_type = parser.declared_string_type };712 var translator = AcceleratorKeyCodepointTranslator{
713 .string_type = parser.declared_string_type,
714 .output_code_page = options.output_code_page,
715 };
627716
628 const first_codepoint = translator.translate(try parser.next()) orelse return error.EmptyAccelerator;717 const first_codepoint = translator.translate(try parser.next()) orelse return error.EmptyAccelerator;
629 // 0 is treated as a terminator, so this is equivalent to an empty string718 // 0 is treated as a terminator, so this is equivalent to an empty string
630 if (first_codepoint == 0) return error.EmptyAccelerator;719 if (first_codepoint == 0) return error.EmptyAccelerator;
631720
632 if (first_codepoint == '^') {721 if (first_codepoint == '^') {
633 // Note: Emitting this warning unconditonally whenever ^ is the first character722 // Note: Emitting this warning unconditionally whenever ^ is the first character
634 // matches the Win32 RC behavior, but it's questionable whether or not723 // matches the Win32 RC behavior, but it's questionable whether or not
635 // the warning should be emitted for ^^ since that results in the ASCII724 // the warning should be emitted for ^^ since that results in the ASCII
636 // character ^ being written to the .res.725 // character ^ being written to the .res.
...@@ -638,11 +727,18 @@ pub fn parseAcceleratorKeyString(bytes: SourceBytes, is_virt: bool, options: lit...@@ -638,11 +727,18 @@ pub fn parseAcceleratorKeyString(bytes: SourceBytes, is_virt: bool, options: lit
638 try options.diagnostics.?.diagnostics.append(.{727 try options.diagnostics.?.diagnostics.append(.{
639 .err = .ascii_character_not_equivalent_to_virtual_key_code,728 .err = .ascii_character_not_equivalent_to_virtual_key_code,
640 .type = .warning,729 .type = .warning,
730 .code_page = bytes.code_page,
641 .token = options.diagnostics.?.token,731 .token = options.diagnostics.?.token,
642 });732 });
643 }733 }
644734
645 const c = translator.translate(try parser.next()) orelse return error.InvalidControlCharacter;735 const c = translator.translate(try parser.next()) orelse return error.InvalidControlCharacter;
736
737 const third_codepoint = translator.translate(try parser.next());
738 // 0 is treated as a terminator, so a 0 in the third position is fine but
739 // anything else is too many codepoints for an accelerator
740 if (third_codepoint != null and third_codepoint.? != 0) return error.InvalidControlCharacter;
741
646 switch (c) {742 switch (c) {
647 '^' => return '^', // special case743 '^' => return '^', // special case
648 'a'...'z', 'A'...'Z' => return std.ascii.toUpper(@intCast(c)) - 0x40,744 'a'...'z', 'A'...'Z' => return std.ascii.toUpper(@intCast(c)) - 0x40,
...@@ -699,44 +795,44 @@ test "accelerator keys" {...@@ -699,44 +795,44 @@ test "accelerator keys" {
699 try std.testing.expectEqual(@as(u16, 1), try parseAcceleratorKeyString(795 try std.testing.expectEqual(@as(u16, 1), try parseAcceleratorKeyString(
700 .{ .slice = "\"^a\"", .code_page = .windows1252 },796 .{ .slice = "\"^a\"", .code_page = .windows1252 },
701 false,797 false,
702 .{},798 .{ .output_code_page = .windows1252 },
703 ));799 ));
704 try std.testing.expectEqual(@as(u16, 1), try parseAcceleratorKeyString(800 try std.testing.expectEqual(@as(u16, 1), try parseAcceleratorKeyString(
705 .{ .slice = "\"^A\"", .code_page = .windows1252 },801 .{ .slice = "\"^A\"", .code_page = .windows1252 },
706 false,802 false,
707 .{},803 .{ .output_code_page = .windows1252 },
708 ));804 ));
709 try std.testing.expectEqual(@as(u16, 26), try parseAcceleratorKeyString(805 try std.testing.expectEqual(@as(u16, 26), try parseAcceleratorKeyString(
710 .{ .slice = "\"^Z\"", .code_page = .windows1252 },806 .{ .slice = "\"^Z\"", .code_page = .windows1252 },
711 false,807 false,
712 .{},808 .{ .output_code_page = .windows1252 },
713 ));809 ));
714 try std.testing.expectEqual(@as(u16, '^'), try parseAcceleratorKeyString(810 try std.testing.expectEqual(@as(u16, '^'), try parseAcceleratorKeyString(
715 .{ .slice = "\"^^\"", .code_page = .windows1252 },811 .{ .slice = "\"^^\"", .code_page = .windows1252 },
716 false,812 false,
717 .{},813 .{ .output_code_page = .windows1252 },
718 ));814 ));
719815
720 try std.testing.expectEqual(@as(u16, 'a'), try parseAcceleratorKeyString(816 try std.testing.expectEqual(@as(u16, 'a'), try parseAcceleratorKeyString(
721 .{ .slice = "\"a\"", .code_page = .windows1252 },817 .{ .slice = "\"a\"", .code_page = .windows1252 },
722 false,818 false,
723 .{},819 .{ .output_code_page = .windows1252 },
724 ));820 ));
725 try std.testing.expectEqual(@as(u16, 0x6162), try parseAcceleratorKeyString(821 try std.testing.expectEqual(@as(u16, 0x6162), try parseAcceleratorKeyString(
726 .{ .slice = "\"ab\"", .code_page = .windows1252 },822 .{ .slice = "\"ab\"", .code_page = .windows1252 },
727 false,823 false,
728 .{},824 .{ .output_code_page = .windows1252 },
729 ));825 ));
730826
731 try std.testing.expectEqual(@as(u16, 'C'), try parseAcceleratorKeyString(827 try std.testing.expectEqual(@as(u16, 'C'), try parseAcceleratorKeyString(
732 .{ .slice = "\"c\"", .code_page = .windows1252 },828 .{ .slice = "\"c\"", .code_page = .windows1252 },
733 true,829 true,
734 .{},830 .{ .output_code_page = .windows1252 },
735 ));831 ));
736 try std.testing.expectEqual(@as(u16, 0x6363), try parseAcceleratorKeyString(832 try std.testing.expectEqual(@as(u16, 0x6363), try parseAcceleratorKeyString(
737 .{ .slice = "\"cc\"", .code_page = .windows1252 },833 .{ .slice = "\"cc\"", .code_page = .windows1252 },
738 true,834 true,
739 .{},835 .{ .output_code_page = .windows1252 },
740 ));836 ));
741837
742 // \x00 or any escape that evaluates to zero acts as a terminator, everything past it838 // \x00 or any escape that evaluates to zero acts as a terminator, everything past it
...@@ -744,93 +840,93 @@ test "accelerator keys" {...@@ -744,93 +840,93 @@ test "accelerator keys" {
744 try std.testing.expectEqual(@as(u16, 'a'), try parseAcceleratorKeyString(840 try std.testing.expectEqual(@as(u16, 'a'), try parseAcceleratorKeyString(
745 .{ .slice = "\"a\\0bcdef\"", .code_page = .windows1252 },841 .{ .slice = "\"a\\0bcdef\"", .code_page = .windows1252 },
746 false,842 false,
747 .{},843 .{ .output_code_page = .windows1252 },
748 ));844 ));
749845
750 // \x80 is € in Windows-1252, which is Unicode codepoint 20AC846 // \x80 is € in Windows-1252, which is Unicode codepoint 20AC
751 try std.testing.expectEqual(@as(u16, 0x20AC), try parseAcceleratorKeyString(847 try std.testing.expectEqual(@as(u16, 0x20AC), try parseAcceleratorKeyString(
752 .{ .slice = "\"\x80\"", .code_page = .windows1252 },848 .{ .slice = "\"\x80\"", .code_page = .windows1252 },
753 false,849 false,
754 .{},850 .{ .output_code_page = .windows1252 },
755 ));851 ));
756 // This depends on the code page, though, with codepage 65001, \x80852 // This depends on the code page, though, with codepage 65001, \x80
757 // on its own is invalid UTF-8 so it gets converted to the replacement character853 // on its own is invalid UTF-8 so it gets converted to the replacement character
758 try std.testing.expectEqual(@as(u16, 0xFFFD), try parseAcceleratorKeyString(854 try std.testing.expectEqual(@as(u16, 0xFFFD), try parseAcceleratorKeyString(
759 .{ .slice = "\"\x80\"", .code_page = .utf8 },855 .{ .slice = "\"\x80\"", .code_page = .utf8 },
760 false,856 false,
761 .{},857 .{ .output_code_page = .windows1252 },
762 ));858 ));
763 try std.testing.expectEqual(@as(u16, 0xCCAC), try parseAcceleratorKeyString(859 try std.testing.expectEqual(@as(u16, 0xCCAC), try parseAcceleratorKeyString(
764 .{ .slice = "\"\x80\x80\"", .code_page = .windows1252 },860 .{ .slice = "\"\x80\x80\"", .code_page = .windows1252 },
765 false,861 false,
766 .{},862 .{ .output_code_page = .windows1252 },
767 ));863 ));
768 // This also behaves the same with escaped characters864 // This also behaves the same with escaped characters
769 try std.testing.expectEqual(@as(u16, 0x20AC), try parseAcceleratorKeyString(865 try std.testing.expectEqual(@as(u16, 0x20AC), try parseAcceleratorKeyString(
770 .{ .slice = "\"\\x80\"", .code_page = .windows1252 },866 .{ .slice = "\"\\x80\"", .code_page = .windows1252 },
771 false,867 false,
772 .{},868 .{ .output_code_page = .windows1252 },
773 ));869 ));
774 // Even with utf8 code page870 // Even with utf8 code page
775 try std.testing.expectEqual(@as(u16, 0x20AC), try parseAcceleratorKeyString(871 try std.testing.expectEqual(@as(u16, 0x20AC), try parseAcceleratorKeyString(
776 .{ .slice = "\"\\x80\"", .code_page = .utf8 },872 .{ .slice = "\"\\x80\"", .code_page = .utf8 },
777 false,873 false,
778 .{},874 .{ .output_code_page = .windows1252 },
779 ));875 ));
780 try std.testing.expectEqual(@as(u16, 0xCCAC), try parseAcceleratorKeyString(876 try std.testing.expectEqual(@as(u16, 0xCCAC), try parseAcceleratorKeyString(
781 .{ .slice = "\"\\x80\\x80\"", .code_page = .windows1252 },877 .{ .slice = "\"\\x80\\x80\"", .code_page = .windows1252 },
782 false,878 false,
783 .{},879 .{ .output_code_page = .windows1252 },
784 ));880 ));
785 // Wide string with the actual characters behaves like the ASCII string version881 // Wide string with the actual characters behaves like the ASCII string version
786 try std.testing.expectEqual(@as(u16, 0xCCAC), try parseAcceleratorKeyString(882 try std.testing.expectEqual(@as(u16, 0xCCAC), try parseAcceleratorKeyString(
787 .{ .slice = "L\"\x80\x80\"", .code_page = .windows1252 },883 .{ .slice = "L\"\x80\x80\"", .code_page = .windows1252 },
788 false,884 false,
789 .{},885 .{ .output_code_page = .windows1252 },
790 ));886 ));
791 // But wide string with escapes behaves differently887 // But wide string with escapes behaves differently
792 try std.testing.expectEqual(@as(u16, 0x8080), try parseAcceleratorKeyString(888 try std.testing.expectEqual(@as(u16, 0x8080), try parseAcceleratorKeyString(
793 .{ .slice = "L\"\\x80\\x80\"", .code_page = .windows1252 },889 .{ .slice = "L\"\\x80\\x80\"", .code_page = .windows1252 },
794 false,890 false,
795 .{},891 .{ .output_code_page = .windows1252 },
796 ));892 ));
797 // and invalid escapes within wide strings get skipped893 // and invalid escapes within wide strings get skipped
798 try std.testing.expectEqual(@as(u16, 'z'), try parseAcceleratorKeyString(894 try std.testing.expectEqual(@as(u16, 'z'), try parseAcceleratorKeyString(
799 .{ .slice = "L\"\\Hz\"", .code_page = .windows1252 },895 .{ .slice = "L\"\\Hz\"", .code_page = .windows1252 },
800 false,896 false,
801 .{},897 .{ .output_code_page = .windows1252 },
802 ));898 ));
803899
804 // any non-A-Z codepoints are illegal900 // any non-A-Z codepoints are illegal
805 try std.testing.expectError(error.ControlCharacterOutOfRange, parseAcceleratorKeyString(901 try std.testing.expectError(error.ControlCharacterOutOfRange, parseAcceleratorKeyString(
806 .{ .slice = "\"^\x83\"", .code_page = .windows1252 },902 .{ .slice = "\"^\x83\"", .code_page = .windows1252 },
807 false,903 false,
808 .{},904 .{ .output_code_page = .windows1252 },
809 ));905 ));
810 try std.testing.expectError(error.ControlCharacterOutOfRange, parseAcceleratorKeyString(906 try std.testing.expectError(error.ControlCharacterOutOfRange, parseAcceleratorKeyString(
811 .{ .slice = "\"^1\"", .code_page = .windows1252 },907 .{ .slice = "\"^1\"", .code_page = .windows1252 },
812 false,908 false,
813 .{},909 .{ .output_code_page = .windows1252 },
814 ));910 ));
815 try std.testing.expectError(error.InvalidControlCharacter, parseAcceleratorKeyString(911 try std.testing.expectError(error.InvalidControlCharacter, parseAcceleratorKeyString(
816 .{ .slice = "\"^\"", .code_page = .windows1252 },912 .{ .slice = "\"^\"", .code_page = .windows1252 },
817 false,913 false,
818 .{},914 .{ .output_code_page = .windows1252 },
819 ));915 ));
820 try std.testing.expectError(error.EmptyAccelerator, parseAcceleratorKeyString(916 try std.testing.expectError(error.EmptyAccelerator, parseAcceleratorKeyString(
821 .{ .slice = "\"\"", .code_page = .windows1252 },917 .{ .slice = "\"\"", .code_page = .windows1252 },
822 false,918 false,
823 .{},919 .{ .output_code_page = .windows1252 },
824 ));920 ));
825 try std.testing.expectError(error.AcceleratorTooLong, parseAcceleratorKeyString(921 try std.testing.expectError(error.AcceleratorTooLong, parseAcceleratorKeyString(
826 .{ .slice = "\"hello\"", .code_page = .windows1252 },922 .{ .slice = "\"hello\"", .code_page = .windows1252 },
827 false,923 false,
828 .{},924 .{ .output_code_page = .windows1252 },
829 ));925 ));
830 try std.testing.expectError(error.ControlCharacterOutOfRange, parseAcceleratorKeyString(926 try std.testing.expectError(error.ControlCharacterOutOfRange, parseAcceleratorKeyString(
831 .{ .slice = "\"^\x80\"", .code_page = .windows1252 },927 .{ .slice = "\"^\x80\"", .code_page = .windows1252 },
832 false,928 false,
833 .{},929 .{ .output_code_page = .windows1252 },
834 ));930 ));
835931
836 // Invalid UTF-8 gets converted to 0xFFFD, multiple invalids get shifted and added together932 // Invalid UTF-8 gets converted to 0xFFFD, multiple invalids get shifted and added together
...@@ -838,40 +934,81 @@ test "accelerator keys" {...@@ -838,40 +934,81 @@ test "accelerator keys" {
838 try std.testing.expectEqual(@as(u16, 0xFCFD), try parseAcceleratorKeyString(934 try std.testing.expectEqual(@as(u16, 0xFCFD), try parseAcceleratorKeyString(
839 .{ .slice = "\"\x80\x80\"", .code_page = .utf8 },935 .{ .slice = "\"\x80\x80\"", .code_page = .utf8 },
840 false,936 false,
841 .{},937 .{ .output_code_page = .windows1252 },
842 ));938 ));
843 try std.testing.expectEqual(@as(u16, 0xFCFD), try parseAcceleratorKeyString(939 try std.testing.expectEqual(@as(u16, 0xFCFD), try parseAcceleratorKeyString(
844 .{ .slice = "L\"\x80\x80\"", .code_page = .utf8 },940 .{ .slice = "L\"\x80\x80\"", .code_page = .utf8 },
845 false,941 false,
846 .{},942 .{ .output_code_page = .windows1252 },
847 ));943 ));
848944
849 // Codepoints >= 0x10000945 // Codepoints >= 0x10000
850 try std.testing.expectEqual(@as(u16, 0xDD00), try parseAcceleratorKeyString(946 try std.testing.expectEqual(@as(u16, 0xDD00), try parseAcceleratorKeyString(
851 .{ .slice = "\"\xF0\x90\x84\x80\"", .code_page = .utf8 },947 .{ .slice = "\"\xF0\x90\x84\x80\"", .code_page = .utf8 },
852 false,948 false,
853 .{},949 .{ .output_code_page = .windows1252 },
854 ));950 ));
855 try std.testing.expectEqual(@as(u16, 0xDD00), try parseAcceleratorKeyString(951 try std.testing.expectEqual(@as(u16, 0xDD00), try parseAcceleratorKeyString(
856 .{ .slice = "L\"\xF0\x90\x84\x80\"", .code_page = .utf8 },952 .{ .slice = "L\"\xF0\x90\x84\x80\"", .code_page = .utf8 },
857 false,953 false,
858 .{},954 .{ .output_code_page = .windows1252 },
859 ));955 ));
860 try std.testing.expectEqual(@as(u16, 0x9C01), try parseAcceleratorKeyString(956 try std.testing.expectEqual(@as(u16, 0x9C01), try parseAcceleratorKeyString(
861 .{ .slice = "\"\xF4\x80\x80\x81\"", .code_page = .utf8 },957 .{ .slice = "\"\xF4\x80\x80\x81\"", .code_page = .utf8 },
862 false,958 false,
863 .{},959 .{ .output_code_page = .windows1252 },
864 ));960 ));
865 // anything before or after a codepoint >= 0x10000 causes an error961 // anything before or after a codepoint >= 0x10000 causes an error
866 try std.testing.expectError(error.AcceleratorTooLong, parseAcceleratorKeyString(962 try std.testing.expectError(error.AcceleratorTooLong, parseAcceleratorKeyString(
867 .{ .slice = "\"a\xF0\x90\x80\x80\"", .code_page = .utf8 },963 .{ .slice = "\"a\xF0\x90\x80\x80\"", .code_page = .utf8 },
868 false,964 false,
869 .{},965 .{ .output_code_page = .windows1252 },
870 ));966 ));
871 try std.testing.expectError(error.AcceleratorTooLong, parseAcceleratorKeyString(967 try std.testing.expectError(error.AcceleratorTooLong, parseAcceleratorKeyString(
872 .{ .slice = "\"\xF0\x90\x80\x80a\"", .code_page = .utf8 },968 .{ .slice = "\"\xF0\x90\x80\x80a\"", .code_page = .utf8 },
873 false,969 false,
874 .{},970 .{ .output_code_page = .windows1252 },
971 ));
972
973 // Misc special cases
974 try std.testing.expectEqual(@as(u16, 0xFFFD), try parseAcceleratorKeyString(
975 .{ .slice = "\"\\777\"", .code_page = .utf8 },
976 false,
977 .{ .output_code_page = .utf8 },
978 ));
979 try std.testing.expectEqual(@as(u16, 0xFFFF), try parseAcceleratorKeyString(
980 .{ .slice = "L\"\\7777777\"", .code_page = .utf8 },
981 false,
982 .{ .output_code_page = .utf8 },
983 ));
984 try std.testing.expectEqual(@as(u16, 0x01), try parseAcceleratorKeyString(
985 .{ .slice = "L\"\\200001\"", .code_page = .utf8 },
986 false,
987 .{ .output_code_page = .utf8 },
988 ));
989 // Escape of a codepoint >= 0x10000 omits the high surrogate pair
990 try std.testing.expectEqual(@as(u16, 0xDF48), try parseAcceleratorKeyString(
991 .{ .slice = "L\"\\𐍈\"", .code_page = .utf8 },
992 false,
993 .{ .output_code_page = .utf8 },
994 ));
995 // Invalid escape code is skipped, allows for 2 codepoints afterwards
996 try std.testing.expectEqual(@as(u16, 0x7878), try parseAcceleratorKeyString(
997 .{ .slice = "L\"\\kxx\"", .code_page = .utf8 },
998 false,
999 .{ .output_code_page = .utf8 },
1000 ));
1001 // Escape of a codepoint >= 0x10000 allows for a codepoint afterwards
1002 try std.testing.expectEqual(@as(u16, 0x4878), try parseAcceleratorKeyString(
1003 .{ .slice = "L\"\\𐍈x\"", .code_page = .utf8 },
1004 false,
1005 .{ .output_code_page = .utf8 },
1006 ));
1007 // Input code page of 1252, output code page of utf-8
1008 try std.testing.expectEqual(@as(u16, 0xFFFD), try parseAcceleratorKeyString(
1009 .{ .slice = "\"\\270\"", .code_page = .windows1252 },
1010 false,
1011 .{ .output_code_page = .utf8 },
875 ));1012 ));
876}1013}
8771014
lib/compiler/resinator/source_mapping.zig+438-41
...@@ -38,7 +38,7 @@ pub const ParseAndRemoveLineCommandsOptions = struct {...@@ -38,7 +38,7 @@ pub const ParseAndRemoveLineCommandsOptions = struct {
38///38///
39/// If `options.initial_filename` is provided, that filename is guaranteed to be39/// If `options.initial_filename` is provided, that filename is guaranteed to be
40/// within the `mappings.files` table and `root_filename_offset` will be set appropriately.40/// within the `mappings.files` table and `root_filename_offset` will be set appropriately.
41pub fn parseAndRemoveLineCommands(allocator: Allocator, source: []const u8, buf: []u8, options: ParseAndRemoveLineCommandsOptions) !ParseLineCommandsResult {41pub fn parseAndRemoveLineCommands(allocator: Allocator, source: []const u8, buf: []u8, options: ParseAndRemoveLineCommandsOptions) error{ OutOfMemory, InvalidLineCommand, LineNumberOverflow }!ParseLineCommandsResult {
42 var parse_result = ParseLineCommandsResult{42 var parse_result = ParseLineCommandsResult{
43 .result = undefined,43 .result = undefined,
44 .mappings = .{},44 .mappings = .{},
...@@ -53,12 +53,41 @@ pub fn parseAndRemoveLineCommands(allocator: Allocator, source: []const u8, buf:...@@ -53,12 +53,41 @@ pub fn parseAndRemoveLineCommands(allocator: Allocator, source: []const u8, buf:
53 parse_result.mappings.root_filename_offset = try parse_result.mappings.files.put(allocator, initial_filename);53 parse_result.mappings.root_filename_offset = try parse_result.mappings.files.put(allocator, initial_filename);
54 }54 }
5555
56 // This implementation attempts to be comment and string aware in order
57 // to avoid errant #line <num> "<filename>" within multiline comments
58 // leading to problems in the source mapping after comments are removed,
59 // but it is not a perfect implementation (intentionally).
60 //
61 // The current implementation does not handle cases like
62 // /* foo */ #line ...
63 // #line ... // foo
64 // #line ... /* foo ...
65 // etc
66 //
67 // (the first example will not be recognized as a #line command, the second
68 // and third will error with InvalidLineCommand)
69 //
70 // This is fine, though, since #line commands are generated by the
71 // preprocessor so in normal circumstances they will be well-formed and
72 // consistent. The only realistic way the imperfect implementation could
73 // affect a 'real' use-case would be someone taking the output of a
74 // preprocessor, editing it manually to add comments before/after #line
75 // commands, and then running it through resinator with /:no-preprocess.
76
56 std.debug.assert(buf.len >= source.len);77 std.debug.assert(buf.len >= source.len);
57 var result = UncheckedSliceWriter{ .slice = buf };78 var result = UncheckedSliceWriter{ .slice = buf };
58 const State = enum {79 const State = enum {
59 line_start,80 line_start,
60 preprocessor,81 preprocessor,
61 non_preprocessor,82 non_preprocessor,
83 forward_slash,
84 line_comment,
85 multiline_comment,
86 multiline_comment_end,
87 single_quoted,
88 single_quoted_escape,
89 double_quoted,
90 double_quoted_escape,
62 };91 };
63 var state: State = .line_start;92 var state: State = .line_start;
64 var index: usize = 0;93 var index: usize = 0;
...@@ -66,8 +95,8 @@ pub fn parseAndRemoveLineCommands(allocator: Allocator, source: []const u8, buf:...@@ -66,8 +95,8 @@ pub fn parseAndRemoveLineCommands(allocator: Allocator, source: []const u8, buf:
66 var preprocessor_start: usize = 0;95 var preprocessor_start: usize = 0;
67 var line_number: usize = 1;96 var line_number: usize = 1;
68 while (index < source.len) : (index += 1) {97 while (index < source.len) : (index += 1) {
69 const c = source[index];98 var c = source[index];
70 switch (state) {99 state: switch (state) {
71 .line_start => switch (c) {100 .line_start => switch (c) {
72 '#' => {101 '#' => {
73 preprocessor_start = index;102 preprocessor_start = index;
...@@ -93,6 +122,27 @@ pub fn parseAndRemoveLineCommands(allocator: Allocator, source: []const u8, buf:...@@ -93,6 +122,27 @@ pub fn parseAndRemoveLineCommands(allocator: Allocator, source: []const u8, buf:
93 pending_start = index;122 pending_start = index;
94 }123 }
95 },124 },
125 '/' => {
126 if (!current_mapping.ignore_contents) {
127 result.writeSlice(source[pending_start orelse index .. index + 1]);
128 pending_start = null;
129 }
130 state = .forward_slash;
131 },
132 '\'' => {
133 if (!current_mapping.ignore_contents) {
134 result.writeSlice(source[pending_start orelse index .. index + 1]);
135 pending_start = null;
136 }
137 state = .single_quoted;
138 },
139 '"' => {
140 if (!current_mapping.ignore_contents) {
141 result.writeSlice(source[pending_start orelse index .. index + 1]);
142 pending_start = null;
143 }
144 state = .double_quoted;
145 },
96 else => {146 else => {
97 state = .non_preprocessor;147 state = .non_preprocessor;
98 if (pending_start != null) {148 if (pending_start != null) {
...@@ -107,25 +157,246 @@ pub fn parseAndRemoveLineCommands(allocator: Allocator, source: []const u8, buf:...@@ -107,25 +157,246 @@ pub fn parseAndRemoveLineCommands(allocator: Allocator, source: []const u8, buf:
107 }157 }
108 },158 },
109 },159 },
160 .forward_slash => switch (c) {
161 '\r', '\n' => {
162 const is_crlf = formsLineEndingPair(source, c, index + 1);
163 if (!current_mapping.ignore_contents) {
164 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
165
166 result.write(c);
167 if (is_crlf) result.write(source[index + 1]);
168 line_number += 1;
169 }
170 if (is_crlf) index += 1;
171 state = .line_start;
172 pending_start = null;
173 },
174 '/' => {
175 if (!current_mapping.ignore_contents) {
176 result.write(c);
177 }
178 state = .line_comment;
179 },
180 '*' => {
181 if (!current_mapping.ignore_contents) {
182 result.write(c);
183 }
184 state = .multiline_comment;
185 },
186 else => {
187 if (!current_mapping.ignore_contents) {
188 result.write(c);
189 }
190 state = .non_preprocessor;
191 },
192 },
193 .line_comment => switch (c) {
194 '\r', '\n' => {
195 const is_crlf = formsLineEndingPair(source, c, index + 1);
196 if (!current_mapping.ignore_contents) {
197 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
198
199 result.write(c);
200 if (is_crlf) result.write(source[index + 1]);
201 line_number += 1;
202 }
203 if (is_crlf) index += 1;
204 state = .line_start;
205 pending_start = null;
206 },
207 else => {
208 if (!current_mapping.ignore_contents) {
209 result.write(c);
210 }
211 },
212 },
213 .multiline_comment => switch (c) {
214 '\r', '\n' => {
215 const is_crlf = formsLineEndingPair(source, c, index + 1);
216 if (!current_mapping.ignore_contents) {
217 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
218
219 result.write(c);
220 if (is_crlf) result.write(source[index + 1]);
221 line_number += 1;
222 }
223 if (is_crlf) index += 1;
224 pending_start = null;
225 },
226 '*' => {
227 if (!current_mapping.ignore_contents) {
228 result.write(c);
229 }
230 state = .multiline_comment_end;
231 },
232 else => {
233 if (!current_mapping.ignore_contents) {
234 result.write(c);
235 }
236 },
237 },
238 .multiline_comment_end => switch (c) {
239 '\r', '\n' => {
240 const is_crlf = formsLineEndingPair(source, c, index + 1);
241 if (!current_mapping.ignore_contents) {
242 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
243
244 result.write(c);
245 if (is_crlf) result.write(source[index + 1]);
246 line_number += 1;
247 }
248 if (is_crlf) index += 1;
249 state = .multiline_comment;
250 pending_start = null;
251 },
252 '/' => {
253 if (!current_mapping.ignore_contents) {
254 result.write(c);
255 }
256 state = .non_preprocessor;
257 },
258 '*' => {
259 if (!current_mapping.ignore_contents) {
260 result.write(c);
261 }
262 // stay in multiline_comment_end state
263 },
264 else => {
265 if (!current_mapping.ignore_contents) {
266 result.write(c);
267 }
268 state = .multiline_comment;
269 },
270 },
271 .single_quoted => switch (c) {
272 '\r', '\n' => {
273 const is_crlf = formsLineEndingPair(source, c, index + 1);
274 if (!current_mapping.ignore_contents) {
275 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
276
277 result.write(c);
278 if (is_crlf) result.write(source[index + 1]);
279 line_number += 1;
280 }
281 if (is_crlf) index += 1;
282 state = .line_start;
283 pending_start = null;
284 },
285 '\\' => {
286 if (!current_mapping.ignore_contents) {
287 result.write(c);
288 }
289 state = .single_quoted_escape;
290 },
291 '\'' => {
292 if (!current_mapping.ignore_contents) {
293 result.write(c);
294 }
295 state = .non_preprocessor;
296 },
297 else => {
298 if (!current_mapping.ignore_contents) {
299 result.write(c);
300 }
301 },
302 },
303 .single_quoted_escape => switch (c) {
304 '\r', '\n' => {
305 const is_crlf = formsLineEndingPair(source, c, index + 1);
306 if (!current_mapping.ignore_contents) {
307 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
308
309 result.write(c);
310 if (is_crlf) result.write(source[index + 1]);
311 line_number += 1;
312 }
313 if (is_crlf) index += 1;
314 state = .line_start;
315 pending_start = null;
316 },
317 else => {
318 if (!current_mapping.ignore_contents) {
319 result.write(c);
320 }
321 state = .single_quoted;
322 },
323 },
324 .double_quoted => switch (c) {
325 '\r', '\n' => {
326 const is_crlf = formsLineEndingPair(source, c, index + 1);
327 if (!current_mapping.ignore_contents) {
328 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
329
330 result.write(c);
331 if (is_crlf) result.write(source[index + 1]);
332 line_number += 1;
333 }
334 if (is_crlf) index += 1;
335 state = .line_start;
336 pending_start = null;
337 },
338 '\\' => {
339 if (!current_mapping.ignore_contents) {
340 result.write(c);
341 }
342 state = .double_quoted_escape;
343 },
344 '"' => {
345 if (!current_mapping.ignore_contents) {
346 result.write(c);
347 }
348 state = .non_preprocessor;
349 },
350 else => {
351 if (!current_mapping.ignore_contents) {
352 result.write(c);
353 }
354 },
355 },
356 .double_quoted_escape => switch (c) {
357 '\r', '\n' => {
358 const is_crlf = formsLineEndingPair(source, c, index + 1);
359 if (!current_mapping.ignore_contents) {
360 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
361
362 result.write(c);
363 if (is_crlf) result.write(source[index + 1]);
364 line_number += 1;
365 }
366 if (is_crlf) index += 1;
367 state = .line_start;
368 pending_start = null;
369 },
370 else => {
371 if (!current_mapping.ignore_contents) {
372 result.write(c);
373 }
374 state = .double_quoted;
375 },
376 },
110 .preprocessor => switch (c) {377 .preprocessor => switch (c) {
111 '\r', '\n' => {378 '\r', '\n' => {
112 // Now that we have the full line we can decide what to do with it379 // Now that we have the full line we can decide what to do with it
113 const preprocessor_str = source[preprocessor_start..index];380 const preprocessor_str = source[preprocessor_start..index];
114 const is_crlf = formsLineEndingPair(source, c, index + 1);
115 if (std.mem.startsWith(u8, preprocessor_str, "#line")) {381 if (std.mem.startsWith(u8, preprocessor_str, "#line")) {
116 try handleLineCommand(allocator, preprocessor_str, &current_mapping);382 try handleLineCommand(allocator, preprocessor_str, &current_mapping);
383 const is_crlf = formsLineEndingPair(source, c, index + 1);
384 if (is_crlf) index += 1;
385 state = .line_start;
386 pending_start = null;
117 } else {387 } else {
118 if (!current_mapping.ignore_contents) {388 // Backtrack and reparse the line in the non_preprocessor state,
119 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);389 // since it's possible that this line contains a multiline comment
120390 // start, etc.
121 const line_ending_len: usize = if (is_crlf) 2 else 1;391 state = .non_preprocessor;
122 result.writeSlice(source[pending_start.? .. index + line_ending_len]);392 index = pending_start.?;
123 line_number += 1;393 pending_start = null;
124 }394 // TODO: This is a hacky way to implement this, c needs to be
395 // updated since we're using continue :state here
396 c = source[index];
397 // continue to avoid the index += 1 of the while loop
398 continue :state .non_preprocessor;
125 }399 }
126 if (is_crlf) index += 1;
127 state = .line_start;
128 pending_start = null;
129 },400 },
130 else => {},401 else => {},
131 },402 },
...@@ -143,6 +414,24 @@ pub fn parseAndRemoveLineCommands(allocator: Allocator, source: []const u8, buf:...@@ -143,6 +414,24 @@ pub fn parseAndRemoveLineCommands(allocator: Allocator, source: []const u8, buf:
143 state = .line_start;414 state = .line_start;
144 pending_start = null;415 pending_start = null;
145 },416 },
417 '/' => {
418 if (!current_mapping.ignore_contents) {
419 result.write(c);
420 }
421 state = .forward_slash;
422 },
423 '\'' => {
424 if (!current_mapping.ignore_contents) {
425 result.write(c);
426 }
427 state = .single_quoted;
428 },
429 '"' => {
430 if (!current_mapping.ignore_contents) {
431 result.write(c);
432 }
433 state = .double_quoted;
434 },
146 else => {435 else => {
147 if (!current_mapping.ignore_contents) {436 if (!current_mapping.ignore_contents) {
148 result.write(c);437 result.write(c);
...@@ -153,7 +442,16 @@ pub fn parseAndRemoveLineCommands(allocator: Allocator, source: []const u8, buf:...@@ -153,7 +442,16 @@ pub fn parseAndRemoveLineCommands(allocator: Allocator, source: []const u8, buf:
153 } else {442 } else {
154 switch (state) {443 switch (state) {
155 .line_start => {},444 .line_start => {},
156 .non_preprocessor => {445 .forward_slash,
446 .line_comment,
447 .multiline_comment,
448 .multiline_comment_end,
449 .single_quoted,
450 .single_quoted_escape,
451 .double_quoted,
452 .double_quoted_escape,
453 .non_preprocessor,
454 => {
157 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);455 try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
158 },456 },
159 .preprocessor => {457 .preprocessor => {
...@@ -207,34 +505,40 @@ pub fn handleLineEnd(allocator: Allocator, post_processed_line_number: usize, ma...@@ -207,34 +505,40 @@ pub fn handleLineEnd(allocator: Allocator, post_processed_line_number: usize, ma
207505
208 try mapping.set(post_processed_line_number, current_mapping.line_num, filename_offset);506 try mapping.set(post_processed_line_number, current_mapping.line_num, filename_offset);
209507
210 current_mapping.line_num += 1;508 current_mapping.line_num = std.math.add(usize, current_mapping.line_num, 1) catch return error.LineNumberOverflow;
211 current_mapping.pending = false;509 current_mapping.pending = false;
212}510}
213511
214// TODO: Might want to provide diagnostics on invalid line commands instead of just returning512// TODO: Might want to provide diagnostics on invalid line commands instead of just returning
215pub fn handleLineCommand(allocator: Allocator, line_command: []const u8, current_mapping: *CurrentMapping) error{OutOfMemory}!void {513pub fn handleLineCommand(allocator: Allocator, line_command: []const u8, current_mapping: *CurrentMapping) error{ OutOfMemory, InvalidLineCommand }!void {
216 // TODO: Are there other whitespace characters that should be included?514 // TODO: Are there other whitespace characters that should be included?
217 var tokenizer = std.mem.tokenizeAny(u8, line_command, " \t");515 var tokenizer = std.mem.tokenizeAny(u8, line_command, " \t");
218 const line_directive = tokenizer.next() orelse return; // #line516 const line_directive = tokenizer.next() orelse return error.InvalidLineCommand; // #line
219 if (!std.mem.eql(u8, line_directive, "#line")) return;517 if (!std.mem.eql(u8, line_directive, "#line")) return error.InvalidLineCommand;
220 const linenum_str = tokenizer.next() orelse return;518 const linenum_str = tokenizer.next() orelse return error.InvalidLineCommand;
221 const linenum = std.fmt.parseUnsigned(usize, linenum_str, 10) catch return;519 const linenum = std.fmt.parseUnsigned(usize, linenum_str, 10) catch return error.InvalidLineCommand;
520 if (linenum == 0) return error.InvalidLineCommand;
222521
223 var filename_literal = tokenizer.rest();522 var filename_literal = tokenizer.rest();
224 while (filename_literal.len > 0 and std.ascii.isWhitespace(filename_literal[filename_literal.len - 1])) {523 while (filename_literal.len > 0 and std.ascii.isWhitespace(filename_literal[filename_literal.len - 1])) {
225 filename_literal.len -= 1;524 filename_literal.len -= 1;
226 }525 }
227 if (filename_literal.len < 2) return;526 if (filename_literal.len < 2) return error.InvalidLineCommand;
228 const is_quoted = filename_literal[0] == '"' and filename_literal[filename_literal.len - 1] == '"';527 const is_quoted = filename_literal[0] == '"' and filename_literal[filename_literal.len - 1] == '"';
229 if (!is_quoted) return;528 if (!is_quoted) return error.InvalidLineCommand;
230 const filename = parseFilename(allocator, filename_literal[1 .. filename_literal.len - 1]) catch |err| switch (err) {529 const unquoted_filename = filename_literal[1 .. filename_literal.len - 1];
530
531 // Ignore <builtin> and <command line>
532 if (std.mem.eql(u8, unquoted_filename, "<builtin>") or std.mem.eql(u8, unquoted_filename, "<command line>")) return;
533
534 const filename = parseFilename(allocator, unquoted_filename) catch |err| switch (err) {
231 error.OutOfMemory => |e| return e,535 error.OutOfMemory => |e| return e,
232 else => return,536 else => return error.InvalidLineCommand,
233 };537 };
234 defer allocator.free(filename);538 defer allocator.free(filename);
235539
236 // \x00 bytes in the filename is incompatible with how StringTable works540 // \x00 bytes in the filename is incompatible with how StringTable works
237 if (std.mem.indexOfScalar(u8, filename, '\x00') != null) return;541 if (std.mem.indexOfScalar(u8, filename, '\x00') != null) return error.InvalidLineCommand;
238542
239 current_mapping.line_num = linenum;543 current_mapping.line_num = linenum;
240 current_mapping.filename.clearRetainingCapacity();544 current_mapping.filename.clearRetainingCapacity();
...@@ -494,8 +798,12 @@ pub const SourceMappings = struct {...@@ -494,8 +798,12 @@ pub const SourceMappings = struct {
494 if (node.key.filename_offset != filename_offset) {798 if (node.key.filename_offset != filename_offset) {
495 break :need_new_node true;799 break :need_new_node true;
496 }800 }
497 const exist_delta = @as(i64, @intCast(node.key.corresponding_start_line)) - @as(i64, @intCast(node.key.start_line));801 // TODO: These use i65 to avoid truncation when any of the line number values
498 const cur_delta = @as(i64, @intCast(corresponding_line_num)) - @as(i64, @intCast(line_num));802 // use all 64 bits of the usize. In reality, line numbers can't really
803 // get that large so limiting the line number and using a smaller iX
804 // type here might be a better solution.
805 const exist_delta = @as(i65, @intCast(node.key.corresponding_start_line)) - @as(i65, @intCast(node.key.start_line));
806 const cur_delta = @as(i65, @intCast(corresponding_line_num)) - @as(i65, @intCast(line_num));
499 if (exist_delta != cur_delta) {807 if (exist_delta != cur_delta) {
500 break :need_new_node true;808 break :need_new_node true;
501 }809 }
...@@ -578,15 +886,8 @@ pub const SourceMappings = struct {...@@ -578,15 +886,8 @@ pub const SourceMappings = struct {
578 inorder_node.key.start_line -= span_diff;886 inorder_node.key.start_line -= span_diff;
579887
580 // This can only really happen if there are #line commands within888 // This can only really happen if there are #line commands within
581 // a multiline comment, which in theory should be skipped over.889 // a multiline comment, which should be skipped over.
582 // However, currently, parseAndRemoveLineCommands is not aware of890 std.debug.assert(prev.key.start_line <= inorder_node.key.start_line);
583 // comments at all.
584 //
585 // TODO: Make parseAndRemoveLineCommands aware of comments/strings
586 // and turn this into an assertion
587 if (prev.key.start_line > inorder_node.key.start_line) {
588 return error.InvalidSourceMappingCollapse;
589 }
590 prev = inorder_node;891 prev = inorder_node;
591 }892 }
592 self.end_line -= span_diff;893 self.end_line -= span_diff;
...@@ -594,7 +895,7 @@ pub const SourceMappings = struct {...@@ -594,7 +895,7 @@ pub const SourceMappings = struct {
594895
595 /// Returns true if the line is from the main/root file (i.e. not a file that has been896 /// Returns true if the line is from the main/root file (i.e. not a file that has been
596 /// `#include`d).897 /// `#include`d).
597 pub fn isRootFile(self: *SourceMappings, line_num: usize) bool {898 pub fn isRootFile(self: *const SourceMappings, line_num: usize) bool {
598 const source = self.get(line_num) orelse return false;899 const source = self.get(line_num) orelse return false;
599 return source.filename_offset == self.root_filename_offset;900 return source.filename_offset == self.root_filename_offset;
600 }901 }
...@@ -803,9 +1104,6 @@ test "in place" {...@@ -803,9 +1104,6 @@ test "in place" {
803}1104}
8041105
805test "line command within a multiline comment" {1106test "line command within a multiline comment" {
806 // TODO: Enable once parseAndRemoveLineCommands is comment-aware
807 if (true) return error.SkipZigTest;
808
809 try testParseAndRemoveLineCommands(1107 try testParseAndRemoveLineCommands(
810 \\/*1108 \\/*
811 \\#line 1 "irrelevant.rc"1109 \\#line 1 "irrelevant.rc"
...@@ -825,4 +1123,103 @@ test "line command within a multiline comment" {...@@ -825,4 +1123,103 @@ test "line command within a multiline comment" {
825 \\1123 \\
826 \\*/1124 \\*/
827 , .{ .initial_filename = "blah.rc" });1125 , .{ .initial_filename = "blah.rc" });
1126
1127 // * but without / directly after
1128 try testParseAndRemoveLineCommands(
1129 \\/** /
1130 \\#line 1 "irrelevant.rc"
1131 \\*/
1132 , &[_]ExpectedSourceSpan{
1133 .{ .start_line = 1, .end_line = 1, .filename = "blah.rc" },
1134 .{ .start_line = 2, .end_line = 2, .filename = "blah.rc" },
1135 .{ .start_line = 3, .end_line = 3, .filename = "blah.rc" },
1136 },
1137 \\/** /
1138 \\#line 1 "irrelevant.rc"
1139 \\*/
1140 , .{ .initial_filename = "blah.rc" });
1141
1142 // /** and **/
1143 try testParseAndRemoveLineCommands(
1144 \\/**
1145 \\#line 1 "irrelevant.rc"
1146 \\**/
1147 \\foo
1148 , &[_]ExpectedSourceSpan{
1149 .{ .start_line = 1, .end_line = 1, .filename = "blah.rc" },
1150 .{ .start_line = 2, .end_line = 2, .filename = "blah.rc" },
1151 .{ .start_line = 3, .end_line = 3, .filename = "blah.rc" },
1152 .{ .start_line = 20, .end_line = 20, .filename = "blah.rc" },
1153 },
1154 \\/**
1155 \\#line 1 "irrelevant.rc"
1156 \\**/
1157 \\#line 20 "blah.rc"
1158 \\foo
1159 , .{ .initial_filename = "blah.rc" });
1160}
1161
1162test "whitespace preservation" {
1163 try testParseAndRemoveLineCommands(
1164 \\ /
1165 \\/
1166 , &[_]ExpectedSourceSpan{
1167 .{ .start_line = 1, .end_line = 1, .filename = "blah.rc" },
1168 .{ .start_line = 2, .end_line = 2, .filename = "blah.rc" },
1169 },
1170 \\ /
1171 \\/
1172 , .{ .initial_filename = "blah.rc" });
1173}
1174
1175test "preprocessor line with a multiline comment after" {
1176 try testParseAndRemoveLineCommands(
1177 \\#pragma test /*
1178 \\
1179 \\*/
1180 , &[_]ExpectedSourceSpan{
1181 .{ .start_line = 1, .end_line = 1, .filename = "blah.rc" },
1182 .{ .start_line = 2, .end_line = 2, .filename = "blah.rc" },
1183 .{ .start_line = 3, .end_line = 3, .filename = "blah.rc" },
1184 },
1185 \\#pragma test /*
1186 \\
1187 \\*/
1188 , .{ .initial_filename = "blah.rc" });
1189}
1190
1191test "comment after line command" {
1192 var mut_source = "#line 1 \"blah.rc\" /*".*;
1193 try std.testing.expectError(error.InvalidLineCommand, parseAndRemoveLineCommands(std.testing.allocator, &mut_source, &mut_source, .{}));
1194}
1195
1196test "line command with 0 as line number" {
1197 var mut_source = "#line 0 \"blah.rc\"".*;
1198 try std.testing.expectError(error.InvalidLineCommand, parseAndRemoveLineCommands(std.testing.allocator, &mut_source, &mut_source, .{}));
1199}
1200
1201test "line number limits" {
1202 // TODO: Avoid usize for line numbers
1203 if (@sizeOf(usize) != 8) return error.SkipZigTest;
1204
1205 // greater than i64 max
1206 try testParseAndRemoveLineCommands(
1207 \\
1208 , &[_]ExpectedSourceSpan{
1209 .{ .start_line = 11111111111111111111, .end_line = 11111111111111111111, .filename = "blah.rc" },
1210 },
1211 \\#line 11111111111111111111 "blah.rc"
1212 , .{ .initial_filename = "blah.rc" });
1213
1214 // equal to u64 max, overflows on line number increment
1215 {
1216 var mut_source = "#line 18446744073709551615 \"blah.rc\"".*;
1217 try std.testing.expectError(error.LineNumberOverflow, parseAndRemoveLineCommands(std.testing.allocator, &mut_source, &mut_source, .{}));
1218 }
1219
1220 // greater than u64 max
1221 {
1222 var mut_source = "#line 18446744073709551616 \"blah.rc\"".*;
1223 try std.testing.expectError(error.InvalidLineCommand, parseAndRemoveLineCommands(std.testing.allocator, &mut_source, &mut_source, .{}));
1224 }
828}1225}