Skip to content

Commit c84be50

Browse files
committed
libexpr: diagnostics framework & error recovery basics
1 parent 816e3cc commit c84be50

File tree

8 files changed

+316
-37
lines changed

8 files changed

+316
-37
lines changed

src/libexpr/diagnostic.hh

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
/// diagnostic.hh - This file declares records that related to nix diagnostic
2+
///
3+
/// Diagnostics are structures with a main message,
4+
/// and optionally some additional information (body).
5+
///
6+
/// For diagnostics with a body,
7+
/// they may need a special overrided function to format the message.
8+
///
9+
#pragma once
10+
11+
#include <string>
12+
#include <vector>
13+
14+
#include "error.hh"
15+
#include "nixexpr.hh"
16+
17+
namespace nix {
18+
19+
/**
20+
* The base class for all diagnostics.
21+
* concret diagnostic types are defined in Diagnostic*.inc
22+
*/
23+
struct Diag
24+
{
25+
/**
26+
* The location of some diagnostic, currently it is at the beginning of tokens
27+
*/
28+
PosIdx loc;
29+
30+
/**
31+
* Unique identifier for internal use.
32+
*/
33+
enum Kind {
34+
#define DIAG_MERGE(SNAME, CNAME, SEVERITY, MESSAGE) DK_##CNAME,
35+
#include "diagnostics/merge.inc"
36+
};
37+
38+
Diag() = default;
39+
Diag(PosIdx loc)
40+
: loc(loc){};
41+
42+
/**
43+
* Each diagnostic contains a severity field,
44+
* should be "Fatal", "Error" or "Warning"
45+
* this will affect the eval process.
46+
*
47+
* "Fatal" -- non-recoverable while parsing.
48+
* "Error" -- recoverable while parsing, but should not eval
49+
* "Warning" -- recoverable while parsing, and we can eval the AST
50+
* "Note" -- some additional information about the error.
51+
*/
52+
enum Severity { DS_Fatal, DS_Error, DS_Warning, DS_Note };
53+
54+
[[nodiscard]] virtual Kind kind() const = 0;
55+
56+
/**
57+
* \brief short name.
58+
* There might be a human readable short name that controls the diagnostic
59+
* For example, one may pass -Wno-dup-formal to suppress duplicated formals.
60+
* A special case for parsing errors, generated from bison
61+
* have the sname "bison"
62+
*/
63+
[[nodiscard]] virtual std::string_view sname() const = 0;
64+
65+
/** Get severity */
66+
[[nodiscard]] virtual Severity severity() const = 0;
67+
68+
/**
69+
* Format printable diagnostic, with string interpolated
70+
* e.g. "invalid integer %1%" -> "invalid integer 'bar'"
71+
*/
72+
[[nodiscard]] virtual std::string_view format() const = 0;
73+
74+
virtual ~Diag() = default;
75+
76+
static Verbosity getVerb(Severity s)
77+
{
78+
switch (s) {
79+
case DS_Error:
80+
case DS_Fatal:
81+
return lvlError;
82+
case DS_Warning:
83+
return lvlWarn;
84+
case DS_Note:
85+
return lvlNotice;
86+
}
87+
}
88+
89+
[[nodiscard]] ErrorInfo getErrorInfo(const PosTable & positions) const
90+
{
91+
return ErrorInfo{.msg = std::string(format()), .errPos = positions[loc]};
92+
}
93+
94+
using Notes = std::vector<std::shared_ptr<Diag>>;
95+
96+
[[nodiscard]] virtual Notes getNotes() const
97+
{
98+
return {};
99+
}
100+
};
101+
102+
struct DiagWithNotes : Diag
103+
{
104+
Diag::Notes notes;
105+
[[nodiscard]] Diag::Notes getNotes() const override
106+
{
107+
return notes;
108+
}
109+
};
110+
111+
#define DIAG_SIMPLE(SNAME, CNAME, SEVERITY, MESSAGE) \
112+
struct Diag##CNAME : Diag \
113+
{ \
114+
std::string_view format() const override \
115+
{ \
116+
return MESSAGE; \
117+
} \
118+
std::string_view sname() const override \
119+
{ \
120+
return SNAME; \
121+
} \
122+
Severity severity() const override \
123+
{ \
124+
return DS_##SEVERITY; \
125+
} \
126+
Kind kind() const override \
127+
{ \
128+
return DK_##CNAME; \
129+
} \
130+
Diag##CNAME() = default; \
131+
Diag##CNAME(PosIdx pos) \
132+
: Diag(pos) \
133+
{ \
134+
} \
135+
};
136+
#include "diagnostics/kinds.inc"
137+
#undef DIAG_SIMPLE
138+
139+
#define DIAG_BODY(SNAME, CNAME, SEVERITY, MESSAGE, BODY) struct Diag##CNAME : Diag BODY;
140+
#include "diagnostics/kinds.inc"
141+
#undef DIAG_BODY
142+
143+
// Implement trivial functions except ::format
144+
#define DIAG_BODY(SNAME, CNAME, SEVERITY, MESSAGE, BODY) \
145+
inline std::string_view Diag##CNAME::sname() const \
146+
{ \
147+
return SNAME; \
148+
} \
149+
inline Diag::Severity Diag##CNAME::severity() const \
150+
{ \
151+
return DS_##SEVERITY; \
152+
} \
153+
inline Diag::Kind Diag##CNAME::kind() const \
154+
{ \
155+
return DK_##CNAME; \
156+
}
157+
#include "diagnostics/kinds.inc"
158+
#undef DIAG_BODY
159+
160+
inline DiagInvalidInteger::DiagInvalidInteger(PosIdx loc, std::string text)
161+
: Diag(loc)
162+
, text(std::move(text))
163+
{
164+
}
165+
166+
inline DiagInvalidFloat::DiagInvalidFloat(PosIdx loc, std::string text)
167+
: Diag(loc)
168+
, text(std::move(text))
169+
{
170+
}
171+
172+
inline std::string_view DiagInvalidInteger::format() const
173+
{
174+
return text;
175+
}
176+
177+
inline std::string_view DiagInvalidFloat::format() const
178+
{
179+
return text;
180+
}
181+
182+
inline std::string_view DiagBisonParse::format() const
183+
{
184+
return err;
185+
}
186+
187+
struct DiagnosticEngine
188+
{
189+
std::vector<std::unique_ptr<Diag>> errors;
190+
std::vector<std::unique_ptr<Diag>> warnings;
191+
192+
void add(std::unique_ptr<Diag> D)
193+
{
194+
switch (D->severity()) {
195+
case Diag::DS_Fatal:
196+
case Diag::DS_Error: {
197+
errors.emplace_back(std::move(D));
198+
break;
199+
}
200+
case Diag::DS_Warning: {
201+
warnings.emplace_back(std::move(D));
202+
break;
203+
}
204+
case Diag::DS_Note: {
205+
// todo: unreachble
206+
assert(0);
207+
}
208+
}
209+
}
210+
211+
void checkRaise(const PosTable & positions) const
212+
{
213+
if (!errors.empty()) {
214+
const Diag * back = errors[0].get();
215+
throw ParseError(back->getErrorInfo(positions));
216+
}
217+
}
218+
};
219+
220+
} // namespace nix

