summaryrefslogtreecommitdiff
path: root/src/test/java/com/seaofnodes/simple/ParserDiagnosticTest.java
blob: 2b7eea7878497a38f8beb0e854021a7c55e99880 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
package com.seaofnodes.simple;

import com.seaofnodes.simple.codegen.CodeGen;
import com.seaofnodes.simple.parser.Parser;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import static org.junit.Assert.*;

public class ParserDiagnosticTest {
    private static Parser.ParseException rejected(String source) {
        try {
            new CodeGen(source).driver(CodeGen.Phase.Parse);
            throw new AssertionError("Accepted invalid source");
        } catch( Parser.ParseException e ) {
            return e;
        }
    }

    private static void diagnostic(String source, int line, int column, String message) {
        var error=rejected(source);
        assertEquals(message,error.getMessage());
        assertEquals("Test.smp:"+line+":"+column+": error: "+message
            +"\n    "+source.split("\n",-1)[line-1]+"\n    "+"_".repeat(column-1)+"^",error.diagnostic());
    }

    @Test public void testNumericLiteralErrors() {
        diagnostic("return 9223372036854775808;",1,8,"Integer literal is outside the signed 64-bit range");
        diagnostic("return 1e;",1,8,"Invalid floating-point literal '1e'");
        diagnostic("return 1.2.3f;",1,8,"Invalid floating-point literal '1.2.3f'");
        diagnostic("return 012;",1,8,"Syntax error: integer values cannot start with '0'");
    }

    @Test public void testUnclosedCommentsAndStrings() {
        diagnostic("/* never closed",1,1,"Unclosed block comment");
        diagnostic("return 0; /*",1,11,"Unclosed block comment");
        diagnostic("return \"abc\n",1,8,"Unclosed string");
        diagnostic("return \"abc\\",1,8,"Unclosed string");
        // A comment at byte zero used to index -1; a final // was not recognized.
        new CodeGen("/* ok */ return 0; //").driver(CodeGen.Phase.Parse);
    }

    @Test public void testCharacterLiteralsAndEscapes() {
        diagnostic("return '';",1,8,"Empty character literal");
        diagnostic("return 'ab';",1,8,"Character literal must contain exactly one byte");
        diagnostic("return 'a ';",1,8,"Character literal must contain exactly one byte");
        diagnostic("return '",1,8,"Unclosed character literal");
        diagnostic("return 'a\n",1,8,"Unclosed character literal");
        diagnostic("return '\\q';",1,9,"Unknown character escape \\q");
        diagnostic("return \"\\q\";",1,9,"Unknown string escape \\q");
        new CodeGen("return ' ' + '\\'' + '\\\\';").driver(CodeGen.Phase.Parse);
    }

    @Test public void testDeferredLiteralErrors() {
        diagnostic("struct S {\n new S={ ->\n  val x='ab';\n };\n};",3,9,
                   "Character literal must contain exactly one byte");
        diagnostic("struct S {\n new S={ ->\n  val x=\"abc",3,9,"Unclosed string");
        diagnostic("for(int i=0; i<1; i+='ab') {}",1,22,
                   "Character literal must contain exactly one byte");
        diagnostic("struct S {\n new S={ ->\n  val x=\"outer %(\"bad\\q\")\";\n };\n};",3,22,
                   "Unknown string escape \\q");
    }

    @Test public void testIdentifierAndKeywordLocations() {
        diagnostic("int = 1;",1,5,"Expected an identifier, found '='");
        diagnostic("int true=1;",1,5,"Expected an identifier, found 'true'");
        diagnostic("int",1,4,"Expected an identifier, found 'EOF'");
        diagnostic("return if;",1,8,"Syntax error, expected `an identifier or expression` but found `if`");
        diagnostic("\n  break;",2,3,"No active loop for a break or continue");
        diagnostic("\n  continue;",2,3,"No active loop for a break or continue");
    }

    @Test public void testFinalAssignmentLocations() {
        diagnostic("val x=1;\n  x=2;",2,3,"Cannot reassign final 'x'");
        diagnostic("val x=1;\n  x++;",2,3,"Cannot reassign final 'x'");
        diagnostic("val x=1;\n  ++x;",2,5,"Cannot reassign final 'x'");
    }

    @Test public void testParameterAndConstructorLocations() {
        diagnostic("val f={int x, int x -> x;};",1,19,"Duplicate parameter 'x'");
        diagnostic("val f={int ^x -> x;};",1,12,"'^' requires a struct parameter");
        diagnostic("struct S { new Wrong={ -> }; };",1,16,"Constructor name 'Wrong' must match type name 'S'");
        diagnostic("int? x;",1,4,"Type i32 cannot be null");
        diagnostic("return new int;",1,12,"Cannot allocate a i32");
    }

    @Test public void testMemberLocation() {
        diagnostic("struct S { int x; };\nval s=new S;\nreturn s.missing;",3,10,
                   "Accessing unknown field 'missing' from '*Test.S'");
        diagnostic("struct S { int x; };\nval s=new S;\nreturn s.;",3,10,"Expected an identifier, found ';'");
    }

