diff --git a/CHANGELOG.md b/CHANGELOG.md index 84f13c9b2..7eb7c2199 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Dates are in `YYYY-MM-DD` format and versions are in [semantic versioning](http: - [#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. +- [#500](https://github.com/wordplaydev/wordplay/issues/500). Improved explanation when there's a space between an evaluation's name and inputs. ## 0.10.1 2024-06-22 diff --git a/src/conflicts/SeparatedEvaluate.ts b/src/conflicts/SeparatedEvaluate.ts new file mode 100644 index 000000000..e0a2a769e --- /dev/null +++ b/src/conflicts/SeparatedEvaluate.ts @@ -0,0 +1,38 @@ +import type Context from '@nodes/Context'; +import NodeRef from '@locale/NodeRef'; +import Conflict from './Conflict'; +import concretize from '../locale/concretize'; +import type Locales from '../locale/Locales'; +import type Block from '@nodes/Block'; +import type Reference from '@nodes/Reference'; + +export default class SeparatedEvaluate extends Conflict { + readonly name: Reference; + readonly inputs: Block; + readonly structure: boolean; + + constructor(name: Reference, inputs: Block, structure: boolean) { + super(false); + + this.name = name; + this.inputs = inputs; + this.structure = structure; + } + + getConflictingNodes() { + return { + primary: { + node: this.name, + explanation: (locales: Locales, context: Context) => + concretize( + locales, + locales.get( + (l) => l.node.Evaluate.conflict.SeparatedEvaluate, + ), + new NodeRef(this.name, locales, context), + this.structure, + ), + }, + }; + } +} diff --git a/src/locale/NodeTexts.ts b/src/locale/NodeTexts.ts index 88a65ba0d..c659d0eea 100644 --- a/src/locale/NodeTexts.ts +++ b/src/locale/NodeTexts.ts @@ -368,6 +368,10 @@ type NodeTexts = { * When a list of inputs is given but isn't last. */ InputListMustBeLast: InternalConflictText; + /** + * When something looks like an Evaluate with space + */ + SeparatedEvaluate: InternalConflictText; }> & Exceptions<{ /** diff --git a/src/locale/en-US.json b/src/locale/en-US.json index 27d35b42f..aaecb2888 100644 --- a/src/locale/en-US.json +++ b/src/locale/en-US.json @@ -624,7 +624,8 @@ "primary": "I don't know of an input by this name", "secondary": "I don't think I belong here" }, - "InputListMustBeLast": "list of inputs must be last" + "InputListMustBeLast": "list of inputs must be last", + "SeparatedEvaluate": "Is $1 the name of a $2[$structure|$function] you're trying to evaluate? Try removing the space after me, so I know it's an @Evaluate and not a separate @Block." }, "exception": { "FunctionException": { diff --git a/src/nodes/Evaluate.ts b/src/nodes/Evaluate.ts index 7af6355c0..726ba342d 100644 --- a/src/nodes/Evaluate.ts +++ b/src/nodes/Evaluate.ts @@ -61,6 +61,9 @@ import type Locales from '../locale/Locales'; import UnionType from './UnionType'; import NoExpressionType from './NoExpressionType'; import StructureDefinitionType from './StructureDefinitionType'; +import Block from './Block'; +import Reference from './Reference'; +import SeparatedEvaluate from '@conflicts/SeparatedEvaluate'; type Mapping = { expected: Bind; @@ -422,7 +425,7 @@ export default class Evaluate extends Expression { } computeConflicts(context: Context): Conflict[] { - const conflicts = []; + const conflicts: Conflict[] = []; if (this.close === undefined) conflicts.push( @@ -595,6 +598,53 @@ export default class Evaluate extends Expression { } } + // If there are two consectutive IncompatibleInput conflicts, and the first is a StructureDefinitionType and the next is a block, + // offer to remove the space separating them. + + const possibleEvaluates: [ + Reference, + boolean, + Block, + Conflict, + Conflict, + ][] = []; + conflicts.find((conflict, index, conflicts) => { + const next = conflicts[index + 1]; + if ( + conflict instanceof IncompatibleInput && + (conflict.givenType instanceof StructureDefinitionType || + conflict.givenType instanceof FunctionType) && + next instanceof IncompatibleInput && + next.givenNode instanceof Block + ) { + const ref = conflict.givenNode + .nodes() + .findLast((n): n is Reference => n instanceof Reference); + if (ref instanceof Reference) + possibleEvaluates.push([ + ref, + conflict.givenType instanceof StructureDefinitionType, + next.givenNode, + conflict, + next, + ]); + } + }); + + for (const [ + ref, + structure, + block, + first, + second, + ] of possibleEvaluates) { + // Remove the two conflicts from the list. + conflicts.splice(conflicts.indexOf(first), 1); + conflicts.splice(conflicts.indexOf(second), 1); + // Add a new one. + conflicts.push(new SeparatedEvaluate(ref, block, structure)); + } + return conflicts; } diff --git a/src/parser/Tokens.ts b/src/parser/Tokens.ts index b8f2ae9d3..7cee43f02 100644 --- a/src/parser/Tokens.ts +++ b/src/parser/Tokens.ts @@ -108,7 +108,7 @@ export default class Tokens { return types.find((type) => this.nextIs(type)) !== undefined; } - /** Returns true if and only if the next token is the specified type. */ + /** Returns true if and only if the next token has no preceding space. */ nextLacksPrecedingSpace(): boolean { return this.hasNext() && !this.#spaces.hasSpace(this.#unread[0]); } diff --git a/src/parser/parseExpression.ts b/src/parser/parseExpression.ts index 909fa6c82..dd07cb3da 100644 --- a/src/parser/parseExpression.ts +++ b/src/parser/parseExpression.ts @@ -515,8 +515,7 @@ function parseListAccess(left: Expression, tokens: Tokens): Expression { left = new ListAccess(left, open, index, close); // But wait, is it a function evaluation? - if (nextIsEvaluate(tokens) && tokens.nextLacksPrecedingSpace()) - left = parseEvaluate(left, tokens); + if (nextIsEvaluate(tokens)) left = parseEvaluate(left, tokens); }, () => tokens.nextIs(Sym.ListOpen), ); @@ -573,8 +572,7 @@ function parseSetOrMapAccess(left: Expression, tokens: Tokens): Expression { left = new SetOrMapAccess(left, open, key, close); // But wait, is it a function evaluation? - if (nextIsEvaluate(tokens) && tokens.nextLacksPrecedingSpace()) - left = parseEvaluate(left, tokens); + if (nextIsEvaluate(tokens)) left = parseEvaluate(left, tokens); }, () => tokens.hasNext() && tokens.nextIs(Sym.SetOpen), ); @@ -798,6 +796,7 @@ export function parseStructure(tokens: Tokens): StructureDefinition { } function nextIsEvaluate(tokens: Tokens): boolean { + // If the next token is a line break, then it's not an evaluate. if (!tokens.nextLacksPrecedingSpace()) return false; const rollbackToken = tokens.peek(); @@ -924,11 +923,7 @@ function parsePropertyReference(left: Expression, tokens: Tokens): Expression { } // But wait, is it a function evaluation? - if ( - tokens.nextIsOneOf(Sym.EvalOpen, Sym.TypeOpen) && - tokens.nextLacksPrecedingSpace() - ) - left = parseEvaluate(left, tokens); + if (nextIsEvaluate(tokens)) left = parseEvaluate(left, tokens); }, () => tokens.nextIs(Sym.Access), ); diff --git a/static/locales/es-MX/es-MX.json b/static/locales/es-MX/es-MX.json index eaa7aa436..50939504e 100644 --- a/static/locales/es-MX/es-MX.json +++ b/static/locales/es-MX/es-MX.json @@ -604,7 +604,8 @@ "primary": "No conozco un input con este nombre", "secondary": "No creo que pertenezca aquí" }, - "InputListMustBeLast": "la lista de inputs debe ir al final" + "InputListMustBeLast": "la lista de inputs debe ir al final", + "SeparatedEvaluate": "$?" }, "exception": { "FunctionException": { diff --git a/static/locales/example/example.json b/static/locales/example/example.json index 3bdd58f7f..b1b3a27fa 100644 --- a/static/locales/example/example.json +++ b/static/locales/example/example.json @@ -400,7 +400,8 @@ "NotInstantiable": "$?", "UnexpectedInput": { "primary": "$?", "secondary": "$?" }, "UnknownInput": { "primary": "$?", "secondary": "$?" }, - "InputListMustBeLast": "$?" + "InputListMustBeLast": "$?", + "SeparatedEvaluate": "$?" }, "exception": { "FunctionException": { diff --git a/static/locales/ko-KR/ko-KR.json b/static/locales/ko-KR/ko-KR.json index 53217d21c..3ceca9337 100644 --- a/static/locales/ko-KR/ko-KR.json +++ b/static/locales/ko-KR/ko-KR.json @@ -617,7 +617,8 @@ "primary": "이런 이름의 입력수는 모르는걸", "secondary": "나는 여기에 속하지 않는것 같아" }, - "InputListMustBeLast": "list of inputs must be last" + "InputListMustBeLast": "$?", + "SeparatedEvaluate": "$?" }, "exception": { "FunctionException": { diff --git a/static/locales/zh-CN/zh-CN.json b/static/locales/zh-CN/zh-CN.json index b2c9c86c0..9c7d7f1cf 100644 --- a/static/locales/zh-CN/zh-CN.json +++ b/static/locales/zh-CN/zh-CN.json @@ -625,7 +625,8 @@ "primary": "我不知道这个名字的输入", "secondary": "我觉得我不应该在这里" }, - "InputListMustBeLast": "输入列表必须放在最后" + "InputListMustBeLast": "输入列表必须放在最后", + "SeparatedEvaluate": "$?" }, "exception": { "FunctionException": { diff --git a/static/locales/zh-TW/zh-TW.json b/static/locales/zh-TW/zh-TW.json index c6081dd78..0ce96efa1 100644 --- a/static/locales/zh-TW/zh-TW.json +++ b/static/locales/zh-TW/zh-TW.json @@ -625,7 +625,8 @@ "primary": "我不知道這個名字的輸入", "secondary": "我覺得我不應該在這裡" }, - "InputListMustBeLast": "輸入清單必須放在最後" + "InputListMustBeLast": "輸入清單必須放在最後", + "SeparatedEvaluate": "$?" }, "exception": { "FunctionException": { diff --git a/static/schemas/Locale.json b/static/schemas/Locale.json index dfe68056f..f3708a7e5 100644 --- a/static/schemas/Locale.json +++ b/static/schemas/Locale.json @@ -3495,6 +3495,10 @@ "$ref": "#/definitions/InternalConflictText", "description": "When the structure definition given is an interface, and can't be created" }, + "SeparatedEvaluate": { + "$ref": "#/definitions/InternalConflictText", + "description": "When something looks like an Evaluate with space" + }, "UnexpectedInput": { "$ref": "#/definitions/ConflictText", "description": "When an input value is given but not expected Description inputs: $1 = evaluate with unexected input, $2: unexpected input" @@ -3516,7 +3520,8 @@ "NotInstantiable", "UnexpectedInput", "UnknownInput", - "InputListMustBeLast" + "InputListMustBeLast", + "SeparatedEvaluate" ], "type": "object" },