From 56081d44433ff6434fd46df93a6ee84617062896 Mon Sep 17 00:00:00 2001 From: "Amy J. Ko" Date: Sat, 29 Jun 2024 14:17:17 -0700 Subject: [PATCH] Ensured type errors when a structure definition is given instead of a structure value. --- CHANGELOG.md | 1 + src/basis/BasisConstants.ts | 1 + src/locale/NodeTexts.ts | 3 +- src/locale/en-US.json | 8 ++++ src/nodes/Evaluate.ts | 13 ++++-- src/nodes/Generics.ts | 38 ++++++++-------- src/nodes/NameType.ts | 7 ++- src/nodes/PropertyBind.ts | 10 ++-- src/nodes/StreamDefinitionType.ts | 2 +- src/nodes/StructureDefinition.ts | 5 +- src/nodes/StructureDefinitionType.ts | 68 ++++++++++++++++++++++++++++ src/nodes/StructureType.ts | 7 ++- static/locales/es-MX/es-MX.json | 6 +++ static/locales/example/example.json | 6 +++ static/locales/ko-KR/ko-KR.json | 6 +++ static/locales/zh-CN/zh-CN.json | 6 +++ static/locales/zh-TW/zh-TW.json | 6 +++ static/schemas/Locale.json | 4 ++ static/schemas/Tutorial.json | 1 + 19 files changed, 166 insertions(+), 32 deletions(-) create mode 100644 src/nodes/StructureDefinitionType.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e09f6213c..84f13c9b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Dates are in `YYYY-MM-DD` format and versions are in [semantic versioning](http: - [#503](https://github.com/wordplaydev/wordplay/issues/503). Prevent infinite loops in parser. - [#504](https://github.com/wordplaydev/wordplay/issues/504). Account for non-fixed-width characters in caret positioning. - [#488](https://github.com/wordplaydev/wordplay/issues/488). Added animations off indicator on stage. +- Ensured type errors when a structure definition is given instead of a structure value. ## 0.10.1 2024-06-22 diff --git a/src/basis/BasisConstants.ts b/src/basis/BasisConstants.ts index abbdca93f..8deee0646 100644 --- a/src/basis/BasisConstants.ts +++ b/src/basis/BasisConstants.ts @@ -17,6 +17,7 @@ export type BasisTypeName = | 'column' | 'stream' | 'streamdefinition' + | 'structuredefinition' | 'conversion' | 'function' | 'union' diff --git a/src/locale/NodeTexts.ts b/src/locale/NodeTexts.ts index 46d01b9d3..88a65ba0d 100644 --- a/src/locale/NodeTexts.ts +++ b/src/locale/NodeTexts.ts @@ -170,7 +170,7 @@ type NodeTexts = { DuplicateName: { conflict: ConflictText; resolution: Template; - } + }; /** When a shared bind has a duplicate name that's shared. Description inputs: $1: The duplicate */ DuplicateShare: ConflictText; /** @@ -636,6 +636,7 @@ type NodeTexts = { */ UnimplementedInterface: InternalConflictText; }>; + StructureDefinitionType: DescriptiveNodeText; /** * A table literal, e.g., `⎡a•# b•#⎦⎡1 2⎦` * Description inputs: $1 = the number of rows diff --git a/src/locale/en-US.json b/src/locale/en-US.json index f5d3fdea3..27d35b42f 100644 --- a/src/locale/en-US.json +++ b/src/locale/en-US.json @@ -1050,6 +1050,14 @@ "UnimplementedInterface": "I implement $1 but haven't implemented $2" } }, + "StructureDefinitionType": { + "name": "structure definition", + "emotion": "kind", + "description": "definition of $1", + "doc": [ + "I refer to some kind of @StructureDefinition. People don't usually refer to me directly" + ] + }, "TableLiteral": { "name": "table literal", "description": "$1 row table", diff --git a/src/nodes/Evaluate.ts b/src/nodes/Evaluate.ts index b1969fb57..7af6355c0 100644 --- a/src/nodes/Evaluate.ts +++ b/src/nodes/Evaluate.ts @@ -59,6 +59,8 @@ import NameType from './NameType'; import { NonFunctionType } from './NonFunctionType'; import type Locales from '../locale/Locales'; import UnionType from './UnionType'; +import NoExpressionType from './NoExpressionType'; +import StructureDefinitionType from './StructureDefinitionType'; type Mapping = { expected: Bind; @@ -502,7 +504,12 @@ export default class Evaluate extends Expression { // Figure out what type this expected input is. Resolve any type variables to concrete values. if (given instanceof Expression) { - const givenType = given.getType(context); + // Don't rely on the bind's specified type, since it's not a reliable source of type information. Ask it's value directly. + const givenType = + given instanceof Bind + ? given.value?.getType(context) ?? + new NoExpressionType(given) + : given.getType(context); if (!expectedType.accepts(givenType, context, given)) conflicts.push( new IncompatibleInput( @@ -603,8 +610,8 @@ export default class Evaluate extends Expression { return type instanceof FunctionType && type.definition ? type.definition - : type instanceof StructureType - ? type.definition + : type instanceof StructureDefinitionType + ? type.type.definition : type instanceof StreamDefinitionType ? type.definition : undefined; diff --git a/src/nodes/Generics.ts b/src/nodes/Generics.ts index 65deb6d4d..76994b32d 100644 --- a/src/nodes/Generics.ts +++ b/src/nodes/Generics.ts @@ -17,6 +17,7 @@ import { UnknownVariableType } from './UnknownVariableType'; import type ConversionDefinition from './ConversionDefinition'; import type Convert from './Convert'; import UnionType from './UnionType'; +import StructureType from './StructureType'; export type EvaluationType = Evaluate | BinaryEvaluate | UnaryEvaluate; @@ -33,13 +34,14 @@ export default function getConcreteExpectedType( definition: FunctionDefinition | StructureDefinition | StreamDefinition, input: Bind | undefined, evaluation: EvaluationType, - context: Context + context: Context, ): Type { let type; // If the input is undefined, we're getting the output type of the function or structure. if (input === undefined) { - if (definition instanceof StructureDefinition) - return definition.getType(context); + if (definition instanceof StructureDefinition) { + return new StructureType(definition); + } const functionType = definition.getType(context); if ( !( @@ -55,7 +57,7 @@ export default function getConcreteExpectedType( // Verify that the bind provided exists on the given evaluate. if (!definition.inputs.includes(input)) throw Error( - "The Bind given doesn't exist on this function or structure. Something is broken!" + "The Bind given doesn't exist on this function or structure. Something is broken!", ); // Get the bind's type, resolving any non-type variable references and converting any type variables to variable types. type = input.getType(context); @@ -77,7 +79,7 @@ export default function getConcreteExpectedType( const abstractTypes: (NameType | NumberType)[] = type.nodes( (n): n is NameType | NumberType => (n instanceof NameType && n.isTypeVariable(context)) || - (n instanceof NumberType && n.hasDerivedUnit()) + (n instanceof NumberType && n.hasDerivedUnit()), ); moreAbstractTypes = abstractTypes.length > 0; const nextAbstractType = abstractTypes[0]; @@ -89,12 +91,12 @@ export default function getConcreteExpectedType( nextAbstractType, definition, evaluation, - context + context, ) : getConcreteNumberInput( nextAbstractType, evaluation, - context + context, ); // Clone the current type, replacing the abstract type with the concrete type. type = type.replace(nextAbstractType, concreteType); @@ -116,7 +118,7 @@ export default function getConcreteExpectedType( function getConcreteNumberInput( type: NumberType, evaluation: EvaluationType, - context: Context + context: Context, ) { // If the type is abstract, concretize it. Otherwise, just return the existing concrete type. return type.unit instanceof Function @@ -132,7 +134,7 @@ function getConcreteTypeVariable( type: NameType, definition: FunctionDefinition | StructureDefinition | StreamDefinition, evaluation: EvaluationType, - context: Context + context: Context, ): Type { if (definition instanceof StreamDefinition) return new UnknownVariableType(evaluation); @@ -147,7 +149,7 @@ function getConcreteTypeVariable( // First, the easy case: let's see if the evaluate has a type input that defines this type variable. See if the type for the type variable was provided explicitly in the evaluation. // What is the index of the type variable in the definition? const typeVarIndex = definition.types?.variables.findIndex( - (v) => v === typeVariable + (v) => v === typeVariable, ); if (typeVarIndex !== undefined && evaluation instanceof Evaluate) { const typeInputs = evaluation.getTypeInputs(); @@ -173,7 +175,7 @@ function getConcreteTypeVariable( const structureType = evaluation.fun.structure.getType(context); const typeInput = structureType.resolveTypeVariable( type.getName(), - context + context, ); if (typeInput !== undefined) return typeInput; } @@ -183,7 +185,7 @@ function getConcreteTypeVariable( const structureType = evaluation.left.getType(context); const typeInput = structureType.resolveTypeVariable( type.getName(), - context + context, ); if (typeInput !== undefined) return typeInput; } @@ -202,7 +204,7 @@ function getConcreteTypeVariable( if (input instanceof Bind && input.type !== undefined) return canBeTypeVariable(typeVariable, input.type, context); else return false; - } + }, ); // Let's see if any input is a function with a variable output @@ -215,7 +217,7 @@ function getConcreteTypeVariable( return canBeTypeVariable( typeVariable, input.type.output, - context + context, ); } else return false; }); @@ -246,7 +248,7 @@ function getConcreteTypeVariable( i instanceof Bind && inputWithVariableType .getNames() - .find((n) => i.hasName(n)) !== undefined + .find((n) => i.hasName(n)) !== undefined, ) as Bind | undefined; if (namedInput !== undefined) { // Infer the type of the type variable from the input's value expression. @@ -292,7 +294,7 @@ export function getConcreteConversionTypeVariable( type: NameType, definition: ConversionDefinition, convert: Convert, - context: Context + context: Context, ): Type { // Get the type of the input on the convert. const inputType = convert.expression.getType(context); @@ -308,7 +310,7 @@ export function getConcreteConversionTypeVariable( function canBeTypeVariable( variable: TypeVariable, type: Type, - context: Context + context: Context, ) { const types = type instanceof UnionType ? type.enumerate() : [type]; // Are any of them type variables with a matching name? @@ -316,6 +318,6 @@ function canBeTypeVariable( (type) => type instanceof NameType && type.isTypeVariable(context) && - variable.hasName(type.getName()) + variable.hasName(type.getName()), ); } diff --git a/src/nodes/NameType.ts b/src/nodes/NameType.ts index 4799d97c5..01635fd1e 100644 --- a/src/nodes/NameType.ts +++ b/src/nodes/NameType.ts @@ -168,8 +168,11 @@ export default class NameType extends Type { // Type variable? If it has a constraint, return that type. Otherwise return a variable type. else if (definition instanceof TypeVariable) { if (definition.type) return definition.type; - else return new VariableType(definition); - } + else { + return new VariableType(definition); + } + } else if (definition instanceof StructureDefinition) + return new StructureType(definition, this.types?.types ?? []); // Some other type? Get the definition's type. else return definition.getType(context); } diff --git a/src/nodes/PropertyBind.ts b/src/nodes/PropertyBind.ts index 9a046b2d3..a32498f2d 100644 --- a/src/nodes/PropertyBind.ts +++ b/src/nodes/PropertyBind.ts @@ -28,9 +28,9 @@ import { buildBindings } from './Evaluate'; import ExceptionValue from '@values/ExceptionValue'; import Evaluation from '@runtime/Evaluation'; import StartEvaluation from '@runtime/StartEvaluation'; -import StructureType from './StructureType'; import Bind from './Bind'; import InvalidProperty from '@conflicts/InvalidProperty'; +import StructureDefinitionType from './StructureDefinitionType'; export default class PropertyBind extends Expression { readonly reference: PropertyReference; @@ -116,12 +116,14 @@ export default class PropertyBind extends Expression { const bind = this.reference.resolve(context); if ( bind instanceof Bind && - structureType instanceof StructureType && - !structureType.definition.inputs.some((input) => + structureType instanceof StructureDefinitionType && + !structureType.type.definition.inputs.some((input) => input.names.sharesName(bind.names), ) ) - conflicts.push(new InvalidProperty(structureType.definition, this)); + conflicts.push( + new InvalidProperty(structureType.type.definition, this), + ); return conflicts; } diff --git a/src/nodes/StreamDefinitionType.ts b/src/nodes/StreamDefinitionType.ts index fc16b948d..4e035c6d9 100644 --- a/src/nodes/StreamDefinitionType.ts +++ b/src/nodes/StreamDefinitionType.ts @@ -43,7 +43,7 @@ export default class StreamDefinitionType extends Type { } getBasisTypeName(): BasisTypeName { - return 'function'; + return 'streamdefinition'; } clone() { diff --git a/src/nodes/StructureDefinition.ts b/src/nodes/StructureDefinition.ts index 573733aa6..939eefb24 100644 --- a/src/nodes/StructureDefinition.ts +++ b/src/nodes/StructureDefinition.ts @@ -12,7 +12,6 @@ import type Step from '@runtime/Step'; import StructureDefinitionValue from '@values/StructureDefinitionValue'; import type Context from './Context'; import type Definition from './Definition'; -import StructureType from './StructureType'; import Token from './Token'; import type TypeSet from './TypeSet'; import { UnimplementedInterface } from '@conflicts/UnimplementedInterface'; @@ -41,6 +40,8 @@ import Evaluate from './Evaluate'; import ExpressionPlaceholder from './ExpressionPlaceholder'; import DefinitionExpression from './DefinitionExpression'; import type Locales from '../locale/Locales'; +import StructureDefinitionType from './StructureDefinitionType'; +import StructureType from './StructureType'; export default class StructureDefinition extends DefinitionExpression { readonly docs: Docs | undefined; @@ -449,7 +450,7 @@ export default class StructureDefinition extends DefinitionExpression { } computeType(): Type { - return new StructureType(this, []); + return new StructureDefinitionType(new StructureType(this)); } getDependencies(): Expression[] { diff --git a/src/nodes/StructureDefinitionType.ts b/src/nodes/StructureDefinitionType.ts new file mode 100644 index 000000000..b0e0505d6 --- /dev/null +++ b/src/nodes/StructureDefinitionType.ts @@ -0,0 +1,68 @@ +import Type from './Type'; +import type { BasisTypeName } from '../basis/BasisConstants'; +import type TypeSet from './TypeSet'; +import Glyphs from '../lore/Glyphs'; +import type Spaces from '../parser/Spaces'; +import type Locales from '../locale/Locales'; +import type StructureType from './StructureType'; + +export default class StructureDefinitionType extends Type { + readonly type: StructureType; + + constructor(definition: StructureType) { + super(); + + this.type = definition; + } + + getDescriptor() { + return 'StructureDefinitionType'; + } + + getGrammar() { + return []; + } + + computeConflicts() { + return []; + } + + /** Compatible if it's the same structure definition, or the given type is a refinement of the given structure.*/ + acceptsAll(types: TypeSet): boolean { + return types.list().every((type) => { + if ( + type instanceof StructureDefinitionType && + this.type.definition === type.type.definition + ) + return true; + }); + } + + simplify() { + return this; + } + + getDescriptionInputs(locales: Locales) { + return [locales.getName(this.type.definition.names)]; + } + + getBasisTypeName(): BasisTypeName { + return 'structuredefinition'; + } + + clone() { + return new StructureDefinitionType(this.type) as this; + } + + toWordplay(_: Spaces | undefined) { + return `•${this.type.definition.names.toWordplay(_)}`; + } + + getNodeLocale(locales: Locales) { + return locales.get((l) => l.node.StructureDefinitionType); + } + + getGlyphs() { + return Glyphs.Type; + } +} diff --git a/src/nodes/StructureType.ts b/src/nodes/StructureType.ts index 6ce543410..7b7c3da98 100644 --- a/src/nodes/StructureType.ts +++ b/src/nodes/StructureType.ts @@ -14,6 +14,7 @@ import BasisType from './BasisType'; import type Spaces from '../parser/Spaces'; import type Locales from '../locale/Locales'; import type Bind from './Bind'; +import StructureDefinitionType from './StructureDefinitionType'; export const STRUCTURE_NATIVE_TYPE_NAME = 'structure'; @@ -95,7 +96,11 @@ export default class StructureType extends BasisType { // Are any of the given type's interfaces compatible with this? return ( type.definition.interfaces.find((int) => { - return this.accepts(int.getType(context), context); + const interfaceType = int.getType(context); + return ( + interfaceType instanceof StructureDefinitionType && + this.accepts(interfaceType.type, context) + ); }) !== undefined ); }); diff --git a/static/locales/es-MX/es-MX.json b/static/locales/es-MX/es-MX.json index f8ad1ceea..eaa7aa436 100644 --- a/static/locales/es-MX/es-MX.json +++ b/static/locales/es-MX/es-MX.json @@ -1023,6 +1023,12 @@ "UnimplementedInterface": "Implemento $1 pero no he implementado $2" } }, + "StructureDefinitionType": { + "name": "$?", + "emotion": "$?", + "description": "$?", + "doc": ["$?"] + }, "TableLiteral": { "name": "tabla específica", "description": "tabla de $1 filas", diff --git a/static/locales/example/example.json b/static/locales/example/example.json index 7b7281eda..3bdd58f7f 100644 --- a/static/locales/example/example.json +++ b/static/locales/example/example.json @@ -694,6 +694,12 @@ "UnimplementedInterface": "$?" } }, + "StructureDefinitionType": { + "name": "$?", + "emotion": "$?", + "description": "$?", + "doc": ["$?"] + }, "TableLiteral": { "name": "$?", "description": "$?", diff --git a/static/locales/ko-KR/ko-KR.json b/static/locales/ko-KR/ko-KR.json index 53912cb5e..53217d21c 100644 --- a/static/locales/ko-KR/ko-KR.json +++ b/static/locales/ko-KR/ko-KR.json @@ -1009,6 +1009,12 @@ "UnimplementedInterface": "저는 $1을 구현하지만 $2를 구현하지 않았어요" } }, + "StructureDefinitionType": { + "name": "$?", + "emotion": "$?", + "description": "$?", + "doc": ["$?"] + }, "TableLiteral": { "name": "특정 테이블", "description": "$1 행 테이블", diff --git a/static/locales/zh-CN/zh-CN.json b/static/locales/zh-CN/zh-CN.json index 1b1f8d6fc..b2c9c86c0 100644 --- a/static/locales/zh-CN/zh-CN.json +++ b/static/locales/zh-CN/zh-CN.json @@ -1044,6 +1044,12 @@ "UnimplementedInterface": "我实现了1$,但没有实现2$" } }, + "StructureDefinitionType": { + "name": "$?", + "emotion": "$?", + "description": "$?", + "doc": ["$?"] + }, "TableLiteral": { "name": "表面量", "description": "$1 行表格", diff --git a/static/locales/zh-TW/zh-TW.json b/static/locales/zh-TW/zh-TW.json index 6a0f3ceda..c6081dd78 100644 --- a/static/locales/zh-TW/zh-TW.json +++ b/static/locales/zh-TW/zh-TW.json @@ -1044,6 +1044,12 @@ "UnimplementedInterface": "我實作了1$,但沒有實作2$" } }, + "StructureDefinitionType": { + "name": "$?", + "emotion": "$?", + "description": "$?", + "doc": ["$?"] + }, "TableLiteral": { "name": "表面量", "description": "$1 行表格", diff --git a/static/schemas/Locale.json b/static/schemas/Locale.json index 9e156edf8..dfe68056f 100644 --- a/static/schemas/Locale.json +++ b/static/schemas/Locale.json @@ -5739,6 +5739,9 @@ ], "type": "object" }, + "StructureDefinitionType": { + "$ref": "#/definitions/DescriptiveNodeText" + }, "StructureType": { "$ref": "#/definitions/DescriptiveNodeText", "description": "A structure type, internally generated to represent a structure definition. Description inputs: $1 = name of structure" @@ -6539,6 +6542,7 @@ "Source", "StreamDefinition", "StructureDefinition", + "StructureDefinitionType", "TableLiteral", "TextLiteral", "Translation", diff --git a/static/schemas/Tutorial.json b/static/schemas/Tutorial.json index 3da5cfbd9..a71f4a649 100644 --- a/static/schemas/Tutorial.json +++ b/static/schemas/Tutorial.json @@ -239,6 +239,7 @@ "Source", "StreamDefinition", "StructureDefinition", + "StructureDefinitionType", "TableLiteral", "TextLiteral", "Translation",