@bscotch/gml-parser
Version:
A parser for GML (GameMaker Language) files for programmatic manipulation and analysis of GameMaker projects.
246 lines • 9.39 kB
JavaScript
import { arrayWrapped, isArray } from '@bscotch/utility';
import { Type, TypeStore } from './types.js';
export function isTypeOfKind(item, kind) {
return isTypeInstance(item) && item.kind === kind;
}
export function isTypeStoreOfKind(item, kind) {
return isTypeStore(item) && item.kind === kind;
}
export function isTypeOrStoreOfKind(item, kind) {
return isTypeOfKind(item, kind) || isTypeStoreOfKind(item, kind);
}
/**
* Given some kind of type collection, find the first one matching
* a given kind.
*/
export function getTypeOfKind(from, kind) {
if (!from)
return undefined;
const types = getTypes(from);
const kinds = arrayWrapped(kind);
return types.find((t) => kinds.includes(t.kind));
}
export function getTypesOfKind(from, kind) {
if (!from)
return [];
const types = getTypes(from);
const kinds = arrayWrapped(kind);
return types.filter((t) => kinds.includes(t.kind));
}
/** Get the typestore of item, if present. Else get the type on item. */
export function getTypeStoreOrType(item) {
if (isArray(item)) {
return item;
}
if (item.$tag === 'Sym') {
return item.type;
}
else if (item.$tag === 'TypeStore') {
return item;
}
return arrayWrapped(item);
}
export function getTypes(items) {
const types = [];
if (!items) {
return types;
}
for (const item of arrayWrapped(items)) {
if (item.$tag === 'Sym') {
types.push(...item.type.type);
}
else if (item.$tag === 'TypeStore') {
return [...item.type];
}
else {
types.push(item);
}
}
return types;
}
export function isTypeInstance(item) {
return item && '$tag' in item && item.$tag === 'Type';
}
export function isTypeStore(item) {
return item && '$tag' in item && item.$tag === 'TypeStore';
}
/**
* Returns `true` if `narrowed` is a subtype of `type`,
* meaning that it is a subset/narrowed/compatible/same type
* compared to otherType.
*
* Identical types also return `true`.
*/
export function narrows(narrowType, broadType) {
// Convert to type arrays
const narrowTypes = getTypes(narrowType);
const broadTypes = getTypes(broadType);
// All "narrow" types must be a subtype of at least one "broad" type
return narrowTypes.every((narrow) => broadTypes.some((broad) => narrowsType(narrow, broad)));
}
/**
* Returns `true` if `narrowed` is a subtype of `type`,
* meaning that it is a subset/narrowed/compatible/same type
* compared to otherType.
*
* Identical types also return `true`.
*/
function narrowsType(narrowType, broadType) {
if (narrowType === broadType) {
return true;
}
if (['Mixed', 'Any', 'Unknown'].includes(broadType.kind) ||
['Mixed', 'Any'].includes(narrowType.kind)) {
// Then everyone is satisfied all of the time
return true;
}
if (broadType.kind !== narrowType.kind) {
return false;
}
// The subtype must have all of the same members, though it could also have others
for (const member of broadType.listMembers()) {
const matching = narrowType.getMember(member.name);
if (!matching) {
return false;
}
}
// Similarly, the subtype must have the same params (though it could have others)
for (const param of broadType.listParameters()) {
const matching = !param || narrowType.getParameter(param.idx);
if (!matching) {
return false;
}
}
// Check return type
if (broadType.returns &&
(!narrowType.returns || !narrows(narrowType.returns, broadType.returns))) {
return false;
}
// Check the constructs type
if (broadType.isConstructor &&
(!narrowType.isConstructor || !narrows(narrowType.self, broadType.self))) {
return false;
}
return true;
}
/**
* For types inferred from expressions, normalize away utility
* types like `InstanceType` and `ObjectType`, and perform any
* other type-normalization tasks. Returns a new TypeStore, so
* if maintaining reference links is essential this function should
* not be used.
*/
export function normalizeType(inferred, knownTypes) {
const normalized = new TypeStore();
type: for (const type of getTypes(inferred)) {
if (type.kind === 'EnumMember') {
normalized.addType(type.signifier?.parent || type);
continue;
}
else {
for (const [utilityKind, defaultItemKind] of [
['InstanceType', 'Id.Instance'],
['ObjectType', 'Asset.GMObject'],
['StaticType', 'Struct'],
]) {
if (type.kind !== utilityKind)
continue;
if (!type.items?.type.length) {
normalized.addType(knownTypes.get(defaultItemKind) || new Type(defaultItemKind));
}
for (const itemType of getTypes(type.items || [])) {
// Try to convert the type.
const name = itemType.name
? `${defaultItemKind}.${itemType.name}`
: defaultItemKind;
let type = knownTypes.get(name) ||
knownTypes.get(defaultItemKind) ||
new Type(defaultItemKind);
if (itemType.isGeneric) {
// Then extend the type to allow having a generic without mutating the original
type = type.derive().genericize().named(itemType.name);
}
if (utilityKind === 'StaticType') {
// TODO: Create a new type consisting of the type's static members
type = Type.Struct;
for (const member of itemType.listMembers()) {
if (member.static) {
type.addMember(member);
}
}
}
normalized.addType(type);
}
continue type; // so that the fall-through only happens if we didn't find a match
}
}
normalized.addType(type);
}
return normalized;
}
/**
* Given an expected type that might include generics, and an inferred
* type that should map onto it, update a generics map linking generics
* to inferred types by name.
*/
export function updateGenericsMap(expected, inferred, knownTypes,
/** Map of generics by name to their *inferred type* */
generics = new Map()) {
const expectedTypes = getTypes(expected)
.map((t) => normalizeType(t, knownTypes).type)
.flat();
const inferredTypes = getTypes(inferred)
.map((t) => normalizeType(t, knownTypes).type)
.flat();
// The collection of 1 or more expected types is supposed
// to match up with the collection of 1 or more inferred types.
// For the overlap of compatible types we need to recurse through
// the types and their contained types (stored on the `items` property)
// to identify any generics specified in the expected types that we
// can resolve with the inferred types.
for (const expectedType of expectedTypes) {
let generic;
if (expectedType.isGeneric) {
const genericName = expectedType.name;
generic = generics.get(genericName) || new TypeStore();
generics.set(genericName, generic);
}
for (const inferredType of inferredTypes) {
if (narrowsType(inferredType, expectedType)) {
if (generic) {
// Then we can use this inferred type to resolve the generic
generic.addType(inferredType);
}
// Repeat on contained types, if there are any
if (inferredType.items?.hasTypes && expectedType.items?.hasTypes) {
updateGenericsMap(expectedType.items, inferredType.items, knownTypes, generics);
}
}
}
}
return generics;
}
export function replaceGenerics(startingType, knownTypes, generics) {
const startingTypes = getTypes(startingType);
// Recurse through the types and, if we find a generic, replace it!
// The complication is that we don't want to mutate the starting types, we need to replace them (or their containers!) with a new type. The easiest way to do this is to just create new types from the jump.
const replacedTypes = new TypeStore();
for (const startingType of startingTypes) {
const newTypes = startingType.isGeneric && generics.has(startingType.name)
? generics.get(startingType.name)?.type
: [startingType];
for (const type of newTypes || []) {
const newType = type.derive();
if (type.items) {
newType.items = replaceGenerics(type.items, knownTypes, generics);
}
replacedTypes.addType(newType);
}
}
// Remove 'Any' types if there is something more specific
if (replacedTypes.type.find((t) => t.kind !== 'Any')) {
replacedTypes.type = replacedTypes.type.filter((t) => t.kind !== 'Any');
}
return replacedTypes;
}
//# sourceMappingURL=types.checks.js.map