Repository: FontoXML/fontoxpath
Pull request #620 (2023-11-21 10:14:24)

Implement switch expression

bundlemon[bot]: <!-- bundlemon -->
## BundleMon

  
  
  
  
  
<details open>
<summary>Files updated (2)</summary>

Status | Path | Size | Limits
:------------: | ------------ | :------------: | :------------:
:white_check_mark: | fontoxpath.js<br/> | 77.72KB (+201B +0.25%) | -
:white_check_mark: | fontoxpath.esm.js<br/> | 77.8KB (+195B +0.25%) | -
</details>

  
  


Total files change +396B +0.25%

Final result: :white_check_mark:

[View report in BundleMon website ➡️](https://app.bundlemon.dev/projects/6437c8f89a7cb205c434db8d/reports/655c8337dbe5c628041835f0)

---
<p align="center"><a href="https://app.bundlemon.dev/projects/6437c8f89a7cb205c434db8d/reports?branch=switchexpr&resolution=all" target="_blank" rel="noreferrer noopener">Current branch size history</a> | <a href="https://app.bundlemon.dev/projects/6437c8f89a7cb205c434db8d/reports?branch=master&resolution=days" target="_blank" rel="noreferrer noopener">Target branch size history</a></p>
coveralls: 
[![Coverage Status](https://coveralls.io/builds/64086755/badge)](https://coveralls.io/builds/64086755)

coverage: 91.59% (+0.01%) from 91.58%
when pulling **98dcd59b52cc4a9a6b76133678fcee4fde3ffa7a on switchexpr**
into **dd07d62fd1dea7c171d208f63ac0aa9d38b70719 on master**.


Patch:
diff --git a/package-lock.json b/package-lock.json
index c4ab928c..95df4494 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
 	"name": "fontoxpath",
-	"version": "3.30.2",
+	"version": "3.30.3",
 	"lockfileVersion": 2,
 	"requires": true,
 	"packages": {
 		"": {
 			"name": "fontoxpath",
-			"version": "3.30.2",
+			"version": "3.30.3",
 			"license": "MIT",
 			"dependencies": {
 				"prsc": "4.0.0",
diff --git a/package.json b/package.json
index 00bf11ae..b16373e9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "fontoxpath",
-	"version": "3.30.2",
+	"version": "3.30.3",
 	"description": "A minimalistic XPath 3.1 engine in JavaScript",
 	"main": "dist/fontoxpath.js",
 	"module": "dist/fontoxpath.esm.js",
diff --git a/src/expressions/dataTypes/Value.ts b/src/expressions/dataTypes/Value.ts
index 857fb6a6..311b94c3 100644
--- a/src/expressions/dataTypes/Value.ts
+++ b/src/expressions/dataTypes/Value.ts
@@ -81,10 +81,11 @@ export const enum ValueType {
 }
 
 /**
- * Handles the occurances in the XPath specs.
- * Zero or one matches to '?';
- * One or more matches to '+';
- * Zero or more matches to '*'
+ * Handles the occurences in the XPath specs.
+ *
+ * * Zero or one matches to '?';
+ * * One or more matches to '+';
+ * * Zero or more matches to '*';
  *
  * @private
  */
diff --git a/src/expressions/xquery/SwitchExpression.ts b/src/expressions/xquery/SwitchExpression.ts
new file mode 100644
index 00000000..fb77e6f8
--- /dev/null
+++ b/src/expressions/xquery/SwitchExpression.ts
@@ -0,0 +1,135 @@
+import ISequence from '../dataTypes/ISequence';
+import sequenceFactory from '../dataTypes/sequenceFactory';
+import { SequenceType } from '../dataTypes/Value';
+import DynamicContext from '../DynamicContext';
+import ExecutionParameters from '../ExecutionParameters';
+import Expression, { RESULT_ORDERINGS } from '../Expression';
+import sequenceDeepEqual, {
+	itemDeepEqual,
+} from '../functions/builtInFunctions_sequences_deepEqual';
+import PossiblyUpdatingExpression, { SequenceCallbacks } from '../PossiblyUpdatingExpression';
+import Specificity from '../Specificity';
+import StaticContext from '../StaticContext';
+import atomizeSequence from '../util/atomizeSequence';
+import { IterationHint } from '../util/iterators';
+import { errXUST0001 } from '../xquery-update/XQueryUpdateFacilityErrors';
+import { errXPTY0004 } from './XQueryErrors';
+
+export type SwitchExpressionClause = {
+	caseClauseExpression: PossiblyUpdatingExpression;
+	tests: Expression[];
+};
+
+class SwitchExpression extends PossiblyUpdatingExpression {
+	private _amountOfCases: number;
+	private _argExpression: Expression;
+	private _testsByCase: Expression[][];
+
+	constructor(
+		argExpression: Expression,
+		caseClauses: SwitchExpressionClause[],
+		defaultExpression: PossiblyUpdatingExpression,
+		type: SequenceType,
+	) {
+		const specificity = new Specificity({});
+		super(
+			specificity,
+			[
+				argExpression,
+				defaultExpression,
+				...caseClauses.map((clause) => clause.caseClauseExpression),
+			].concat(...caseClauses.map((clause) => clause.tests.map((test) => test))),
+			{
+				canBeStaticallyEvaluated: false,
+				peer: false,
+				resultOrder: RESULT_ORDERINGS.UNSORTED,
+				subtree: false,
+			},
+			type,
+		);
+		this._argExpression = argExpression;
+		this._amountOfCases = caseClauses.length;
+		this._testsByCase = caseClauses.map((clause) => clause.tests);
+	}
+
+	public performFunctionalEvaluation(
+		dynamicContext: DynamicContext,
+		executionParameters: ExecutionParameters,
+		sequenceCallbacks: ((dynamicContext: DynamicContext) => ISequence)[],
+	) {
+		// Pick the argumentExpression.
+		const evaluatedExpression = atomizeSequence(
+			sequenceCallbacks[0](dynamicContext),
+			executionParameters,
+		);
+		const [
+			_argSequenceCallback,
+			defaultSequenceCallback,
+			...caseClauseAndTestSequenceCallbacks
+		] = sequenceCallbacks;
+		// Map over all values the type test, and return the result.
+		return evaluatedExpression.switchCases({
+			multiple: () => {
+				throw errXPTY0004(
+					'The operand for a switch expression should result in zero or one item',
+				);
+			},
+			default: () => {
+				const singleValue = evaluatedExpression.first();
+				const isEmpty = !singleValue;
+
+				for (let i = 0; i < this._amountOfCases; i++) {
+					const results = this._testsByCase[i].map((x) =>
+						x.evaluateMaybeStatically(dynamicContext, executionParameters),
+					);
+
+					for (const result of results) {
+						const atomizedResult = atomizeSequence(result, executionParameters);
+						if (atomizedResult.isEmpty()) {
+							if (isEmpty) {
+								return caseClauseAndTestSequenceCallbacks[i](dynamicContext);
+							}
+							continue;
+						}
+
+						if (!atomizedResult.isSingleton()) {
+							throw errXPTY0004(
+								'The operand for a switch case should result in zero or one item',
+							);
+						}
+
+						if (isEmpty) {
+							continue;
+						}
+
+						const first = atomizedResult.first();
+
+						if (
+							itemDeepEqual(
+								dynamicContext,
+								executionParameters,
+								null,
+								singleValue,
+								first,
+							).next(IterationHint.NONE).value
+						) {
+							return caseClauseAndTestSequenceCallbacks[i](dynamicContext);
+						}
+					}
+				}
+
+				return defaultSequenceCallback(dynamicContext);
+			},
+		});
+	}
+
+	public performStaticEvaluation(staticContext: StaticContext) {
+		super.performStaticEvaluation(staticContext);
+
+		if (this._argExpression.isUpdating) {
+			throw errXUST0001();
+		}
+	}
+}
+
+export default SwitchExpression;
diff --git a/src/parsing/compileAstToExpression.ts b/src/parsing/compileAstToExpression.ts
index babe7aec..39fb3c10 100644
--- a/src/parsing/compileAstToExpression.ts
+++ b/src/parsing/compileAstToExpression.ts
@@ -68,6 +68,7 @@ import ElementConstructor from '../expressions/xquery/ElementConstructor';
 import PIConstructor from '../expressions/xquery/PIConstructor';
 import TextConstructor from '../expressions/xquery/TextConstructor';
 import TypeSwitchExpression from '../expressions/xquery/TypeSwitchExpression';
+import SwitchExpression from '../expressions/xquery/SwitchExpression';
 import astHelper, { IAST } from './astHelper';
 
 const COMPILATION_OPTIONS = {
@@ -204,6 +205,8 @@ function compile(ast: IAST, compilationOptions: CompilationOptions): Expression
 
 		case 'typeswitchExpr':
 			return typeswitchExpr(ast, compilationOptions);
+		case 'switchExpr':
+			return switchExpr(ast, compilationOptions);
 
 		// XQuery node constructors
 		case 'elementConstructor':
@@ -1481,7 +1484,7 @@ function typeswitchExpr(ast: IAST, compilationOptions: CompilationOptions) {
 	const caseClause = astHelper.getChildren(ast, 'typeswitchExprCaseClause');
 
 	const caseClauseExpressions = caseClause.map((caseClauseExpression) => {
-		const sequenceTypesAstNodes: IAST[] =
+		const caseNodes: IAST[] =
 			astHelper.getChildren(caseClauseExpression, 'sequenceTypeUnion').length === 0
 				? [astHelper.getFirstChild(caseClauseExpression, 'sequenceType')]
 				: astHelper.getChildren(
@@ -1496,7 +1499,7 @@ function typeswitchExpr(ast: IAST, compilationOptions: CompilationOptions) {
 
 		return {
 			caseClauseExpression: resultExpression,
-			typeTests: sequenceTypesAstNodes.map((sequenceTypeAstNode: IAST) => {
+			typeTests: caseNodes.map((sequenceTypeAstNode: IAST) => {
 				const occurrenceIndicator = astHelper.getFirstChild(
 					sequenceTypeAstNode,
 					'occurrenceIndicator',
@@ -1522,6 +1525,44 @@ function typeswitchExpr(ast: IAST, compilationOptions: CompilationOptions) {
 	return new TypeSwitchExpression(argExpr, caseClauseExpressions, defaultExpression, type);
 }
 
+function switchExpr(ast: IAST, compilationOptions: CompilationOptions) {
+	if (!compilationOptions.allowXQuery) {
+		throw new Error('XPST0003: Use of XQuery functionality is not allowed in XPath context');
+	}
+
+	const type = astHelper.getAttribute(ast, 'type');
+
+	const argExpr = compile(
+		astHelper.getFirstChild(astHelper.getFirstChild(ast, 'argExpr'), '*'),
+		compilationOptions,
+	);
+
+	const caseClauses = astHelper.getChildren(ast, 'switchExprCaseClause');
+
+	const caseClauseExpressions = caseClauses.map((caseClauseExpression) => {
+		const caseNodes: IAST[] = astHelper.getChildren(caseClauseExpression, 'switchCaseExpr');
+
+		const resultExpression = compile(
+			astHelper.followPath(caseClauseExpression, ['resultExpr', '*']),
+			compilationOptions,
+		) as PossiblyUpdatingExpression;
+
+		return {
+			caseClauseExpression: resultExpression,
+			tests: caseNodes.map((caseNode) =>
+				compile(astHelper.getFirstChild(caseNode, '*'), compilationOptions),
+			),
+		};
+	});
+
+	const defaultExpression = compile(
+		astHelper.followPath(ast, ['switchExprDefaultClause', 'resultExpr', '*']),
+		compilationOptions,
+	) as PossiblyUpdatingExpression;
+
+	return new SwitchExpression(argExpr, caseClauseExpressions, defaultExpression, type);
+}
+
 export default function (xPathAst: IAST, compilationOptions: CompilationOptions): Expression {
 	return compile(xPathAst, compilationOptions);
 }
diff --git a/src/parsing/prscParser.ts b/src/parsing/prscParser.ts
index 1287f7a3..7f62f52f 100644
--- a/src/parsing/prscParser.ts
+++ b/src/parsing/prscParser.ts
@@ -1813,25 +1813,33 @@ function generateParser(options: { outputDebugInfo: boolean; xquery: boolean }):
 
 	const switchCaseClause: Parser<IAST> = then(
 		plus(
-			map(precededMultiple([tokens.CASE, whitespacePlus], switchCaseOperand), (x) => [
-				'switchCaseExpr',
-				x,
-			]),
+			then(
+				map(
+					precededMultiple([tokens.CASE, whitespacePlus], cut(switchCaseOperand)),
+					(x) => ['switchCaseExpr', x],
+				),
+				cut(whitespacePlus),
+				(x, _) => x,
+			),
 		),
-		precededMultiple([whitespacePlus, tokens.RETURN, whitespacePlus], exprSingle),
+		cut(precededMultiple([tokens.RETURN, whitespacePlus], cut(exprSingle))),
 		(operands, exprPart) =>
 			['switchExprCaseClause', ...operands, ['resultExpr', exprPart]] as IAST,
 	);
 
 	const switchExpr: Parser<IAST> = then3(
-		precededMultiple([tokens.SWITCH, whitespace, tokens.BRACE_OPEN], expr),
-		precededMultiple(
-			[whitespace, tokens.BRACE_CLOSE, whitespace],
-			plus(followed(switchCaseClause, whitespace)),
+		precededMultiple([tokens.SWITCH, whitespace, tokens.BRACE_OPEN], cut(expr)),
+		cut(
+			precededMultiple(
+				[whitespace, tokens.BRACE_CLOSE, cut(whitespace)],
+				plus(followed(switchCaseClause, whitespace)),
+			),
 		),
-		precededMultiple(
-			[tokens.DEFAULT, whitespacePlus, tokens.RETURN, whitespacePlus],
-			exprSingle,
+		cut(
+			precededMultiple(
+				[tokens.DEFAULT, whitespacePlus, tokens.RETURN, whitespacePlus],
+				exprSingle,
+			),
 		),
 		(exprPart, clauses, resultExpr) => [
 			'switchExpr',
diff --git a/src/registerXQueryModule.ts b/src/registerXQueryModule.ts
index 25004bba..d0c7055b 100644
--- a/src/registerXQueryModule.ts
+++ b/src/registerXQueryModule.ts
@@ -49,10 +49,15 @@ export default function registerXQueryModule(
 	moduleString: string,
 	options: { debug: boolean } = { debug: false },
 ): string {
-	const parsedModule = parseExpression(moduleString, {
-		allowXQuery: true,
-		debug: options['debug'],
-	});
+	let parsedModule;
+	try {
+		parsedModule = parseExpression(moduleString, {
+			allowXQuery: true,
+			debug: options['debug'],
+		});
+	} catch (error) {
+		printAndRethrowError(moduleString, error);
+	}
 
 	annotateAst(parsedModule, new AnnotationContext(undefined));
 


Test patch:
diff --git a/test/assets/failingXQueryXTestNames.csv b/test/assets/failingXQueryXTestNames.csv
index 91e30755..049247f6 100644
--- a/test/assets/failingXQueryXTestNames.csv
+++ b/test/assets/failingXQueryXTestNames.csv
@@ -2670,8 +2670,6 @@ string-constructor-026,Parse error
 string-constructor-910,Parse error
 string-constructor-911,Parse error
 string-constructor-912,Parse error
-switch-004,Parse error
-switch-005,Parse error
 try-001,Parse error
 try-002,Parse error
 try-003,Parse error
diff --git a/test/assets/runnableTestSets.csv b/test/assets/runnableTestSets.csv
index 09794f25..9b8ff536 100644
--- a/test/assets/runnableTestSets.csv
+++ b/test/assets/runnableTestSets.csv
@@ -408,7 +408,7 @@ prod-SequenceType,true
 prod-SquareArrayConstructor,true
 prod-StepExpr,true
 prod-StringConstructor,true
-prod-SwitchExpr,false
+prod-SwitchExpr,true
 prod-TreatExpr,false
 prod-TryCatchExpr,false
 prod-TypeswitchExpr,true
diff --git a/test/assets/unrunnableTestCases.csv b/test/assets/unrunnableTestCases.csv
index 013e0a8e..da65a04c 100644
--- a/test/assets/unrunnableTestCases.csv
+++ b/test/assets/unrunnableTestCases.csv
@@ -3443,6 +3443,7 @@ string-constructor-028,Error: 1: ( 2:         declare variable $n external := <e
 string-constructor-910,AssertionError: expected [Function] to throw error matching /FOTY0013/ but got '1: \n2:         declare variable $n e…'
 string-constructor-911,AssertionError: expected [Function] to throw error matching /FOTY0013/ but got '1: \n2:         declare variable $n e…'
 string-constructor-912,Error: 1:  2:         declare variable $n as xs:integer external := 10;  3:         ``[There were `{1 to $n}` green bottles]``            ^ 4:        = "There were 1 2 3 4 5 6 7 8 9 10 green bottles"  Error: XPST0003: Failed to parse script. Expected -,+,validate,(#,(,#,parent::,ancestor::,preceding-sibling::,preceding::,ancestor-or-self::,..,child::,descendant::,attribute::,self::,descendant-or-self::,following-sibling::,following::,document-node(,element,attribute,schema-element,schema-attribute(,processing-instruction(,processing-instruction(),comment(),text(),namespace-node(),node(),:*,Q,[A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD],[\uD800-\uDB7F],/,//   at <>:3:9 - 3:10
+switch-018,AssertionError: Expected XPath            let $v := xs:time('00:00:00Z')          return switch($v)          case xs:time('24:00:00Z') return 1          default return 2        to resolve to 1: expected false to be true
 typeswitchhc16,Error: XPST0008, The variable i is not in scope.
 typeswitchhc20,Error: XPST0008, The variable i is not in scope.
 K-sequenceExprTypeswitch-6,Error: No selector counterpart for: voidSequenceType.
@@ -3876,7 +3877,6 @@ UseCaseR31-002,Error: Not implemented: groupByClause is not implemented yet.
 UseCaseR31-003,Error: Not implemented: groupByClause is not implemented yet.
 UseCaseR31-004,Error: 1: let $result := (        ^ 2:         declare namespace map="http://www.w3.org/2005/xpath-functions/map"; 3:   Error: XPST0003: Failed to parse script. Expected end of input   at <>:1:5 - 1:6
 UseCaseR31-009,Error: 1: let $result := (        ^ 2:       declare namespace map="http://www.w3.org/2005/xpath-functions/map"; 3:       declare variable $book:= (<book>  Error: XPST0003: Failed to parse script. Expected end of input   at <>:1:5 - 1:6
-UseCaseR31-012,Error: No selector counterpart for: switchExpr.
 UseCaseR31-015,Error: Not implemented: groupByClause is not implemented yet.
 UseCaseR31-016,Error: 1: deep-equal((              ^ 2:               declare variable $s :=[['A', 'DT'], ['bride', 'NN'], [',', ','], ['you', 'PRP'], ['know', 'VBP'], [',', ','], ['must', 'MD'], 3:  ['appear', 'VB'], ['like', 'IN'], ['a', 'DT'], ['bride', 'NN'], [',', ','], ['but', 'CC'], ['my', 'PRP$'],  Error: XPST0003: Failed to parse script. Expected end of input   at <>:1:11 - 1:12
 UseCaseR31-017,Error: 1: deep-equal((              ^ 2:               declare variable $s :=[['A', 'DT'], ['bride', 'NN'], [',', ','], ['you', 'PRP'], ['know', 'VBP'], [',', ','], ['must', 'MD'], 3:  ['appear', 'VB'], ['like', 'IN'], ['a', 'DT'], ['bride', 'NN'], [',', ','], ['but', 'CC'], ['my', 'PRP$'],  Error: XPST0003: Failed to parse script. Expected end of input   at <>:1:11 - 1:12
diff --git a/test/specs/parsing/registerXQueryModule.tests.ts b/test/specs/parsing/registerXQueryModule.tests.ts
index be06d1c1..f4834560 100644
--- a/test/specs/parsing/registerXQueryModule.tests.ts
+++ b/test/specs/parsing/registerXQueryModule.tests.ts
@@ -345,4 +345,13 @@ declare function bar:baz() {
 			'XPST0017',
 		);
 	});
+
+	it('throws a proper error for parse errors', () => {
+		chai.expect(() => {
+			registerXQueryModule(
+				`declare function local:one() { 1 }(:missing semicolon:)
+				declare function local:two() { 2 };`,
+			);
+		}).to.throw('XPST0003: Failed to parse script. Expected ;');
+	});
 });
diff --git a/test/specs/parsing/xquery/SwitchExpression.tests.ts b/test/specs/parsing/xquery/SwitchExpression.tests.ts
new file mode 100644
index 00000000..dcc6ad0c
--- /dev/null
+++ b/test/specs/parsing/xquery/SwitchExpression.tests.ts
@@ -0,0 +1,102 @@
+import * as chai from 'chai';
+
+import { evaluateXPath, evaluateXPathToNumber } from 'fontoxpath';
+
+describe('Switch', () => {
+	it('works', () =>
+		chai.assert.equal(
+			evaluateXPathToNumber(
+				`
+switch (1)
+case 1 return 1
+default return 0`,
+
+				null,
+				null,
+				null,
+				{
+					debug: true,
+					language: evaluateXPath.XQUERY_3_1_LANGUAGE,
+				},
+			),
+			1,
+		));
+
+	it('works hitting the default case', () =>
+		chai.assert.equal(
+			evaluateXPathToNumber(
+				`
+switch (0)
+case 1 return 1
+default return 0`,
+
+				null,
+				null,
+				null,
+				{
+					debug: true,
+					language: evaluateXPath.XQUERY_3_1_LANGUAGE,
+				},
+			),
+			0,
+		));
+
+	it('works hitting the empty sequence case', () =>
+		chai.assert.equal(
+			evaluateXPathToNumber(
+				`
+switch (())
+case () return 1
+default return 0`,
+
+				null,
+				null,
+				null,
+				{
+					debug: true,
+					language: evaluateXPath.XQUERY_3_1_LANGUAGE,
+				},
+			),
+			1,
+		));
+
+	it('works hitting the empty sequence case as a default', () =>
+		chai.assert.equal(
+			evaluateXPathToNumber(
+				`
+switch (())
+case 1 return 1
+default return 0`,
+
+				null,
+				null,
+				null,
+				{
+					debug: true,
+					language: evaluateXPath.XQUERY_3_1_LANGUAGE,
+				},
+			),
+			0,
+		));
+
+	it('works hitting overload cases', () =>
+		chai.assert.equal(
+			evaluateXPathToNumber(
+				`
+switch (1)
+case ()
+case 1 return 1
+
+default return 0`,
+
+				null,
+				null,
+				null,
+				{
+					debug: true,
+					language: evaluateXPath.XQUERY_3_1_LANGUAGE,
+				},
+			),
+			1,
+		));
+});