    @Test public void testInitializerAndOperatorLocations() {
        diagnostic("val x = null;",1,9,"Cannot infer a type from null; declare a nullable type explicitly");
        diagnostic("u8[] bytes=\"C\";",1,6,"C data binding requires an integer or float type");
        diagnostic("return 1 < 2 > 3;",1,14,"Mixing relational directions in a chained relational test");
        var shift=rejected("return 1 << 2.0;");
        assertTrue(shift.getMessage(),shift.getMessage().startsWith("Cannot '<<'"));
        assertEquals("Test.smp:1:10: error: "+shift.getMessage()
            +"\n    return 1 << 2.0;\n    _________^",shift.diagnostic());
    }

    @Test public void testCLILexicalErrorsWithoutStackTrace() throws Exception {
        checkCLI("return 9223372036854775808;",
            ":1:8: error: Integer literal is outside the signed 64-bit range"
            +"\n    return 9223372036854775808;\n    _______^\n");
        checkCLI("/* missing close",
            ":1:1: error: Unclosed block comment\n    /* missing close\n    ^\n");
    }

    @Test public void testMissingInitializer() {
        var error=rejected("// declaration\n\nval _validMove() {\n");
        assertEquals("Syntax error, expected `=expression` but found `(`",error.getMessage());
        assertEquals("Test.smp:3:15: error: "+error.getMessage()
            +"\n    val _validMove() {\n    ______________^",error.diagnostic());
    }

    @Test public void testFoundIdentifierPointsAtItsStart() {
        var error=rejected("int x=1\nreturn x;");
        assertEquals("Test.smp:2:1: error: Syntax error, expected `;` but found `return`"
            +"\n    return x;\n    ^",error.diagnostic());
    }

    @Test public void testEOF() {
        var error=rejected("return 1");
        assertEquals("Test.smp:1:9: error: Syntax error, expected `;` but found `EOF`"
            +"\n    return 1\n    ________^",error.diagnostic());
    }

    @Test public void testCRLFTabsAndUTF8() {
        var error=rejected("// π\r\n\tint x=1\r\n\treturn x;");
        assertEquals("Test.smp:3:9: error: Syntax error, expected `;` but found `return`"
            +"\n            return x;\n    ________^",error.diagnostic());
        var unicode=rejected("val x=\"π\" return 1;");
        assertEquals("Test.smp:1:11: error: Syntax error, expected `;` but found `return`"
            +"\n    val x=\"π\" return 1;\n    __________^",unicode.diagnostic());
    }

    @Test public void testDeferredForUpdateLocation() {
        // The update is parsed after the body, so lexer line counters can be stale.
        var error=rejected("for(int i=0; i<3; i+=) {\n    int x=i;\n}\nreturn 0;");
        assertEquals("Test.smp:1:22: error: Syntax error, expected `an identifier or expression` but found `)`"
            +"\n    for(int i=0; i<3; i+=) {\n    _____________________^",error.diagnostic());
    }

    @Test public void testCLIReportsSourceWithoutStackTrace() throws Exception {
        checkCLI("// declaration\nval _validMove() {\n",
            ":2:15: error: Syntax error, expected `=expression` but found `(`"
            +"\n    val _validMove() {\n    ______________^\n");
    }

    @Test public void testUndefinedNameKeepsItsUseSite() {
        var error=rejected("val make={ -> val board=new u8[9]; board; };\n"
            +"val read={ ->\n    return board[0];\n};\nreturn read();");
        assertEquals("Test.smp:3:12: error: Undefined name 'board'"
            +"\n        return board[0];\n    ___________^",error.diagnostic());
    }

    @Test public void testUndefinedAssignmentPointsAtName() {
        var error=rejected("\n    board = 1;");
        assertEquals("Test.smp:2:5: error: Undefined name 'board'"
            +"\n        board = 1;\n    ____^",error.diagnostic());
    }

    @Test public void testCLIUndefinedNameWithoutStackTrace() throws Exception {
        checkCLI("// unresolved\nreturn board[0];\n",
            ":2:8: error: Undefined name 'board'"
            +"\n    return board[0];\n    _______^\n");
    }

    private static void checkCLI(String text, String expected) throws Exception {
        Path dir=Files.createDirectories(Path.of("build/diagnostics/source with spaces"));
        Path source=dir.resolve("Bad.smp").toAbsolutePath();
        Files.writeString(source,text);
        String java=Path.of(System.getProperty("java.home"),"bin","java").toString();
        Process child=new ProcessBuilder(java,"-ea","-cp",System.getProperty("java.class.path"),
            Simple.class.getName(),"--norun","-o","build/diagnostics/objects/",source.toString()).start();
        try {
            assertTrue("Compiler did not exit",child.waitFor(20,TimeUnit.SECONDS));
            assertEquals(1,child.exitValue());
            assertEquals("",new String(child.getInputStream().readAllBytes(),StandardCharsets.UTF_8));
            String diagnostic=new String(child.getErrorStream().readAllBytes(),StandardCharsets.UTF_8)
                .replace("\r\n","\n");
            assertEquals(source+expected,diagnostic);
        } finally {
            child.destroyForcibly();
        }
    }
}