Repository: FontoXML/fontoxpath (ts)

Issue:
Map lookup operator (?) is type to return a map, causing an error when multiplying it
For example, the query `random-number-generator()?next * 100`  causes an error. While it should just work.

Repro:
 https://xpath.playground.fontoxml.com/?state=H4sIACEO0GIAA6tWys1PSVWyMtBRKkssykxMykktVrJSqq5V0lGqyM0BMm2AlIK%2BHYgfWJpaVAkU0ihKzEvJz9XNK81NSi3STU%2FNSy1KLMkv0tC0hwhpKmgpGBoAtSTn55WkVpTANCrVAgCbm7mncAAAAA%3D%3D

Pull request:
Fix PathExpr attempting to annotate a lookup as a map(*)
That's not always true...

Fixes #510 

Patch:
diff --git a/demo/package-lock.json b/demo/package-lock.json
index f15a5a3e..905630bd 100644
--- a/demo/package-lock.json
+++ b/demo/package-lock.json
@@ -6,6 +6,7 @@
 		"": {
 			"dependencies": {
 				"prsc": "^2.0.1",
+				"typescript": "^4.5.5",
 				"xspattern": "^1.2.1"
 			},
 			"devDependencies": {
@@ -1336,6 +1337,19 @@
 			"integrity": "sha512-7Lp30MtJO7YHZW19yJWhkuJrf+Y78COHopeZli7fiWrbIRvodsSXZIa7cFX/c4PFwFJt55N/7Pp/t6VH0fwBVQ==",
 			"dev": true
 		},
+		"node_modules/acorn": {
+			"version": "7.4.1",
+			"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
+			"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
+			"dev": true,
+			"peer": true,
+			"bin": {
+				"acorn": "bin/acorn"
+			},
+			"engines": {
+				"node": ">=0.4.0"
+			}
+		},
 		"node_modules/aggregate-error": {
 			"version": "3.0.1",
 			"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz",
@@ -1370,6 +1384,13 @@
 				"node": ">=4"
 			}
 		},
+		"node_modules/arg": {
+			"version": "4.1.3",
+			"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+			"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+			"dev": true,
+			"peer": true
+		},
 		"node_modules/babel-plugin-dynamic-import-node": {
 			"version": "2.3.3",
 			"resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz",
@@ -1894,6 +1915,16 @@
 				"node": ">= 0.4"
 			}
 		},
+		"node_modules/diff": {
+			"version": "4.0.2",
+			"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+			"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+			"dev": true,
+			"peer": true,
+			"engines": {
+				"node": ">=0.3.1"
+			}
+		},
 		"node_modules/duplexer3": {
 			"version": "0.1.4",
 			"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
@@ -2029,10 +2060,9 @@
 			"dev": true
 		},
 		"node_modules/fsevents": {
-			"version": "2.1.3",
-			"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
-			"integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
-			"deprecated": "\"Please update to latest v2.3 or v2.2\"",
+			"version": "2.3.2",
+			"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+			"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
 			"dev": true,
 			"hasInstallScript": true,
 			"optional": true,
@@ -2531,6 +2561,13 @@
 				"sourcemap-codec": "^1.4.4"
 			}
 		},
+		"node_modules/make-error": {
+			"version": "1.3.6",
+			"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+			"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+			"dev": true,
+			"peer": true
+		},
 		"node_modules/memory-fs": {
 			"version": "0.5.0",
 			"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz",
@@ -3242,18 +3279,18 @@
 			}
 		},
 		"node_modules/rollup": {
-			"version": "2.8.2",
-			"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.8.2.tgz",
-			"integrity": "sha512-LRzMcB8V1M69pSvf6uCbR+W9OPCy5FuxcIwqioWg5RKidrrqKbzjJF9pEGXceaMVkbptNFZgIVJlUokCU0sfng==",
+			"version": "1.32.1",
+			"resolved": "https://registry.npmjs.org/rollup/-/rollup-1.32.1.tgz",
+			"integrity": "sha512-/2HA0Ec70TvQnXdzynFffkjA6XN+1e2pEv/uKS5Ulca40g2L7KuOE3riasHoNVHOsFD5KKZgDsMk1CP3Tw9s+A==",
 			"dev": true,
+			"peer": true,
+			"dependencies": {
+				"@types/estree": "*",
+				"@types/node": "*",
+				"acorn": "^7.1.0"
+			},
 			"bin": {
 				"rollup": "dist/bin/rollup"
-			},
-			"engines": {
-				"node": ">=10.0.0"
-			},
-			"optionalDependencies": {
-				"fsevents": "~2.1.2"
 			}
 		},
 		"node_modules/rollup-plugin-babel": {
@@ -3398,6 +3435,21 @@
 				"snowpack": "dist-node/index.bin.js"
 			}
 		},
