parser for types + start moving all types to separate modules
This commit is contained in:
parent
8eec5b9239
commit
1d826ea8d4
11 changed files with 277 additions and 88 deletions
15
examples/parser.js
Normal file
15
examples/parser.js
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { parse } from "../parser/parser.js";
|
||||||
|
import { prettyGenT, prettyT } from "../util/pretty.js";
|
||||||
|
|
||||||
|
console.log(prettyT(parse("Int"))); // Int
|
||||||
|
|
||||||
|
console.log(prettyT(parse("Int * Bool"))); // (Int ⨯ Bool)
|
||||||
|
|
||||||
|
console.log(prettyT(parse("(((((((Int)))) => ((Bool)))))"))); // (Int => Bool)
|
||||||
|
|
||||||
|
console.log(prettyT(parse("#0((Int * #0) + Unit)"))) // #0((Int ⨯ #0) + Unit)
|
||||||
|
|
||||||
|
console.log(prettyGenT(parse("∀a: #0((a * #0) + Unit"))); // ∀a: #0((a ⨯ #0) + Unit)
|
||||||
|
|
||||||
|
console.log(prettyGenT(parse("∀a,b,c,d: (a*b) + (c*d)"))); // ∀a,b,c,d: ((a ⨯ b) + (c ⨯ d))
|
||||||
|
|
||||||
|
|
@ -85,3 +85,12 @@ const unified = unify(
|
||||||
makeGeneric(() => linkedListOfInt));
|
makeGeneric(() => linkedListOfInt));
|
||||||
|
|
||||||
console.log(prettyGenT(unified)); // ∀: #0((Int ⨯ #0) + Unit)
|
console.log(prettyGenT(unified)); // ∀: #0((Int ⨯ #0) + Unit)
|
||||||
|
|
||||||
|
// unification (strange case)
|
||||||
|
|
||||||
|
const unified2 = unify(
|
||||||
|
makeGeneric(() => listOfSetOfSelf),
|
||||||
|
genericList,
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(prettyGenT(unified2)); // ∀: #0[{#0}]
|
||||||
|
|
@ -96,7 +96,7 @@ export const mergeTwoWay = (m1, m2) => {
|
||||||
// fType, aType: generic types to unify
|
// fType, aType: generic types to unify
|
||||||
// fStack, aStack: internal use.
|
// fStack, aStack: internal use.
|
||||||
const __unify = (typeVars, fType, aType, fStack=[], aStack=[]) => {
|
const __unify = (typeVars, fType, aType, fStack=[], aStack=[]) => {
|
||||||
console.log("__unify", {typeVars, fType, aType, fStack, aStack});
|
// console.log("__unify", {typeVars, fType, aType, fStack, aStack});
|
||||||
if (typeVars.has(fType)) {
|
if (typeVars.has(fType)) {
|
||||||
// simplest case: formalType is a type paramater
|
// simplest case: formalType is a type paramater
|
||||||
// => substitute with actualType
|
// => substitute with actualType
|
||||||
|
|
@ -182,12 +182,13 @@ const __unify = (typeVars, fType, aType, fStack=[], aStack=[]) => {
|
||||||
export const unify = (fGenericType, aGenericType) => {
|
export const unify = (fGenericType, aGenericType) => {
|
||||||
let allTypeVars;
|
let allTypeVars;
|
||||||
[allTypeVars, fGenericType, aGenericType] = safeUnionTypeVars(fGenericType, aGenericType);
|
[allTypeVars, fGenericType, aGenericType] = safeUnionTypeVars(fGenericType, aGenericType);
|
||||||
const {genericType} = __unify(allTypeVars, fGenericType.type, aGenericType.type);
|
const {genericType, substitutions} = __unify(allTypeVars, fGenericType.type, aGenericType.type);
|
||||||
|
// console.log('unification complete! substitutions:', substitutions);
|
||||||
return recomputeTypeVars(genericType);
|
return recomputeTypeVars(genericType);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const substitute = (type, substitutions, stack=[]) => {
|
export const substitute = (type, substitutions, stack=[]) => {
|
||||||
console.log('substitute...', {type, substitutions, stack});
|
// console.log('substitute...', {type, substitutions, stack});
|
||||||
return substitutions.get(type)
|
return substitutions.get(type)
|
||||||
|| {
|
|| {
|
||||||
symbol: type.symbol,
|
symbol: type.symbol,
|
||||||
|
|
|
||||||
174
parser/parser.js
Normal file
174
parser/parser.js
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
import { Bool, Char, Double, Int, Unit } from "../primitives/types.js";
|
||||||
|
import { dictType, fnType, lsType, prodType, setType, sumType } from "../structures/types.js";
|
||||||
|
|
||||||
|
const bracketOperators = new Map([
|
||||||
|
['(', [')', null]],
|
||||||
|
['[', [']', lsType]],
|
||||||
|
['{', ['}', setType]],
|
||||||
|
|
||||||
|
// can only occur at beginning
|
||||||
|
// we use these to extract the type variables
|
||||||
|
['∀', [':', null]],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const infixOperators = new Map([
|
||||||
|
['+', sumType],
|
||||||
|
['|', sumType],
|
||||||
|
['⨯', prodType],
|
||||||
|
['*', prodType],
|
||||||
|
['→', fnType],
|
||||||
|
['->', fnType],
|
||||||
|
['⇒', dictType],
|
||||||
|
['=>', dictType],
|
||||||
|
|
||||||
|
// only used for type variables (e.g., ∀a,b,c:)
|
||||||
|
[',', fnX => fnY => {
|
||||||
|
const x = fnX();
|
||||||
|
const y = fnY();
|
||||||
|
return Array.isArray(x) ? x.concat(y) : [x].concat(y)
|
||||||
|
}],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const a = Symbol('a');
|
||||||
|
const b = Symbol('b');
|
||||||
|
const c = Symbol('c');
|
||||||
|
const d = Symbol('d');
|
||||||
|
const e = Symbol('e');
|
||||||
|
|
||||||
|
const primitives = new Map([
|
||||||
|
['Int', Int],
|
||||||
|
['Double', Double],
|
||||||
|
['Bool', Bool],
|
||||||
|
['Char', Char],
|
||||||
|
['Unit', Unit],
|
||||||
|
['a', a],
|
||||||
|
['b', b],
|
||||||
|
['c', c],
|
||||||
|
['d', d],
|
||||||
|
['e', e],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const TOKENS = [
|
||||||
|
...bracketOperators.keys(),
|
||||||
|
...[...bracketOperators.values()].map(v => v[0]),
|
||||||
|
...infixOperators.keys(),
|
||||||
|
...primitives.keys(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// console.log('TOKENS =', TOKENS);
|
||||||
|
|
||||||
|
const tokenize = expr => {
|
||||||
|
const tokens = [];
|
||||||
|
let i=0;
|
||||||
|
outerloop: while (i<expr.length) {
|
||||||
|
if (/\s/.test(expr[i])) {
|
||||||
|
i++;
|
||||||
|
continue outerloop; // skip whitespace
|
||||||
|
}
|
||||||
|
if (expr[i] === '#') {
|
||||||
|
const label = '#' + parseInt(expr.slice(i+1));
|
||||||
|
tokens.push(label);
|
||||||
|
i += label.length;
|
||||||
|
continue outerloop;
|
||||||
|
}
|
||||||
|
for (const token of TOKENS) {
|
||||||
|
if (expr.startsWith(token, i)) {
|
||||||
|
tokens.push(token);
|
||||||
|
i += token.length;
|
||||||
|
continue outerloop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`Couldn't match any token at position ${i} in\n ${expr}\n ${' '.repeat(i)}^`);
|
||||||
|
}
|
||||||
|
// console.log({tokens});
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
const consumeGroup = (tokens) => {
|
||||||
|
const bracket = bracketOperators.get(tokens[0]);
|
||||||
|
if (bracket === undefined) {
|
||||||
|
// no group, just a single token:
|
||||||
|
const [firstToken, ...rest] = tokens;
|
||||||
|
return [[firstToken], null, rest];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// find where group ends:
|
||||||
|
const [closing, fn] = bracket;
|
||||||
|
const opening = tokens[0]
|
||||||
|
let depth = 1;
|
||||||
|
let i = 1;
|
||||||
|
for (; i<tokens.length; i++) {
|
||||||
|
if (tokens[i] === opening) {
|
||||||
|
depth++;
|
||||||
|
}
|
||||||
|
else if (tokens[i] === closing) {
|
||||||
|
depth--;
|
||||||
|
}
|
||||||
|
if (depth === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const tokensInGroup = tokens.slice(1, i); // don't include brackets
|
||||||
|
const rest = tokens.slice(i+1);
|
||||||
|
return [tokensInGroup, fn, rest];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseGroup = (tokensInGroup, fn, labels, label) => {
|
||||||
|
// console.log('parseGroup ', tokensInGroup, fn);
|
||||||
|
return (fn === null)
|
||||||
|
? __parse(tokensInGroup, labels, label)
|
||||||
|
: fn(self => {
|
||||||
|
return __parse(tokensInGroup, extendLabels(labels, label, self));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const extendLabels = (labels, label, self) => {
|
||||||
|
return (label === null) ? labels : new Map([...labels, [label, self]])
|
||||||
|
};
|
||||||
|
|
||||||
|
const __parse = (tokens, labels = new Map(), label = null) => {
|
||||||
|
// console.log('parse ', tokens);
|
||||||
|
if (tokens[0].startsWith('#')) {
|
||||||
|
if (labels.has(tokens[0])) {
|
||||||
|
return labels.get(tokens[0]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// pass label and parse 'rest'
|
||||||
|
return __parse(tokens.slice(1), labels, tokens[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tokens.length === 1) {
|
||||||
|
return primitives.get(tokens[0]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const [lhsTokens, fnGrp, rest] = consumeGroup(tokens);
|
||||||
|
if (rest.length === 0) {
|
||||||
|
return parseGroup(lhsTokens, fnGrp, labels, label);
|
||||||
|
}
|
||||||
|
const [operator, ...rhsTokens] = rest;
|
||||||
|
for (const [operatorChar, fn] of infixOperators) {
|
||||||
|
if (operator === operatorChar) {
|
||||||
|
return fn
|
||||||
|
(self => {
|
||||||
|
return parseGroup(lhsTokens, fnGrp, extendLabels(labels, label, self));
|
||||||
|
})(self => {
|
||||||
|
return __parse(rhsTokens, extendLabels(labels, label, self));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("unknown operator: "+operator)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parse = expr => {
|
||||||
|
const tokens = tokenize(expr);
|
||||||
|
if (tokens[0] === '∀') {
|
||||||
|
// generic type
|
||||||
|
const [typeVarTokens, _, rest] = consumeGroup(tokens);
|
||||||
|
const typeVars = [].concat(__parse(typeVarTokens))
|
||||||
|
const type = __parse(rest);
|
||||||
|
return { typeVars, type };
|
||||||
|
}
|
||||||
|
return __parse(tokens);
|
||||||
|
}
|
||||||
|
|
@ -3,52 +3,8 @@
|
||||||
// A Product-type always has only two fields, called "left" and "right".
|
// A Product-type always has only two fields, called "left" and "right".
|
||||||
// Product-types of more fields (called Structs) can be constructed by nesting Product-types.
|
// Product-types of more fields (called Structs) can be constructed by nesting Product-types.
|
||||||
|
|
||||||
import { makeGeneric } from "../generics/generics.js";
|
|
||||||
import { GenericType, Type } from "../primitives/types.js";
|
|
||||||
import { typedFnType } from "./types.js";
|
|
||||||
import { prodType } from "./types.js";
|
|
||||||
|
|
||||||
// In JS, all products are encoded in the same way:
|
// In JS, all products are encoded in the same way:
|
||||||
export const newProduct = l => r => ({l, r});
|
export const newProduct = l => r => ({l, r});
|
||||||
export const getLeft = product => product.l;
|
export const getLeft = product => product.l;
|
||||||
export const getRight = product => product.r;
|
export const getRight = product => product.r;
|
||||||
|
|
||||||
export const ModuleProduct = {l: [
|
|
||||||
// binary type constructor
|
|
||||||
// Type -> Type -> Type
|
|
||||||
...typedFnType(prodType, fnType =>
|
|
||||||
fnType
|
|
||||||
(Type)
|
|
||||||
(fnType
|
|
||||||
(Type)
|
|
||||||
(Type)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
|
|
||||||
// a -> b -> (a, b)
|
|
||||||
...typedFnType(newProduct, fnType =>
|
|
||||||
makeGeneric((a, b) =>
|
|
||||||
fnType
|
|
||||||
(a)
|
|
||||||
(fnType
|
|
||||||
(b)
|
|
||||||
(prodType(() => a)(() => b))
|
|
||||||
)
|
|
||||||
), GenericType),
|
|
||||||
|
|
||||||
// (a, b) -> a
|
|
||||||
...typedFnType(getLeft, fnType =>
|
|
||||||
makeGeneric((a, b) =>
|
|
||||||
fnType
|
|
||||||
(prodType(() => a)(() => b))
|
|
||||||
(a)
|
|
||||||
), GenericType),
|
|
||||||
|
|
||||||
// (a, b) -> b
|
|
||||||
...typedFnType(getRight, fnType =>
|
|
||||||
makeGeneric((a, b) =>
|
|
||||||
fnType
|
|
||||||
(prodType(() => a)(() => b))
|
|
||||||
(b)
|
|
||||||
), GenericType),
|
|
||||||
]};
|
|
||||||
|
|
|
||||||
27
structures/product.types.js
Normal file
27
structures/product.types.js
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { makeGeneric } from "../generics/generics.js";
|
||||||
|
import { Type, GenericType } from "../primitives/types.js";
|
||||||
|
import { newProduct, getLeft, getRight } from "./product.js";
|
||||||
|
import { typedFnType, prodType } from "./types.js";
|
||||||
|
|
||||||
|
export const ModuleProduct = {
|
||||||
|
l: [
|
||||||
|
// binary type constructor
|
||||||
|
// Type -> Type -> Type
|
||||||
|
...typedFnType(prodType, fnType => fnType(Type)(fnType(Type)(Type)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
// a -> b -> (a, b)
|
||||||
|
...typedFnType(newProduct, fnType => makeGeneric((a, b) => fnType(a)(fnType(b)(prodType(() => a)(() => b))
|
||||||
|
)
|
||||||
|
), GenericType),
|
||||||
|
|
||||||
|
// (a, b) -> a
|
||||||
|
...typedFnType(getLeft, fnType => makeGeneric((a, b) => fnType(prodType(() => a)(() => b))(a)
|
||||||
|
), GenericType),
|
||||||
|
|
||||||
|
// (a, b) -> b
|
||||||
|
...typedFnType(getRight, fnType => makeGeneric((a, b) => fnType(prodType(() => a)(() => b))(b)
|
||||||
|
), GenericType),
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
@ -1,7 +1,3 @@
|
||||||
import { fnType, setType } from "./types.js";
|
|
||||||
import { Int } from "../primitives/types.js";
|
|
||||||
import { makeGeneric } from "../generics/generics.js";
|
|
||||||
|
|
||||||
import createRBTree from "functional-red-black-tree";
|
import createRBTree from "functional-red-black-tree";
|
||||||
import { inspect } from "node:util";
|
import { inspect } from "node:util";
|
||||||
|
|
||||||
|
|
@ -9,6 +5,7 @@ export class RBTreeWrapper {
|
||||||
constructor(tree) {
|
constructor(tree) {
|
||||||
this.tree = tree;
|
this.tree = tree;
|
||||||
}
|
}
|
||||||
|
// pretty print to console
|
||||||
[inspect.custom](depth, options, inspect) {
|
[inspect.custom](depth, options, inspect) {
|
||||||
const entries = [];
|
const entries = [];
|
||||||
this.tree.forEach((key,val) => {entries.push(`${inspect(key)} => ${inspect(val)}`);});
|
this.tree.forEach((key,val) => {entries.push(`${inspect(key)} => ${inspect(val)}`);});
|
||||||
|
|
@ -19,7 +16,6 @@ export class RBTreeWrapper {
|
||||||
// (a -> a -> Int) -> Set(a)
|
// (a -> a -> Int) -> Set(a)
|
||||||
export const emptySet = compareFn => new RBTreeWrapper(createRBTree((x, y) => compareFn(x)(y)));
|
export const emptySet = compareFn => new RBTreeWrapper(createRBTree((x, y) => compareFn(x)(y)));
|
||||||
|
|
||||||
// const emptySetType = makeGeneric(a => fnType(() => fnType(a)(fnType(a)(Int)))(() => set(() => a)));
|
|
||||||
|
|
||||||
export const has = set => key => set.tree.get(key) === true;
|
export const has = set => key => set.tree.get(key) === true;
|
||||||
export const add = set => key => set.tree.get(key) === true ? set : new RBTreeWrapper(set.tree.insert(key, true));
|
export const add = set => key => set.tree.get(key) === true ? set : new RBTreeWrapper(set.tree.insert(key, true));
|
||||||
|
|
@ -42,33 +38,3 @@ export const read = iter => ifNotDone => ifDone => {
|
||||||
export const forEach = set => fn => {
|
export const forEach = set => fn => {
|
||||||
set.tree.forEach(key => { fn(key); });
|
set.tree.forEach(key => { fn(key); });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ModuleSet = {l:[
|
|
||||||
// // Type -> Type
|
|
||||||
// ...typedFnType(setType, fnType =>
|
|
||||||
// fnType
|
|
||||||
// /* in */ (Type)
|
|
||||||
// /* out */ (Type)
|
|
||||||
// ),
|
|
||||||
|
|
||||||
// {i: emptySet , t: emptySetType},
|
|
||||||
// {i: emptySetType, t: GenericType },
|
|
||||||
|
|
||||||
// ...typedFnType(has, fnType =>
|
|
||||||
// makeGeneric(a =>
|
|
||||||
// fnType
|
|
||||||
// /* in */ (set(() => a))
|
|
||||||
// /* out */ (fnType
|
|
||||||
// /* in */ (a)
|
|
||||||
// /* out */ (Bool)
|
|
||||||
// )), GenericType),
|
|
||||||
|
|
||||||
// ...typedFnType(add, fnType =>
|
|
||||||
// makeGeneric(a =>
|
|
||||||
// fnType
|
|
||||||
// /* in */ (set(() => a))
|
|
||||||
// /* out */ (fnType
|
|
||||||
// /* in */ (a)
|
|
||||||
// /* out */ (set(() => a))
|
|
||||||
// )), GenericType),
|
|
||||||
]};
|
|
||||||
|
|
|
||||||
31
structures/set.types.js
Normal file
31
structures/set.types.js
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { makeGeneric } from "../generics/generics.js";
|
||||||
|
import { Int } from "../primitives/types.js";
|
||||||
|
import { emptySet, has, add } from "./set.js";
|
||||||
|
import { fnType, setType } from "./types.js";
|
||||||
|
|
||||||
|
const emptySetType = makeGeneric(a =>
|
||||||
|
fnType
|
||||||
|
// comparison function:
|
||||||
|
(_ => fnType
|
||||||
|
(_ => a)
|
||||||
|
(_ => fnType(_ => a)(_ => Int)))
|
||||||
|
// the set:
|
||||||
|
(_ => setType(_ => a))
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ModuleSet = {
|
||||||
|
l: [
|
||||||
|
// Type -> Type
|
||||||
|
...typedFnType(setType, fnType => fnType(_ => Type)(_ => Type)
|
||||||
|
),
|
||||||
|
|
||||||
|
{ i: emptySet, t: emptySetType },
|
||||||
|
{ i: emptySetType, t: GenericType },
|
||||||
|
|
||||||
|
...typedFnType(has, fnType => makeGeneric(a => fnType(_ => setType(_ => a))(_ => fnType(_ => a)(_ => Bool)
|
||||||
|
)), GenericType),
|
||||||
|
|
||||||
|
...typedFnType(add, fnType => makeGeneric(a => fnType(setType(_ => a))(fnType(a)(setType(_ => a))
|
||||||
|
)), GenericType),
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
@ -52,3 +52,8 @@ export const lsType = makeTypeConstructor(symbolList)(1);
|
||||||
|
|
||||||
export const symbolSet = Symbol('Set');
|
export const symbolSet = Symbol('Set');
|
||||||
export const setType = makeTypeConstructor(symbolSet)(1);
|
export const setType = makeTypeConstructor(symbolSet)(1);
|
||||||
|
|
||||||
|
// Dict type
|
||||||
|
|
||||||
|
export const symbolDict = Symbol('Dict');
|
||||||
|
export const dictType = makeTypeConstructor(symbolDict)(2);
|
||||||
|
|
@ -39,12 +39,16 @@ const __makeTypeConstructor = (symbol, nAry, params) => {
|
||||||
if (nAry === 0) {
|
if (nAry === 0) {
|
||||||
return { symbol, params };
|
return { symbol, params };
|
||||||
}
|
}
|
||||||
return typeParam => {
|
// only for debugging, do we give the function a name
|
||||||
if (typeof typeParam !== 'function') {
|
const fName = `${symbol.description.toLowerCase()}Type${params.length>0?params.length:''}`;
|
||||||
throw new Error("all type params must be functions");
|
return {
|
||||||
|
[fName]: typeParam => {
|
||||||
|
if (typeof typeParam !== 'function') {
|
||||||
|
throw new Error("all type params must be functions");
|
||||||
|
}
|
||||||
|
return __makeTypeConstructor(symbol, nAry-1, params.concat([typeParam]));
|
||||||
}
|
}
|
||||||
return __makeTypeConstructor(symbol, nAry-1, params.concat([typeParam]));
|
}[fName];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new nominal type
|
// Creates a new nominal type
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { inspect } from 'node:util';
|
import { inspect } from 'node:util';
|
||||||
import { symbolFunction, symbolList, symbolProduct, symbolSet, symbolSum } from '../structures/types.js';
|
import { symbolDict, symbolFunction, symbolList, symbolProduct, symbolSet, symbolSum } from '../structures/types.js';
|
||||||
import { mapRecursiveStructure } from './util.js';
|
import { mapRecursiveStructure } from './util.js';
|
||||||
|
|
||||||
export function pretty(obj) {
|
export function pretty(obj) {
|
||||||
|
|
@ -35,6 +35,7 @@ const renderType = (symbol, annot, params) => {
|
||||||
[symbolFunction]: `${annot}(${params[0]} -> ${params[1]})`,
|
[symbolFunction]: `${annot}(${params[0]} -> ${params[1]})`,
|
||||||
[symbolSum] : `${annot}(${params[0]} + ${params[1]})`,
|
[symbolSum] : `${annot}(${params[0]} + ${params[1]})`,
|
||||||
[symbolProduct] : `${annot}(${params[0]} ⨯ ${params[1]})`,
|
[symbolProduct] : `${annot}(${params[0]} ⨯ ${params[1]})`,
|
||||||
|
[symbolDict] : `${annot}(${params[0]} => ${params[1]})`,
|
||||||
}[symbol] || symbol.description;
|
}[symbol] || symbol.description;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue