Skip to content

Commit

Permalink
feat: REPLの機能強化 (#850)
Browse files Browse the repository at this point in the history
* REPLをscriptsに追加

* ドキュメントに追記

* Interpreterを保存

* REPLを複数行入力に対応

* REPLの終了コマンドを変更

* 改行時に深さが変わらないように

Co-authored-by: salano_ym <[email protected]>

---------

Co-authored-by: salano_ym <[email protected]>
  • Loading branch information
takejohn and salano-ym authored Nov 17, 2024
1 parent 6118f9e commit ce845b1
Show file tree
Hide file tree
Showing 12 changed files with 108 additions and 30 deletions.
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 @@ -280,6 +285,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 Down Expand Up @@ -40,6 +41,16 @@ export class AiScriptSyntaxError extends AiScriptError {
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 @@ -84,7 +84,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 @@ -158,7 +158,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 @@ -184,7 +184,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 @@ -241,7 +241,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 @@ -288,7 +288,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 @@ -324,6 +324,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 @@ -578,6 +581,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 @@ -622,6 +628,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);
}
}

0 comments on commit ce845b1

Please sign in to comment.