diff --git a/lib/std/zig/Ast.zig b/lib/std/zig/Ast.zig index 17da4f5315..a756834c58 100644 --- a/lib/std/zig/Ast.zig +++ b/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. diff --git a/lib/std/zig/parse.zig b/lib/std/zig/parse.zig index a70d0309e3..cf27e6b1c7 100644 --- a/lib/std/zig/parse.zig +++ b/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(.{ diff --git a/lib/std/zig/render.zig b/lib/std/zig/render.zig index f17ee1e097..0f6fcac8b7 100644 --- a/lib/std/zig/render.zig +++ b/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); diff --git a/src/AstGen.zig b/src/AstGen.zig index 9bc10f25e8..0652a6232f 100644 --- a/src/AstGen.zig +++ b/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( diff --git a/src/Module.zig b/src/Module.zig index 3631e41f25..b9e50355fd 100644 --- a/src/Module.zig +++ b/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) { diff --git a/src/Zir.zig b/src/Zir.zig index b7e3e60916..b8ff7ae50f 100644 --- a/src/Zir.zig +++ b/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, diff --git a/src/print_zir.zig b/src/print_zir.zig index 9c79ad1a37..6396f11467 100644 --- a/src/print_zir.zig +++ b/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("); diff --git a/test/behavior.zig b/test/behavior.zig index 6b08465429..db6863a8b0 100644 --- a/test/behavior.zig +++ b/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"); diff --git a/test/behavior/decltest.zig b/test/behavior/decltest.zig new file mode 100644 index 0000000000..f731f80fb2 --- /dev/null +++ b/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; +}