+		"node_modules/snowpack/node_modules/rollup": {
+			"version": "2.77.0",
+			"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.77.0.tgz",
+			"integrity": "sha512-vL8xjY4yOQEw79DvyXLijhnhh+R/O9zpF/LEgkCebZFtb6ELeN9H3/2T0r8+mp+fFTBHZ5qGpOpW2ela2zRt3g==",
+			"dev": true,
+			"bin": {
+				"rollup": "dist/bin/rollup"
+			},
+			"engines": {
+				"node": ">=10.0.0"
+			},
+			"optionalDependencies": {
+				"fsevents": "~2.3.2"
+			}
+		},
 		"node_modules/source-map": {
 			"version": "0.5.7",
 			"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -3608,6 +3660,32 @@
 				"tree-kill": "cli.js"
 			}
 		},
+		"node_modules/ts-node": {
+			"version": "8.10.2",
+			"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz",
+			"integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==",
+			"dev": true,
+			"peer": true,
+			"dependencies": {
+				"arg": "^4.1.0",
+				"diff": "^4.0.1",
+				"make-error": "^1.1.1",
+				"source-map-support": "^0.5.17",
+				"yn": "3.1.1"
+			},
+			"bin": {
+				"ts-node": "dist/bin.js",
+				"ts-node-script": "dist/bin-script.js",
+				"ts-node-transpile-only": "dist/bin-transpile.js",
+				"ts-script": "dist/bin-script-deprecated.js"
+			},
+			"engines": {
+				"node": ">=6.0.0"
+			},
+			"peerDependencies": {
+				"typescript": ">=2.7"
+			}
+		},
 		"node_modules/tslib": {
 			"version": "1.11.2",
 			"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz",
@@ -3643,6 +3721,18 @@
 				"url": "https://github.com/sponsors/sindresorhus"
 			}
 		},
+		"node_modules/typescript": {
+			"version": "4.5.5",
+			"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz",
+			"integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==",
+			"bin": {
+				"tsc": "bin/tsc",
+				"tsserver": "bin/tsserver"
+			},
+			"engines": {
+				"node": ">=4.2.0"
+			}
+		},
 		"node_modules/unicode-canonical-property-names-ecmascript": {
 			"version": "1.0.4",
 			"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
@@ -3902,6 +3992,16 @@
 				"camelcase": "^5.0.0",
 				"decamelize": "^1.2.0"
 			}
+		},
+		"node_modules/yn": {
+			"version": "3.1.1",
+			"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+			"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+			"dev": true,
+			"peer": true,
+			"engines": {
+				"node": ">=6"
+			}
 		}
 	},
 	"dependencies": {
@@ -5010,6 +5110,13 @@
 			"integrity": "sha512-7Lp30MtJO7YHZW19yJWhkuJrf+Y78COHopeZli7fiWrbIRvodsSXZIa7cFX/c4PFwFJt55N/7Pp/t6VH0fwBVQ==",
 			"dev": true
 		},
+		"acorn": {
+			"version": "7.4.1",
+			"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
+			"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
+			"dev": true,
+			"peer": true
+		},
 		"aggregate-error": {
 			"version": "3.0.1",
 			"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz",
@@ -5035,6 +5142,13 @@
 				"color-convert": "^1.9.0"
 			}
 		},
+		"arg": {
+			"version": "4.1.3",
+			"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+			"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+			"dev": true,
+			"peer": true
+		},
 		"babel-plugin-dynamic-import-node": {
 			"version": "2.3.3",
 			"resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz",
@@ -5460,6 +5574,13 @@
 				"object-keys": "^1.0.12"
 			}
 		},
+		"diff": {
+			"version": "4.0.2",
+			"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+			"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+			"dev": true,
+			"peer": true
+		},
 		"duplexer3": {
 			"version": "0.1.4",
 			"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
@@ -5577,9 +5698,9 @@
 			"dev": true
 		},
 		"fsevents": {
-			"version": "2.1.3",
-			"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
-			"integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
+			"version": "2.3.2",
+			"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+			"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
 			"dev": true,
 			"optional": true
 		},
