Initial support for generics

master
Paul Loyd 2017-11-13 21:11:18 +03:00
parent 16632df5ed
commit 1fc08b013e
8 changed files with 191 additions and 42 deletions

View File

@ -10,6 +10,7 @@ const Command = require('./commands');
const Module = require('./module');
const Scope = require('./scope');
const CircularList = require('./list');
const {isNode} = require('./utils');
class Collector {
constructor(parser, root = '.') {
@ -46,7 +47,7 @@ class Collector {
const scope = this.global.extend(module);
this._freestyle(extractors.declaration, ast.program, scope);
this._freestyle(extractors.declaration, ast.program, scope, null);
this.modules.set(path, module);
@ -69,7 +70,7 @@ class Collector {
}
// Given the AST output of babylon parse, walk through in a depth-first order.
_freestyle(group, root, scope) {
_freestyle(group, root, scope, params) {
let stack;
let parent;
let keys = [];
@ -91,7 +92,7 @@ class Collector {
if (isNode(node) && isAcceptableGroup(group, node)) {
if (!this.roots.has(node)) {
const task = this._collect(group, node, scope);
const task = this._collect(group, node, scope, params);
this.roots.add(node);
this._spawn(task);
}
@ -108,11 +109,11 @@ class Collector {
} while (stack);
}
* _collect(group, node, scope) {
* _collect(group, node, scope, params) {
const extractor = group[node.type];
if (!extractor) {
this._freestyle(group, node, scope);
this._freestyle(group, node, scope, null);
return null;
}
@ -138,8 +139,19 @@ class Collector {
break;
case 'define':
scope.addDefinition(...value.data);
this.schemas.push(value.data[0]);
const [schema, declared] = value.data;
if (declared && params) {
const name = schema.name;
schema.name = generateGenericName(name, params);
scope.addInstance(name, schema, params.map(p => p.value));
} else {
scope.addDefinition(...value.data);
}
this.schemas.push(schema);
break;
case 'external':
@ -151,7 +163,16 @@ class Collector {
break;
case 'query':
result = yield* this._query(scope, value.data);
if (params) {
const param = params.find(p => p.name === value.data[0]);
if (param) {
result = param.value;
break;
}
}
result = yield* this._query(scope, ...value.data);
break;
case 'enter':
@ -172,17 +193,17 @@ class Collector {
result = [];
for (const val of value) {
result.push(yield* this._collect(group, val, scope));
result.push(yield* this._collect(group, val, scope, params));
}
} else {
assert(isNode(value));
result = yield* this._collect(group, value, scope);
result = yield* this._collect(group, value, scope, params);
}
}
}
* _query(scope, name) {
let result = scope.query(name);
* _query(scope, name, params) {
let result = scope.query(name, params);
// TODO: warning.
assert.notEqual(result.type, 'unknown');
@ -190,6 +211,9 @@ class Collector {
// Resulting scope is always the best choice for waiting.
scope = result.scope;
// It's only valid the sequence: E*[CT]?F,
// where E - external, C - declaration, T - template, F - definition.
switch (result.type) {
case 'external':
const modulePath = scope.resolve(result.info.path);
@ -199,7 +223,7 @@ class Collector {
const module = this.modules.get(modulePath);
const {imported} = result.info;
while ((result = module.query(imported)).type === 'unknown') {
while ((result = module.query(imported, params)).type === 'unknown') {
yield;
}
@ -208,16 +232,27 @@ class Collector {
}
// TODO: reexports.
assert.equal(result.type, 'declaration');
assert(result.type === 'declaration' || result.type === 'template');
scope = result.scope;
name = result.name;
// Fallthrough.
case 'declaration':
this._freestyle(extractors.definition, result.node, scope);
case 'template':
let tmplParams = null;
while ((result = scope.query(name)).type === 'declaration') {
if (result.type === 'template') {
tmplParams = result.params.map((p, i) => ({
name: p.name,
value: params[i] || p.default,
}));
}
this._freestyle(extractors.definition, result.node, scope, tmplParams);
while ((result = scope.query(name, params)).type !== 'definition') {
assert.notEqual(result.type, 'external');
yield;
}
@ -231,7 +266,7 @@ class Collector {
* _grabExports(module) {
for (const [scope, name] of module.exports()) {
yield* this._query(scope, name);
yield* this._query(scope, name, null);
}
}
@ -268,10 +303,6 @@ class Collector {
}
}
function isNode(it) {
return it && typeof it === 'object' && it.type;
}
function pathToNamespace(path) {
const pathObj = pathlib.parse(path);
@ -288,4 +319,15 @@ function isAcceptableGroup(group, node) {
return group.entries.includes(node.type);
}
function generateGenericName(base, params) {
let name = base + '_';
for (const {value} of params) {
assert.equal(typeof value, 'string');
name += '_' + value;
}
return name;
}
module.exports = Collector;

View File

@ -1,8 +1,8 @@
'use strict';
class Command {
static declare(name, node) {
return new Command('declare', [name, node]);
static declare(name, node, params) {
return new Command('declare', [name, node, params]);
}
static define(schema, declared = true) {
@ -17,8 +17,8 @@ class Command {
return new Command('provide', [name, reference]);
}
static query(name) {
return new Command('query', name);
static query(name, params = null) {
return new Command('query', [name, params]);
}
static enter() {

View File

@ -3,7 +3,7 @@
const assert = require('assert');
const {declare, define, external, provide, query, enter, exit, namespace} = require('./commands');
const {partition} = require('./utils');
const {partition, isNode} = require('./utils');
const definition = {
entries: [
@ -199,8 +199,13 @@ const definition = {
* GenericTypeAnnotation(node) {
const name = yield node.id;
const params = node.typeParameters && (yield node.typeParameters);
const schema = yield query(name);
const schema = yield query(name, params);
if (typeof schema === 'string') {
return schema;
}
if (schema.$unwrap) {
return schema.type;
@ -215,6 +220,10 @@ const definition = {
return makeFullname(schema);
},
* TypeParameterInstantiation(node) {
return yield node.params;
},
* FunctionTypeAnnotation(node) {
return null;
},
@ -385,32 +394,46 @@ const declaration = {
* TypeAlias(node) {
const name = yield node.id;
const params = node.typeParameters && (yield node.typeParameters);
yield declare(name, node);
yield declare(name, node, params);
return name;
},
* InterfaceDeclaration(node) {
const name = yield node.id;
const params = node.typeParameters && (yield node.typeParameters);
yield declare(name, node);
yield declare(name, node, params);
return name;
},
* ClassDeclaration(node) {
const name = yield node.id;
const params = node.typeParameters && (yield node.typeParameters);
// TODO: do it only for "all"-mode.
const body = node.body;
yield body.body.filter(is('ClassMethod'));
yield declare(name, node);
yield declare(name, node, params);
return name;
},
* TypeParameterDeclaration(node) {
return yield node.params;
},
* TypeParameter(node) {
return {
name: node.name,
default: node.default ? yield node.default : null,
};
},
/*
* Utility.
*/

View File

@ -20,7 +20,7 @@ class Module {
this._exports.set(name, [scope, reference]);
}
query(name) {
query(name, params) {
const result = this._exports.get(name);
if (!result) {
@ -31,7 +31,7 @@ class Module {
const [scope, reference] = result;
return scope.query(reference);
return scope.query(reference, params);
}
resolve(path) {

View File

@ -37,22 +37,43 @@ class Scope {
return new Scope(this, module || this.module);
}
addDeclaration(name, node) {
addDeclaration(name, node, params) {
assert(!this.entries.has(name));
this.entries.set(name, {
type: 'declaration',
const isTemplate = Boolean(params);
const entry = {
type: isTemplate ? 'template' : 'declaration',
name,
node,
scope: this,
});
};
if (isTemplate) {
entry.params = params;
entry.instances = [];
}
this.entries.set(name, entry);
}
addInstance(name, schema, params) {
const template = this.entries.get(name);
assert(template);
assert.equal(template.type, 'template');
template.instances.push({params, schema});
}
addDefinition(schema, declared) {
const decl = this.entries.get(schema.name);
if (declared) {
assert.equal((this.entries.get(schema.name) || {}).type, 'declaration');
assert(decl);
assert.equal(decl.type, 'declaration');
} else {
assert(!this.entries.has(schema.name));
assert(!decl);
}
this.entries.set(schema.name, {
@ -84,11 +105,26 @@ class Scope {
return this.module.resolve(path);
}
query(name) {
const result = this.entries.get(name);
query(name, params) {
const entry = this.entries.get(name);
if (result) {
return result;
if (entry && entry.type === 'template') {
assert(params);
const augmented = entry.params.map((p, i) => params[i] || p.default);
const schema = findInstance(entry, augmented);
if (schema) {
return {
type: 'definition',
schema,
scope: entry.scope,
};
}
}
if (entry) {
return entry;
}
if (this.parent) {
@ -101,4 +137,17 @@ class Scope {
}
}
function findInstance(template, queried) {
for (const {schema, params} of template.instances) {
// TODO: compare complex structures.
const same = params.every((p, i) => p === queried[i]);
if (same) {
return schema;
}
}
return null;
}
module.exports = Scope;

View File

@ -11,6 +11,11 @@ function partition(iter, predicate) {
return [left, right];
}
function isNode(it) {
return it && typeof it === 'object' && it.type;
}
module.exports = {
partition,
isNode,
};

10
tests/generics.js Normal file
View File

@ -0,0 +1,10 @@
type A<T, K> = {
t: T,
k: K,
};
type X = {
a: A<string, number>,
};
export {X};

20
tests/generics.json Normal file
View File

@ -0,0 +1,20 @@
{
"schemas": [
{
"type": "record",
"name": "A__string_double",
"namespace": "generics",
"fields": [
{"name": "t", "type": "string"},
{"name": "k", "type": "double"}
]
},
{
"type": "record",
"name": "X",
"namespace": "generics",
"fields": [{"name": "a", "type": "A__string_double"}]
}
],
"taskCount": 4
}