src/libexpr/diagnostics/kinds.inc

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/// diagnostics/kinds.inc, provides declarations for diagnostics.
2+
3+
// provides: DIAG_SIMPLE(SNAME, CNAME, SEVERITY, MESSAGE)
4+
// provides: DIAG_BODY(SNAME, CNAME, SEVERITY, MESSAGE, BODY)
5+
// provides: DIAG_NOTE_SIMPLE(SNAME, CNAME, SEVERITY, MESSAGE)
6+
// provides: DIAG_NOTE_BODY(SNAME, CNAME, SEVERITY, MESSAGE, BODY)
7+
8+
//=============================================================================/
9+
// DIAG_SIMPLE(SName, ClassName, Severity, Message)
10+
// "simple" means they have no additional body
11+
//=============================================================================/
12+
13+
#ifdef DIAG_SIMPLE
14+
DIAG_SIMPLE("path-trailing-slash", PathTrailingSlash, Error, "path has a trailing slash")
15+
DIAG_SIMPLE("dynamic-in-let", DynamicInLet, Error, "dynamic attributes not allowed in let")
16+
DIAG_SIMPLE("dynamic-in-inherit", InheritDynamic, Error, "dynamic attributes not allowed in inherit")
17+
DIAG_SIMPLE("url-literal", URLLiteral, Warning, "url literals are disabled")
18+
DIAG_SIMPLE("hpath-impure", HPath, Error, "hpath cannot be resolved in pure mode")
19+
#endif
20+
21+
#define COMMON_METHOD \
22+
Severity severity() const override; \
23+
Kind kind() const override; \
24+
std::string_view format() const override; \
25+
std::string_view sname() const override;
26+
27+
//=============================================================================/
28+
// DIAG_BODY(SName, ClassName, Severity, Message, Body)
29+
// diagnostics with special body
30+
//=============================================================================/
31+
#ifdef DIAG_BODY
32+
DIAG_BODY("bison", BisonParse, Fatal, "", {
33+
std::string err;
34+
COMMON_METHOD
35+
})
36+
DIAG_BODY("invalid-integer", InvalidInteger, Fatal, "", {
37+
std::string text;
38+
COMMON_METHOD
39+
DiagInvalidInteger(PosIdx loc, std::string text);
40+
})
41+
42+
DIAG_BODY("invalid-float", InvalidFloat, Fatal, "", {
43+
std::string text;
44+
COMMON_METHOD
45+
DiagInvalidFloat(PosIdx loc, std::string text);
46+
})
47+
48+
#undef COMMON_METHOD
49+
#endif // DIAG_BODY

