Skip to content

Commit 86eb9de

Browse files
authored
文法エラーに位置情報を追加 (#413)
* error location for parser * type error location * fix error message
1 parent 8101167 commit 86eb9de

10 files changed

Lines changed: 51 additions & 45 deletions

File tree

etc/aiscript.api.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,18 @@ class AiScriptRuntimeError extends AiScriptError {
3636

3737
// @public
3838
class AiScriptSyntaxError extends AiScriptError {
39-
constructor(message: string, info?: any);
39+
constructor(message: string, loc: Loc, info?: any);
40+
// (undocumented)
41+
loc: Loc;
4042
// (undocumented)
4143
name: string;
4244
}
4345

4446
// @public
4547
class AiScriptTypeError extends AiScriptError {
46-
constructor(message: string, info?: any);
48+
constructor(message: string, loc: Loc, info?: any);
49+
// (undocumented)
50+
loc: Loc;
4751
// (undocumented)
4852
name: string;
4953
}

src/error.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { Loc } from './node.js';
2+
13
export abstract class AiScriptError extends Error {
24
// name is read by Error.prototype.toString
35
public name = 'AiScript';
@@ -30,17 +32,17 @@ export class NonAiScriptError extends AiScriptError {
3032
*/
3133
export class AiScriptSyntaxError extends AiScriptError {
3234
public name = 'Syntax';
33-
constructor(message: string, info?: any) {
34-
super(message, info);
35+
constructor(message: string, public loc: Loc, info?: any) {
36+
super(`${message} (Line ${loc.line}, Column ${loc.column})`, info);
3537
}
3638
}
3739
/**
3840
* Type validation(parser/plugins/validate-type) errors.
3941
*/
4042
export class AiScriptTypeError extends AiScriptError {
4143
public name = 'Type';
42-
constructor(message: string, info?: any) {
43-
super(message, info);
44+
constructor(message: string, public loc: Loc, info?: any) {
45+
super(`${message} (Line ${loc.line}, Column ${loc.column})`, info);
4446
}
4547
}
4648

src/parser/plugins/validate-keyword.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ const reservedWord = [
4242
// 'out',
4343
];
4444

45-
function throwReservedWordError(name: string): void {
46-
throw new AiScriptSyntaxError(`Reserved word "${name}" cannot be used as variable name.`);
45+
function throwReservedWordError(name: string, loc: Ast.Loc): void {
46+
throw new AiScriptSyntaxError(`Reserved word "${name}" cannot be used as variable name.`, loc);
4747
}
4848

4949
function validateNode(node: Ast.Node): Ast.Node {
@@ -53,20 +53,20 @@ function validateNode(node: Ast.Node): Ast.Node {
5353
case 'ns':
5454
case 'identifier': {
5555
if (reservedWord.includes(node.name)) {
56-
throwReservedWordError(node.name);
56+
throwReservedWordError(node.name, node.loc);
5757
}
5858
break;
5959
}
6060
case 'meta': {
6161
if (node.name != null && reservedWord.includes(node.name)) {
62-
throwReservedWordError(node.name);
62+
throwReservedWordError(node.name, node.loc);
6363
}
6464
break;
6565
}
6666
case 'fn': {
6767
for (const arg of node.args) {
6868
if (reservedWord.includes(arg.name)) {
69-
throwReservedWordError(arg.name);
69+
throwReservedWordError(arg.name, node.loc);
7070
}
7171
}
7272
break;

src/parser/scanner.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export class Scanner implements ITokenStream {
7575
*/
7676
public expect(kind: TokenKind): void {
7777
if (this.kind !== kind) {
78-
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[this.kind]}`);
78+
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[this.kind]}`, this.token.loc);
7979
}
8080
}
8181

@@ -140,7 +140,7 @@ export class Scanner implements ITokenStream {
140140
this.stream.next();
141141
token = TOKEN(TokenKind.OpenSharpBracket, loc, { hasLeftSpacing });
142142
} else {
143-
throw new AiScriptSyntaxError('invalid character: "#"');
143+
throw new AiScriptSyntaxError('invalid character: "#"', loc);
144144
}
145145
break;
146146
}
@@ -327,7 +327,7 @@ export class Scanner implements ITokenStream {
327327
token = wordToken;
328328
break;
329329
}
330-
throw new AiScriptSyntaxError(`invalid character: "${this.stream.char}"`);
330+
throw new AiScriptSyntaxError(`invalid character: "${this.stream.char}"`, loc);
331331
}
332332
break;
333333
}
@@ -432,7 +432,7 @@ export class Scanner implements ITokenStream {
432432
this.stream.next();
433433
}
434434
if (fractional.length === 0) {
435-
throw new AiScriptSyntaxError('digit expected');
435+
throw new AiScriptSyntaxError('digit expected', loc);
436436
}
437437
}
438438
let value;
@@ -456,7 +456,7 @@ export class Scanner implements ITokenStream {
456456
switch (state) {
457457
case 'string': {
458458
if (this.stream.eof) {
459-
throw new AiScriptSyntaxError('unexpected EOF');
459+
throw new AiScriptSyntaxError('unexpected EOF', loc);
460460
}
461461
if (this.stream.char === '\\') {
462462
this.stream.next();
@@ -474,7 +474,7 @@ export class Scanner implements ITokenStream {
474474
}
475475
case 'escape': {
476476
if (this.stream.eof) {
477-
throw new AiScriptSyntaxError('unexpected EOF');
477+
throw new AiScriptSyntaxError('unexpected EOF', loc);
478478
}
479479
value += this.stream.char;
480480
this.stream.next();
@@ -501,7 +501,7 @@ export class Scanner implements ITokenStream {
501501
case 'string': {
502502
// テンプレートの終了が無いままEOFに達した
503503
if (this.stream.eof) {
504-
throw new AiScriptSyntaxError('unexpected EOF');
504+
throw new AiScriptSyntaxError('unexpected EOF', loc);
505505
}
506506
// エスケープ
507507
if (this.stream.char === '\\') {
@@ -537,7 +537,7 @@ export class Scanner implements ITokenStream {
537537
case 'escape': {
538538
// エスケープ対象の文字が無いままEOFに達した
539539
if (this.stream.eof) {
540-
throw new AiScriptSyntaxError('unexpected EOF');
540+
throw new AiScriptSyntaxError('unexpected EOF', loc);
541541
}
542542
// 普通の文字として取り込み
543543
buf += this.stream.char;
@@ -549,7 +549,7 @@ export class Scanner implements ITokenStream {
549549
case 'expr': {
550550
// 埋め込み式の終端記号が無いままEOFに達した
551551
if (this.stream.eof) {
552-
throw new AiScriptSyntaxError('unexpected EOF');
552+
throw new AiScriptSyntaxError('unexpected EOF', loc);
553553
}
554554
// skip spasing
555555
if (spaceChars.includes(this.stream.char)) {

src/parser/streams/token-stream.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export class TokenStream implements ITokenStream {
101101
*/
102102
public expect(kind: TokenKind): void {
103103
if (this.kind !== kind) {
104-
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[this.kind]}`);
104+
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[this.kind]}`, this.token.loc);
105105
}
106106
}
107107

src/parser/syntaxes/common.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export function parseParams(s: ITokenStream): { name: string, argType?: Ast.Node
2222
if (s.kind === TokenKind.Comma) {
2323
s.next();
2424
} else if (!s.token.hasLeftSpacing) {
25-
throw new AiScriptSyntaxError('separator expected');
25+
throw new AiScriptSyntaxError('separator expected', s.token.loc);
2626
}
2727
}
2828

@@ -61,7 +61,7 @@ export function parseBlock(s: ITokenStream): Ast.Node[] {
6161
steps.push(parseStatement(s));
6262

6363
if ((s.kind as TokenKind) !== TokenKind.NewLine && (s.kind as TokenKind) !== TokenKind.CloseBrace) {
64-
throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.');
64+
throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.', s.token.loc);
6565
}
6666
while ((s.kind as TokenKind) === TokenKind.NewLine) {
6767
s.next();
@@ -101,7 +101,7 @@ function parseFnType(s: ITokenStream): Ast.Node {
101101
if (s.kind === TokenKind.Comma) {
102102
s.next();
103103
} else if (!s.token.hasLeftSpacing) {
104-
throw new AiScriptSyntaxError('separator expected');
104+
throw new AiScriptSyntaxError('separator expected', s.token.loc);
105105
}
106106
}
107107
const type = parseType(s);

src/parser/syntaxes/expressions.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ function parsePrefix(s: ITokenStream, minBp: number): Ast.Node {
7070
if (expr.type === 'num') {
7171
return NODE('num', { value: expr.value }, loc);
7272
} else {
73-
throw new AiScriptSyntaxError('currently, sign is only supported for number literal.');
73+
throw new AiScriptSyntaxError('currently, sign is only supported for number literal.', loc);
7474
}
7575
// TODO: 将来的にサポートされる式を拡張
7676
// return NODE('plus', { expr }, loc);
@@ -80,7 +80,7 @@ function parsePrefix(s: ITokenStream, minBp: number): Ast.Node {
8080
if (expr.type === 'num') {
8181
return NODE('num', { value: -1 * expr.value }, loc);
8282
} else {
83-
throw new AiScriptSyntaxError('currently, sign is only supported for number literal.');
83+
throw new AiScriptSyntaxError('currently, sign is only supported for number literal.', loc);
8484
}
8585
// TODO: 将来的にサポートされる式を拡張
8686
// return NODE('minus', { expr }, loc);
@@ -89,7 +89,7 @@ function parsePrefix(s: ITokenStream, minBp: number): Ast.Node {
8989
return NODE('not', { expr }, loc);
9090
}
9191
default: {
92-
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`);
92+
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`, loc);
9393
}
9494
}
9595
}
@@ -161,7 +161,7 @@ function parseInfix(s: ITokenStream, left: Ast.Node, minBp: number): Ast.Node {
161161
return NODE('or', { left, right }, loc);
162162
}
163163
default: {
164-
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`);
164+
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`, loc);
165165
}
166166
}
167167
}
@@ -186,7 +186,7 @@ function parsePostfix(s: ITokenStream, expr: Ast.Node): Ast.Node {
186186
}, loc);
187187
}
188188
default: {
189-
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`);
189+
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`, loc);
190190
}
191191
}
192192
}
@@ -231,13 +231,13 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Node {
231231
const exprStream = new TokenStream(element.children!);
232232
const expr = parseExpr(exprStream, false);
233233
if (exprStream.kind !== TokenKind.EOF) {
234-
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[exprStream.token.kind]}`);
234+
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[exprStream.token.kind]}`, exprStream.token.loc);
235235
}
236236
values.push(expr);
237237
break;
238238
}
239239
default: {
240-
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[element.kind]}`);
240+
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[element.kind]}`, element.loc);
241241
}
242242
}
243243
}
@@ -283,7 +283,7 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Node {
283283
return expr;
284284
}
285285
}
286-
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.kind]}`);
286+
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.kind]}`, loc);
287287
}
288288