@@ -5979,6 +6100,13 @@
 				"sourcemap-codec": "^1.4.4"
 			}
 		},
+		"make-error": {
+			"version": "1.3.6",
+			"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+			"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+			"dev": true,
+			"peer": true
+		},
 		"memory-fs": {
 			"version": "0.5.0",
 			"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz",
@@ -6535,12 +6663,15 @@
 			}
 		},
 		"rollup": {
-			"version": "2.8.2",
-			"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.8.2.tgz",
-			"integrity": "sha512-LRzMcB8V1M69pSvf6uCbR+W9OPCy5FuxcIwqioWg5RKidrrqKbzjJF9pEGXceaMVkbptNFZgIVJlUokCU0sfng==",
+			"version": "1.32.1",
+			"resolved": "https://registry.npmjs.org/rollup/-/rollup-1.32.1.tgz",
+			"integrity": "sha512-/2HA0Ec70TvQnXdzynFffkjA6XN+1e2pEv/uKS5Ulca40g2L7KuOE3riasHoNVHOsFD5KKZgDsMk1CP3Tw9s+A==",
 			"dev": true,
+			"peer": true,
 			"requires": {
-				"fsevents": "~2.1.2"
+				"@types/estree": "*",
+				"@types/node": "*",
+				"acorn": "^7.1.0"
 			}
 		},
 		"rollup-plugin-babel": {
@@ -6665,6 +6796,17 @@
 				"rollup-plugin-terser": "^5.1.1",
 				"validate-npm-package-name": "^3.0.0",
 				"yargs-parser": "^16.1.0"
+			},
+			"dependencies": {
+				"rollup": {
+					"version": "2.77.0",
+					"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.77.0.tgz",
+					"integrity": "sha512-vL8xjY4yOQEw79DvyXLijhnhh+R/O9zpF/LEgkCebZFtb6ELeN9H3/2T0r8+mp+fFTBHZ5qGpOpW2ela2zRt3g==",
+					"dev": true,
+					"requires": {
+						"fsevents": "~2.3.2"
+					}
+				}
 			}
 		},
 		"source-map": {
@@ -6839,6 +6981,20 @@
 			"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
 			"dev": true
 		},
+		"ts-node": {
+			"version": "8.10.2",
+			"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz",
+			"integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==",
+			"dev": true,
+			"peer": true,
+			"requires": {
+				"arg": "^4.1.0",
+				"diff": "^4.0.1",
+				"make-error": "^1.1.1",
+				"source-map-support": "^0.5.17",
+				"yn": "3.1.1"
+			}
+		},
 		"tslib": {
 			"version": "1.11.2",
 			"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz",
@@ -6860,6 +7016,11 @@
 			"integrity": "sha512-EUV9jo4sffrwlg8s0zDhP0T2WD3pru5Xi0+HTE3zTUmBaZNhfkite9PdSJwdXLwPVW0jnAHT56pZHIOYckPEiw==",
 			"dev": true
 		},
+		"typescript": {
+			"version": "4.5.5",
+			"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz",
+			"integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA=="
+		},
 		"unicode-canonical-property-names-ecmascript": {
 			"version": "1.0.4",
 			"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
@@ -7087,6 +7248,13 @@
 				"camelcase": "^5.0.0",
 				"decamelize": "^1.2.0"
 			}
+		},
+		"yn": {
+			"version": "3.1.1",
+			"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+			"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+			"dev": true,
+			"peer": true
 		}
 	}
 }
