Commit 3bbe6a28e0

Jacob G-W <jacoblevgw@gmail.com>
2022-01-27 21:23:28
stage2: add decltests
1 parent 0b7347f
lib/std/zig/Ast.zig
@@ -2519,7 +2519,7 @@ pub const Node = struct {
         root,
         /// `usingnamespace lhs;`. rhs unused. main_token is `usingnamespace`.
         @"usingnamespace",
-        /// lhs is test name token (must be string literal), if any.
+        /// lhs is test name token (must be string literal or identifier), if any.
         /// rhs is the body node.
         test_decl,
         /// lhs is the index into extra_data.
lib/std/zig/parse.zig
@@ -500,10 +500,16 @@ const Parser = struct {
         }
     }
 
-    /// TestDecl <- KEYWORD_test STRINGLITERALSINGLE? Block
+    /// TestDecl <- KEYWORD_test (STRINGLITERALSINGLE / IDENTIFIER)? Block
     fn expectTestDecl(p: *Parser) !Node.Index {
         const test_token = p.assertToken(.keyword_test);
-        const name_token = p.eatToken(.string_literal);
+        const name_token = switch (p.token_tags[p.nextToken()]) {
+            .string_literal, .identifier => p.tok_i - 1,
+            else => blk: {
+                p.tok_i -= 1;
+                break :blk null;
+            },
+        };
         const block_node = try p.parseBlock();
         if (block_node == 0) return p.fail(.expected_block);
         return p.addNode(.{
lib/std/zig/render.zig
@@ -151,7 +151,8 @@ fn renderMember(gpa: Allocator, ais: *Ais, tree: Ast, decl: Ast.Node.Index, spac
         .test_decl => {
             const test_token = main_tokens[decl];
             try renderToken(ais, tree, test_token, .space);
-            if (token_tags[test_token + 1] == .string_literal) {
+            const test_name_tag = token_tags[test_token + 1];
+            if (test_name_tag == .string_literal or test_name_tag == .identifier) {
                 try renderToken(ais, tree, test_token + 1, .space);
             }
             try renderExpression(gpa, ais, tree, datas[decl].rhs, space);
src/AstGen.zig
@@ -105,8 +105,8 @@ pub fn generate(gpa: Allocator, tree: Ast) Allocator.Error!Zir {
     };
     defer astgen.deinit(gpa);
 
-    // String table indexes 0 and 1 are reserved for special meaning.
-    try astgen.string_bytes.appendSlice(gpa, &[_]u8{ 0, 0 });
+    // String table indexes 0, 1, 2 are reserved for special meaning.
+    try astgen.string_bytes.appendSlice(gpa, &[_]u8{ 0, 0, 0 });
 
     // We expect at least as many ZIR instructions and extra data items
     // as AST nodes.
@@ -3736,13 +3736,78 @@ fn testDecl(
     };
     defer decl_block.unstack();
 
+    const main_tokens = tree.nodes.items(.main_token);
+    const token_tags = tree.tokens.items(.tag);
+    const test_token = main_tokens[node];
+    const test_name_token = test_token + 1;
+    const test_name_token_tag = token_tags[test_name_token];
+    const is_decltest = test_name_token_tag == .identifier;
     const test_name: u32 = blk: {
-        const main_tokens = tree.nodes.items(.main_token);
-        const token_tags = tree.tokens.items(.tag);
-        const test_token = main_tokens[node];
-        const str_lit_token = test_token + 1;
-        if (token_tags[str_lit_token] == .string_literal) {
-            break :blk try astgen.testNameString(str_lit_token);
+        if (test_name_token_tag == .string_literal) {
+            break :blk try astgen.testNameString(test_name_token);
+        } else if (test_name_token_tag == .identifier) {
+            const ident_name_raw = tree.tokenSlice(test_name_token);
+
+            if (mem.eql(u8, ident_name_raw, "_")) return astgen.failTok(test_name_token, "'_' used as an identifier without @\"_\" syntax", .{});
+
+            // if not @"" syntax, just use raw token slice
+            if (ident_name_raw[0] != '@') {
+                if (primitives.get(ident_name_raw)) |_| return astgen.failTok(test_name_token, "cannot test a primitive", .{});
+
+                if (ident_name_raw.len >= 2) integer: {
+                    const first_c = ident_name_raw[0];
+                    if (first_c == 'i' or first_c == 'u') {
+                        _ = switch (first_c == 'i') {
+                            true => .signed,
+                            false => .unsigned,
+                        };
+                        _ = parseBitCount(ident_name_raw[1..]) catch |err| switch (err) {
+                            error.Overflow => return astgen.failTok(
+                                test_name_token,
+                                "primitive integer type '{s}' exceeds maximum bit width of 65535",
+                                .{ident_name_raw},
+                            ),
+                            error.InvalidCharacter => break :integer,
+                        };
+                        return astgen.failTok(test_name_token, "cannot test a primitive", .{});
+                    }
+                }
+            }
+
+            // Local variables, including function parameters.
+            const name_str_index = try astgen.identAsString(test_name_token);
+            var s = scope;
+            var found_already: ?Ast.Node.Index = null; // we have found a decl with the same name already
+            var num_namespaces_out: u32 = 0;
+            var capturing_namespace: ?*Scope.Namespace = null;
+            while (true) switch (s.tag) {
+                .local_val, .local_ptr => unreachable, // a test cannot be in a local scope
+                .gen_zir => s = s.cast(GenZir).?.parent,
+                .defer_normal, .defer_error => s = s.cast(Scope.Defer).?.parent,
+                .namespace => {
+                    const ns = s.cast(Scope.Namespace).?;
+                    if (ns.decls.get(name_str_index)) |i| {
+                        if (found_already) |f| {
+                            return astgen.failTokNotes(test_name_token, "ambiguous reference", .{}, &.{
+                                try astgen.errNoteNode(f, "declared here", .{}),
+                                try astgen.errNoteNode(i, "also declared here", .{}),
+                            });
+                        }
+                        // We found a match but must continue looking for ambiguous references to decls.
+                        found_already = i;
+                    }
+                    num_namespaces_out += 1;
+                    capturing_namespace = ns;
+                    s = ns.parent;
+                },
+                .top => break,
+            };
+            if (found_already == null) {
+                const ident_name = try astgen.identifierTokenString(test_name_token);
+                return astgen.failTok(test_name_token, "use of undeclared identifier '{s}'", .{ident_name});
+            }
+
+            break :blk name_str_index;
         }
         // String table index 1 has a special meaning here of test decl with no name.
         break :blk 1;
@@ -3804,9 +3869,15 @@ fn testDecl(
         const line_delta = decl_block.decl_line - gz.decl_line;
         wip_members.appendToDecl(line_delta);
     }
-    wip_members.appendToDecl(test_name);
+    if (is_decltest)
+        wip_members.appendToDecl(2) // 2 here means that it is a decltest, look at doc comment for name
+    else
+        wip_members.appendToDecl(test_name);
     wip_members.appendToDecl(block_inst);
-    wip_members.appendToDecl(0); // no doc comments on test decls
+    if (is_decltest)
+        wip_members.appendToDecl(test_name) // the doc comment on a decltest represents it's name
+    else
+        wip_members.appendToDecl(0); // no doc comments on test decls
 }
 
 fn structDeclInner(
src/Module.zig
@@ -4170,6 +4170,7 @@ fn scanDecl(iter: *ScanDeclIter, decl_sub_index: usize, flags: u4) SemaError!voi
     const line_off = zir.extra[decl_sub_index + 4];
     const line = iter.parent_decl.relativeToLine(line_off);
     const decl_name_index = zir.extra[decl_sub_index + 5];
+    const decl_doccomment_index = zir.extra[decl_sub_index + 7];
     const decl_index = zir.extra[decl_sub_index + 6];
     const decl_block_inst_data = zir.instructions.items(.data)[decl_index].pl_node;
     const decl_node = iter.parent_decl.relativeToNodeIndex(decl_block_inst_data.src_node);
@@ -4193,6 +4194,11 @@ fn scanDecl(iter: *ScanDeclIter, decl_sub_index: usize, flags: u4) SemaError!voi
             iter.unnamed_test_index += 1;
             break :name try std.fmt.allocPrintZ(gpa, "test_{d}", .{i});
         },
+        2 => name: {
+            is_named_test = true;
+            const test_name = zir.nullTerminatedString(decl_doccomment_index);
+            break :name try std.fmt.allocPrintZ(gpa, "decltest.{s}", .{test_name});
+        },
         else => name: {
             const raw_name = zir.nullTerminatedString(decl_name_index);
             if (raw_name.len == 0) {
src/print_zir.zig
@@ -1443,20 +1443,24 @@ const Writer = struct {
             } else if (decl_name_index == 1) {
                 try stream.writeByteNTimes(' ', self.indent);
                 try stream.writeAll("test");
+            } else if (decl_name_index == 2) {
+                try stream.writeByteNTimes(' ', self.indent);
+                try stream.print("[{d}] decltest {s}", .{ sub_index, self.code.nullTerminatedString(doc_comment_index) });
             } else {
                 const raw_decl_name = self.code.nullTerminatedString(decl_name_index);
                 const decl_name = if (raw_decl_name.len == 0)
                     self.code.nullTerminatedString(decl_name_index + 1)
                 else
                     raw_decl_name;
-                const test_str = if (raw_decl_name.len == 0) "test " else "";
+                const test_str = if (raw_decl_name.len == 0) "test \"" else "";
                 const export_str = if (is_exported) "export " else "";
 
                 try self.writeDocComment(stream, doc_comment_index);
 
                 try stream.writeByteNTimes(' ', self.indent);
-                try stream.print("[{d}] {s}{s}{s}{}", .{
-                    sub_index, pub_str, test_str, export_str, std.zig.fmtId(decl_name),
+                const endquote_if_test: []const u8 = if (raw_decl_name.len == 0) "\"" else "";
+                try stream.print("[{d}] {s}{s}{s}{}{s}", .{
+                    sub_index, pub_str, test_str, export_str, std.zig.fmtId(decl_name), endquote_if_test,
                 });
                 if (align_inst != .none) {
                     try stream.writeAll(" align(");
src/Zir.zig
@@ -2579,10 +2579,11 @@ pub const Inst = struct {
     ///        - 0 means comptime or usingnamespace decl.
     ///          - if name == 0 `is_exported` determines which one: 0=comptime,1=usingnamespace
     ///        - 1 means test decl with no name.
+    ///        - 2 means that the test is a decltest, doc_comment gives the name of the identifier
     ///        - if there is a 0 byte at the position `name` indexes, it indicates
     ///          this is a test decl, and the name starts at `name+1`.
     ///        value: Index,
-    ///        doc_comment: u32, // 0 if no doc comment
+    ///        doc_comment: u32, 0 if no doc comment, if this is a decltest, doc_comment references the decl name in the string table
     ///        align: Ref, // if corresponding bit is set
     ///        link_section_or_address_space: { // if corresponding bit is set.
     ///            link_section: Ref,
test/behavior/decltest.zig
@@ -0,0 +1,7 @@
+pub fn the_add_function(a: u32, b: u32) u32 {
+    return a + b;
+}
+
+test the_add_function {
+    if (the_add_function(1, 2) != 3) unreachable;
+}
test/behavior.zig
@@ -49,6 +49,11 @@ test {
     _ = @import("behavior/type.zig");
     _ = @import("behavior/var_args.zig");
 
+    // tests that don't pass for stage1
+    if (builtin.zig_backend != .stage1) {
+        _ = @import("behavior/decltest.zig");
+    }
+
     if (builtin.zig_backend != .stage2_arm and builtin.zig_backend != .stage2_x86_64) {
         // Tests that pass (partly) for stage1, llvm backend, C backend, wasm backend.
         _ = @import("behavior/bitcast.zig");