lib/semantic.js (2,875 lines of code) (raw):
'use strict';
const path = require('path');
const fs = require('fs');
const assert = require('assert');
const stripComments = require('strip-json-comments');
const { Tag } = require('./tag');
const Lexer = require('./lexer');
const Parser = require('./parser');
const Env = require('./env');
const {
isBasicType,
isNumber,
isCompare,
isInteger,
isTmpVariable,
getDarafile
} = require('./util');
const builtin = require('./builtin');
const existsChecker = new Map();
function replace(origin, target) {
Object.keys(origin).forEach((key) => {
delete origin[key];
});
Object.keys(target).forEach((key) => {
origin[key] = target[key];
});
}
function display(item) {
if (item.type === 'basic') {
return item.name;
}
if (item.type === 'map') {
return `map[${display(item.keyType)}]${display(item.valueType)}`;
}
if (item.type === 'entry') {
return `entry[${display(item.valueType)}]`;
}
if (item.type === 'array') {
return `[${display(item.itemType)}]`;
}
if (item.type === 'model') {
if (item.moduleName) {
return `${item.moduleName}#${item.name}`;
}
return item.name;
}
if (item.type === 'class') {
if (item.moduleName) {
return `[${item.moduleName}#${item.name}]`;
}
return `[${item.name}]`;
}
if (item.type === 'module_instance') {
return item.name;
}
if (item.type === 'enum') {
return item.name;
}
if (item.type === 'typedef') {
return item.name;
}
if (item.type === 'asyncIterator' || item.type === 'iterator') {
return `${item.type}[${display(item.valueType)}]`;
}
console.log(item);
throw new Error('unimplemented.');
}
function _basic(name) {
return {
type: 'basic',
name: name
};
}
function _model(name, moduleName, extendOn) {
return {
type: 'model',
name: name,
moduleName,
extendOn
};
}
function _enum(name, moduleName) {
return {
type: 'enum',
name: name,
moduleName
};
}
function _typedef(name, moduleName) {
return {
type: 'typedef',
name: name,
moduleName
};
}
function _type(id) {
if (id.tag === Tag.TYPE) {
return _basic(id.lexeme);
}
if (id.tag === Tag.ID) {
return _model(id.lexeme);
}
console.log(id);
throw new Error(`unsupported`);
}
function isSameNumber(expect, actual){
if (isNumber(expect.name) && isNumber(actual.name)) {
if (expect.name === 'number') {
return true;
}
if (isInteger(expect.name) && actual.name === 'integer') {
return true;
}
if ((expect.name === 'long' || expect.name === 'ulong') && isInteger(actual.name)) {
return true;
}
if (expect.name === 'ulong' && actual.name === 'long') {
return true;
}
}
return false;
}
function isSameType(expect, actual) {
if(actual.type === 'basic' && actual.name === '$type') {
return true;
}
if (expect.type === 'basic' && actual.type === 'basic') {
return expect.name === actual.name;
}
if (expect.type === 'model' && actual.type === 'model') {
return expect.name === actual.name && expect.moduleName === actual.moduleName;
}
if (expect.type === 'array' && actual.type === 'array') {
return isSameType(expect.itemType, actual.itemType);
}
if (expect.type === 'map' && actual.type === 'map') {
return isSameType(expect.keyType, actual.keyType) &&
isSameType(expect.valueType, actual.valueType);
}
if (expect.type === 'iterator' && actual.type === 'iterator') {
return isSameType(expect.valueType, actual.valueType);
}
if (expect.type === 'asyncIterator' && actual.type === 'asyncIterator') {
return isSameType(expect.valueType, actual.valueType);
}
if (expect.type === 'module_instance' && actual.type === 'module_instance') {
return expect.name === actual.name;
}
if (expect.type === 'enum' && actual.type === 'enum') {
return expect.name === actual.name;
}
if (expect.type === 'typedef' && actual.type === 'typedef') {
return expect.moduleName === actual.moduleName && expect.name === actual.name;
}
return false;
}
function isExtendOn(expect, actual){
let isExtendOn = false;
if(!actual.extendOn) {
return isExtendOn;
}
actual.extendOn.forEach(base => {
if(isExtendOn) {
return;
}
isExtendOn = isSameType(expect, base);
});
return isExtendOn;
}
function isAssignable(expected, actual, expr) {
if (isSameType(expected, actual)) {
return true;
}
if (isSameNumber(expected, actual)) {
return true;
}
if(expected.type === 'model' && actual.type === 'model') {
// $Model vs model
if(expected.name === '$Model') {
return true;
}
// $Error vs exception
if(expected.name === '$Error' && actual.isException) {
return true;
}
if (isExtendOn(expected, actual)) {
return true;
}
}
// actual is null
if (actual.type === 'basic' && actual.name === 'null') {
return true;
}
if (expected.type === 'map' && actual.type === 'map') {
if (expr && expr.type === 'object' && expr.fields.length === 0) {
return true;
}
if (isAssignable(expected.valueType, actual.valueType)) {
return true;
}
}
if (expected.type === 'entry' && actual.type === 'entry') {
if (expr && expr.type === 'object' && expr.fields.length === 0) {
return true;
}
if (isAssignable(expected.valueType, actual.valueType)) {
return true;
}
}
if (expected.type === 'array' && actual.type === 'array') {
if (expr && expr.type === 'array' && expr.items.length === 0) {
return true;
}
if (isAssignable(expected.itemType, actual.itemType)) {
return true;
}
}
if (expected.type === 'basic' && expected.name === 'readable') {
// readable = string should ok
if (actual.type === 'basic' && actual.name === 'string') {
return true;
}
// readable = bytes should ok
if (actual.type === 'basic' && actual.name === 'bytes') {
return true;
}
}
if (expected.type === 'basic' && expected.name === 'any') {
// any = other type
return true;
}
if (expected.type === 'basic' && expected.name === 'object') {
// object = other type
return true;
}
if (expected.type === 'module_instance' && actual.type === 'module_instance') {
if (actual.parentModuleIds.includes(expected.name)) {
// basicModule = new derivedModule
return true;
}
}
return false;
}
function eql(expects, actuals) {
if (expects.length !== actuals.length) {
return false;
}
for (var i = 0; i < expects.length; i++) {
const expect = expects[i];
const actual = actuals[i];
if (isSameType(expect, actual)) {
continue;
}
if (isSameNumber(expect, actual)) {
continue;
}
// actual is null
if (actual.type === 'basic' && actual.name === 'null') {
continue;
}
if(expect.type === 'model' && actual.type === 'model') {
// $Model vs model
if(expect.name === '$Model') {
continue;
}
// $Error vs exception
if(expect.name === '$Error' && actual.isException) {
continue;
}
if (isExtendOn(expect, actual)) {
continue;
}
}
if (expect.type === 'map' && expect.keyType.name === 'string') {
// expect: object
// actual: model
if (actual.type === 'model') {
continue;
}
}
if (expect.type === 'basic' && actual.type === 'basic') {
if (expect.name === 'integer' && actual.name === 'number') {
continue;
}
if (expect.name === 'long' && actual.name === 'number') {
continue;
}
}
if (expect.type === 'basic' && expect.name === 'any') {
// expect: any
continue;
}
// Model vs object
if (expect.type === 'model' && actual.type === 'map') {
continue;
}
// Model vs any
if (expect.type === 'model' && actual.type === 'basic' && actual.name === 'any') {
continue;
}
if (expect.type !== actual.type) {
return false;
}
const type = expect.type;
if (type === 'map') {
if (expect.keyType.name === actual.keyType.name) {
if (expect.valueType.name === 'any') {
// map[string]any vs map[string]string
continue;
}
if (isAssignable(expect.valueType, actual.valueType)) {
continue;
}
}
}
if (type === 'entry') {
if (expect.valueType.name === 'any') {
// entry[any] vs entry[string]
continue;
}
if (isAssignable(expect.valueType, actual.valueType)) {
continue;
}
}
if (type === 'array') {
if (expect.itemType.name === 'any') {
// [any] vs [string]
continue;
}
if (isAssignable(expect.itemType, actual.itemType)) {
continue;
}
}
return false;
}
return true;
}
// map to model
function isNeedToModel(expect, actual) {
if (isSameType(expect, actual)) {
return false;
}
if (actual.type === 'basic' && actual.name === 'null') {
return false;
}
if (expect.type !== 'model') {
return false;
}
return true;
}
// model to map
function isNeedToMap(expect, actual) {
if (isSameType(expect, actual)) {
return false;
}
if (expect.type === 'basic' && expect.name === 'any') {
return false;
}
if (actual.type === 'basic' && actual.name === 'null') {
// model vs null
if (expect.type === 'model') {
return false;
}
}
if (actual.type !== 'model') {
// only model can't be cast
return false;
}
if (expect.type === 'model' && expect.name === '$Model' && actual.type === 'model') {
return false;
}
return true;
}
class TypeChecker {
constructor(source, filename, root, libraries, mainPkg = undefined) {
this.source = source;
this.filename = filename;
this.mainPkg = mainPkg;
// 方法: apis、functions
this.methods = new Map();
// 属性
this.typedefs = new Map();
// 属性
this.properties = new Map();
// 模型
this.models = new Map();
// 枚举
this.enums = new Map();
// 依赖
this.dependencies = new Map();
// 内部依赖
this.innerDep = new Map();
// model依赖关系
this.modelDep = new Map();
//执行编译的root
if (!root) {
this.root = path.dirname(this.filename);
} else {
this.root = root;
}
//libraries依赖安装路径表
if (!libraries) {
const lockFilePath = path.join(this.root, '.libraries.json');
if (fs.existsSync(lockFilePath)) {
this.libraries = JSON.parse(fs.readFileSync(lockFilePath, 'utf-8'));
} else {
this.libraries = {};
}
} else {
this.libraries = libraries;
}
existsChecker.set(filename, this);
}
error(message, token) {
if (token) {
const loc = token.loc;
console.error(`${this.filename}:${loc.start.line}:${loc.start.column}`);
console.error(`${this.source.split('\n')[loc.start.line - 1]}`);
console.error(`${' '.repeat(loc.start.column - 1)}^`);
}
throw new SyntaxError(message);
}
findProperty(model, propName, isException = false, moduleName, extendFrom = []) {
let find = model.modelBody.nodes.find((item) => {
return item.fieldName.lexeme === propName;
});
if(find) {
return {
modelField: find,
modelName: model.modelName.lexeme,
extendFrom: extendFrom,
};
}
if(!model.extendOn && !isException) {
return;
}
let extendModel;
let checker = this;
if(!model.extendOn && isException) {
extendModel = builtin.get('$Error');
isException = false;
} else if (model.extendOn.type === 'moduleModel') {
const [ main, ...path ] = model.extendOn.path;
moduleName = main.lexeme;
checker = this.dependencies.get(moduleName);
const typeName = path.map((item) => {
return item.lexeme;
}).join('.');
extendModel = checker.models.get(typeName);
} else if (model.extendOn.type === 'subModel') {
let modelName = model.extendOn.path.map((tag) => {
return tag.lexeme;
}).join('.');
extendModel = this.models.get(modelName);
} else if (model.extendOn.idType === 'builtin_model') {
extendModel = builtin.get(model.extendOn.lexeme);
} else {
extendModel = this.models.get(model.extendOn.lexeme);
}
// console.log(model)
extendFrom.push({
moduleName,
modelName: extendModel.modelName.lexeme
});
find = checker.findProperty(extendModel, propName, isException, moduleName, extendFrom);
if(!find) {
return;
}
return {
moduleName,
...find
};
}
checkModels(ast) {
const models = ast.moduleBody.nodes.filter((item) => {
return item.type === 'model' || item.type === 'exception';
});
models.forEach((node) => {
const name = node.type === 'model' ? node.modelName : node.exceptionName;
const key = name.lexeme;
// 重复定义检查
if (this.models.has(key)) {
this.error(`redefined model or exception "${key}"`, name);
}
this.models.set(key, node);
});
// 允许先使用,后定义,所以先全部设置到 types 中,再进行检查
models.forEach((node) => {
const name = node.type === 'model' ? node.modelName : node.exceptionName;
if(node.extendOn) {
this.checkType(node.extendOn);
this.modelDep.set(name.lexeme, node.extendOn);
}
if(node.type === 'model') {
this.visitModel(node);
} else if(node.type === 'exception') {
this.visitException(node);
} else {
this.error('unimplement');
}
});
}
checkEnum(ast) {
const enums = ast.moduleBody.nodes.filter((item) => {
return item.type === 'enum';
});
enums.forEach((node) => {
const key = node.enumName.lexeme;
// 重复定义检查
if (this.enums.has(key)) {
this.error(`redefined enum "${key}"`, node.enumName);
}
this.enums.set(key, node);
});
enums.forEach((node) => {
this.visitEnum(node);
});
}
checkTypes(ast) {
this.vidCounter = new Map();
ast.moduleBody.nodes.filter((item) => {
return item.type === 'type';
}).forEach((node) => {
this.checkType(node.value);
const key = node.vid.lexeme;
if (this.properties.has(key)) {
// 重复定义检查
this.error(`redefined type "${key}"`, node.vid);
}
this.properties.set(key, node);
this.vidCounter.set(key, 0);
});
}
checkAPIs(ast) {
ast.moduleBody.nodes.forEach((item) => {
if (item.type === 'api') {
this.visitAPI(item);
}
});
}
checkFunctions(ast) {
ast.moduleBody.nodes.forEach((item) => {
if (item.type === 'function') {
this.visitFun(item);
}
});
}
checkInit(ast) {
const inits = ast.moduleBody.nodes.filter((item) => {
return item.type === 'init';
});
if (inits.length > 1) {
this.error('Only one init can be allowed.', inits[1]);
}
}
postCheckInit(ast) {
const api = ast.moduleBody.nodes.find((item) => {
return item.type === 'api';
});
const func = ast.moduleBody.nodes.find((item) => {
// non-static function
return item.type === 'function' && item.isStatic === false;
});
const init = ast.moduleBody.nodes.find((item) => {
return item.type === 'init';
});
if (api || func) {
if (!init && !this.parentModuleId) {
this.error('Must have a init when there is a api or non-static function');
}
}
if (init) {
const paramMap = this.visitParams(init.params, {
needValidateParams: true
});
if (init.initBody) {
const local = new Env();
// merge the parameters into local env
for (const [key, value] of paramMap) {
local.set(key, value);
}
const env = {
isStatic: false,
isAsync: false,
isInitMethod: true,
local: local
};
this.visitStmts(init.initBody, env);
}
}
this.init = init;
}
checkExtends(ast) {
if (!ast.extends) {
return;
}
const aliasId = ast.extends.lexeme;
if (!this.dependencies.has(aliasId)) {
this.error(`the extends "${aliasId}" wasn't imported`, ast.extends);
}
this.parentModuleId = aliasId;
}
checkImports(ast) {
if (ast.imports.length === 0) {
return;
}
const filePath = this.filename;
const pkgDir = path.dirname(filePath);
const pkgPath = this.mainPkg ? getDarafile(this.mainPkg) : getDarafile(pkgDir);
if (!fs.existsSync(pkgPath)) {
this.error(`the Darafile not exists`);
}
const pkg = JSON.parse(stripComments(fs.readFileSync(pkgPath, 'utf-8')));
pkg.libraries = pkg.libraries || {};
for (let i = 0; i < ast.imports.length; i++) {
const item = ast.imports[i];
const aliasId = item.lexeme;
let mainPkg = !!item.innerPath && path.dirname(pkgPath);
const mainModule = item.mainModule;
if (!mainModule && !mainPkg && !pkg.libraries[aliasId] && pkg.name !== aliasId) {
this.error(`the import "${aliasId}" not defined in Darafile`, item);
}
if(mainModule && !pkg.libraries[mainModule] && pkg.name !== mainModule) {
this.error(`the import "${mainModule}" not defined in Darafile`, item);
}
if (this.dependencies.has(aliasId)) {
this.error(`the module id "${aliasId}" has been imported`, item);
}
const specPath = pkg.libraries[aliasId] || pkg.main;
let realSpecPath;
if(mainModule) {
const key = pkg.libraries[mainModule];
const module = item.module;
if (!this.libraries[key]) {
this.error(`the module id "${aliasId}" has not installed, use \`dara install\` first`, item);
}
const libPath = path.join(this.root, this.libraries[key]);
const libMetaPath = getDarafile(libPath);
const libMeta = JSON.parse(stripComments(fs.readFileSync(libMetaPath, 'utf-8')));
if(!libMeta.exports || !libMeta.exports[module]) {
this.error(`the submodule id "${module}" has not been exported in "${mainModule}"`, item);
}
realSpecPath = path.join(libPath, libMeta.exports[module]);
mainPkg = libPath;
} else if(mainPkg) {
if (item.innerPath.endsWith('.dara') || item.innerPath.endsWith('.tea') || item.innerPath.endsWith('.spec')) {
realSpecPath = path.join(pkgDir, item.innerPath);
} else if(fs.existsSync(path.join(pkgDir, `${item.innerPath}.dara`))){
realSpecPath = path.join(pkgDir, `${item.innerPath}.dara`);
} else if(fs.existsSync(path.join(pkgDir, `${item.innerPath}.tea`))){
realSpecPath = path.join(pkgDir, `${item.innerPath}.tea`);
} else if(fs.existsSync(path.join(pkgDir, `${item.innerPath}.spec`))){
realSpecPath = path.join(pkgDir, `${item.innerPath}.spec`);
}
} else if (specPath.startsWith('./') || specPath.startsWith('../')) {
if (specPath.endsWith('.dara') || specPath.endsWith('.tea') || specPath.endsWith('.spec')) {
realSpecPath = path.join(pkgDir, specPath);
} else {
const libMetaPath = getDarafile(path.join(pkgDir, specPath));
const libMeta = JSON.parse(stripComments(fs.readFileSync(libMetaPath, 'utf-8')));
realSpecPath = path.join(pkgDir, specPath, libMeta.main);
}
} else {
const key = `${specPath}`;
if (!this.libraries[key]) {
this.error(`the module id "${aliasId}" has not installed, use \`dara install\` first`, item);
}
const libPath = path.join(this.root, this.libraries[key]);
const libMetaPath = getDarafile(libPath);
const libMeta = JSON.parse(stripComments(fs.readFileSync(libMetaPath, 'utf-8')));
realSpecPath = path.join(libPath, libMeta.main);
}
const source = fs.readFileSync(realSpecPath, 'utf-8');
const lexer = new Lexer(source, realSpecPath);
const parser = new Parser(lexer);
const depAst = parser.program();
const checker = existsChecker.get(realSpecPath) || new TypeChecker(source, realSpecPath, this.root, this.libraries, mainPkg).check(depAst);
this.dependencies.set(aliasId, checker);
if(mainPkg) {
this.innerDep.set(aliasId, checker.ast);
}
this.usedExternModel.set(aliasId, new Set());
}
}
checkExports(){
const filePath = this.filename;
const pkgDir = path.dirname(filePath);
const pkgPath = this.mainPkg ? getDarafile(this.mainPkg) : getDarafile(pkgDir);
if (!fs.existsSync(pkgPath)) {
return;
}
const pkg = JSON.parse(stripComments(fs.readFileSync(pkgPath, 'utf-8')));
if(!pkg.exports) {
return;
}
const exports = Object.keys(pkg.exports);
for (let i = 0; i < exports.length; i++) {
const aliasId = exports[i];
if(this.innerDep.has(aliasId)) {
continue;
}
const exportSpec = path.join(pkgDir, pkg.exports[aliasId]);
const source = fs.readFileSync(exportSpec, 'utf-8');
const lexer = new Lexer(source, exportSpec);
const parser = new Parser(lexer);
const depAst = parser.program();
const checker = existsChecker.get(exportSpec) || new TypeChecker(source, exportSpec, this.root, this.libraries, true).check(depAst);
this.innerDep.set(aliasId, checker.ast);
}
}
check(ast) {
assert.equal(ast.type, 'module');
// 类型系统
this.usedExternModel = new Map();
this.aliasNames = new Map();
this.usedTypes = new Map();
this.checkImports(ast);
this.checkExtends(ast);
// typedef check
this.checkTypedefs(ast);
this.checkConsts(ast);
// enums
this.checkEnum(ast);
// models & sub-models
this.checkModels(ast);
// virtual variables & virtual methods
this.checkTypes(ast);
// Check module init
this.checkInit(ast);
this.preCheckMethods(ast);
// apis
this.checkAPIs(ast);
// functions
this.checkFunctions(ast);
// post check for init: if have any apis or non-static function, must have init
this.postCheckInit(ast);
// check unused virtualVariable & virtualMethod
this.postCheckTypes(ast);
if(!this.mainPkg) {
this.checkExports(ast);
}
ast.models = {};
for (var [key, value] of this.models) {
ast.models[key] = value;
}
ast.usedExternModel = this.usedExternModel;
ast.usedExternException = this.resolveUsedExceptions(this.usedExternModel);
ast.conflictModels = this.resolveConflictModels(ast.usedExternModel);
ast.usedTypes = this.usedTypes;
ast.innerDep = this.innerDep;
ast.aliasNames = this.aliasNames;
// save the final ast in checker
this.ast = ast;
return this;
}
getExtendOn(ast, moduleName) {
let extendOn = [];
let extendModel, name, checker;
if (ast.type === 'moduleModel') {
const [ main, ...path ] = ast.path;
moduleName = main.lexeme;
checker = this.dependencies.get(moduleName);
name = path.map((item) => {
return item.lexeme;
}).join('.');
extendModel = checker.models.get(name);
} else if (ast.type === 'subModel') {
name = ast.path.map((tag) => {
return tag.lexeme;
}).join('.');
extendModel = this.models.get(name);
}else if (ast.idType === 'builtin_model') {
name = ast.lexeme;
extendModel = builtin.get(name);
} else {
name = ast.lexeme;
extendModel = this.models.get(name);
}
if(!extendModel) {
return extendOn;
}
extendOn.push(_model(name, moduleName));
if(extendModel.extendOn) {
extendOn = extendOn.concat(this.getExtendOn(extendModel.extendOn, moduleName));
}
return extendOn;
}
getModel(name, moduleName) {
if(!this.modelDep.has(name)) {
return _model(name, moduleName);
}
const ast = this.modelDep.get(name);
return _model(name, moduleName, this.getExtendOn(ast, moduleName));
}
resolveConflictModels(usedExternModel) {
var conflicts = new Map();
var names = new Map();
for (const [name] of this.models) {
names.set(name, '');
}
for (const [moduleName, models] of usedExternModel) {
for (var name of models) {
if (names.has(name)) {
conflicts.set(`${moduleName}:${name}`, true);
const conflictModule = names.get(name);
if (conflictModule) {
conflicts.set(`${conflictModule}:${name}`, true);
} else {
conflicts.set(name, true);
}
}
names.set(name, moduleName);
}
}
return conflicts;
}
resolveUsedExceptions(usedExternModel) {
const exceptions = new Map();
for (const [moduleName, models] of usedExternModel) {
const checker = this.dependencies.get(moduleName);
for (var name of models) {
const model = checker.models.get(name);
if (model && model.isException) {
if(!exceptions.has(moduleName)) {
exceptions.set(moduleName, new Set());
}
const set = exceptions.get(moduleName);
set.add(name);
}
}
}
return exceptions;
}
checkTypedefs(ast) {
ast.moduleBody.nodes.filter((item) => {
return item.type === 'typedef';
}).forEach((node) => {
const key = node.value.lexeme;
if (this.typedefs.has(key)) {
// 重复定义检查
this.error(`redefined typedef "${key}"`, node.value);
}
this.typedefs.set(key, node);
});
}
checkConsts(ast) {
this.consts = new Map();
ast.moduleBody.nodes.filter((item) => {
return item.type === 'const';
}).forEach((node) => {
const tag = node.constValue.tag;
let type;
switch (tag) {
case Tag.STRING:
type = 'string';
break;
case Tag.NUMBER:
type = 'number';
break;
case Tag.BOOL:
type = 'boolean';
break;
}
this.consts.set(node.constName.lexeme, {
type,
value: node.constValue
});
});
}
postCheckTypes(ast) {
if (process.env.TEA_WARNING === '1') {
for (const [key, value] of this.vidCounter) {
if (value === 0) {
console.log(`the type ${key} is unused.`);
}
}
}
}
preCheckMethods(ast) {
ast.moduleBody.nodes.forEach((node) => {
if (node.type === 'api') {
const key = node.apiName.lexeme;
// 重复定义检查
if (this.methods.has(key)) {
this.error(`redefined api "${key}"`, node.apiName);
}
this.methods.set(key, node);
} else if (node.type === 'function') {
const key = node.functionName.lexeme;
// 重复定义检查
if (this.methods.has(key)) {
this.error(`redefined function "${key}"`, node.functionName);
}
this.methods.set(key, node);
}
});
}
visitFun(ast) {
assert.equal(ast.type, 'function');
const paramMap = this.visitParams(ast.params, {});
this.checkType(ast.returnType);
const returnType = ast.returnType;
if(returnType.type === 'iterator' && ast.isAsync) {
this.error(`async function return type must be asyncIterator`, ast.functionName);
}
if (ast.functionBody) {
const local = new Env();
// merge the parameters into local env
for (const [key, value] of paramMap) {
local.set(key, value);
}
const env = {
returnType,
isStatic: ast.isStatic,
isAsync: ast.isAsync,
local: local
};
this.visitFunctionBody(ast.functionBody, env);
if (returnType.tag === Tag.TYPE && returnType.lexeme === 'void') {
// no check for void
return;
}
if (!this.hasReturnStmt(ast.functionBody.stmts)) {
this.error(`no return statement`, ast.functionName);
}
}
}
visitFunctionBody(ast, env) {
assert.equal(ast.type, 'functionBody');
this.visitStmts(ast.stmts, env);
}
hasReturnStmt(ast) {
assert.equal(ast.type, 'stmts');
if (ast.stmts.length === 0) {
return false;
}
const stmt = ast.stmts[ast.stmts.length - 1];
if (stmt.type === 'return') {
return true;
}
if (stmt.type === 'throw') {
return true;
}
if (stmt.type === 'yield') {
return true;
}
if (stmt.type === 'if') {
if (!this.hasReturnStmt(stmt.stmts)) {
return false;
}
for (let index = 0; index < stmt.elseIfs.length; index++) {
const branch = stmt.elseIfs[index];
if (!this.hasReturnStmt(branch.stmts)) {
return false;
}
}
if (!stmt.elseStmts) {
return false;
}
if (!this.hasReturnStmt(stmt.elseStmts)) {
return false;
}
return true;
}
if (stmt.type === 'try') {
let tryReturn = true;
let catchReturn = true;
let finallyReturn = true;
if (!this.hasReturnStmt(stmt.tryBlock)) {
tryReturn = false;
}
if (stmt.catchBlock && !this.hasReturnStmt(stmt.catchBlock)) {
catchReturn = false;
}
if (!stmt.finallyBlock || !this.hasReturnStmt(stmt.finallyBlock)) {
finallyReturn = false;
}
if (finallyReturn) {
return true;
}
if (!tryReturn) {
return false;
}
if (!catchReturn) {
return false;
}
return true;
}
// TODO: while, for
if (stmt.type === 'while') {
return true;
}
if (stmt.type === 'for') {
return true;
}
return false;
}
visitAPI(ast) {
assert.equal(ast.type, 'api');
const paramMap = this.visitParams(ast.params, {
needValidateParams: true
});
this.checkType(ast.returnType);
const returnType = ast.returnType;
if(returnType.type === 'iterator') {
this.error(`api return type must be asyncIterator`, ast.apiName);
}
const local = new Env();
// merge the parameters into local env
for (const [key, value] of paramMap) {
local.set(key, value);
}
const env = {
returnType,
isAsync: true,
local: local
};
env.local.set('__request', _model('$Request'));
this.visitAPIBody(ast.apiBody, env);
ast.isAsync = true;
if (ast.returns) {
env.inReturnsBlock = true;
env.local.set('__response', _model('$Response'));
this.visitReturnBody(ast.returns, env);
if (!(returnType.tag === Tag.TYPE && returnType.lexeme === 'void') &&
!this.hasReturnStmt(ast.returns.stmts)) {
this.error(`no return statement`, ast.apiName);
}
}
if (ast.runtimeBody) {
this.visitObject(ast.runtimeBody, env);
}
}
visitParams(ast, env) {
assert.equal(ast.type, 'params');
const paramMap = new Map();
for (var i = 0; i < ast.params.length; i++) {
const node = ast.params[i];
assert.equal(node.type, 'param');
const name = node.paramName.lexeme;
if (paramMap.has(node.paramName.lexeme)) {
// 重复参数名检查
this.error(`redefined parameter "${name}"`, node.paramName);
}
this.checkType(node.paramType);
// TODO: 默认值类型检查
const type = this.getType(node.paramType);
paramMap.set(name, type);
if (type.type === 'model' && env.needValidateParams) {
node.needValidate = true;
}
}
return paramMap;
}
checkMagicType(node) {
if (node.type === 'array') {
return this.checkMagicType(node.subType);
}
if (node.type === 'map') {
return this.checkMagicType(node.valueType);
}
if(node.type === 'entry') {
return this.checkMagicType(node.valueType);
}
if (node.tag === Tag.TYPE && node.lexeme === '$type') {
return true;
}
return false;
}
getMagicType(node, realType) {
if (node.type === 'array') {
return {
type: 'array',
itemType: this.getMagicType(node.subType, realType)
};
}
if (node.type === 'map') {
return {
type: 'map',
keyType: _type(node.keyType),
valueType: this.getMagicType(node.valueType, realType)
};
}
if(node.type === 'entry') {
return {
type: 'entry',
valueType: this.getMagicType(node.valueType, realType)
};
}
if (node.tag === Tag.TYPE && node.lexeme === '$type') {
return realType;
}
}
checkType(node) {
if (node.type === 'array') {
this.checkType(node.subType);
return;
}
if (node.type === 'map') {
this.checkType(node.valueType);
return;
}
if (node.type === 'entry') {
this.checkType(node.valueType);
return;
}
if (node.type === 'iterator' || node.type === 'asyncIterator') {
this.checkType(node.valueType);
return;
}
if (node.tag === Tag.TYPE) {
this.usedTypes.set(node.lexeme, true);
return;
}
const modelName = node.lexeme;
if (node.tag === Tag.ID) {
const idType = this.getIdType(modelName);
if (idType) {
node.idType = idType;
return;
}
this.error(`model "${modelName}" undefined`, node);
}
if (node.type === 'subModel_or_moduleModel') {
const [mainId, ...rest] = node.path;
const idType = this.getIdType(mainId.lexeme);
mainId.idType = idType;
// submodel
if (idType === 'model') {
const typeName = node.path.map((item) => {
return item.lexeme;
}).join('.');
if (!this.models.has(typeName)) {
this.error(`the submodel ${typeName} is inexist`, mainId);
}
node.type = 'subModel';
return;
}
if (idType === 'module') {
const checker = this.dependencies.get(mainId.lexeme);
const typeName = rest.map((item) => {
return item.lexeme;
}).join('.');
if (checker.models.has(typeName)) {
node.type = 'moduleModel';
this.usedExternModel.get(mainId.lexeme).add(typeName);
} else if (checker.enums.has(typeName)) {
node.type = 'moduleEnum';
this.usedExternModel.get(mainId.lexeme).add(typeName);
} else if (checker.typedefs.has(typeName)) {
node.type = 'moduleTypedef';
this.usedExternModel.get(mainId.lexeme).add(typeName);
} else {
this.error(`the model ${typeName} is inexist in ${mainId.lexeme}`, mainId);
}
return;
}
}
}
visitAPIBody(ast, env) {
assert.equal(ast.type, 'apiBody');
for (let i = 0; i < ast.stmts.stmts.length; i++) {
this.visitStmt(ast.stmts.stmts[i], env);
}
}
visitStmt(ast, env) {
if (ast.type === 'return') {
this.visitReturn(ast, env);
} else if (ast.type === 'yield') {
this.visitYield(ast, env);
} else if (ast.type === 'if') {
this.visitIf(ast, env);
} else if (ast.type === 'throw') {
this.visitThrow(ast, env);
} else if (ast.type === 'assign') {
this.visitAssign(ast, env);
} else if (ast.type === 'retry') {
if (!env.inReturnsBlock) {
this.error(`retry only can be in returns block`, ast);
}
} else if (ast.type === 'break') {
// unnecessary to check
} else if (ast.type === 'declare') {
this.visitDeclare(ast, env);
} else if (ast.type === 'try') {
this.visitTry(ast, env);
} else if (ast.type === 'while') {
this.visitWhile(ast, env);
} else if (ast.type === 'for') {
this.visitFor(ast, env);
} else {
this.visitExpr(ast, env);
}
}
visitFor(ast, env) {
assert.equal(ast.type, 'for');
env.local = new Env(env.local);
this.visitExpr(ast.list, env);
const listType = this.getExprType(ast.list, env);
if (listType.type === 'array'){
env.local.set(ast.id.lexeme, listType.itemType);
this.visitStmts(ast.stmts, env);
} else if(listType.type === 'iterator'
|| listType.type === 'asyncIterator') {
env.local.set(ast.id.lexeme, listType.valueType);
this.visitStmts(ast.stmts, env);
} else {
this.error(`the list in for must be array type`, ast.list);
}
env.local = env.local.preEnv;
}
isBooleanType(expr, env) {
const type = this.getExprType(expr, env);
return type.type === 'basic' && type.name === 'boolean';
}
isStringType(expr, env) {
const type = this.getExprType(expr, env);
return type.type === 'basic' && type.name === 'string';
}
isNumberType(expr, env) {
const type = this.getExprType(expr, env);
return type.type === 'basic' && isNumber(type.name);
}
visitWhile(ast, env) {
assert.equal(ast.type, 'while');
env.local = new Env(env.local);
this.visitExpr(ast.condition, env);
if (!this.isBooleanType(ast.condition, env)) {
this.error(`the condition expr must be boolean type`, ast.condition);
}
this.visitStmts(ast.stmts, env);
env.local = env.local.preEnv;
}
visitTry(ast, env) {
assert.equal(ast.type, 'try');
this.visitStmts(ast.tryBlock, env);
ast.catchBlocks.forEach(block => {
// create new local
var local = new Env(env.local);
let type = _model('$Error');
if(block.id.type) {
this.checkType(block.id.type);
type = this.getType(block.id.type);
}
local.set(block.id.lexeme, type);
env.local = local;
this.visitStmts(block.catchStmts, env);
// restore the local
env.local = env.local.preEnv;
});
if (ast.finallyBlock) {
this.visitStmts(ast.finallyBlock, env);
}
}
getParameterType(type, moduleName) {
if (type.tag === Tag.TYPE && type.lexeme === 'object') {
return {
type: 'map',
keyType: _basic('string'),
valueType: _basic('any')
};
} else if (type.type === 'map') {
return {
type: 'map',
keyType: _basic('string'),
valueType: this.getParameterType(type.valueType, moduleName)
};
} else if (type.type === 'iterator' || type.type === 'asyncIterator') {
return {
type: type.type,
valueType: this.getParameterType(type.valueType, moduleName)
};
} else if (type.tag === Tag.TYPE) {
return _basic(type.lexeme);
} else if (type.tag === Tag.ID && builtin.has(type.lexeme)) {
const ast = builtin.get(type.lexeme);
if(ast.type === 'model') {
return _model(type.lexeme);
}
return {
type: 'module_instance',
name: type.lexeme
};
} else if (type.tag === Tag.ID && type.lexeme.startsWith('$')) {
return _model(type.lexeme);
} else if (type.tag === Tag.ID && type.idType === 'model') {
const checker = moduleName ? this.dependencies.get(moduleName) : this;
return checker.getModel(type.lexeme, moduleName);
} else if (type.tag === Tag.ID && this.dependencies.has(type.lexeme)) {
return {
type: 'module_instance',
name: type.lexeme
};
} else if (type.tag === Tag.ID && type.idType === 'typedef') {
return _typedef(type.lexeme, moduleName);
} else if (type.tag === Tag.ID) {
return this.getModel(type.lexeme);
} else if (
type.type === 'moduleModel' ||
type.type === 'subModel' ||
type.type === 'subModel_or_moduleModel') {
if (moduleName && type.type === 'subModel') {
const checker = this.dependencies.get(moduleName);
return checker.getModel(type.path.map((item) => {
return item.lexeme;
}).join('.'), moduleName);
}
const [mainId, ...rest] = type.path;
const idType = this.getIdType(mainId.lexeme);
if (idType === 'module') {
const checker = this.dependencies.get(mainId.lexeme);
const typeName = rest.map((item) => {
return item.lexeme;
}).join('.');
if (checker.models.has(typeName)) {
return checker.getModel(rest.map((item) => {
return item.lexeme;
}).join('.'), mainId.lexeme);
} else if (checker.enums.has(typeName)) {
return _enum(rest.map((item) => {
return item.lexeme;
}).join('.'), mainId.lexeme);
} else if (checker.typedefs.has(typeName)) {
return _typedef(rest.map((item) => {
return item.lexeme;
}).join('.'), mainId.lexeme);
}
}
if (idType === 'model') {
return this.getModel(type.path.map((item) => {
return item.lexeme;
}).join('.'));
}
} else if (type.type === 'moduleTypedef') {
const [mainId, ...rest] = type.path;
return _typedef(rest.map((item) => {
return item.lexeme;
}).join('.'), mainId.lexeme);
} else if (type.type === 'array') {
return {
type: 'array',
itemType: this.getParameterType(type.subType, moduleName)
};
}
console.log(type);
throw new Error('un-implemented');
}
getParameterTypes(def, moduleName) {
const expected = [];
const params = def.params.params;
for (let i = 0; i < params.length; i++) {
expected.push(this.getParameterType(params[i].paramType, moduleName));
}
return expected;
}
visitStaticCall(ast, env) {
assert.equal(ast.left.type, 'static_call');
const moduleId = ast.left.id;
const moduleName = moduleId.lexeme;
const checker = this.dependencies.get(moduleName) || builtin.get(moduleName);
const method = ast.left.propertyPath[0];
const methodName = method.lexeme;
const def = checker.methods.get(methodName);
if (!def) {
this.error(`the static function "${methodName}" is undefined in ${moduleName}`, method);
}
if (!def.isStatic) {
this.error(`the "${methodName}" is not static function`, method);
}
const expected = this.getParameterTypes(def, moduleName);
const actual = [];
for (let i = 0; i < ast.args.length; i++) {
const arg = ast.args[i];
this.visitExpr(arg, env);
const type = this.getExprType(arg, env);
actual.push(type);
}
if (!eql(expected, actual)) {
this.error(`the parameter` +
` types are mismatched. expected ` +
`${moduleName}.${methodName}(${expected.map((item) => display(item)).join(', ')}), but ` +
`${moduleName}.${methodName}(${actual.map((item) => display(item)).join(', ')})`, method);
}
for (let i = 0; i < ast.args.length; i++) {
const arg = ast.args[i];
arg.expectedType = expected[i];
arg.needCast = isNeedToMap(expected[i], arg.inferred);
}
ast.isAsync = def.isAsync;
ast.isStatic = def.isStatic;
ast.hasThrow = def.isAsync || def.hasThrow;
ast.inferred = this.getType(def.returnType, moduleName);
}
getIdType(id) {
// 不检查普通变量,仅作为类型时的检查
if (this.models.has(id)) {
return 'model';
} else if (this.dependencies.has(id)) {
return 'module';
} else if (builtin.has(id)) {
const ast = builtin.get(id);
if(ast.type === 'model') {
return 'builtin_model';
}
return 'builtin_module';
} else if (this.enums.has(id)) {
return 'enum';
} else if (this.typedefs.has(id)) {
return 'typedef';
}
return '';
}
checkId(id, env) {
if (id.tag === Tag.VID) {
return this.checkVid(id, env);
}
// 未定义变量检查
const name = id.lexeme;
if (env.local && env.local.hasDefined(name)) {
// 是否在作用域链上定义过
id.type = 'variable';
return;
}
if (this.models.has(name)) {
id.type = 'model';
// model 类型
return;
}
if (builtin.has(name)) {
// alias
id.type = 'builtin_module';
return;
}
if (this.dependencies.has(name)) {
// alias
id.type = 'module';
return;
}
if (this.enums.has(name)) {
// alias
id.type = 'enum';
return;
}
if (isTmpVariable(id.type)) {
this.visitExpr(id, env);
return;
}
this.error(`variable "${name}" undefined`, id);
}
getChecker(moduleName) {
if(builtin.has(moduleName)) {
return builtin.get(moduleName);
}
if (this.dependencies.has(moduleName)) {
return this.dependencies.get(moduleName);
}
let current = this;
while (current && current.parentModuleId) {
current = this.dependencies.get(current.parentModuleId);
if (current.dependencies.has(moduleName)) {
return current.dependencies.get(moduleName);
}
}
return null;
}
getParentModuleIds(moduleName) {
let current = this.dependencies.get(moduleName);
const parentModuleIds = [];
while (current && current.parentModuleId) {
parentModuleIds.push(current.parentModuleId);
current = current.dependencies.get(current.parentModuleId);
}
return parentModuleIds;
}
getInstanceProperty(vid) {
let current = this;
if (current.properties.has(vid)) {
// check B
return current.properties.get(vid);
}
while (current.parentModuleId) {
current = current.dependencies.get(current.parentModuleId);
// check C, D, E
if (current.properties.has(vid)) {
return current.properties.get(vid);
}
}
return null;
}
getArrayFiledType(field, modelName, moduleName) {
if (field.fieldItemType.tag === Tag.TYPE) {
return {
type: 'array',
itemType: _basic(field.fieldItemType.lexeme)
};
}
// for filed: [{}]
if (field.fieldItemType.type === 'modelBody') {
return {
type: 'array',
itemType: _model(modelName, moduleName)
};
}
// for filed: [[]]
if (field.fieldItemType.fieldType === 'array') {
return {
type: 'array',
itemType: this.getArrayFiledType(field.fieldItemType, modelName, moduleName)
};
}
// for filed: [map[string]]
if (field.fieldItemType.type === 'map') {
return {
type: 'array',
itemType: {
type: 'map',
keyType: _basic(field.fieldItemType.keyType.lexeme),
valueType: this.getType(field.fieldItemType.valueType, moduleName)
}
};
}
// for filed: [ A.B ]
if (field.fieldItemType.type === 'moduleModel') {
const [mainId, ...rest] = field.fieldItemType.path;
let filedName = rest.map((tag) => {
return tag.lexeme;
}).join('.');
return {
type: 'array',
itemType: this.getModel(filedName, mainId.lexeme)
};
}
return {
type: 'array',
itemType: this.getModel(field.fieldItemType.lexeme, moduleName)
};
}
getFieldType(find, modelName, moduleName) {
if (find.fieldValue.fieldType === 'map') {
if (find.fieldValue.valueType.idType === 'model') {
return {
type: 'map',
keyType: _basic(find.fieldValue.keyType.lexeme),
valueType: _model(find.fieldValue.valueType.lexeme, moduleName)
};
}
if (find.fieldValue.valueType.type === 'array') {
return {
type: 'map',
keyType: _basic(find.fieldValue.keyType.lexeme),
valueType: this.getType(find.fieldValue.valueType, moduleName)
};
}
if (find.fieldValue.valueType.type === 'moduleModel') {
const [mainId, ...rest] = find.fieldValue.valueType.path;
let filedName = rest.map((tag) => {
return tag.lexeme;
}).join('.');
return {
type: 'map',
keyType: _basic(find.fieldValue.keyType.lexeme),
valueType: this.getModel(filedName, mainId.lexeme)
};
}
return {
type: 'map',
keyType: _basic(find.fieldValue.keyType.lexeme),
valueType: _basic(find.fieldValue.valueType.lexeme)
};
}
if (find.fieldValue.fieldType === 'entry') {
if (find.fieldValue.valueType.idType === 'model') {
return {
type: 'entry',
valueType: this.getModel(find.fieldValue.valueType.lexeme, moduleName)
};
}
if (find.fieldValue.valueType.type === 'array') {
return {
type: 'entry',
valueType: this.getType(find.fieldValue.valueType, moduleName)
};
}
return {
type: 'entry',
valueType: _basic(find.fieldValue.valueType.lexeme)
};
}
if (find.fieldValue.type === 'modelBody') {
return this.getModel([modelName, find.fieldName.lexeme].join('.'), moduleName);
}
if (find.fieldValue.fieldType === 'array') {
return this.getArrayFiledType(find.fieldValue, find.fieldValue.itemType, moduleName);
}
if (find.fieldValue.fieldType.tag === Tag.ID) {
const id = find.fieldValue.fieldType.lexeme;
if (this.models.has(id)) {
return this.getModel(id, moduleName);
}
if (this.dependencies.has(id)) {
return {
type: 'module_instance',
name: id
};
}
if (this.enums.has(id)) {
return _enum(id, moduleName);
}
if (this.typedefs.has(id)) {
return _typedef(id, moduleName);
}
if(builtin.has(id)) {
return this.getModel(find.fieldValue.fieldType.lexeme);
}
return this.getModel(find.fieldValue.fieldType.lexeme, moduleName);
}
if (find.fieldValue.fieldType.type === 'moduleModel') {
const [mainId, ...rest] = find.fieldValue.fieldType.path;
let filedName = rest.map((tag) => {
return tag.lexeme;
}).join('.');
return this.getModel(filedName, mainId.lexeme);
}
if (find.fieldValue.fieldType.type === 'moduleEnum') {
const [mainId, ...rest] = find.fieldValue.fieldType.path;
let filedName = rest.map((tag) => {
return tag.lexeme;
}).join('.');
return _enum(filedName, mainId.lexeme);
}
if (find.fieldValue.fieldType.type === 'moduleTypedef') {
const [mainId, ...rest] = find.fieldValue.fieldType.path;
let filedName = rest.map((tag) => {
return tag.lexeme;
}).join('.');
return _typedef(filedName, mainId.lexeme);
}
if (find.fieldValue.fieldType.type === 'subModel') {
let modelName = find.fieldValue.fieldType.path.map((tag) => {
return tag.lexeme;
}).join('.');
return this.getModel(modelName);
}
return _basic(find.fieldValue.fieldType);
}
getStaticMethod(name) {
let method = this.methods.get(name);
if (method && method.isStatic) {
return { method };
}
return null;
}
getBuiltinModule(moduleName) {
if(isNumber(moduleName)) {
return '$Number';
} else if(moduleName === 'readable' || moduleName === 'writable') {
return '$Stream';
} else if(moduleName === 'string') {
return '$String';
} else if(moduleName === 'array') {
return '$Array';
} else if(moduleName === 'bytes') {
return '$Bytes';
} else if(moduleName === 'map') {
return '$Map';
} else if(moduleName === 'entry') {
return '$Entry';
}
throw new Error('un-implemented');
}
getInstanceMethod(name, checkerName) {
if(builtin.has(name)) {
const builtinMethod = builtin.get(name);
return {
method: builtinMethod,
};
}
let current = this;
if (current.methods.has(name)) {
// check B
let method = current.methods.get(name);
if (!method.isStatic) {
return {
method,
module: checkerName
};
}
}
while (current.parentModuleId) {
let moduleName = current.parentModuleId;
current = current.dependencies.get(moduleName);
// check C, D, E
if (current.methods.has(name)) {
// check B
let method = current.methods.get(name);
if (!method.isStatic) {
return {
method,
module: moduleName
};
}
}
}
return null;
}
checkVid(vid, env) {
const name = vid.lexeme;
if (!this.getInstanceProperty(name)) {
this.error(`the type "${name}" is undefined`, vid);
}
if (env.isStatic) {
this.error(`virtual variable can not used in static function`, vid);
}
this.vidCounter.set(name, this.vidCounter.get(name) + 1);
}
checkProperty(ast, env) {
if (ast.id.lexeme === '__module') {
const [key] = ast.propertyPath;
const target = this.consts.get(key.lexeme);
if (!target) {
this.error(`the const ${key.lexeme} is undefined`, key);
}
replace(ast, target);
return;
}
this.checkId(ast.id, env);
// check property
const type = this.getVariableType(ast.id, env);
ast.id.inferred = type;
if (type.type === 'model' || type.type === 'map') {
// { type: 'map', keyTypeName: 'string', valueTypeName: 'any' }
const currentPath = [ast.id.lexeme];
let current = type;
ast.propertyPathTypes = new Array(ast.propertyPath.length);
for (let i = 0; i < ast.propertyPath.length; i++) {
let prop = ast.propertyPath[i];
let find = this.getPropertyType(current, prop.lexeme);
if (!find) {
this.error(`The property ${prop.lexeme} is undefined ` +
`in model ${currentPath.join('.')}(${current.name})`, prop);
}
ast.propertyPathTypes[i] = find;
currentPath.push(prop.lexeme);
current = find;
}
return;
}
if (this.models.has(ast.id.lexeme)) {
// submodel M.N
let current = this.models.get(ast.id.lexeme);
const currentPath = [ast.id.lexeme];
for (let i = 0; i < ast.propertyPath.length; i++) {
let prop = ast.propertyPath[i];
currentPath.push(prop.lexeme);
let find = this.findProperty(current, prop.lexeme, current.isException);
if (!find) {
this.error(`The model ${currentPath.join('.')} is undefined`, prop);
}
current = find.modelField;
}
ast.realType = {
type: 'class',
name: currentPath.join('.')
};
return;
}
if (this.dependencies.has(ast.id.lexeme)) {
// Alias.M.N
return;
}
if (this.enums.has(ast.id.lexeme)) {
// E.K
return;
}
const name = ast.id.lexeme;
this.error(`The type of '${name}' must be model, object or map`, ast.id);
}
visitExpr(ast, env) {
if (ast.type === 'string') {
// noop();
} else if (ast.type === 'number') {
// noop();
} else if (ast.type === 'boolean') {
// noop();
} else if (ast.type === 'null') {
// noop();
} else if (ast.type === 'group') {
this.visitExpr(ast.expr, env);
} else if (ast.type === 'property_access') {
this.checkProperty(ast, env);
} else if (ast.type === 'object') {
this.visitObject(ast, env);
} else if (ast.type === 'variable') {
this.checkId(ast.id, env);
} else if (ast.type === 'virtualVariable') {
this.checkVid(ast.vid, env);
} else if (ast.type === 'template_string') {
for (var i = 0; i < ast.elements.length; i++) {
var item = ast.elements[i];
if (item.type === 'expr') {
this.visitExpr(item.expr, env);
}
}
} else if (ast.type === 'call') {
this.visitCall(ast, env);
} else if (ast.type === 'construct') {
this.visitConstruct(ast, env);
} else if (ast.type === 'construct_model') {
this.visitConstructModel(ast, env);
} else if (ast.type === 'array') {
this.visitArray(ast, env);
} else if (ast.type === 'and' || ast.type === 'or') {
this.visitExpr(ast.left, env);
// the expr type should be boolean
if (!this.isBooleanType(ast.left, env)) {
this.error(`the left expr must be boolean type`, ast.left);
}
this.visitExpr(ast.right, env);
// the expr type should be boolean
if (!this.isBooleanType(ast.right, env)) {
this.error(`the right expr must be boolean type`, ast.right);
}
} else if (ast.type === 'not') {
this.visitExpr(ast.expr, env);
if (!this.isBooleanType(ast.expr, env)) {
this.error(`the expr after ! must be boolean type`, ast.expr);
}
} else if (ast.type === 'increment' || ast.type === 'decrement') {
this.visitExpr(ast.expr, env);
if(!this.isNumberType(ast.expr, env)) {
this.error(`the increment/decrement expr must be number type`, ast.expr);
}
} else if (isCompare(ast.type)) {
this.visitExpr(ast.left, env);
const leftType = this.getExprType(ast.left, env);
this.visitExpr(ast.right, env);
const rightType = this.getExprType(ast.right, env);
if(ast.type !== 'eq' && ast.type !== 'neq' &&
(!(leftType.type === 'basic' && isNumber(leftType.name)) ||
!(rightType.type === 'basic' && isNumber(rightType.name)))) {
this.error(`${ast.type} can only operate number type, ` +
`but left: ${display(leftType)} and right: ${display(rightType)}`, ast);
}
if (!eql([leftType], [rightType])) {
this.error(`${display(leftType)} can only compare with ${display(leftType)} type, ` +
`but ${display(rightType)}`, ast);
}
} else if (ast.type === 'add') {
this.visitExpr(ast.left, env);
const leftType = this.getExprType(ast.left, env);
this.visitExpr(ast.right, env);
const rightType = this.getExprType(ast.right, env);
if (!eql([leftType], [rightType])) {
this.error(`${display(leftType)} can only add with ${display(leftType)} type, ` +
`but ${display(rightType)}`, ast);
}
} else if (ast.type === 'subtract' || ast.type === 'div' || ast.type === 'multi') {
this.visitExpr(ast.left, env);
const leftType = this.getExprType(ast.left, env);
this.visitExpr(ast.right, env);
const rightType = this.getExprType(ast.right, env);
if(!(leftType.type === 'basic' && isNumber(leftType.name)) ||
!(rightType.type === 'basic' && isNumber(rightType.name))) {
this.error(`${ast.type} can only operate number type, ` +
`but left: ${display(leftType)} and right: ${display(rightType)}`, ast);
}
if (!eql([leftType], [rightType])) {
this.error(`${display(leftType)} can only ${ast.type} with ${display(leftType)} type, ` +
`but ${display(rightType)}`, ast);
}
} else if (ast.type === 'super') {
this.visitSuperCall(ast, env);
} else if (ast.type === 'map_access') {
let mainType;
if (ast.propertyPath) {
this.checkProperty(ast, env);
mainType = this.calculatePropertyType(ast, env);
} else {
this.checkId(ast.id, env);
mainType = this.getVariableType(ast.id, env);
}
this.visitExpr(ast.accessKey, env);
if (mainType.type === 'map') {
if (!this.isStringType(ast.accessKey, env)) {
this.error(`The key expr type must be string type`, ast.accessKey);
}
} else if (mainType.type === 'array') {
ast.type = 'array_access';
if (!this.isNumberType(ast.accessKey, env)) {
this.error(`The key expr type must be number type`, ast.accessKey);
}
} else {
this.error(`the [] form only support map or array type`, ast.accessKey);
}
} else {
throw new Error('unimplemented.');
}
ast.inferred = this.getExprType(ast, env);
}
visitSuperCall(ast, env) {
if (!env.isInitMethod) {
this.error(`super only allowed in init method`, ast);
}
if (!this.parentModuleId) {
this.error(`this module have no parent module`, ast);
}
const parent = this.getChecker(this.parentModuleId);
const expected = this.getParameterTypes(parent.init, this.parentModuleId);
const actual = [];
for (let i = 0; i < ast.args.length; i++) {
const arg = ast.args[i];
this.visitExpr(arg, env);
const type = this.getExprType(arg, env);
actual.push(type);
}
if (!eql(expected, actual)) {
this.error(`the parameter` +
` types are mismatched. expected ` +
`${this.parentModuleId}(${expected.map((item) => display(item)).join(', ')}), but ` +
`${this.parentModuleId}(${actual.map((item) => display(item)).join(', ')})`, ast);
}
for (let i = 0; i < ast.args.length; i++) {
const arg = ast.args[i];
arg.needCast = isNeedToMap(expected[i], arg.inferred);
arg.expectedType = expected[i];
}
}
checkConstructModelFields(ast, model, modelName, env) {
if (!ast.object) {
return;
}
let aliasId = ast.aliasId.lexeme;
this.visitObject(ast.object, env);
for (let i = 0; i < ast.object.fields.length; i++) {
const field = ast.object.fields[i];
const name = field.fieldName.lexeme;
let find;
if (ast.aliasId.isModel) {
find = this.findProperty(model, name, model.isException);
if (!find) {
this.error(`the property "${name}" is undefined in model "${modelName}"`, field.fieldName);
}
} else {
const checker = this.dependencies.get(aliasId);
find = checker.findProperty(model, name, model.isException);
if (!find) {
this.error(`the property "${name}" is undefined in model "${aliasId}.${modelName}"`, field.fieldName);
}
}
const currentModelName = find.modelName;
const modelField = find.modelField;
const type = this.getExprType(field.expr, env);
const moduleName = find.moduleName || aliasId;
let expected;
if (ast.aliasId.isModel) {
expected = this.getFieldType(modelField, currentModelName);
} else {
const checker = this.dependencies.get(aliasId);
expected = checker.getFieldType(modelField, currentModelName, moduleName);
}
if (!eql([expected], [type])) {
this.error(`the field type are mismatched. expected ` +
`${display(expected)}, but ${display(type)}`, field.fieldName);
}
field.extendFrom = find.extendFrom;
field.inferred = type;
field.expectedType = expected;
}
}
visitConstructModel(ast, env) {
const aliasId = ast.aliasId.lexeme;
if (this.dependencies.has(aliasId)) {
ast.aliasId.isModule = true;
const checker = this.dependencies.get(aliasId);
const modelId = ast.propertyPath.map((item) => {
return item.lexeme;
}).join('.');
const model = checker.models.get(modelId);
if (!model) {
this.error(`the model "${modelId}" is undefined in module "${aliasId}"`, ast.propertyPath[0]);
}
ast.aliasId.isException = model.isException;
this.usedExternModel.get(aliasId).add(modelId);
this.checkConstructModelFields(ast, model, `${modelId}`, env);
return;
}
if (builtin.has(aliasId)) {
const model = builtin.get(aliasId);
assert.ok(model.type === 'model');
ast.aliasId.isModel = true;
this.checkConstructModelFields(ast, model, aliasId, env);
return;
}
if (this.models.has(aliasId)) {
const model = this.models.get(aliasId);
ast.aliasId.isModel = true;
ast.aliasId.isException = model.isException;
if (ast.propertyPath.length === 0) {
this.checkConstructModelFields(ast, model, aliasId, env);
return;
}
const fullPath = [aliasId, ...ast.propertyPath.map((item) => {
return item.lexeme;
})];
for (let i = 1; i < fullPath.length; i++) {
const subModelName = fullPath.slice(0, i + 1).join('.');
if (!this.models.has(subModelName)) {
this.error(`the model "${subModelName}" is undefined`, ast.propertyPath[i]);
}
}
const modelName = fullPath.join('.');
const subModel = this.models.get(modelName);
this.checkConstructModelFields(ast, subModel, modelName, env);
return;
}
this.error(`expected "${aliasId}" is module or model`, ast.aliasId);
}
visitCall(ast, env) {
assert.ok(ast.type === 'call');
if (ast.left.type === 'method_call') {
this.visitMethodCall(ast, env);
return;
}
// need fix the type by id type
if (ast.left.type === 'static_or_instance_call') {
const id = ast.left.id;
this.checkId(id, env);
if (env.local.hasDefined(id.lexeme) || isTmpVariable(id.type)) {
ast.left.type = 'instance_call';
this.visitInstanceCall(ast, env);
return;
}
if (this.dependencies.has(id.lexeme) || builtin.has(id.lexeme)) {
ast.left.type = 'static_call';
this.visitStaticCall(ast, env);
return;
}
if (id.tag === Tag.VID) {
this.checkVid(id, env);
const type = this.getVariableType(ast.left.id, env);
if (type.type === 'module_instance') {
ast.left.type = 'instance_call';
this.visitInstanceCall(ast, env);
return;
}
}
}
throw new Error('un-implemented');
}
visitInstanceCall(ast, env) {
assert.equal(ast.left.type, 'instance_call');
const type = this.getVariableType(ast.left.id, env);
let moduleName = type.name;
if(isBasicType(type.type)) {
moduleName = this.getBuiltinModule(type.type);
ast.builtinModule = moduleName;
ast.left.id.moduleType = {
type: 'basic',
name: type.type,
};
} else if(type.type === 'basic' && isBasicType(type.name)) {
moduleName = this.getBuiltinModule(type.name);
ast.builtinModule = moduleName;
ast.left.id.moduleType = {
type: 'basic',
name: type.name,
};
} else if(type.type === 'model') {
moduleName = '$ModelInstance';
ast.builtinModule = moduleName;
ast.left.id.moduleType = {
type: 'model',
name: type.name,
};
} else if(builtin.has(moduleName)) {
ast.builtinModule = moduleName;
ast.left.id.moduleType = {
type: 'builtin_module',
name: moduleName,
};
} else if (ast.left.id.tag === Tag.ID) {
moduleName = type.name;
ast.left.id.moduleType = {
type: 'module',
name: moduleName
};
}
const checker = this.getChecker(moduleName);
const method = ast.left.propertyPath[0];
const name = method.lexeme;
const def = checker.getInstanceMethod(name, moduleName);
if (!def) {
this.error(`the instance function/api "${name}" is undefined in ${moduleName}`, method);
}
const { method: definedApi, module: moduleNameOfDef } = def;
const actual = [];
for (let i = 0; i < ast.args.length; i++) {
const arg = ast.args[i];
this.visitExpr(arg, env);
const type = this.getExprType(arg, env);
actual.push(type);
}
const expected = this.getParameterTypes(definedApi, moduleNameOfDef);
if (!eql(expected, actual)) {
this.error(`the parameter` +
` types are mismatched. expected ` +
`${moduleName}.${name}(${expected.map((item) => display(item)).join(', ')}), but ` +
`${moduleName}.${name}(${actual.map((item) => display(item)).join(', ')})`, method);
}
for (let i = 0; i < ast.args.length; i++) {
const arg = ast.args[i];
arg.needCast = isNeedToMap(expected[i], arg.inferred);
arg.expectedType = expected[i];
}
if(this.checkMagicType(definedApi.returnType)) {
const magicType = this.getInstanceType(ast.left.id, env);
ast.inferred = this.getMagicType(definedApi.returnType, magicType);
} else {
ast.inferred = this.getType(definedApi.returnType, moduleName);
}
ast.isAsync = definedApi.isAsync;
ast.isStatic = definedApi.isStatic;
ast.hasThrow = definedApi.isAsync || definedApi.hasThrow;
}
visitMethodCall(ast, env) {
assert.equal(ast.left.type, 'method_call');
const id = ast.left.id;
const name = id.lexeme;
const staticMethod = this.getStaticMethod(name);
const instanceMethod = this.getInstanceMethod(name);
const defined = staticMethod || instanceMethod;
if (!defined) {
this.error(`the api/function "${name}" is undefined`, id);
}
const { method: definedApi, module: moduleName } = defined;
if (definedApi.type === 'api' && !env.isAsync) {
this.error(`the api only can be used in async function`, id);
}
if (definedApi.type === 'function') {
if (!env.isAsync && definedApi.isAsync) {
this.error(`the async function only can be used in async function`, id);
}
}
if (ast.args.length !== definedApi.params.params.length) {
this.error(`the parameters are mismatched, expect ${definedApi.params.params.length} ` +
`parameters, actual ${ast.args.length}`, id);
}
const expected = this.getParameterTypes(definedApi, moduleName);
const actual = [];
for (let i = 0; i < ast.args.length; i++) {
const arg = ast.args[i];
this.visitExpr(arg, env);
const type = this.getExprType(arg, env);
actual.push(type);
}
if (!eql(expected, actual)) {
this.error(`the parameter` +
` types are mismatched. expected ` +
`${name}(${expected.map((item) => display(item)).join(', ')}), but ` +
`${name}(${actual.map((item) => display(item)).join(', ')})`, id);
}
for (let i = 0; i < ast.args.length; i++) {
const arg = ast.args[i];
arg.needCast = isNeedToMap(expected[i], arg.inferred);
arg.expectedType = expected[i];
}
ast.isStatic = definedApi.type === 'api' ? false : definedApi.isStatic;
ast.isAsync = definedApi.type === 'api' ? true : definedApi.isAsync;
ast.inferred = this.getType(definedApi.returnType);
ast.hasThrow = !builtin.has(name) && (ast.isAsync || definedApi.hasThrow);
}
visitConstruct(ast, env) {
const aliasId = ast.aliasId.lexeme;
if (!this.dependencies.has(aliasId) && !builtin.has(aliasId)) {
this.error(`the module "${aliasId}" is not imported`, ast.aliasId);
}
const checker = this.dependencies.get(aliasId) || builtin.get(aliasId);
if (!checker.init) {
this.error(`the module "${aliasId}" don't has init`, ast.aliasId);
}
const actual = [];
for (let i = 0; i < ast.args.length; i++) {
const arg = ast.args[i];
this.visitExpr(arg, env);
actual.push(arg.inferred);
}
const expected = this.getParameterTypes(checker.init, aliasId);
if (!eql(expected, actual)) {
this.error(`the parameter` +
` types are mismatched. expected ` +
`new ${aliasId}(${expected.map((item) => display(item)).join(', ')}), but ` +
`new ${aliasId}(${actual.map((item) => display(item)).join(', ')})`, ast.aliasId);
}
for (let i = 0; i < ast.args.length; i++) {
const arg = ast.args[i];
arg.needCast = isNeedToMap(expected[i], arg.inferred);
arg.expectedType = expected[i];
}
}
visitArray(ast, env) {
assert.equal(ast.type, 'array');
for (var i = 0; i < ast.items.length; i++) {
this.visitExpr(ast.items[i], env);
}
}
getVariableType(id, env) {
if (id.tag === Tag.VID) {
const def = this.getInstanceProperty(id.lexeme);
return this.getType(def.value);
}
const name = id.lexeme;
if (env.local && env.local.hasDefined(name)) {
// 返回作用域链上定义的值
return env.local.get(name);
}
if (this.enums.has(name)) {
return _enum(name);
}
if (this.models.has(name)) {
return _basic('class');
}
if (this.dependencies.has(name)) {
return _basic('class');
}
if (isTmpVariable(id.type)) {
return this.getExprType(id, env);
}
console.log(id);
throw new Error('Can not get the type for variable');
}
getPropertyType(type, propName) {
if (type.type === 'map') {
if (type.valueType.name === 'any') {
return _basic('any');
}
if (!type.valueType.name) {
return type.valueType;
}
if (isBasicType(type.valueType.name)) {
return _basic(type.valueType.name);
}
return this.getModel(type.valueType.name, type.valueType.moduleName);
}
if (type.type === 'model') {
let model, checker = this;
if (type.moduleName) {
checker = this.dependencies.get(type.moduleName);
model = checker.models.get(type.name);
} else if (builtin.has(type.name)) {
model = builtin.get(type.name);
} else {
model = this.models.get(type.name);
}
const find = checker.findProperty(model, propName, model.isException);
if (!find) {
return;
}
const moduleName = find.moduleName || type.moduleName;
return this.getFieldType(find.modelField, find.modelName, moduleName);
}
}
calculatePropertyType(ast, env) {
const type = this.getVariableType(ast.id, env);
if(!ast.id.inferred) {
ast.id.inferred = type;
}
if(!ast.propertyPathTypes) {
ast.propertyPathTypes = new Array(ast.propertyPath.length);
}
if (type.type === 'model' || type.type === 'map') {
let current = type;
for (let i = 0; i < ast.propertyPath.length; i++) {
let prop = ast.propertyPath[i];
current = this.getPropertyType(current, prop.lexeme);
ast.propertyPathTypes[i] = current;
}
return current;
}
if (this.enums.has(ast.id.lexeme)) {
return _enum(ast.id.lexeme);
}
if (this.models.has(ast.id.lexeme)) {
return _basic('class');
}
if(builtin.get(ast.id.lexeme)) {
const builtinModel = builtin.get(ast.id.lexeme);
if(builtinModel.type === 'model') {
return _model(ast.id.lexeme);
}
}
if (this.dependencies.has(ast.id.lexeme)) {
const checker = this.dependencies.get(ast.id.lexeme);
const [ mainType ] = ast.propertyPath;
if (checker.enums.has(mainType.lexeme)) {
return _enum(mainType.lexeme, ast.id.lexeme);
}
return _basic('class');
}
console.log(ast);
throw new Error('unknown type');
}
getExprType(ast, env) {
if (!ast) {
return _basic('void');
}
if (ast.inferred) {
return ast.inferred;
}
if (ast.type === 'property_access' || ast.type === 'property') {
return this.calculatePropertyType(ast, env);
}
if (ast.type === 'map_access') {
let type;
if (ast.propertyPath) {
type = this.calculatePropertyType(ast, env);
} else {
type = this.getVariableType(ast.id, env);
}
if (type.type === 'array') {
return type.itemType;
} else if (type.type === 'map') {
return type.valueType;
}
}
if (ast.type === 'array_access') {
let type;
if (ast.propertyPath) {
type = this.calculatePropertyType(ast, env);
} else {
type = this.getVariableType(ast.id, env);
}
if (type.type === 'array') {
return type.itemType;
}
}
if (ast.type === 'string') {
return _basic('string');
}
if (ast.type === 'number') {
return _basic(ast.value.type);
}
if (ast.type === 'boolean') {
return _basic('boolean');
}
if (ast.type === 'object') {
if (ast.fields.length === 0) {
return {
type: 'map',
keyType: _basic('string'),
valueType: _basic('any')
};
}
var current;
var same = true;
for (let i = 0; i < ast.fields.length; i++) {
const field = ast.fields[i];
if (field.type === 'objectField') {
let type = this.getExprType(field.expr, env);
if (current && !isSameType(current, type)) {
same = false;
break;
}
current = type;
} else if (field.type === 'expandField') {
let type = this.getExprType(field.expr, env);
if (type.type === 'map') {
if (current && !isSameType(current, type.valueType)) {
same = false;
break;
}
current = type.valueType;
} else {
same = false;
break;
}
}
}
return {
type: 'map',
keyType: _basic('string'),
valueType: same ? current : _basic('any')
};
}
if (ast.type === 'variable') {
return this.getVariableType(ast.id, env);
}
if (ast.type === 'virtualVariable') {
const type = this.getInstanceProperty(ast.vid.lexeme);
return this.getType(type.value);
}
if (ast.type === 'null') {
return _basic('null');
}
if (ast.type === 'template_string') {
return _basic('string');
}
if (ast.type === 'call') {
assert.ok(ast.inferred);
return ast.inferred;
}
if (ast.type === 'super') {
return {
type: 'module_instance',
name: this.parentModuleId,
parentModuleIds: this.getParentModuleIds(this.parentModuleId),
};
}
if (ast.type === 'construct') {
return {
type: 'module_instance',
name: ast.aliasId.lexeme,
parentModuleIds: this.getParentModuleIds(ast.aliasId.lexeme),
};
}
if (ast.type === 'construct_model') {
if (ast.aliasId.isModel) {
const model = this.getModel([ast.aliasId.lexeme, ...ast.propertyPath.map((item) => {
return item.lexeme;
})].join('.'));
model.isException = ast.aliasId.isException;
return model;
}
const moduleName = ast.aliasId.lexeme;
const checker = this.dependencies.get(moduleName);
const model = checker.getModel(ast.propertyPath.map((item) => {
return item.lexeme;
}).join('.'), ast.aliasId.lexeme);
model.isException = ast.aliasId.isException;
return model;
}
if (ast.type === 'array') {
if (ast.items.length === 0) {
return {
type: 'array',
itemType: _basic('any')
};
}
let current;
let same = true;
for (let i = 0; i < ast.items.length; i++) {
const type = this.getExprType(ast.items[i], env);
if (current && !isSameType(current, type)) {
same = false;
break;
}
current = type;
}
return {
type: 'array',
itemType: same ? current : _basic('any')
};
}
if (ast.type === 'group') {
return this.getExprType(ast.expr, env);
}
if (isCompare(ast.type)) {
return _basic('boolean');
}
if (ast.type === 'and' || ast.type === 'or') {
return _basic('boolean');
}
if (ast.type === 'increment' || ast.type === 'decrement') {
return this.getExprType(ast.expr, env);
}
if (ast.type === 'add' || ast.type === 'subtract'
|| ast.type === 'div' || ast.type === 'multi') {
return this.getExprType(ast.right, env);
}
console.log(ast);
throw new Error('can not get type');
}
getInstanceType(id, env) {
const type = this.getVariableType(id, env);
if(!isBasicType(type.type) && type.type !== 'model') {
this.error('$type can only be used as basic type instacne call.', type);
}
if(type.type === 'array') {
return type.itemType;
}
if(type.type === 'map') {
return type.valueType;
}
if(type.type === 'entry') {
return type.valueType;
}
return type;
}
getType(t, extern) {
if (t.tag === Tag.TYPE && t.lexeme === 'object') {
return {
type: 'map',
keyType: _basic('string'),
valueType: _basic('any')
};
}
if (t.tag === Tag.TYPE) {
this.usedTypes.set(t.lexeme, true);
return _basic(t.lexeme);
}
if (t.tag === Tag.ID) {
if (t.idType === 'module' || t.idType === 'builtin_module') {
return {
type: 'module_instance',
name: t.lexeme,
parentModuleIds: this.getParentModuleIds(t.lexeme),
};
}
if (t.lexeme.startsWith('$')) {
return _model(t.lexeme);
}
if (extern) {
this.usedExternModel.get(extern).add(t.lexeme);
if (t.idType === 'typedef') {
return _typedef(t.lexeme, extern);
}
if (t.idType === 'enum') {
return _enum(t.lexeme, extern);
}
const checker = this.dependencies.get(extern);
return checker.getModel(t.lexeme, extern);
}
if (this.dependencies.has(t.lexeme)) {
return {
type: 'module_instance',
name: t.lexeme,
parentModuleIds: this.getParentModuleIds(t.lexeme),
};
}
if (this.enums.has(t.lexeme)) {
return _enum(t.lexeme);
}
if (this.typedefs.has(t.lexeme)) {
return _typedef(t.lexeme);
}
return this.getModel(t.lexeme);
}
if (t.type === 'array') {
return {
type: 'array',
itemType: this.getType(t.subType)
};
}
if (t.type === 'map') {
if (t.valueType.type === 'subModel_or_moduleModel') {
this.checkType(t.valueType);
return {
type: 'map',
keyType: _type(t.keyType),
valueType: t.valueType
};
}
return {
type: 'map',
keyType: _type(t.keyType),
valueType: this.getType(t.valueType)
};
}
if (t.type === 'entry') {
if (t.valueType.type === 'subModel_or_moduleModel') {
this.checkType(t.valueType);
return {
type: 'entry',
valueType: t.valueType
};
}
return {
type: 'entry',
valueType: this.getType(t.valueType)
};
}
if (t.type === 'iterator' || t.type === 'asyncIterator') {
if (t.valueType.type === 'subModel_or_moduleModel') {
this.checkType(t.valueType);
return {
type: t.type,
valueType: t.valueType
};
}
return {
type: t.type,
valueType: this.getType(t.valueType)
};
}
if (t.type === 'subModel_or_moduleModel') {
this.checkType(t);
}
if (t.type === 'moduleModel') {
const [mainId, ...rest] = t.path;
const typeName = rest.map((item) => {
return item.lexeme;
}).join('.');
const moduleName = mainId.lexeme;
this.usedExternModel.get(moduleName).add(typeName);
const checker = this.dependencies.get(moduleName);
return checker.getModel(typeName, moduleName);
}
if (t.type === 'subModel') {
let modelName = t.path.map((tag) => {
return tag.lexeme;
}).join('.');
return this.getModel(modelName);
}
if (t.type === 'moduleEnum') {
const [mainId, ...rest] = t.path;
const typeName = rest.map((item) => {
return item.lexeme;
}).join('.');
this.usedExternModel.get(mainId.lexeme).add(typeName);
return _enum(typeName, mainId.lexeme);
}
if (t.type === 'moduleTypedef') {
const [mainId, ...rest] = t.path;
const typeName = rest.map((item) => {
return item.lexeme;
}).join('.');
this.usedExternModel.get(mainId.lexeme).add(typeName);
return _typedef(typeName, mainId.lexeme);
}
// return _model(t.path.map((item) => {
// return item.lexeme;
// }).join('.'));
console.log(t);
throw new Error('un-implemented');
}
visitObject(ast, env) {
assert.equal(ast.type, 'object');
for (var i = 0; i < ast.fields.length; i++) {
this.visitObjectField(ast.fields[i], env);
}
ast.inferred = this.getExprType(ast, env);
}
visitObjectField(ast, env) {
if (ast.type === 'objectField') {
this.visitExpr(ast.expr, env);
} else if (ast.type === 'expandField') {
this.visitExpr(ast.expr, env);
const type = ast.expr.inferred;
if (type.type === 'model' || type.type === 'map') {
return;
}
const name = ast.expr.id.lexeme;
this.error(`the expand field "${name}" should be an ` +
`object or model`, ast.expr.id);
}
}
visitReturnBody(ast, env) {
assert.equal(ast.type, 'returnBody');
this.visitStmts(ast.stmts, env);
}
visitStmts(ast, env) {
assert.equal(ast.type, 'stmts');
for (var i = 0; i < ast.stmts.length; i++) {
const node = ast.stmts[i];
this.visitStmt(node, env);
}
}
visitReturn(ast, env) {
assert.equal(ast.type, 'return');
if (ast.expr) {
this.visitExpr(ast.expr, env);
}
if (env.isInitMethod) {
if(ast.expr) {
this.error(`should not have return value in init method`, ast);
}
return;
}
// return type check
var actual = this.getExprType(ast.expr, env);
var expect = this.getType(env.returnType);
ast.needCast = isNeedToModel(expect, actual);
if (!eql([expect], [actual])) {
this.error(`the return type is not expected, expect: ${display(expect)}, actual: ${display(actual)}`, ast);
}
if (ast.needCast) {
ast.expectedType = expect;
}
}
visitYield(ast, env) {
assert.equal(ast.type, 'yield');
if (ast.expr) {
this.visitExpr(ast.expr, env);
}
if (env.isInitMethod && ast.expr) {
this.error(`should not have return value in init method`, ast);
}
// return type check
var actual = this.getExprType(ast.expr, env);
var yieldType = this.getType(env.returnType);
var expect = yieldType.valueType;
ast.needCast = isNeedToModel(expect, actual);
if (!eql([expect], [actual])) {
this.error(`the return type is not expected, expect: ${display(expect)}, actual: ${display(actual)}`, ast);
}
if (ast.needCast) {
ast.expectedType = expect;
}
}
visitAssign(ast, env) {
assert.equal(ast.type, 'assign');
if (ast.left.type === 'virtualVariable') {
this.checkVid(ast.left.vid, env);
} else if (ast.left.type === 'variable') {
this.checkId(ast.left.id, env);
} else if (ast.left.type === 'property') {
this.checkProperty(ast.left, env);
} else if (ast.left.type === 'map_access') {
let mainType;
if (ast.left.propertyPath) {
this.checkProperty(ast.left, env);
mainType = this.calculatePropertyType(ast.left, env);
} else {
this.checkId(ast.left.id, env);
mainType = this.getVariableType(ast.left.id, env);
}
if (mainType.type === 'map') {
if (!this.isStringType(ast.left.accessKey, env)) {
this.error(`The key expr type must be string type`, ast.left.accessKey);
}
} else if (mainType.type === 'array') {
ast.left.type = 'array_access';
if (!this.isNumberType(ast.left.accessKey, env)) {
this.error(`The key expr type must be number type`, ast.left.accessKey);
}
} else {
this.error(`the [] form only support map or array type`, ast.left.accessKey);
}
} else {
throw new Error('unimplemented');
}
const expected = this.getExprType(ast.left, env);
ast.left.inferred = expected;
this.visitExpr(ast.expr, env);
const actual = this.getExprType(ast.expr, env);
if (!isAssignable(expected, actual, ast.expr)) {
this.error(`can't assign ${display(actual)} to ${display(expected)}`, ast.expr);
}
if (expected.type === 'basic' && expected.name === 'readable') {
if (actual.type === 'basic' && actual.name === 'bytes' ||
actual.type === 'basic' && actual.name === 'string') {
ast.expr.needToReadable = true;
}
}
}
visitDeclare(ast, env) {
const id = ast.id.lexeme;
// 当前作用域是否定义过
if (env.local.has(id)) {
this.error(`the id "${id}" was defined`, ast.id);
}
this.visitExpr(ast.expr, env);
const type = this.getExprType(ast.expr, env);
let expected;
if (type.type === 'basic' && type.name === 'null') {
if (!ast.expectedType) {
this.error(`must declare type when value is null`, ast.id);
}
expected = this.getType(ast.expectedType);
} else {
if (ast.expectedType) {
expected = this.getType(ast.expectedType);
if (!isAssignable(expected, type, ast.expr)) {
this.error(`declared variable with mismatched type, ` +
`expected: ${display(expected)}, actual: ${display(type)}`, ast.id);
}
}
}
env.local.set(id, expected || type);
ast.expr.inferred = expected || type;
}
visitThrow(ast, env) {
if(ast.expr.type === 'object') {
this.visitObject(ast.expr, env);
} else if(ast.expr.type === 'construct_model') {
this.visitConstructModel(ast.expr, env);
} else {
this.error('unimplement', ast);
}
}
visitIf(ast, env) {
assert.equal(ast.type, 'if');
this.visitExpr(ast.condition, env);
this.visitStmts(ast.stmts, env);
for (let i = 0; i < ast.elseIfs.length; i++) {
const branch = ast.elseIfs[i];
this.visitExpr(branch.condition, env);
this.visitStmts(branch.stmts, env);
}
if (ast.elseStmts) {
this.visitStmts(ast.elseStmts, env);
}
}
arrayFieldFlat(arrayField){
if (arrayField.tag === Tag.ID) {
const typeId = arrayField.lexeme;
const type = this.getIdType(typeId);
if (!type) {
this.error(`the type "${typeId}" is undefined`, arrayField);
}
arrayField.idType = type;
} else if (arrayField.tag === Tag.TYPE) {
// TODO
} else if (arrayField.type === 'map') {
this.checkType(arrayField.valueType);
} else if (arrayField.type === 'subModel_or_moduleModel') {
this.checkType(arrayField);
} else if (arrayField.fieldType === 'array') {
return this.arrayFieldFlat(arrayField.fieldItemType);
} else {
return arrayField;
}
}
flatModel(root, modelBody, modelName, extendOn, type = 'model') {
const keys = new Map();
for (var i = 0; i < modelBody.nodes.length; i++) {
const node = modelBody.nodes[i];
const fieldName = node.fieldName.lexeme;
if (keys.has(fieldName)) {
this.error(`redefined field "${fieldName}" in model "${modelName}"`, node.fieldName);
}
keys.set(fieldName, true);
const fieldValue = node.fieldValue;
if (fieldValue.type === 'modelBody') {
this.flatModel(root, fieldValue, `${modelName}.${node.fieldName.lexeme}`);
} else if (fieldValue.type === 'fieldType') {
// check the type
if (fieldValue.fieldType.tag === Tag.ID) {
const typeId = fieldValue.fieldType.lexeme;
const type = this.getIdType(typeId);
if (!type) {
this.error(`the type "${typeId}" is undefined`, fieldValue.fieldType);
}
fieldValue.fieldType.idType = type;
}
if (fieldValue.fieldType === 'array') {
const modelBody = this.arrayFieldFlat(node.fieldValue.fieldItemType);
if (modelBody) {
const submodel = `${modelName}.${fieldName}`;
this.flatModel(root, modelBody, submodel);
node.fieldValue.itemType = submodel;
}
}
if (fieldValue.fieldType === 'map') {
this.checkType(fieldValue.valueType);
}
if (fieldValue.fieldType === 'entry') {
this.checkType(fieldValue.valueType);
}
if (fieldValue.fieldType.type === 'subModel_or_moduleModel') {
this.checkType(fieldValue.fieldType);
}
if (typeof fieldValue.fieldType === 'string') {
this.usedTypes.set(fieldValue.fieldType, true);
}
} else {
throw new Error('unimplemented');
}
}
const isException = type === 'exception';
modelBody.extendFileds = this.getExtendFileds(extendOn, isException);
this.models.set(modelName, {
type: type,
isException: isException,
extendOn: extendOn,
modelName: {
tag: Tag.ID,
lexeme: modelName
},
modelBody: modelBody,
annotation: undefined
});
}
getExtendFileds(extendOn, isException, keys) {
keys = keys || new Map();
if(!extendOn && !isException) {
return [];
}
let extendModel, moduleName;
let checker = this;
if(!extendOn && isException) {
extendModel = builtin.get('$Error');
isException = false;
} else if (extendOn.type === 'moduleModel') {
const [ main, ...path ] = extendOn.path;
moduleName = main.lexeme;
checker = this.dependencies.get(moduleName);
const typeName = path.map((item) => {
return item.lexeme;
}).join('.');
extendModel = checker.models.get(typeName);
} else if (extendOn.type === 'subModel') {
let modelName = extendOn.path.map((tag) => {
return tag.lexeme;
}).join('.');
extendModel = this.models.get(modelName);
} else if (extendOn.idType === 'builtin_model') {
extendModel = builtin.get(extendOn.lexeme);
} else {
extendModel = this.models.get(extendOn.lexeme);
}
if(!extendModel) {
return [];
}
let extendFileds = extendModel.modelBody.nodes.filter(node => {
const fieldName = node.fieldName.lexeme;
if(keys.has(fieldName)) {
return false;
}
keys.set(fieldName, true);
// non
// node.tokenRange = [];
return true;
});
if(extendModel.extendOn) {
return extendFileds.concat(this.getExtendFileds(extendModel.extendOn, isException, keys));
}
return extendFileds;
}
visitModel(ast) {
assert.equal(ast.type, 'model');
const modelName = ast.modelName.lexeme;
this.flatModel(ast, ast.modelBody, modelName, ast.extendOn, ast.type);
}
visitException(ast) {
assert.equal(ast.type, 'exception');
const exceptionName = ast.exceptionName.lexeme;
this.flatModel(ast, ast.exceptionBody, exceptionName, ast.extendOn, ast.type);
}
visitEnum(ast) {
assert.equal(ast.type, 'enum');
const enumName = ast.enumName.lexeme;
const enumType = this.getType(ast.enumType);
// only suport string & number
if (enumType.type !== 'basic' &&
enumType.name !== 'string' && !isNumber(enumType.name)) {
this.error(`enum "${enumName}" has a wrong type, enum only suppot string or number.`);
}
for (var i = 0; i < ast.enumBody.nodes.length; i++) {
const enumAttrs = ast.enumBody.nodes[i].enumAttrs;
let attrNames = new Map();
enumAttrs.forEach(attr => {
if (attrNames.has(attr.attrName.lexeme)) {
this.error(`the enum attribute "${attr.attrName.lexeme}" is redefined.`);
}
attrNames.set(attr.attrName.lexeme, true);
if (attr.attrName.lexeme === 'value') {
// enum's value declare just like assign
const valueType = this.getExprType(attr.attrValue);
if (!isAssignable(enumType, valueType)) {
this.error(`the enum types are mismatched. expected ${enumType.name}, but ${valueType.name}`);
}
}
});
if (!attrNames.has('value')) {
this.error(`enum "${enumName}" must have attribute "value".`);
}
}
}
}
function getChecker(source, filePath) {
const lexer = new Lexer(source, filePath);
const parser = new Parser(lexer);
const ast = parser.program();
const it = builtin.keys();
let key = it.next();
while(!key.done) {
const ast = builtin.get(key.value);
if(ast.type === 'module' && !ast.model) {
builtin.set(key.value, new TypeChecker(source, filePath, null, null, 'builtin').check(ast));
}
key = it.next();
}
return new TypeChecker(source, filePath).check(ast);
}
function analyze(source, filePath) {
const checker = getChecker(source, filePath);
return checker.ast;
}
exports.analyze = analyze;
exports.getChecker = getChecker;