diff --git a/demo/package.json b/demo/package.json
index a988eb89..5d2fd910 100644
--- a/demo/package.json
+++ b/demo/package.json
@@ -5,6 +5,7 @@
 	},
 	"dependencies": {
 		"prsc": "^2.0.1",
+		"typescript": "^4.5.5",
 		"xspattern": "^1.2.1"
 	},
 	"devDependencies": {
diff --git a/src/typeInference/annotatePathExpr.ts b/src/typeInference/annotatePathExpr.ts
index 92e22b83..331c85d2 100644
--- a/src/typeInference/annotatePathExpr.ts
+++ b/src/typeInference/annotatePathExpr.ts
@@ -63,7 +63,7 @@ function annotateXPathAxis(axis: string): SequenceType {
 }
 
 function annotateStep(step: IAST, annotationContext: AnnotationContext): SequenceType {
-	let seqType;
+	let seqType: SequenceType = null;
 	const item = { type: ValueType.ITEM, mult: SequenceMultiplicity.ZERO_OR_MORE };
 	if (!step) {
 		return item;
@@ -96,6 +96,10 @@ function annotateStep(step: IAST, annotationContext: AnnotationContext): Sequenc
 				}
 				break;
 			}
+			case 'lookup':
+				// Lookups change the return type
+				seqType = { type: ValueType.ITEM, mult: SequenceMultiplicity.ZERO_OR_MORE };
+				break;
 			case 'attributeTest':
 			case 'anyElementTest':
 			case 'piTest':
@@ -116,7 +120,6 @@ function annotateStep(step: IAST, annotationContext: AnnotationContext): Sequenc
 			case 'Wildcard':
 			case 'predicate':
 			case 'predicates':
-			case 'lookup':
 		}
 	}
 


Test patch:
diff --git a/test/specs/annotation.tests.ts b/test/specs/annotation.tests.ts
index 33bca653..ea77302b 100644
--- a/test/specs/annotation.tests.ts
+++ b/test/specs/annotation.tests.ts
@@ -2,7 +2,9 @@ import * as chai from 'chai';
 import {
 	SequenceMultiplicity,
 	SequenceType,
-	ValueType,
+    sequenceTypeToString,
+    ValueType,
+    valueTypeToString,
 } from 'fontoxpath/expressions/dataTypes/Value';
 import astHelper from 'fontoxpath/parsing/astHelper';
 import parseExpression from 'fontoxpath/parsing/parseExpression';
@@ -32,11 +34,8 @@ function assertValueType(
 		followSpecificPath ? followSpecificPath : ['mainModule', 'queryBody', '*']
 	);
 	const resultType = astHelper.getAttribute(upperNode, 'type') as SequenceType;
-	if (!resultType) {
-		chai.assert.isTrue(expectedType === null || expectedType === undefined);
-	} else {
-		chai.assert.deepEqual(resultType.type, expectedType);
-	}
+
+	chai.assert.equal(resultType && resultType.type, expectedType, `Expected ${resultType ? sequenceTypeToString(resultType) : '"untyped type"'} to equal ${valueTypeToString(expectedType)}`);
 }
 
 describe('Annotating constants', () => {
@@ -92,8 +91,17 @@ describe('Path expression test', () => {
 	it('Path expression test annotate as nodes with test', () => {
 		assertValueType('//*[@someAttribute]', ValueType.NODE, undefined);
 	});
-	it('Path expression test annotate as items with predicate', () => {
-		assertValueType('(array {1,2,3,4,5,6,7})?*[. mod 2 = 1]', ValueType.ARRAY, undefined);
+	it('Path expression test annotate as items with predicate: array with wildcard', () => {
+		assertValueType('(array {1,2,3,4,5,6,7})?*[. mod 2 = 1]', undefined, undefined);
+	});
+	it('Path expression test annotate as items with predicate: array with lookup', () => {
+		assertValueType('(array{1,2,3,4,5,6,7})?1', undefined, undefined);
+	});
+	it('Path expression test annotate as items with predicate: map with wildcard', () => {
+		assertValueType('(map {"a": "b"})?*', undefined, undefined);
+	});
+	it('Path expression test annotate as items with predicate: map with lookup', () => {
+		assertValueType('(map {"a": "b"})?a', undefined, undefined);
 	});
 });
 
diff --git a/test/specs/parsing/postfix/Lookup.tests.ts b/test/specs/parsing/postfix/Lookup.tests.ts
index cef54257..25908fb3 100644
--- a/test/specs/parsing/postfix/Lookup.tests.ts
+++ b/test/specs/parsing/postfix/Lookup.tests.ts
@@ -66,6 +66,15 @@ describe('lookup operator', () => {
 			);
 		});
 
+		it('Annotatest correctly', () => {
+			// This caused a bug because the lookup was type-found to a map.
+			chai.assert.isTrue(
+				evaluateXPathToBoolean(
+					'map { "first" : 2}?first * 10 = 20'
+				)
+			);
+		});
+
 		it('can lookup in an array', () => {
 			chai.assert.isTrue(evaluateXPathToBoolean('[4, 5, 6]?2 eq 5'));
 		});