Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: REPLの機能強化 #850

Merged
merged 6 commits into from
Nov 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,10 @@ $ npm run start
```
$ npm run parse
```

## 4. REPL上で実行
コマンドライン上で対話的にコードを実行します。
以下のコマンドを実行し、コードを入力します。
```
$npm run repl
```
6 changes: 6 additions & 0 deletions etc/aiscript.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ class AiScriptTypeError extends AiScriptError {
pos: Pos;
}

// @public
class AiScriptUnexpectedEOFError extends AiScriptSyntaxError {
constructor(pos: Pos, info?: unknown);
}

// @public
class AiScriptUserError extends AiScriptRuntimeError {
constructor(message: string, info?: unknown);
Expand Down Expand Up @@ -284,6 +289,7 @@ declare namespace errors {
AiScriptError,
NonAiScriptError,
AiScriptSyntaxError,
AiScriptUnexpectedEOFError,
AiScriptTypeError,
AiScriptNamespaceError,
AiScriptRuntimeError,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"scripts": {
"start": "node ./scripts/start.mjs",
"parse": "node ./scripts/parse.mjs",
"repl": "node ./scripts/repl.mjs",
"ts": "npm run ts-esm && npm run ts-dts",
"ts-esm": "tsc --outDir built/esm",
"ts-dts": "tsc --outDir built/dts --declaration true --emitDeclarationOnly true --declarationMap true",
Expand Down
38 changes: 29 additions & 9 deletions console.js → scripts/repl.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as readline from 'readline/promises';
import chalk from 'chalk';
import { Parser, Interpreter, utils } from '@syuilo/aiscript';
import { errors, Parser, Interpreter, utils } from '@syuilo/aiscript';
const { valToString } = utils;

const i = readline.createInterface({
Expand All @@ -12,9 +12,9 @@ console.log(
`Welcome to AiScript!
https://github.com/syuilo/aiscript

Type 'exit' to end this session.`);
Type '.exit' to end this session.`);

const getInterpreter = () => new Interpreter({}, {
const interpreter = new Interpreter({}, {
in(q) {
return i.question(q + ': ');
},
Expand All @@ -36,14 +36,34 @@ const getInterpreter = () => new Interpreter({}, {
}
});

let interpreter;
async function getAst() {
let script = '';
let a = await i.question('>>> ');
while (true) {
try {
if (a === '.exit') return null;
script += a;
let ast = Parser.parse(script);
script = '';
return ast;
} catch(e) {
if (e instanceof errors.AiScriptUnexpectedEOFError) {
script += '\n';
a = await i.question('... ');
} else {
script = '';
throw e;
}
}
}
}

async function main(){
let a = await i.question('> ');
interpreter?.abort();
if (a === 'exit') return false;
try {
let ast = Parser.parse(a);
interpreter = getInterpreter();
let ast = await getAst();
if (ast == null) {
return false;
}
await interpreter.exec(ast);
} catch(e) {
console.log(chalk.red(`${e}`));
Expand Down
11 changes: 11 additions & 0 deletions src/error.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TokenKind } from './parser/token.js';

Check warning on line 1 in src/error.ts

View workflow job for this annotation

GitHub Actions / lint

'TokenKind' is defined but never used. Allowed unused vars must match /^_/u
import type { Pos } from './node.js';

export abstract class AiScriptError extends Error {
Expand All @@ -12,7 +13,7 @@
this.info = info;

// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {

Check warning on line 16 in src/error.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary conditional, value is always truthy
Error.captureStackTrace(this, AiScriptError);
}
}
Expand Down Expand Up @@ -40,6 +41,16 @@
super(`${message} (Line ${pos.line}, Column ${pos.column})`, info);
}
}

/**
* Unexpected EOF errors.
*/
export class AiScriptUnexpectedEOFError extends AiScriptSyntaxError {
constructor(pos: Pos, info?: unknown) {
super('unexpected EOF', pos, info);
}
}

/**
* Type validation(parser/plugins/validate-type) errors.
*/
Expand Down
15 changes: 8 additions & 7 deletions src/parser/scanner.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AiScriptSyntaxError } from '../error.js';
import { AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../error.js';
import { CharStream } from './streams/char-stream.js';
import { TOKEN, TokenKind } from './token.js';
import { unexpectedTokenError } from './utils.js';

import type { ITokenStream } from './streams/token-stream.js';
import type { Token, TokenPosition } from './token.js';
Expand Down Expand Up @@ -96,7 +97,7 @@ export class Scanner implements ITokenStream {
*/
public expect(kind: TokenKind): void {
if (!this.is(kind)) {
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[this.getTokenKind()]}`, this.getPos());
throw unexpectedTokenError(this.getTokenKind(), this.getPos());
}
}

Expand Down Expand Up @@ -456,7 +457,7 @@ export class Scanner implements ITokenStream {
switch (state) {
case 'string': {
if (this.stream.eof) {
throw new AiScriptSyntaxError('unexpected EOF', pos);
throw new AiScriptUnexpectedEOFError(pos);
}
if (this.stream.char === '\\') {
this.stream.next();
Expand All @@ -474,7 +475,7 @@ export class Scanner implements ITokenStream {
}
case 'escape': {
if (this.stream.eof) {
throw new AiScriptSyntaxError('unexpected EOF', pos);
throw new AiScriptUnexpectedEOFError(pos);
}
value += this.stream.char;
this.stream.next();
Expand Down Expand Up @@ -502,7 +503,7 @@ export class Scanner implements ITokenStream {
case 'string': {
// テンプレートの終了が無いままEOFに達した
if (this.stream.eof) {
throw new AiScriptSyntaxError('unexpected EOF', pos);
throw new AiScriptUnexpectedEOFError(pos);
}
// エスケープ
if (this.stream.char === '\\') {
Expand Down Expand Up @@ -538,7 +539,7 @@ export class Scanner implements ITokenStream {
case 'escape': {
// エスケープ対象の文字が無いままEOFに達した
if (this.stream.eof) {
throw new AiScriptSyntaxError('unexpected EOF', pos);
throw new AiScriptUnexpectedEOFError(pos);
}
// 普通の文字として取り込み
buf += this.stream.char;
Expand All @@ -550,7 +551,7 @@ export class Scanner implements ITokenStream {
case 'expr': {
// 埋め込み式の終端記号が無いままEOFに達した
if (this.stream.eof) {
throw new AiScriptSyntaxError('unexpected EOF', pos);
throw new AiScriptUnexpectedEOFError(pos);
}
// skip spasing
if (spaceChars.includes(this.stream.char)) {
Expand Down
4 changes: 2 additions & 2 deletions src/parser/streams/token-stream.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AiScriptSyntaxError } from '../../error.js';
import { TOKEN, TokenKind } from '../token.js';
import { unexpectedTokenError } from '../utils.js';
import type { Token, TokenPosition } from '../token.js';

/**
Expand Down Expand Up @@ -131,7 +131,7 @@ export class TokenStream implements ITokenStream {
*/
public expect(kind: TokenKind): void {
if (!this.is(kind)) {
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[this.getTokenKind()]}`, this.getPos());
throw unexpectedTokenError(this.getTokenKind(), this.getPos());
}
}

Expand Down
11 changes: 10 additions & 1 deletion src/parser/syntaxes/common.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { TokenKind } from '../token.js';
import { AiScriptSyntaxError } from '../../error.js';
import { AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../../error.js';
import { NODE } from '../utils.js';
import { parseStatement } from './statements.js';
import { parseExpr } from './expressions.js';
Expand Down Expand Up @@ -75,6 +75,9 @@ export function parseParams(s: ITokenStream): Ast.Fn['params'] {
case TokenKind.CloseParen: {
break;
}
case TokenKind.EOF: {
throw new AiScriptUnexpectedEOFError(s.getPos());
}
default: {
throw new AiScriptSyntaxError('separator expected', s.getPos());
}
Expand Down Expand Up @@ -116,6 +119,9 @@ export function parseBlock(s: ITokenStream): (Ast.Statement | Ast.Expression)[]
case TokenKind.CloseBrace: {
break;
}
case TokenKind.EOF: {
throw new AiScriptUnexpectedEOFError(s.getPos());
}
default: {
throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.', s.getPos());
}
Expand Down Expand Up @@ -184,6 +190,9 @@ function parseFnType(s: ITokenStream): Ast.TypeSource {
s.next();
break;
}
case TokenKind.EOF: {
throw new AiScriptUnexpectedEOFError(s.getPos());
}
default: {
throw new AiScriptSyntaxError('separator expected', s.getPos());
}
Expand Down
23 changes: 16 additions & 7 deletions src/parser/syntaxes/expressions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AiScriptSyntaxError } from '../../error.js';
import { NODE } from '../utils.js';
import { AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../../error.js';
import { NODE, unexpectedTokenError } from '../utils.js';
import { TokenStream } from '../streams/token-stream.js';
import { TokenKind } from '../token.js';
import { parseBlock, parseOptionalSeparator, parseParams, parseType } from './common.js';
Expand Down Expand Up @@ -92,7 +92,7 @@ function parsePrefix(s: ITokenStream, minBp: number): Ast.Expression {
return NODE('not', { expr }, startPos, endPos);
}
default: {
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`, startPos);
throw unexpectedTokenError(op, startPos);
}
}
}
Expand Down Expand Up @@ -166,7 +166,7 @@ function parseInfix(s: ITokenStream, left: Ast.Expression, minBp: number): Ast.E
return NODE('or', { left, right }, startPos, endPos);
}
default: {
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`, startPos);
throw unexpectedTokenError(op, startPos);
}
}
}
Expand All @@ -192,7 +192,7 @@ function parsePostfix(s: ITokenStream, expr: Ast.Expression): Ast.Expression {
}, startPos, s.getPos());
}
default: {
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[op]}`, startPos);
throw unexpectedTokenError(op, startPos);
}
}
}
Expand Down Expand Up @@ -243,7 +243,7 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Expression {
break;
}
default: {
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[element.kind]}`, element.pos);
throw unexpectedTokenError(element.kind, element.pos);
}
}
}
Expand Down Expand Up @@ -290,7 +290,7 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Expression {
return expr;
}
}
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.getTokenKind()]}`, startPos);
throw unexpectedTokenError(s.getTokenKind(), startPos);
}

/**
Expand Down Expand Up @@ -326,6 +326,9 @@ function parseCall(s: ITokenStream, target: Ast.Expression): Ast.Call {
case TokenKind.CloseParen: {
break;
}
case TokenKind.EOF: {
throw new AiScriptUnexpectedEOFError(s.getPos());
}
default: {
throw new AiScriptSyntaxError('separator expected', s.getPos());
}
Expand Down Expand Up @@ -580,6 +583,9 @@ function parseObject(s: ITokenStream, isStatic: boolean): Ast.Obj {
case TokenKind.CloseBrace: {
break;
}
case TokenKind.EOF: {
throw new AiScriptUnexpectedEOFError(s.getPos());
}
default: {
throw new AiScriptSyntaxError('separator expected', s.getPos());
}
Expand Down Expand Up @@ -624,6 +630,9 @@ function parseArray(s: ITokenStream, isStatic: boolean): Ast.Arr {
case TokenKind.CloseBracket: {
break;
}
case TokenKind.EOF: {
throw new AiScriptUnexpectedEOFError(s.getPos());
}
default: {
throw new AiScriptSyntaxError('separator expected', s.getPos());
}
Expand Down
6 changes: 3 additions & 3 deletions src/parser/syntaxes/statements.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AiScriptSyntaxError } from '../../error.js';
import { CALL_NODE, NODE } from '../utils.js';
import { CALL_NODE, NODE, unexpectedTokenError } from '../utils.js';
import { TokenKind } from '../token.js';
import { parseBlock, parseDest, parseParams, parseType } from './common.js';
import { parseExpr } from './expressions.js';
Expand Down Expand Up @@ -78,7 +78,7 @@ export function parseDefStatement(s: ITokenStream): Ast.Definition {
return parseFnDef(s);
}
default: {
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.getTokenKind()]}`, s.getPos());
throw unexpectedTokenError(s.getTokenKind(), s.getPos());
}
}
}
Expand Down Expand Up @@ -117,7 +117,7 @@ function parseVarDef(s: ITokenStream): Ast.Definition {
break;
}
default: {
throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.getTokenKind()]}`, s.getPos());
throw unexpectedTokenError(s.getTokenKind(), s.getPos());
}
}
s.next();
Expand Down
5 changes: 4 additions & 1 deletion src/parser/syntaxes/toplevel.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NODE } from '../utils.js';
import { TokenKind } from '../token.js';
import { AiScriptSyntaxError } from '../../error.js';
import { AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../../error.js';
import { parseDefStatement, parseStatement } from './statements.js';
import { parseExpr } from './expressions.js';

Expand Down Expand Up @@ -105,6 +105,9 @@ export function parseNamespace(s: ITokenStream): Ast.Namespace {
case TokenKind.CloseBrace: {
break;
}
case TokenKind.EOF: {
throw new AiScriptUnexpectedEOFError(s.getPos());
}
default: {
throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.', s.getPos());
}
Expand Down
11 changes: 11 additions & 0 deletions src/parser/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../error.js';
import { TokenKind } from './token.js';
import type { AiScriptError } from '../error.js';
import type * as Ast from '../node.js';

export function NODE<T extends Ast.Node['type']>(
Expand Down Expand Up @@ -29,3 +32,11 @@ export function CALL_NODE(
args,
}, start, end);
}

export function unexpectedTokenError(token: TokenKind, pos: Ast.Pos, info?: unknown): AiScriptError {
if (token === TokenKind.EOF) {
return new AiScriptUnexpectedEOFError(pos, info);
} else {
return new AiScriptSyntaxError(`unexpected token: ${TokenKind[token]}`, pos, info);
}
}
Loading