Skip to content

Commit

Permalink
enhance: Add type declaration (aiscript-dev#146)
Browse files Browse the repository at this point in the history
* parsing types

* parsing types

* parsing types

* add types

* fix

* apply node structure for interpreter

* add a plugin for type validation

* fix

* apply node structure for serializer

* implement type declaration

* add test

* lint

* add type scanning of fn content

* clean code

* refactor: type, type source

* replace `argType` -> `type`
  • Loading branch information
marihachi committed Sep 26, 2021
1 parent 43383a6 commit be10b97
Show file tree
Hide file tree
Showing 8 changed files with 287 additions and 13 deletions.
2 changes: 1 addition & 1 deletion src/interpreter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ export class AiScript {
}

case 'fn': {
return FN(node.args, node.children, scope);
return FN(node.args.map(arg => arg.name), node.children, scope);
}

case 'block': {
Expand Down
9 changes: 8 additions & 1 deletion src/node.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { TypeSource } from './type';

export type Loc = {
start: number;
end: number;
Expand All @@ -7,6 +9,7 @@ export type NDef = {
type: 'def'; // 変数宣言
loc?: Loc; // コード位置
name: string; // 変数名
varType?: TypeSource; // 変数の型
expr: Node; // 式
mut: boolean; // ミュータブルか否か
attr: NAttr[]; // 付加された属性
Expand Down Expand Up @@ -146,7 +149,11 @@ export type NArr = {
export type NFn = {
type: 'fn'; // 関数リテラル
loc?: Loc; // コード位置
args: string[]; // 引数名
args: {
name: string; // 引数名
type?: TypeSource; // 引数の型
}[];
ret?: TypeSource; // 戻り値の型
children: Node[]; // 関数の本体処理
};

Expand Down
2 changes: 2 additions & 0 deletions src/parser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Node } from '../node';
import * as parser from './parser.js';

import { validateKeyword } from './plugins/validate-keyword';
import { validateType } from './plugins/validate-type';
import { setAttribute } from './plugins/set-attribute';

export type ParserPlugin = (nodes: Node[]) => Node[];
Expand All @@ -14,6 +15,7 @@ export class Parser {
constructor() {
this.plugins = [
validateKeyword,
validateType,
setAttribute,
];
}
Expand Down
46 changes: 37 additions & 9 deletions src/parser/parser.peggy
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,10 @@ Meta
// define statement

VarDef
= "#" name:NAME _* "=" _* expr:Expr
{ return createNode('def', { name, expr, mut: false, attr: [] }); }
/ "$" name:NAME _* "<-" _* expr:Expr
{ return createNode('def', { name, expr, mut: true, attr: [] }); }
= "#" name:NAME type:(_* ":" _* @Type)? _* "=" _* expr:Expr
{ return createNode('def', { name, varType: type, expr, mut: false, attr: [] }); }
/ "$" name:NAME type:(_* ":" _* @Type)? _* "<-" _* expr:Expr
{ return createNode('def', { name, varType: type, expr, mut: true, attr: [] }); }

// output statement

Expand Down Expand Up @@ -515,27 +515,31 @@ Arr
// function ------------------------------------------------------------------------------
//

Arg
= name:NAME type:(_* ":" _* @Type)?
{ return { name, type }; }

Args
= head:NAME tails:(","? _+ name:NAME { return name; })*
= head:Arg tails:(","? _+ @Arg)*
{ return [head, ...tails]; }

// define function statement

FnDef
= "@" name:NAME "(" _* args:Args? _* ")" _* "{" _* content:Statements? _* "}"
= "@" name:NAME "(" _* args:Args? _* ")" ret:(_* ":" _* @Type)? _* "{" _* content:Statements? _* "}"
{
return createNode('def', {
name: name,
expr: createNode('fn', { args }, content || []),
expr: createNode('fn', { args: args || [], ret: ret }, content || []),
mut: false,
attr: []
});
}

// function expression

Fn = "@(" _* args:Args? _* ")" _* "{" _* content:Statements? _* "}"
{ return createNode('fn', { args }, content || []); }
Fn = "@(" _* args:Args? _* ")" ret:(_* ":" _* @Type)? _* "{" _* content:Statements? _* "}"
{ return createNode('fn', { args: args || [], ret: ret }, content || []); }



Expand Down Expand Up @@ -563,6 +567,30 @@ StaticObj



//
// type ----------------------------------------------------------------------------------
//

Type
= FnType
/ NamedType

FnType
= "@(" _* args:ArgTypes? _* ")" _* "=>" _* result:Type
{ return createNode('fn', { args: args || [], result }); }

ArgTypes
= head:Type tails:(","? _+ @Type)*
{ return [head, ...tails]; }

NamedType
= name:NAME __* "<" __* inner:Type __* ">"
{ return createNode('named', { name, inner }); }
/ name:NAME
{ return createNode('named', { name, inner: null }); }



//
// general -------------------------------------------------------------------------------
//
Expand Down
38 changes: 38 additions & 0 deletions src/parser/plugins/validate-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import * as aiscript from '../..';
import { SemanticError } from '../../error';
import { Node } from '../../node';
import { getTypeBySource } from '../../type';

function validate(node: Node) {
switch (node.type) {
case 'def': {
if (node.varType != null) {
getTypeBySource(node.varType);
}
validate(node.expr);
break;
}
case 'fn': {
for (const arg of node.args) {
if (arg.type != null) {
getTypeBySource(arg.type);
}
}
if (node.ret != null) {
getTypeBySource(node.ret);
}
for (const n of node.children) {
validate(n);
}
break;
}
}
// TODO: ブロックも全部スキャン
}

export function validateType(nodes: Node[]): Node[] {
for (const node of nodes) {
validate(node);
}
return nodes;
}
4 changes: 2 additions & 2 deletions src/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function serializeOne(node: Node | null | undefined): Bin | null {
case 'num': return [types[node.type], node.value];
case 'str': return [types[node.type], node.value];
case 'arr': return [types[node.type], serialize(node.value)];
case 'fn': return [types[node.type], node.args, serialize(node.children)];
case 'fn': return [types[node.type], node.args.map(arg => [arg.name]), serialize(node.children)];
case 'obj': return [types[node.type], Array.from(node.value.entries()).map(x => [x[0], serializeOne(x[1])])];
case 'prop': return [types[node.type], node.obj, node.path];
case 'propCall': return [types[node.type], node.obj, node.path, serialize(node.args)];
Expand Down Expand Up @@ -98,7 +98,7 @@ export function deserializeOne(bin: Bin | null): Node | undefined {
case types.num: return { type, value: bin[1], } as NNum;
case types.str: return { type, value: bin[1], } as NStr;
case types.arr: return { type, value: deserialize(bin[1]), } as NArr;
case types.fn: return { type, args: bin[1], children: deserialize(bin[2]), } as NFn;
case types.fn: return { type, args: bin[1].map(x => ({ name: x[0] })), children: deserialize(bin[2]), } as NFn;
case types.obj: return { type, value: new Map(bin[1].map(x => [x[0], deserializeOne(x[1])])), } as NObj;
case types.prop: return { type, obj: bin[1], path: bin[2], } as NProp;
case types.propCall: return { type, obj: bin[1], path: bin[2], args: deserialize(bin[3]), } as NPropCall;
Expand Down
173 changes: 173 additions & 0 deletions src/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { SemanticError } from './error';
import { Loc } from './node';

// Type source (AST)

export type NamedTypeSource = {
type: 'named'; // 名前付き型
loc?: Loc; // コード位置
name: string; // 型名
inner?: TypeSource; // 内側の型
};

export type FnTypeSource = {
type: 'fn' // 関数の型
loc?: Loc; // コード位置
args: TypeSource[]; // 引数の型
result: TypeSource; // 戻り値の型
};

export type TypeSource = NamedTypeSource | FnTypeSource;

// Type (IR)

export type TSimple<N extends string = string> = {
type: 'simple';
name: N;
}

export function T_SIMPLE<T extends string>(name: T): TSimple<T> {
return {
type: 'simple',
name: name
};
}

export function isAny(x: Type): x is TSimple<'any'> {
return x.type == 'simple' && x.name == 'any';
}

export type TGeneric<N extends string = string> = {
type: 'generic';
name: N;
inners: Type[];
}

export function T_GENERIC<N extends string>(name: N, inners: Type[]): TGeneric<N> {
return {
type: 'generic',
name: name,
inners: inners
};
}

export type TFn = {
type: 'fn';
args: Type[];
result: Type;
};

export function T_FN(args: Type[], result: Type): TFn {
return {
type: 'fn',
args,
result
};
}

export type Type = TSimple | TGeneric | TFn;

// Utility

export function isCompatibleType(a: Type, b: Type): boolean {
if (isAny(a) || isAny(b)) return true;
if (a.type != b.type) return false;

switch (a.type) {
case 'simple': {
b = (b as TSimple); // NOTE: TypeGuardが効かない
if (a.name != b.name) return false;
break;
}
case 'generic': {
b = (b as TGeneric); // NOTE: TypeGuardが効かない
// name
if (a.name != b.name) return false;
// inners
if (a.inners.length != b.inners.length) return false;
for (let i = 0; i < a.inners.length; i++) {
if (!isCompatibleType(a.inners[i], b.inners[i])) return false;
}
break;
}
case 'fn': {
b = (b as TFn);
// fn result
if (!isCompatibleType(a.result, b.result)) return false;
// fn args
if (a.args.length != b.args.length) return false;
for (let i = 0; i < a.args.length; i++) {
if (!isCompatibleType(a.args[i], b.args[i])) return false;
}
break;
}
}

return true;
}

export function getTypeName(type: Type): string {
switch (type.type) {
case 'simple': {
return type.name;
}
case 'generic': {
return `${ type.name }<${ type.inners.map(inner => getTypeName(inner)).join(', ') }>`;
}
case 'fn': {
return `@(${ type.args.map(arg => getTypeName(arg)).join(', ') }) => ${ getTypeName(type.result) }`;
}
}
}

export function getTypeNameBySource(typeSource: TypeSource): string {
switch (typeSource.type) {
case 'named': {
if (typeSource.inner) {
const inner = getTypeNameBySource(typeSource.inner);
return `${ typeSource.name }<${ inner }>`;
} else {
return typeSource.name;
}
}
case 'fn': {
const args = typeSource.args.map(arg => getTypeNameBySource(arg)).join(', ');
const result = getTypeNameBySource(typeSource.result);
return `@(${ args }) => ${ result }`;
}
}
}

export function getTypeBySource(typeSource: TypeSource): Type {
if (typeSource.type == 'named') {
switch (typeSource.name) {
// simple types
case 'null':
case 'bool':
case 'num':
case 'str':
case 'any':
case 'void': {
if (typeSource.inner == null) {
return T_SIMPLE(typeSource.name);
}
break;
}
// alias for Generic types
case 'arr':
case 'obj': {
let innerType: Type;
if (typeSource.inner != null) {
innerType = getTypeBySource(typeSource.inner);
} else {
innerType = T_SIMPLE('any');
}
return T_GENERIC(typeSource.name, [innerType]);
}
}
throw new SemanticError(`Unknown type: '${ getTypeNameBySource(typeSource) }'`);
} else {
const argTypes = typeSource.args.map(arg => getTypeBySource(arg));
return T_FN(argTypes, getTypeBySource(typeSource.result));
}
}
26 changes: 26 additions & 0 deletions test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1229,6 +1229,32 @@ describe('literal', () => {
});
});

describe('type declaration', () => {
it('def', async () => {
const res = await exe(`
#abc: num = 1
$xyz: str <- "abc"
<: [abc xyz]
`);
eq(res, ARR([NUM(1), STR('abc')]));
});

it('fn def', async () => {
const res = await exe(`
@f(x: arr<num>, y: str, z: @(num) => bool): arr<num> {
x[4] <- 0
y <- "abc"
$r: bool <- z(x[1])
x[5] <- if r 5 else 10
x
}
<: f([1, 2, 3], "a", @(n) { n = 1 })
`);
eq(res, ARR([NUM(1), NUM(2), NUM(3), NUM(0), NUM(5)]));
});
});

describe('meta', () => {
it('default meta', async () => {
const res = getMeta(`
Expand Down

0 comments on commit be10b97

Please sign in to comment.