289289
/**
@@ -301,7 +301,7 @@ function parseCall(s: ITokenStream, target: Ast.Node): Ast.Node {
301301
if (s.kind === TokenKind.Comma) {
302302
s.next();
303303
} else if (!s.token.hasLeftSpacing) {
304-
throw new AiScriptSyntaxError('separator expected');
304+
throw new AiScriptSyntaxError('separator expected', s.token.loc);
305305
}
306306
}
307307

@@ -451,11 +451,11 @@ function parseReference(s: ITokenStream): Ast.Node {
451451
if (segs.length > 0) {
452452
if (s.kind === TokenKind.Colon) {
453453
if (s.token.hasLeftSpacing) {
454-
throw new AiScriptSyntaxError('Cannot use spaces in a reference.');
454+
throw new AiScriptSyntaxError('Cannot use spaces in a reference.', s.token.loc);
455455
}
456456
s.next();
457457
if (s.token.hasLeftSpacing) {
458-
throw new AiScriptSyntaxError('Cannot use spaces in a reference.');
458+
throw new AiScriptSyntaxError('Cannot use spaces in a reference.', s.token.loc);
459459
}
460460
} else {
461461
break;
@@ -505,7 +505,7 @@ function parseObject(s: ITokenStream, isStatic: boolean): Ast.Node {
505505
// noop
506506
} else {
507507
if (!s.token.hasLeftSpacing) {
508-
throw new AiScriptSyntaxError('separator expected');
508+
throw new AiScriptSyntaxError('separator expected', s.token.loc);
509509
}
510510
}
511511

@@ -546,7 +546,7 @@ function parseArray(s: ITokenStream, isStatic: boolean): Ast.Node {
546546
// noop
547547
} else {
548548
if (!s.token.hasLeftSpacing) {
549-
throw new AiScriptSyntaxError('separator expected');
549+
throw new AiScriptSyntaxError('separator expected', s.token.loc);
550550
}
551551
}
552552

src/parser/syntaxes/statements.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export function parseDefStatement(s: ITokenStream): Ast.Node {
7272
return parseFnDef(s);
7373
}
7474
default: {
75-
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.kind]}`);
75+
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.kind]}`, s.token.loc);
7676
}
7777
}
7878
}
@@ -112,7 +112,7 @@ function parseVarDef(s: ITokenStream): Ast.Node {
112112
break;
113113
}
114114
default: {
115-
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.kind]}`);
115+
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.kind]}`, s.token.loc);
116116
}
117117
}
118118
s.next();
@@ -213,7 +213,7 @@ function parseEach(s: ITokenStream): Ast.Node {
213213
if (s.kind === TokenKind.Comma) {
214214
s.next();
215215
} else if (!s.token.hasLeftSpacing) {
216-
throw new AiScriptSyntaxError('separator expected');
216+
throw new AiScriptSyntaxError('separator expected', s.token.loc);
217217
}
218218

219219
const items = parseExpr(s, false);
@@ -263,7 +263,7 @@ function parseFor(s: ITokenStream): Ast.Node {
263263
if ((s.kind as TokenKind) === TokenKind.Comma) {
264264
s.next();
265265
} else if (!s.token.hasLeftSpacing) {
266-
throw new AiScriptSyntaxError('separator expected');
266+
throw new AiScriptSyntaxError('separator expected', s.token.loc);
267267
}
268268

269269
const to = parseExpr(s, false);
@@ -326,7 +326,7 @@ function parseStatementWithAttr(s: ITokenStream): Ast.Node {
326326
const statement = parseStatement(s);
327327

328328
if (statement.type !== 'def') {
329-
throw new AiScriptSyntaxError('invalid attribute.');
329+
throw new AiScriptSyntaxError('invalid attribute.', statement.loc);
330330
}
331331
if (statement.attr != null) {
332332
statement.attr.push(...attrs);

src/parser/syntaxes/toplevel.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function parseTopLevel(s: ITokenStream): Ast.Node[] {
3636
}
3737

3838
if ((s.kind as TokenKind) !== TokenKind.NewLine && (s.kind as TokenKind) !== TokenKind.EOF) {
39-
throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.');
39+
throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.', s.token.loc);
4040
}
4141
while ((s.kind as TokenKind) === TokenKind.NewLine) {
4242
s.next();
@@ -82,7 +82,7 @@ export function parseNamespace(s: ITokenStream): Ast.Node {
8282
}
8383

8484
if ((s.kind as TokenKind) !== TokenKind.NewLine && (s.kind as TokenKind) !== TokenKind.CloseBrace) {
85-
throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.');
85+
throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.', s.token.loc);
8686
}
8787
while ((s.kind as TokenKind) === TokenKind.NewLine) {
8888
s.next();

src/type.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ export function getTypeBySource(typeSource: Ast.TypeSource): Type {
151151
return T_GENERIC(typeSource.name, [innerType]);
152152
}
153153
}
154-
throw new AiScriptSyntaxError(`Unknown type: '${getTypeNameBySource(typeSource)}'`);
154+
throw new AiScriptSyntaxError(`Unknown type: '${getTypeNameBySource(typeSource)}'`, typeSource.loc);
155155
} else {
156156
const argTypes = typeSource.args.map(arg => getTypeBySource(arg));
157157
return T_FN(argTypes, getTypeBySource(typeSource.result));

0 commit comments

Comments
 (0)