diff --git a/src/nodes/BasisType.ts b/src/nodes/BasisType.ts index e0ca9f4f4..e5188f1f6 100644 --- a/src/nodes/BasisType.ts +++ b/src/nodes/BasisType.ts @@ -15,15 +15,26 @@ export default abstract class BasisType extends Type { .getStructureDefinition(this.getBasisTypeName()); } + /** All types have the structure type's functions. */ + getAdditionalBasisScope(context: Context): Node | undefined { + return context.getBasis().getStructureDefinition('structure'); + } + /** - * Get the in the basis structure definitions. + * Get basis functions and the structure type functions. */ getDefinitions(_: Node, context: Context): Definition[] { - return ( - context + return [ + // Get the functions defined on the base type + ...(context .getBasis() .getStructureDefinition(this.getBasisTypeName()) - ?.getDefinitions(this) ?? [] - ); + ?.getDefinitions(this) ?? []), + // Include the basis scope functions + ...(this.getAdditionalBasisScope(context)?.getDefinitions( + _, + context + ) ?? []), + ]; } } diff --git a/src/nodes/BinaryEvaluate.test.ts b/src/nodes/BinaryEvaluate.test.ts index 8cd1d5708..fb4f880d9 100644 --- a/src/nodes/BinaryEvaluate.test.ts +++ b/src/nodes/BinaryEvaluate.test.ts @@ -8,8 +8,13 @@ import evaluateCode from '../runtime/evaluate'; test.each([ ['1 · 5', '1 · ""', BinaryEvaluate, IncompatibleInput], - ['(1ms % 5) = 1ms', '(1ms % 5) = 1', BinaryEvaluate, IncompatibleInput, 1], - ['(5ms ÷ 5) = 1ms', '(1ms ÷ 5) = 1', BinaryEvaluate, IncompatibleInput, 1], + [ + '(1ms % 5) = 1ms', + '(1ms % 5) + "hi"', + BinaryEvaluate, + IncompatibleInput, + 1, + ], ['1 + 1', '1 + !', BinaryEvaluate, IncompatibleInput], ['1m + 1m', '1m + 1s', BinaryEvaluate, IncompatibleInput], [ diff --git a/src/nodes/Bind.ts b/src/nodes/Bind.ts index 806860490..df5d6bfcd 100644 --- a/src/nodes/Bind.ts +++ b/src/nodes/Bind.ts @@ -626,6 +626,17 @@ export default class Bind extends Expression { return value; } + /** True if a name and the type matches */ + isEquivalentTo(definition: Definition) { + return ( + definition instanceof Bind && + this.type && + definition.type && + this.type.isEqualTo(definition.type) && + this.sharesName(definition) + ); + } + getNodeLocale(translation: Locale) { return translation.node.Bind; } diff --git a/src/nodes/FunctionDefinition.ts b/src/nodes/FunctionDefinition.ts index c4e738c96..f8a2461c0 100644 --- a/src/nodes/FunctionDefinition.ts +++ b/src/nodes/FunctionDefinition.ts @@ -404,6 +404,22 @@ export default class FunctionDefinition extends DefinitionExpression { ); } + /** True if a name matches, the output matches, and the input type matches. */ + isEquivalentTo(definition: Definition) { + return ( + definition === this || + (definition instanceof FunctionDefinition && + this.output && + definition.output && + this.names.sharesName(definition.names) && + this.output.isEqualTo(definition.output) && + this.inputs.length === definition.inputs.length && + this.inputs.every((input, index) => + input.isEqualTo(definition.inputs[index]) + )) + ); + } + evaluateTypeSet( bind: Bind, original: TypeSet, diff --git a/src/nodes/NameType.ts b/src/nodes/NameType.ts index 261871166..987c46c1a 100644 --- a/src/nodes/NameType.ts +++ b/src/nodes/NameType.ts @@ -129,6 +129,18 @@ export default class NameType extends Type { ); } + /** + * Override get scope to skip over all types, so we don't end up with funky infinite loops with + * other types that might try to resolve NameType. None of the types that might + * contain this can make definitions anyway. + */ + getScope(context: Context): Node | undefined { + return context + .getRoot(this) + ?.getAncestors(this) + .find((node) => !(node instanceof Type)); + } + isTypeVariable(context: Context) { return this.resolve(context) instanceof TypeVariable; } diff --git a/src/nodes/Source.ts b/src/nodes/Source.ts index 777bacd9a..60d5b9a57 100644 --- a/src/nodes/Source.ts +++ b/src/nodes/Source.ts @@ -29,6 +29,7 @@ import Root from './Root'; import Markup from './Markup'; import Purpose from '../concepts/Purpose'; import Tokens from '../parser/Tokens'; +import type Definition from './Definition'; /** A document representing executable Wordplay code and it's various metadata, such as conflicts, tokens, and evaulator. */ export default class Source extends Expression { @@ -127,6 +128,11 @@ export default class Source extends Expression { return true; } + /** Only equal if the same source */ + isEquivalentTo(definition: Definition) { + return definition === this; + } + has(node: Node) { return this.root.has(node); } diff --git a/src/nodes/StreamDefinition.ts b/src/nodes/StreamDefinition.ts index ec30eeadf..2946ce4d1 100644 --- a/src/nodes/StreamDefinition.ts +++ b/src/nodes/StreamDefinition.ts @@ -231,6 +231,11 @@ export default class StreamDefinition extends DefinitionExpression { return current; } + /** Only equal if the same stream definition. */ + isEquivalentTo(definition: Definition) { + return definition === this; + } + getNodeLocale(translation: Locale) { return translation.node.StreamDefinition; } diff --git a/src/nodes/StructureDefinition.ts b/src/nodes/StructureDefinition.ts index 0bfe0d731..6ea293e0d 100644 --- a/src/nodes/StructureDefinition.ts +++ b/src/nodes/StructureDefinition.ts @@ -473,6 +473,11 @@ export default class StructureDefinition extends DefinitionExpression { return this.names; } + /** Only equal if the same structure definition. */ + isEquivalentTo(definition: Definition) { + return definition === this; + } + getNodeLocale(locale: Locale) { return locale.node.StructureDefinition; } diff --git a/src/nodes/StructureType.ts b/src/nodes/StructureType.ts index a7da28384..8f0092915 100644 --- a/src/nodes/StructureType.ts +++ b/src/nodes/StructureType.ts @@ -53,8 +53,15 @@ export default class StructureType extends BasisType { return this.structure.getDefinition(name); } - getDefinitions(node: Node): Definition[] { - return [...this.structure.getDefinitions(node)]; + /** Override to include this structure's definitions, but also the base structure definitions (e.g., =, ≠) */ + getDefinitions(node: Node, context: Context): Definition[] { + return [ + ...this.structure.getDefinitions(node), + ...(this.getAdditionalBasisScope(context)?.getDefinitions( + node, + context + ) ?? []), + ]; } /** Compatible if it's the same structure definition, or the given type is a refinement of the given structure.*/ diff --git a/src/nodes/TypeVariable.ts b/src/nodes/TypeVariable.ts index bd3990230..8980c3d57 100644 --- a/src/nodes/TypeVariable.ts +++ b/src/nodes/TypeVariable.ts @@ -9,6 +9,7 @@ import Type from './Type'; import Sym from './Sym'; import Token from './Token'; import { TYPE_SYMBOL } from '../parser/Symbols'; +import type Definition from './Definition'; export default class TypeVariable extends Node { readonly names: Names; @@ -81,6 +82,11 @@ export default class TypeVariable extends Node { return; } + /** No type variables are ever */ + isEquivalentTo(definition: Definition) { + return definition === this; + } + getNodeLocale(translation: Locale) { return translation.node.TypeVariable; } diff --git a/src/nodes/UnionType.ts b/src/nodes/UnionType.ts index 87e1b2c78..b703023c0 100644 --- a/src/nodes/UnionType.ts +++ b/src/nodes/UnionType.ts @@ -15,6 +15,7 @@ import NoneType from './NoneType'; import Glyphs from '../lore/Glyphs'; import NodeRef from '../locale/NodeRef'; import TypePlaceholder from './TypePlaceholder'; +import type Definition from './Definition'; export default class UnionType extends Type { readonly left: Type; @@ -161,13 +162,32 @@ export default class UnionType extends Type { return []; } - /** Override the base class: basis type scopes are their basis structure definitions. */ - getScope(context: Context): Node | undefined { - // Get the scope of the left and right and only return it if it's the same. - // Otherwise, there is no overlapping scope. - const leftScope = this.left.getScope(context); - const rightScope = this.right.getScope(context); - return leftScope === rightScope ? leftScope : undefined; + getDefinitionsInScope(context: Context): Definition[] { + return this.getDefinitions(this, context); + } + + /** + * Override to search for definitions on both the left and right types, and + * find the intersection of both sets, to allow for a degree of polymorphism. */ + getDefinitions(anchor: Node, context: Context): Definition[] { + // Find the list of definitions in scope of each possible type. + // We generalize because literal types don't affect available definitions. + const definitionSets = this.generalize(context) + .getPossibleTypes(context) + .map((type) => type.getDefinitions(anchor, context)); + + // Find the definitions that intersect across each type's definition list. + // Do this by filtering the first set by all definitions for which all other sets have an equivalent definition. + // This is what allows for polymorphism. + const first = definitionSets[0]; + const rest = definitionSets.slice(1); + return rest.length == 0 + ? first + : first.filter((def1) => + rest.every((definitions) => + definitions.some((def2) => def1.isEquivalentTo(def2)) + ) + ); } getNodeLocale(translation: Locale) {