src/libexpr/diagnostics/merge.inc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// diagnostics/merge.inc, merge all declarations
2+
3+
// provides: DIAG_MERGE(SNAME, CNAME, SEVERITY, MESSAGE)
4+
5+
#ifdef DIAG_MERGE
6+
7+
#define DIAG_SIMPLE(SNAME, CNAME, SEVERITY, MESSAGE) DIAG_MERGE(SNAME, CNAME, SEVERITY, MESSAGE)
8+
#define DIAG_BODY(SNAME, CNAME, SEVERITY, MESSAGE, BODY) DIAG_MERGE(SNAME, CNAME, SEVERITY, MESSAGE)
9+
#define DIAG_NOTE_SIMPLE(SNAME, CNAME, SEVERITY, MESSAGE) DIAG_MERGE(SNAME, CNAME, SEVERITY, MESSAGE)
10+
11+
#include "kinds.inc"
12+
13+
#undef DIAG_NOTE_SIMPLE
14+
#undef DIAG_BODY
15+
#undef DIAG_SIMPLE
16+
17+
#endif

src/libexpr/lexer/lexer.l

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -62,20 +62,17 @@ or { return OR_KW; }
6262
try {
6363
yylval->n = boost::lexical_cast<int64_t>(yytext);
6464
} catch (const boost::bad_lexical_cast &) {
65-
throw ParseError({
66-
.msg = hintfmt("invalid integer '%1%'", yytext),
67-
.errPos = data->state.positions[CUR_POS],
68-
});
65+
data->diags.add(std::make_unique<DiagInvalidInteger>(CUR_POS, yytext));
66+
yyterminate();
6967
}
7068
return INT;
7169
}
7270
{FLOAT} { errno = 0;
7371
yylval->nf = strtod(yytext, 0);
74-
if (errno != 0)
75-
throw ParseError({
76-
.msg = hintfmt("invalid float '%1%'", yytext),
77-
.errPos = data->state.positions[CUR_POS],
78-
});
72+
if (errno != 0) {
73+
data->diags.add(std::make_unique<DiagInvalidFloat>(CUR_POS, yytext));
74+
yyterminate();
75+
}
7976
return FLOAT;
8077
}
8178

