diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index f7b63bf4..ed176ac7 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -190,7 +190,9 @@ declare namespace Ast { Prop, TypeSource, NamedTypeSource, - FnTypeSource + FnTypeSource, + UnionTypeSource, + TypeParam } } export { Ast } @@ -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; @@ -336,6 +339,7 @@ const FN_NATIVE: (fn: VNativeFn["native"]) => VNativeFn; // @public (undocumented) type FnTypeSource = NodeBase & { type: 'fnTypeSource'; + typeParams: TypeParam[]; params: TypeSource[]; result: TypeSource; }; @@ -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 { diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index 2d89264f..acc12d21 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -833,6 +833,7 @@ export class Interpreter { case 'namedTypeSource': case 'fnTypeSource': + case 'unionTypeSource': case 'attr': { throw new Error('invalid node type'); } diff --git a/src/node.ts b/src/node.ts index 19e94e97..2c73d695 100644 --- a/src/node.ts +++ b/src/node.ts @@ -276,6 +276,7 @@ export type If = NodeBase & { export type Fn = NodeBase & { type: 'fn'; // 関数 + typeParams: TypeParam[]; // 型パラメータ params: { dest: Expression; // 引数名 optional: boolean; @@ -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'; // 名前付き型 @@ -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; // パラメータ名 +} diff --git a/src/parser/plugins/validate-keyword.ts b/src/parser/plugins/validate-keyword.ts index 024a1dea..9ccfad78 100644 --- a/src/parser/plugins/validate-keyword.ts +++ b/src/parser/plugins/validate-keyword.ts @@ -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': { @@ -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); } @@ -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; diff --git a/src/parser/plugins/validate-type.ts b/src/parser/plugins/validate-type.ts index 6fc8660f..1d043919 100644 --- a/src/parser/plugins/validate-type.ts +++ b/src/parser/plugins/validate-type.ts @@ -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(); + 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; } diff --git a/src/parser/scanner.ts b/src/parser/scanner.ts index 401f2bd3..5cab470f 100644 --- a/src/parser/scanner.ts +++ b/src/parser/scanner.ts @@ -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 '}': { diff --git a/src/parser/syntaxes/common.ts b/src/parser/syntaxes/common.ts index a9216823..946c112c 100644 --- a/src/parser/syntaxes/common.ts +++ b/src/parser/syntaxes/common.ts @@ -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'; @@ -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] @@ -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 diff --git a/src/parser/syntaxes/expressions.ts b/src/parser/syntaxes/expressions.ts index 8ef6b356..5d6e2641 100644 --- a/src/parser/syntaxes/expressions.ts +++ b/src/parser/syntaxes/expressions.ts @@ -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'; @@ -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 { @@ -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; @@ -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()); } /** diff --git a/src/parser/syntaxes/statements.ts b/src/parser/syntaxes/statements.ts index 5097313f..050c15d5 100644 --- a/src/parser/syntaxes/statements.ts +++ b/src/parser/syntaxes/statements.ts @@ -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'; @@ -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 { @@ -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; @@ -174,6 +182,7 @@ function parseFnDef(s: ITokenStream): Ast.Definition { return NODE('def', { dest, expr: NODE('fn', { + typeParams, params: params, retType: type, children: body, diff --git a/src/parser/syntaxes/types.ts b/src/parser/syntaxes/types.ts new file mode 100644 index 00000000..a29ced1a --- /dev/null +++ b/src/parser/syntaxes/types.ts @@ -0,0 +1,176 @@ +import { AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../../error.js'; +import { TokenKind } from '../token.js'; +import { NODE } from '../utils.js'; +import { parseOptionalSeparator } from './common.js'; + +import type { Ast } from '../../index.js'; +import type { ITokenStream } from '../streams/token-stream.js'; +import type { TypeParam } from '../../node.js'; + +/** + * ```abnf + * Type = FnType / NamedType + * ``` +*/ +export function parseType(s: ITokenStream): Ast.TypeSource { + return parseUnionType(s); +} + +/** + * ```abnf + * TypeParams = "<" TypeParam *(SEP TypeParam) [SEP] ">" + * ``` +*/ +export function parseTypeParams(s: ITokenStream): TypeParam[] { + s.expect(TokenKind.Lt); + s.next(); + + if (s.is(TokenKind.NewLine)) { + s.next(); + } + + const items: TypeParam[] = [parseTypeParam(s)]; + + while (parseOptionalSeparator(s)) { + if (s.is(TokenKind.Gt)) { + break; + } + const item = parseTypeParam(s); + items.push(item); + } + + s.expect(TokenKind.Gt); + s.next(); + + return items; +} + +/** + * ```abnf + * TypeParam = IDENT + * ``` +*/ +function parseTypeParam(s: ITokenStream): TypeParam { + s.expect(TokenKind.Identifier); + const name = s.getTokenValue(); + s.next(); + + return { name }; +} + +/** + * ```abnf + * UnionType = UnionTypeInner *("|" UnionTypeInner) + * ``` +*/ +function parseUnionType(s: ITokenStream): Ast.TypeSource { + const startPos = s.getPos(); + + const first = parseUnionTypeInner(s); + if (!s.is(TokenKind.Or)) { + return first; + } + + const inners = [first]; + do { + s.next(); + inners.push(parseUnionTypeInner(s)); + } while (s.is(TokenKind.Or)); + + return NODE('unionTypeSource', { inners }, startPos, s.getPos()); +} + +/** + * ```abnf + * UnionTypeTerm = FnType / NamedType + * ``` +*/ +function parseUnionTypeInner(s: ITokenStream): Ast.TypeSource { + if (s.is(TokenKind.At)) { + return parseFnType(s); + } else { + return parseNamedType(s); + } +} + +/** + * ```abnf + * FnType = "@" [TypeParams] "(" ParamTypes ")" "=>" Type + * ParamTypes = [Type *(SEP Type)] + * ``` +*/ +function parseFnType(s: ITokenStream): Ast.TypeSource { + const startPos = s.getPos(); + + s.expect(TokenKind.At); + s.next(); + + let typeParams: Ast.TypeParam[]; + if (s.is(TokenKind.Lt)) { + typeParams = parseTypeParams(s); + } else { + typeParams = []; + } + + 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', { typeParams, 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()); +} diff --git a/src/parser/token.ts b/src/parser/token.ts index d4bdaf49..94425cde 100644 --- a/src/parser/token.ts +++ b/src/parser/token.ts @@ -103,6 +103,8 @@ export enum TokenKind { Hat, /** "{" */ OpenBrace, + /** "|" */ + Or, /** "||" */ Or2, /** "}" */ diff --git a/src/parser/visit.ts b/src/parser/visit.ts index efe878bf..a5686997 100644 --- a/src/parser/visit.ts +++ b/src/parser/visit.ts @@ -11,6 +11,9 @@ function visitNodeInner(node: Ast.Node, fn: (node: Ast.Node, ancestors: Ast.Node // nested nodes switch (result.type) { case 'def': { + if (result.varType != null) { + result.varType = visitNodeInner(result.varType, fn, ancestors) as Ast.Definition['varType']; + } result.attr = result.attr.map((attr) => visitNodeInner(attr, fn, ancestors) as Ast.Attribute); result.expr = visitNodeInner(result.expr, fn, ancestors) as Ast.Definition['expr']; break; @@ -79,6 +82,12 @@ function visitNodeInner(node: Ast.Node, fn: (node: Ast.Node, ancestors: Ast.Node if (param.default) { param.default = visitNodeInner(param.default!, fn, ancestors) as Ast.Fn['params'][number]['default']; } + if (param.argType != null) { + param.argType = visitNodeInner(param.argType, fn, ancestors) as Ast.Fn['params'][number]['argType']; + } + } + if (result.retType != null) { + result.retType = visitNodeInner(result.retType, fn, ancestors) as Ast.Fn['retType']; } for (let i = 0; i < result.children.length; i++) { result.children[i] = visitNodeInner(result.children[i]!, fn, ancestors) as Ast.Fn['children'][number]; @@ -198,6 +207,19 @@ function visitNodeInner(node: Ast.Node, fn: (node: Ast.Node, ancestors: Ast.Node )['right']; break; } + + case 'fnTypeSource': { + for (let i = 0; i < result.params.length; i++) { + result.params[i] = visitNodeInner(result.params[i]!, fn, ancestors) as Ast.FnTypeSource['params'][number]; + } + break; + } + case 'unionTypeSource': { + for (let i = 0; i < result.inners.length; i++) { + result.inners[i] = visitNodeInner(result.inners[i]!, fn, ancestors) as Ast.UnionTypeSource['inners'][number]; + } + break; + } } ancestors.pop(); diff --git a/src/type.ts b/src/type.ts index 92f03b4d..4f2d6e61 100644 --- a/src/type.ts +++ b/src/type.ts @@ -47,7 +47,31 @@ export function T_FN(params: Type[], result: Type): TFn { }; } -export type Type = TSimple | TGeneric | TFn; +export type TParam = { + type: 'param'; + name: string; +} + +export function T_PARAM(name: string): TParam { + return { + type: 'param', + name, + }; +} + +export type TUnion = { + type: 'union'; + inners: Type[]; +} + +export function T_UNION(inners: Type[]): TUnion { + return { + type: 'union', + inners, + }; +} + +export type Type = TSimple | TGeneric | TFn | TParam | TUnion; function assertTSimple(t: Type): asserts t is TSimple { if (t.type !== 'simple') { throw new TypeError('assertTSimple failed.'); } } function assertTGeneric(t: Type): asserts t is TGeneric { if (t.type !== 'generic') { throw new TypeError('assertTGeneric failed.'); } } @@ -87,6 +111,14 @@ export function isCompatibleType(a: Type, b: Type): boolean { } break; } + case 'param': { + // TODO + break; + } + case 'union': { + // TODO + break; + } } return true; @@ -103,6 +135,12 @@ export function getTypeName(type: Type): string { case 'fn': { return `@(${type.params.map(param => getTypeName(param)).join(', ')}) { ${getTypeName(type.result)} }`; } + case 'param': { + return type.name; + } + case 'union': { + return type.inners.join(' | '); + } } } @@ -121,17 +159,27 @@ export function getTypeNameBySource(typeSource: Ast.TypeSource): string { const result = getTypeNameBySource(typeSource.result); return `@(${params}) { ${result} }`; } + case 'unionTypeSource': { + return typeSource.inners.map(inner => getTypeBySource(inner)).join(' | '); + } } } -export function getTypeBySource(typeSource: Ast.TypeSource): Type { +export function getTypeBySource(typeSource: Ast.TypeSource, typeParams?: readonly Ast.TypeParam[]): Type { if (typeSource.type === 'namedTypeSource') { + const typeParam = typeParams?.find((param) => param.name === typeSource.name); + if (typeParam != null) { + return T_PARAM(typeParam.name); + } + switch (typeSource.name) { // simple types case 'null': case 'bool': case 'num': case 'str': + case 'error': + case 'never': case 'any': case 'void': { if (typeSource.inner == null) { @@ -144,7 +192,7 @@ export function getTypeBySource(typeSource: Ast.TypeSource): Type { case 'obj': { let innerType: Type; if (typeSource.inner != null) { - innerType = getTypeBySource(typeSource.inner); + innerType = getTypeBySource(typeSource.inner, typeParams); } else { innerType = T_SIMPLE('any'); } @@ -152,8 +200,15 @@ export function getTypeBySource(typeSource: Ast.TypeSource): Type { } } throw new AiScriptSyntaxError(`Unknown type: '${getTypeNameBySource(typeSource)}'`, typeSource.loc.start); + } else if (typeSource.type === 'fnTypeSource') { + let fnTypeParams = typeSource.typeParams; + if (typeParams != null) { + fnTypeParams = fnTypeParams.concat(typeParams); + } + const paramTypes = typeSource.params.map(param => getTypeBySource(param, fnTypeParams)); + return T_FN(paramTypes, getTypeBySource(typeSource.result, fnTypeParams)); } else { - const paramTypes = typeSource.params.map(param => getTypeBySource(param)); - return T_FN(paramTypes, getTypeBySource(typeSource.result)); + const innerTypes = typeSource.inners.map(inner => getTypeBySource(inner, typeParams)); + return T_UNION(innerTypes); } } diff --git a/test/keywords.ts b/test/keywords.ts index ca9fca10..c318b3c5 100644 --- a/test/keywords.ts +++ b/test/keywords.ts @@ -110,6 +110,11 @@ const sampleCodes = Object.entries<(word: string) => string>({ ` ### ${word} 1 `, + + typeParam: word => + ` + @f<${word}>(x): ${word} { x } + `, }); const parser = new Parser(); diff --git a/test/newline.ts b/test/newline.ts index 8d19501f..3425d7b5 100644 --- a/test/newline.ts +++ b/test/newline.ts @@ -95,6 +95,64 @@ describe('empty lines', () => { }); }); + describe('type params', () => { + describe('function', () => { + test.concurrent('empty line before', async () => { + const res = await exe(` + @f< + // comment + T + >(v: T): T { + v + } + <: f(1) + `); + eq(res, NUM(1)); + }); + + test.concurrent('empty line after', async () => { + const res = await exe(` + @f< + T + // comment + >(v: T): T { + v + } + <: f(1) + `); + eq(res, NUM(1)); + }); + }); + + describe('function type', () => { + test.concurrent('empty line before', async () => { + const res = await exe(` + let f: @< + // comment + T + >(T) => T = @(v) { + v + } + <: f(1) + `); + eq(res, NUM(1)); + }); + + test.concurrent('empty line after', async () => { + const res = await exe(` + let f: @< + T + // comment + >(T) => T = @(v) { + v + } + <: f(1) + `); + eq(res, NUM(1)); + }); + }); + }); + describe('function params', () => { test.concurrent('empty line', async () => { const res = await exe(` diff --git a/test/types.ts b/test/types.ts new file mode 100644 index 00000000..d876e00a --- /dev/null +++ b/test/types.ts @@ -0,0 +1,200 @@ +import * as assert from 'assert'; +import { describe, test } from 'vitest'; +import { utils } from '../src'; +import { NUM, STR, NULL, ARR, OBJ, BOOL, TRUE, FALSE, ERROR ,FN_NATIVE } from '../src/interpreter/value'; +import { AiScriptRuntimeError } from '../src/error'; +import { exe, getMeta, eq } from './testutils'; + +describe('function types', () => { + test.concurrent('multiple params', async () => { + const res = await exe(` + let f: @(str, num) => bool = @() { true } + <: f('abc', 123) + `); + eq(res, TRUE); + }); +}); + +describe('generics', () => { + describe('function', () => { + test.concurrent('expr', async () => { + const res = await exe(` + let f = @(v: T): void {} + <: f("a") + `); + eq(res, NULL); + }); + + test.concurrent('consumer', async () => { + const res = await exe(` + @f(v: T): void {} + <: f("a") + `); + eq(res, NULL); + }); + + test.concurrent('identity function', async () => { + const res = await exe(` + @f(v: T): T { v } + <: f(1) + `); + eq(res, NUM(1)); + }); + + test.concurrent('use as inner type', async () => { + const res = await exe(` + @vals(v: obj): arr { + Obj:vals(v) + } + <: vals({ a: 1, b: 2, c: 3 }) + `); + eq(res, ARR([NUM(1), NUM(2), NUM(3)])); + }); + + test.concurrent('use as variable type', async () => { + const res = await exe(` + @f(v: T): void { + let v2: T = v + } + <: f(1) + `); + eq(res, NULL); + }); + + test.concurrent('use as function type', async () => { + const res = await exe(` + @f(v: T): @() => T { + let g: @() => T = @() { v } + g + } + <: f(1)() + `); + eq(res, NUM(1)) + }); + + test.concurrent('curried', async () => { + const res = await exe(` + @concat(a: A): @(B) => str { + @(b: B) { + \`{a}{b}\` + } + } + <: concat("abc")(123) + `); + eq(res, STR('abc123')); + }); + + test.concurrent('new lines', async () => { + const res = await exe(` + @f< + T + U + >(x: T, y: U): arr { + [x, y] + } + <: f("abc", 123) + `); + eq(res, ARR([STR('abc'), NUM(123)])); + }); + + test.concurrent('duplicate', async () => { + await assert.rejects(() => exe(` + @f(v: T) {} + `)); + }); + + test.concurrent('empty', async () => { + await assert.rejects(() => exe(` + @f<>() {} + `)); + }); + }); +}); + +describe('union', () => { + test.concurrent('variable type', async () => { + const res = await exe(` + let a: num | null = null + <: a + `); + eq(res, NULL); + }); + + test.concurrent('more inners', async () => { + const res = await exe(` + let a: str | num | null = null + <: a + `); + eq(res, NULL); + }); + + test.concurrent('inner type', async () => { + const res = await exe(` + let a: arr = ["abc", 123] + <: a + `); + eq(res, ARR([STR('abc'), NUM(123)])); + }); + + test.concurrent('param type', async () => { + const res = await exe(` + @f(x: num | str): str { + \`{x}\` + } + <: f(1) + `); + eq(res, STR('1')); + }); + + test.concurrent('return type', async () => { + const res = await exe(` + @f(): num | str { 1 } + <: f() + `); + eq(res, NUM(1)); + }); + + test.concurrent('type parameter', async () => { + const res = await exe(` + @f(v: T): T | null { null } + <: f(1) + `); + eq(res, NULL); + }); + + test.concurrent('function type', async () => { + const res = await exe(` + let f: @(num | str) => str = @(x) { \`{x}\` } + <: f(1) + `); + eq(res, STR('1')); + }); + + test.concurrent('invalid inner', async () => { + await assert.rejects(() => exe(` + let a: ThisIsAnInvalidTypeName | null = null + `)); + }); +}); + +describe('simple', () => { + test.concurrent('error', async () => { + const res = await exe(` + let a: error = Error:create("Ai") + <: a + `); + eq(res, ERROR('Ai')); + }); + + test.concurrent('never', async () => { + const res = await exe(` + @f() { + let a: never = eval { + return 1 + } + } + <: f() + `); + eq(res, NUM(1)); + }); +}); diff --git a/unreleased/unified-type-annotation.md b/unreleased/unified-type-annotation.md new file mode 100644 index 00000000..480891d4 --- /dev/null +++ b/unreleased/unified-type-annotation.md @@ -0,0 +1,5 @@ +- 以下の型注釈ができるようになりました。 + - 関数宣言および関数型でのジェネリクス + - ユニオン型 + - error型 + - never型