Skip to content

Commit

Permalink
feat: docsと同じ型注釈を行えるようにする (#870)
Browse files Browse the repository at this point in the history
* ファイル分割

* ジェネリクスの型注釈

* 関数の本体で型パラメータを使用できるように

* 関数型内で型パラメータを使用できるように

* ユニオン型の注釈

* インデントをタブに変換

* テスト追加

* 型パラメータ名の重複をエラーに

* 型名にerror,neverを追加

* テスト追加

* コンパイルエラー修正

* 型引数で予約語の使用を禁止

* テストの追加と修正

* lintの対処

* 空行テスト

* CHANGELOG
  • Loading branch information
takejohn authored Dec 21, 2024
1 parent cdb1b17 commit d25808e
Show file tree
Hide file tree
Showing 17 changed files with 629 additions and 104 deletions.
19 changes: 17 additions & 2 deletions etc/aiscript.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,9 @@ declare namespace Ast {
Prop,
TypeSource,
NamedTypeSource,
FnTypeSource
FnTypeSource,
UnionTypeSource,
TypeParam
}
}
export { Ast }
Expand Down Expand Up @@ -320,6 +322,7 @@ const FN: (params: VUserFn["params"], statements: VUserFn["statements"], scope:
// @public (undocumented)
type Fn = NodeBase & {
type: 'fn';
typeParams: TypeParam[];
params: {
dest: Expression;
optional: boolean;
Expand All @@ -336,6 +339,7 @@ const FN_NATIVE: (fn: VNativeFn["native"]) => VNativeFn;
// @public (undocumented)
type FnTypeSource = NodeBase & {
type: 'fnTypeSource';
typeParams: TypeParam[];
params: TypeSource[];
result: TypeSource;
};
Expand Down Expand Up @@ -718,8 +722,19 @@ const TRUE: {
value: boolean;
};

// @public
type TypeParam = {
name: string;
};

// @public (undocumented)
type TypeSource = NamedTypeSource | FnTypeSource;
type TypeSource = NamedTypeSource | FnTypeSource | UnionTypeSource;

// @public (undocumented)
type UnionTypeSource = NodeBase & {
type: 'unionTypeSource';
inners: TypeSource[];
};

declare namespace utils {
export {
Expand Down
1 change: 1 addition & 0 deletions src/interpreter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,7 @@ export class Interpreter {

case 'namedTypeSource':
case 'fnTypeSource':
case 'unionTypeSource':
case 'attr': {
throw new Error('invalid node type');
}
Expand Down
16 changes: 15 additions & 1 deletion src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ export type If = NodeBase & {

export type Fn = NodeBase & {
type: 'fn'; // 関数
typeParams: TypeParam[]; // 型パラメータ
params: {
dest: Expression; // 引数名
optional: boolean;
Expand Down Expand Up @@ -365,7 +366,7 @@ export type Prop = NodeBase & {

// Type source

export type TypeSource = NamedTypeSource | FnTypeSource;
export type TypeSource = NamedTypeSource | FnTypeSource | UnionTypeSource;

export type NamedTypeSource = NodeBase & {
type: 'namedTypeSource'; // 名前付き型
Expand All @@ -375,6 +376,19 @@ export type NamedTypeSource = NodeBase & {

export type FnTypeSource = NodeBase & {
type: 'fnTypeSource'; // 関数の型
typeParams: TypeParam[]; // 型パラメータ
params: TypeSource[]; // 引数の型
result: TypeSource; // 戻り値の型
};

export type UnionTypeSource = NodeBase & {
type: 'unionTypeSource'; // ユニオン型
inners: TypeSource[]; // 含まれる型
};

/**
* 型パラメータ
*/
export type TypeParam = {
name: string; // パラメータ名
}
19 changes: 19 additions & 0 deletions src/parser/plugins/validate-keyword.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ function validateDest(node: Ast.Node): Ast.Node {
});
}

function validateTypeParams(node: Ast.Fn | Ast.FnTypeSource): void {
for (const typeParam of node.typeParams) {
if (reservedWord.includes(typeParam.name)) {
throwReservedWordError(typeParam.name, node.loc);
}
}
}

function validateNode(node: Ast.Node): Ast.Node {
switch (node.type) {
case 'def': {
Expand Down Expand Up @@ -111,6 +119,7 @@ function validateNode(node: Ast.Node): Ast.Node {
break;
}
case 'fn': {
validateTypeParams(node);
for (const param of node.params) {
validateDest(param.dest);
}
Expand All @@ -124,6 +133,16 @@ function validateNode(node: Ast.Node): Ast.Node {
}
break;
}
case 'namedTypeSource': {
if (reservedWord.includes(node.name)) {
throwReservedWordError(node.name, node.loc);
}
break;
}
case 'fnTypeSource': {
validateTypeParams(node);
break;
}
}

return node;
Expand Down
29 changes: 25 additions & 4 deletions src/parser/plugins/validate-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,43 @@ import { getTypeBySource } from '../../type.js';
import { visitNode } from '../visit.js';
import type * as Ast from '../../node.js';

function validateNode(node: Ast.Node): Ast.Node {
function collectTypeParams(node: Ast.Node, ancestors: Ast.Node[]): Ast.TypeParam[] {
const items = [];
if (node.type === 'fn') {
const typeParamNames = new Set<string>();
for (const typeParam of node.typeParams) {
if (typeParamNames.has(typeParam.name)) {
throw new Error(`type parameter name ${typeParam.name} is duplicate`);
}
typeParamNames.add(typeParam.name);
}
items.push(...node.typeParams);
}
for (let i = ancestors.length - 1; i >= 0; i--) {
const ancestor = ancestors[i]!;
if (ancestor.type === 'fn') {
items.push(...ancestor.typeParams);
}
}
return items;
}

function validateNode(node: Ast.Node, ancestors: Ast.Node[]): Ast.Node {
switch (node.type) {
case 'def': {
if (node.varType != null) {
getTypeBySource(node.varType);
getTypeBySource(node.varType, collectTypeParams(node, ancestors));
}
break;
}
case 'fn': {
for (const param of node.params) {
if (param.argType != null) {
getTypeBySource(param.argType);
getTypeBySource(param.argType, collectTypeParams(node, ancestors));
}
}
if (node.retType != null) {
getTypeBySource(node.retType);
getTypeBySource(node.retType, collectTypeParams(node, ancestors));
}
break;
}
Expand Down
2 changes: 1 addition & 1 deletion src/parser/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ export class Scanner implements ITokenStream {
this.stream.next();
return TOKEN(TokenKind.Or2, pos, { hasLeftSpacing });
} else {
throw new AiScriptSyntaxError('invalid character: "|"', pos);
return TOKEN(TokenKind.Or, pos, { hasLeftSpacing });
}
}
case '}': {
Expand Down
87 changes: 1 addition & 86 deletions src/parser/syntaxes/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../../error.js'
import { NODE } from '../utils.js';
import { parseStatement } from './statements.js';
import { parseExpr } from './expressions.js';
import { parseType } from './types.js';

import type { ITokenStream } from '../streams/token-stream.js';
import type * as Ast from '../../node.js';
Expand Down Expand Up @@ -134,16 +135,6 @@ export function parseBlock(s: ITokenStream): (Ast.Statement | Ast.Expression)[]
return steps;
}

//#region Type

export function parseType(s: ITokenStream): Ast.TypeSource {
if (s.is(TokenKind.At)) {
return parseFnType(s);
} else {
return parseNamedType(s);
}
}

/**
* ```abnf
* OptionalSeparator = [SEP]
Expand All @@ -167,79 +158,3 @@ export function parseOptionalSeparator(s: ITokenStream): boolean {
}
}
}

/**
* ```abnf
* FnType = "@" "(" ParamTypes ")" "=>" Type
* ParamTypes = [Type *(SEP Type)]
* ```
*/
function parseFnType(s: ITokenStream): Ast.TypeSource {
const startPos = s.getPos();

s.expect(TokenKind.At);
s.next();
s.expect(TokenKind.OpenParen);
s.next();

const params: Ast.TypeSource[] = [];
while (!s.is(TokenKind.CloseParen)) {
if (params.length > 0) {
switch (s.getTokenKind()) {
case TokenKind.Comma: {
s.next();
break;
}
case TokenKind.EOF: {
throw new AiScriptUnexpectedEOFError(s.getPos());
}
default: {
throw new AiScriptSyntaxError('separator expected', s.getPos());
}
}
}
const type = parseType(s);
params.push(type);
}

s.expect(TokenKind.CloseParen);
s.next();
s.expect(TokenKind.Arrow);
s.next();

const resultType = parseType(s);

return NODE('fnTypeSource', { params, result: resultType }, startPos, s.getPos());
}

/**
* ```abnf
* NamedType = IDENT ["<" Type ">"]
* ```
*/
function parseNamedType(s: ITokenStream): Ast.TypeSource {
const startPos = s.getPos();

let name: string;
if (s.is(TokenKind.Identifier)) {
name = s.getTokenValue();
s.next();
} else {
s.expect(TokenKind.NullKeyword);
s.next();
name = "null";
}

// inner type
let inner: Ast.TypeSource | undefined;
if (s.is(TokenKind.Lt)) {
s.next();
inner = parseType(s);
s.expect(TokenKind.Gt);
s.next();
}

return NODE('namedTypeSource', { name, inner }, startPos, s.getPos());
}

//#endregion Type
14 changes: 11 additions & 3 deletions src/parser/syntaxes/expressions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ 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';
import { parseBlock, parseOptionalSeparator, parseParams } from './common.js';
import { parseBlockOrStatement } from './statements.js';
import { parseType, parseTypeParams } from './types.js';

import type * as Ast from '../../node.js';
import type { ITokenStream } from '../streams/token-stream.js';
Expand Down Expand Up @@ -381,7 +382,7 @@ function parseIf(s: ITokenStream): Ast.If {

/**
* ```abnf
* FnExpr = "@" Params [":" Type] Block
* FnExpr = "@" [TypeParams] Params [":" Type] Block
* ```
*/
function parseFnExpr(s: ITokenStream): Ast.Fn {
Expand All @@ -390,6 +391,13 @@ function parseFnExpr(s: ITokenStream): Ast.Fn {
s.expect(TokenKind.At);
s.next();

let typeParams: Ast.TypeParam[];
if (s.is(TokenKind.Lt)) {
typeParams = parseTypeParams(s);
} else {
typeParams = [];
}

const params = parseParams(s);

let type: Ast.TypeSource | undefined;
Expand All @@ -400,7 +408,7 @@ function parseFnExpr(s: ITokenStream): Ast.Fn {

const body = parseBlock(s);

return NODE('fn', { params: params, retType: type, children: body }, startPos, s.getPos());
return NODE('fn', { typeParams, params, retType: type, children: body }, startPos, s.getPos());
}

/**
Expand Down
13 changes: 11 additions & 2 deletions src/parser/syntaxes/statements.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { AiScriptSyntaxError } from '../../error.js';
import { CALL_NODE, NODE, unexpectedTokenError } from '../utils.js';
import { TokenKind } from '../token.js';
import { parseBlock, parseDest, parseParams, parseType } from './common.js';
import { parseBlock, parseDest, parseParams } from './common.js';
import { parseExpr } from './expressions.js';
import { parseType, parseTypeParams } from './types.js';

import type * as Ast from '../../node.js';
import type { ITokenStream } from '../streams/token-stream.js';
Expand Down Expand Up @@ -144,7 +145,7 @@ function parseVarDef(s: ITokenStream): Ast.Definition {

/**
* ```abnf
* FnDef = "@" IDENT Params [":" Type] Block
* FnDef = "@" IDENT [TypeParams] Params [":" Type] Block
* ```
*/
function parseFnDef(s: ITokenStream): Ast.Definition {
Expand All @@ -159,6 +160,13 @@ function parseFnDef(s: ITokenStream): Ast.Definition {
s.next();
const dest = NODE('identifier', { name }, nameStartPos, s.getPos());

let typeParams: Ast.TypeParam[];
if (s.is(TokenKind.Lt)) {
typeParams = parseTypeParams(s);
} else {
typeParams = [];
}

const params = parseParams(s);

let type: Ast.TypeSource | undefined;
Expand All @@ -174,6 +182,7 @@ function parseFnDef(s: ITokenStream): Ast.Definition {
return NODE('def', {
dest,
expr: NODE('fn', {
typeParams,
params: params,
retType: type,
children: body,
Expand Down
Loading

0 comments on commit d25808e

Please sign in to comment.