@@ -201,10 +198,8 @@ or { return OR_KW; }
201198

202199
<INPATH_SLASH>{ANY} |
203200
<INPATH_SLASH><<EOF>> {
204-
throw ParseError({
205-
.msg = hintfmt("path has a trailing slash"),
206-
.errPos = data->state.positions[CUR_POS],
207-
});
201+
data->diags.add(std::make_unique<DiagPathTrailingSlash>(CUR_POS));
202+
yyterminate();
208203
}
209204

210205
{SPATH} { yylval->path = {yytext, (size_t) yyleng}; return SPATH; }

src/libexpr/parser/epilogue.inc

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,10 @@ Expr * EvalState::parse(
3030

3131
yylex_init(&scanner);
3232
yy_scan_buffer(text, length, scanner);
33-
int res = yyparse(scanner, &data);
33+
yyparse(scanner, &data);
3434
yylex_destroy(scanner);
3535

36-
if (res)
37-
throw ParseError(data.error.value());
36+
data.diags.checkRaise(data.state.positions);
3837

3938
data.result->bindVars(*this, staticEnv);
4039

src/libexpr/parser/parser.y

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,10 @@ expr_function
9797
| WITH expr ';' expr_function
9898
{ $$ = new ExprWith(CUR_POS, $2, $4); }
9999
| LET binds IN expr_function
100-
{ if (!$2->dynamicAttrs.empty())
101-
throw ParseError({
102-
.msg = hintfmt("dynamic attributes not allowed in let"),
103-
.errPos = data->state.positions[CUR_POS]
104-
});
100+
{
101+
if (!$2->dynamicAttrs.empty()) {
102+
data->diags.add(std::make_unique<DiagDynamicInLet>(CUR_POS));
103+
}
105104
$$ = new ExprLet($2, $4);
106105
}
107106
| expr_if
@@ -186,12 +185,7 @@ expr_simple
186185
new ExprString(std::move(path))});
187186
}
188187
| URI {
189-
static bool noURLLiterals = experimentalFeatureSettings.isEnabled(Xp::NoUrlLiterals);
190-
if (noURLLiterals)
191-
throw ParseError({
192-
.msg = hintfmt("URL literals are disabled"),
193-
.errPos = data->state.positions[CUR_POS]
194-
});
188+
data->diags.add(std::make_unique<DiagURLLiteral>(CUR_POS));
195189
$$ = new ExprString(std::string($1));
196190
}
197191
| '(' expr ')' { $$ = $2; }
@@ -234,10 +228,7 @@ path_start
234228
}
235229
| HPATH {
236230
if (evalSettings.pureEval) {
237-
throw Error(
238-
"the path '%s' can not be resolved in pure mode",
239-
std::string_view($1.p, $1.l)
240-
);
231+
data->diags.add(std::make_unique<DiagHPath>(CUR_POS));
241232
}
242233
Path path(getHome() + std::string($1.p + 1, $1.l - 1));
243234
$$ = new ExprPath(std::move(path));
@@ -284,10 +275,7 @@ attrs
284275
$$->push_back(AttrName(data->symbols.create(str->s)));
285276
delete str;
286277
} else
287-
throw ParseError({
288-
.msg = hintfmt("dynamic attributes not allowed in inherit"),
289-
.errPos = data->state.positions[makeCurPos(@2, data)]
290-
});
278+
data->diags.add(std::make_unique<DiagInheritDynamic>(CUR_POS));
291279
}
292280
| { $$ = new AttrPath; }
293281
;

0 commit comments

Comments
 (0)