Repository: FontoXML/fontoxpath
Pull request #549 (2022-10-13 10:41:55)

Codegen improvements
This started out as implementing codegen for the not() function, but grew into a little bit more...

* fixed fn:name function not returning empty string for empty sequence argument
* refactored codegen to generate mostly expressions and automatically manage identifiers rather than having to assign identifiers to everything - this reduced the generated code types to value, generator (paths only) or statement (generator body)
* refactored codegen to pass and combine PartialCompilationResults everywhere instead of plain strings
* aligned conversion functions to spec terminology (effective boolean value, atomization) and checked for correctness
* fixed several instances of variables not being passed / combined (made variables required to reduce errors)
* fixed a performance issue where the child axis would call getChildNodes for each iteration of the generator - it now loops using getFirstChild and getNextSibling
* fixed some cases where buckets were not passed correctly to axis domFacade calls
* fixed some issues in type annotation
* implemented general comparison for sequence vs single value in order to not regress for attribute = value comparisons (previously only accepted because multiplicity for the attribute axis was not annotated correctly)
* implemented codegen support for the context item expression
* implemented codegen support for the node() test
* implemented codegen support for non-axis path steps (filterExpr)
* implemented codegen support for calling custom functions
* implemented codegen support for calling selected built-in functions
* implemented native codegen for the not() function

Things to note:
* Built-in functions use an allowlist, as codegen'ed function calls don't currently have the full staticContext, executionParameters etc.
* I had to add an options argument to executeJavascriptCompiledXPath in order to pass currentContext (required by custom functions)
* I disabled the no-shadowed-variables tslint rule, because I think renaming the callback argument in the many mapPartialCompilationResult to something other than the var being mapped makes the code confusing and may even invite errors where the callback body tries to use the unchecked expression instead of the argument.

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

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

Status | Path | Size | Limits
:------------: | ------------ | :------------: | :------------:
:white_check_mark: | fontoxpath.js<br/> | 83.3KB (+1.07KB +1.31%) | -
:white_check_mark: | fontoxpath.esm.js<br/> | 83.41KB (+1.07KB +1.29%) | -
</details>

  
  


Total files change +2.14KB +1.3%

Final result: :white_check_mark:

[View report in BundleMon website ➡️](https://app.bundlemon.dev/projects/609024effb87a20009a6b096/reports/6347ebc6d4baf13343eac205)

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

Coverage increased (+0.2%) to 91.372% when pulling **3fd53da6c31651b37377f5aa57acac74f0a7f539 on codegen-not-function** into **06b88edba9fff3cba7ba7d902cdb993770f49ca3 on master**.


Patch:
diff --git a/performance/backend.benchmark.ts b/performance/backend.benchmark.ts
index a597c2aa..2752d2e0 100644
--- a/performance/backend.benchmark.ts
+++ b/performance/backend.benchmark.ts
@@ -2,9 +2,9 @@ import benchmarkRunner from '@fontoxml/fonto-benchmark-runner';
 import { Document, Node, parseXmlDocument } from 'slimdom';
 import {
 	evaluateXPathToBoolean,
+	evaluateXPathToFirstNode,
 	evaluateXPathToNodes,
 	ReturnType,
-	evaluateXPathToFirstNode,
 } from '../src/index';
 import jsonMlMapper from '../test/helpers/jsonMlMapper';
 import evaluateXPathWithJsCodegen from '../test/specs/parsing/jsCodegen/evaluateXPathWithJsCodegen';
diff --git a/src/expressions/XPathErrors.ts b/src/expressions/XPathErrors.ts
index 97a9a675..c114e90e 100644
--- a/src/expressions/XPathErrors.ts
+++ b/src/expressions/XPathErrors.ts
@@ -4,7 +4,7 @@ export const errFORG0001 = (value: string, type: ValueType, reason?: string) =>
 	new Error(
 		`FORG0001: Cannot cast ${value} to ${valueTypeToString(type)}${reason ? `, ${reason}` : ''}`
 	);
-export const XPDY0002 = (message: string) => new Error(`XPDY0002: ${message}`);
+export const errXPDY0002 = (message: string) => new Error(`XPDY0002: ${message}`);
 export const errXPTY0004 = (message: string) => new Error(`XPTY0004: ${message}`);
 export const errFOTY0013 = (type: ValueType) =>
 	new Error(`FOTY0013: Atomization is not supported for ${valueTypeToString(type)}.`);
diff --git a/src/expressions/dataTypes/typeHelpers.ts b/src/expressions/dataTypes/typeHelpers.ts
index 7a311195..7197fd46 100644
--- a/src/expressions/dataTypes/typeHelpers.ts
+++ b/src/expressions/dataTypes/typeHelpers.ts
@@ -1,5 +1,5 @@
 import builtinDataTypesByType, { TypeModel } from './builtins/builtinDataTypesByType';
-import { ValueType } from './Value';
+import { SequenceMultiplicity, SequenceType, ValueType } from './Value';
 import { Variety } from './Variety';
 
 export function getPrimitiveTypeName(typeName: ValueType): ValueType | null {
@@ -89,3 +89,25 @@ export function validateRestrictions(value: string, valueType: ValueType): boole
 	}
 	return true;
 }
+
+export function doesTypeAllowEmpty(type: SequenceType | null): boolean {
+	if (!type) {
+		// Unknown types can be anything
+		return true;
+	}
+	return (
+		type.mult === SequenceMultiplicity.ZERO_OR_MORE ||
+		type.mult === SequenceMultiplicity.ZERO_OR_ONE
+	);
+}
+
+export function doesTypeAllowMultiple(type: SequenceType | null): boolean {
+	if (!type) {
+		// Unknown types can be anything
+		return true;
+	}
+	return (
+		type.mult === SequenceMultiplicity.ZERO_OR_MORE ||
+		type.mult === SequenceMultiplicity.ONE_OR_MORE
+	);
+}
diff --git a/src/expressions/functions/builtInFunctions_identifiers.ts b/src/expressions/functions/builtInFunctions_identifiers.ts
index a70e25b3..314c4cb3 100644
--- a/src/expressions/functions/builtInFunctions_identifiers.ts
+++ b/src/expressions/functions/builtInFunctions_identifiers.ts
@@ -6,7 +6,7 @@ import isSubtypeOf from '../dataTypes/isSubtypeOf';
 import sequenceFactory from '../dataTypes/sequenceFactory';
 import { SequenceMultiplicity, ValueType } from '../dataTypes/Value';
 import { BUILT_IN_NAMESPACE_URIS } from '../staticallyKnownNamespaces';
-import { errXPTY0004, XPDY0002 } from '../XPathErrors';
+import { errXPDY0002, errXPTY0004 } from '../XPathErrors';
 import { BuiltinDeclarationType } from './builtInFunctions';
 import FunctionDefinitionType from './FunctionDefinitionType';
 
@@ -44,7 +44,7 @@ const fnId: FunctionDefinitionType = (
 ) => {
 	const targetNodeValue = targetNodeSequence.first();
 	if (!targetNodeValue) {
-		throw XPDY0002('The context is absent, it needs to be present to use id function.');
+		throw errXPDY0002('The context is absent, it needs to be present to use id function.');
 	}
 	if (!isSubtypeOf(targetNodeValue.type, ValueType.NODE)) {
 		throw errXPTY0004(
@@ -102,7 +102,7 @@ const fnIdref: FunctionDefinitionType = (
 ) => {
 	const targetNodeValue = targetNodeSequence.first();
 	if (!targetNodeValue) {
-		throw XPDY0002('The context is absent, it needs to be present to use idref function.');
+		throw errXPDY0002('The context is absent, it needs to be present to use idref function.');
 	}
 	if (!isSubtypeOf(targetNodeValue.type, ValueType.NODE)) {
 		throw errXPTY0004(
diff --git a/src/expressions/functions/builtInFunctions_node.ts b/src/expressions/functions/builtInFunctions_node.ts
index 7d5d8154..31fcaeed 100644
--- a/src/expressions/functions/builtInFunctions_node.ts
+++ b/src/expressions/functions/builtInFunctions_node.ts
@@ -108,7 +108,7 @@ const fnName: FunctionDefinitionType = (
 				staticContext,
 				fnNodeName(dynamicContext, executionParameters, staticContext, sequence)
 			),
-		empty: () => sequenceFactory.empty(),
+		empty: () => sequenceFactory.singleton(createAtomicValue('', ValueType.XSSTRING)),
 	});
 };
 
@@ -403,7 +403,7 @@ const declarations: BuiltinDeclarationType[] = [
 		callFunction: fnName,
 		localName: 'name',
 		namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
-		returnType: { type: ValueType.XSSTRING, mult: SequenceMultiplicity.ZERO_OR_ONE },
+		returnType: { type: ValueType.XSSTRING, mult: SequenceMultiplicity.EXACTLY_ONE },
 	},
 
 	{
diff --git a/src/jsCodegen/CodeGenContext.ts b/src/jsCodegen/CodeGenContext.ts
index 51c6dd4a..7388a69a 100644
--- a/src/jsCodegen/CodeGenContext.ts
+++ b/src/jsCodegen/CodeGenContext.ts
@@ -1,13 +1,67 @@
-import { Bucket } from '../expressions/util/Bucket';
-import { IAST } from '../parsing/astHelper';
 import { NamespaceResolver } from '../types/Options';
-import { FunctionIdentifier, PartialCompilationResult } from './JavaScriptCompiledXPath';
-
-export type CodeGenContext = {
-	emitBaseExpr?: (
-		ast: IAST,
-		identifier: FunctionIdentifier,
-		staticContext: CodeGenContext
-	) => [PartialCompilationResult, Bucket];
-	resolveNamespace: NamespaceResolver;
-};
+import { emitBaseExpr } from './emitBaseExpr';
+import { mapPartialCompilationResult } from './emitHelpers';
+import {
+	acceptAst,
+	GeneratedCodeBaseType,
+	PartialCompilationResult,
+	PartiallyCompiledAstAccepted,
+} from './JavaScriptCompiledXPath';
+
+export class CodeGenContext {
+	public defaultFunctionNamespaceUri: string;
+
+	// We need to create indirection through context for emitBaseExpr to avoid a cyclic dependency
+	// that the Closure compiler can't handle
+	public emitBaseExpr: typeof emitBaseExpr;
+
+	public resolveNamespace: NamespaceResolver;
+
+	private _identifierExprByExpr = new Map<
+		PartiallyCompiledAstAccepted,
+		PartiallyCompiledAstAccepted
+	>();
+
+	private _nextByPrefix = new Map<string, number>();
+
+	public constructor(resolveNamespace: NamespaceResolver, defaultFunctionNamespaceUri: string) {
+		this.resolveNamespace = resolveNamespace;
+		this.defaultFunctionNamespaceUri = defaultFunctionNamespaceUri;
+		this.emitBaseExpr = emitBaseExpr;
+	}
+
+	public getIdentifierFor(
+		expr: PartialCompilationResult,
+		prefix = 'v'
+	): PartialCompilationResult {
+		return mapPartialCompilationResult(expr, (expr) => {
+			let identifierExpr = this._identifierExprByExpr.get(expr);
+			if (!identifierExpr) {
+				const identifier = this._getNewIdentifier(prefix);
+				identifierExpr = acceptAst(identifier, expr.generatedCodeType, [
+					...expr.variables,
+					`const ${identifier} = ${expr.code};`,
+				]);
+				this._identifierExprByExpr.set(expr, identifierExpr);
+				this._identifierExprByExpr.set(identifierExpr, identifierExpr);
+			}
+			return identifierExpr;
+		});
+	}
+
+	public getNewIdentifier(prefix = 'v'): PartiallyCompiledAstAccepted {
+		return this.getVarInScope(this._getNewIdentifier(prefix));
+	}
+
+	public getVarInScope(identifier: string): PartiallyCompiledAstAccepted {
+		const expr = acceptAst(identifier, { type: GeneratedCodeBaseType.Value }, []);
+		this._identifierExprByExpr.set(expr, expr);
+		return expr;
+	}
+
+	private _getNewIdentifier(prefix = 'v'): string {
+		const next = this._nextByPrefix.get(prefix) || 0;
+		this._nextByPrefix.set(prefix, next + 1);
+		return `${prefix}${next}`;
+	}
+}
diff --git a/src/jsCodegen/JavaScriptCompiledXPath.ts b/src/jsCodegen/JavaScriptCompiledXPath.ts
index 8cf66cb2..2f8c5349 100644
--- a/src/jsCodegen/JavaScriptCompiledXPath.ts
+++ b/src/jsCodegen/JavaScriptCompiledXPath.ts
@@ -9,80 +9,40 @@ export enum CompiledResultType {
 }
 
 export type PartiallyCompiledAstAccepted = {
+	// Expression that was generated
 	code: string;
 	// What kind of code was generated.
 	generatedCodeType: GeneratedCodeType;
 	isAstAccepted: true;
 	// Contains variable (and function) declarations for the upper compiled
 	// scope.
-	variables?: string[];
+	variables: string[];
 };
 
 export enum GeneratedCodeBaseType {
 	// A normal value: 5, "test", true, ...
 	Value,
-	// Defining a variable: const test = "test";
-	Variable,
-	// A statement: if (this) { do that; }
+	// A generator function representing a lazy sequence
+	Generator,
+	// Statements, used to build the generator body - doesn't have a value
 	Statement,
-	// A function has been generated: function foo() { return true; }
-	Function,
-	// An iterator is returned.
-	Iterator,
-	// Used in a single case where no code is generated.
-	None,
 }
 
 export type GeneratedCodeType =
 	| { type: GeneratedCodeBaseType.Value }
-	| { type: GeneratedCodeBaseType.Variable }
-	| { type: GeneratedCodeBaseType.Statement }
-	| { returnType: GeneratedCodeType; type: GeneratedCodeBaseType.Function }
-	| { type: GeneratedCodeBaseType.Iterator }
-	| { type: GeneratedCodeBaseType.None };
-
-export function getCompiledValueCode(
-	identifier: string,
-	generatedCodeType: GeneratedCodeType,
-	contextItemName?: string
-): [string, GeneratedCodeType] {
-	switch (generatedCodeType.type) {
-		case GeneratedCodeBaseType.Value:
-			return [identifier, { type: GeneratedCodeBaseType.Value }];
-		case GeneratedCodeBaseType.Variable:
-			return [identifier, { type: GeneratedCodeBaseType.Variable }];
-		case GeneratedCodeBaseType.Function: {
-			const code = `${
-				getCompiledValueCode(
-					`${identifier}(${contextItemName ? contextItemName : 'contextItem'})`,
-					generatedCodeType.returnType,
-					contextItemName
-				)[0]
-			}`;
-			return [code, generatedCodeType.returnType];
-		}
-		case GeneratedCodeBaseType.Iterator: {
-			return [identifier, { type: GeneratedCodeBaseType.Iterator }];
-		}
-		case GeneratedCodeBaseType.None:
-			throw new Error('Trying to get value of generated code with type None');
-		default:
-			throw new Error(
-				'Unreachable! Trying to get compiled value of unsupported GeneratedCodeType.'
-			);
-	}
-}
+	| { type: GeneratedCodeBaseType.Generator }
+	| { type: GeneratedCodeBaseType.Statement };
 
 export function acceptAst(
 	code: string,
 	generatedCodeType: GeneratedCodeType,
-	variables?: string[]
+	variables: string[]
 ): PartiallyCompiledAstAccepted {
 	return {
 		code,
 		generatedCodeType,
-		isAstAccepted: true,
 		variables,
+		isAstAccepted: true,
 	};
 }
 
diff --git a/src/jsCodegen/compileAstToJavaScript.ts b/src/jsCodegen/compileAstToJavaScript.ts
index 9d8bd2c8..fbc56384 100644
--- a/src/jsCodegen/compileAstToJavaScript.ts
+++ b/src/jsCodegen/compileAstToJavaScript.ts
@@ -1,114 +1,36 @@
-import isSubtypeOf from '../expressions/dataTypes/isSubtypeOf';
-import { ValueType } from '../expressions/dataTypes/Value';
+import { SequenceType } from '../expressions/dataTypes/Value';
 import astHelper, { IAST } from '../parsing/astHelper';
 import { ReturnType } from '../parsing/convertXDMReturnValue';
 import { CodeGenContext } from './CodeGenContext';
-import { emitBaseExpr } from './emitBaseExpression';
-import { getDataFromOperandCode } from './emitCompare';
 import {
-	acceptAst,
+	emitConversionToFirstNode,
+	emitConversionToNodes,
+	emitConversionToString,
+	emitEffectiveBooleanValue,
+} from './emitHelpers';
+import {
 	acceptFullyCompiledAst,
-	GeneratedCodeBaseType,
-	GeneratedCodeType,
-	getCompiledValueCode,
 	JavaScriptCompiledXPathResult,
 	PartialCompilationResult,
 	rejectAst,
 } from './JavaScriptCompiledXPath';
-import { determinePredicateTruthValue } from './runtimeLib';
-
-// Return all matching nodes.
-function emitEvaluationToNodes(
-	identifier: string,
-	generatedCodeType: GeneratedCodeType
-): PartialCompilationResult {
-	const [valueCode, valueCodeType] = getCompiledValueCode(identifier, generatedCodeType);
-	if (valueCodeType.type !== GeneratedCodeBaseType.Iterator) {
-		return rejectAst('Return type was not an iterator');
-	}
-
-	return acceptAst(
-		`
-	const nodes = [];
-	for (const node of ${valueCode}) {
-		nodes.push(node);
-	}
-	return nodes;
-	`,
-		{ type: GeneratedCodeBaseType.Statement }
-	);
-}
-
-// Get effective boolean value.
-function emitEvaluationToBoolean(
-	identifier: string,
-	generatedCodeType: GeneratedCodeType
-): PartialCompilationResult {
-	const generated = determinePredicateTruthValue(identifier, '', generatedCodeType);
-	if (!generated.isAstAccepted) {
-		return generated;
-	}
-	return acceptAst(`return ${generated.code};`, { type: GeneratedCodeBaseType.Statement });
-}
-
-// Strings can just be returned as is so lets do that
-function emitEvaluationToString(
-	identifier: string,
-	generatedCodeType: GeneratedCodeType,
-	astType: ValueType
-): PartialCompilationResult {
-	if (astType === undefined) {
-		return rejectAst("Full AST wasn't annotated so we cannot correctly emit a string return");
-	}
-	if (!isSubtypeOf(astType, ValueType.XSSTRING) && !isSubtypeOf(astType, ValueType.ATTRIBUTE)) {
-		return rejectAst(
-			'Not implemented: returning anything but strings and attributes from codegen'
-		);
-	}
-
-	const [valueCode, valueCodeType] = getCompiledValueCode(identifier, generatedCodeType);
-
-	const valueDataCode = getDataFromOperandCode(valueCode, valueCodeType.type, astType);
-	// Note that `getDataFromOperandCode` can return null for empty strings. Stringify these cases
-	return acceptAst(`return ${valueDataCode} || '';`, { type: GeneratedCodeBaseType.Statement });
-}
-
-function emitEvaluationToFirstNode(
-	identifier: string,
-	generatedCodeType: GeneratedCodeType
-): PartialCompilationResult {
-	const [valueCode, valueCodeType] = getCompiledValueCode(identifier, generatedCodeType);
-	if (valueCodeType.type !== GeneratedCodeBaseType.Iterator) {
-		throw new Error('Trying access generated code as an iterator while this is not the case.');
-	}
-
-	return acceptAst(
-		`
-	const firstResult = ${valueCode}.next();
-	if (!firstResult.done) {
-		return firstResult.value
-	}
-	return null;
-	`,
-		{ type: GeneratedCodeBaseType.Statement }
-	);
-}
 
 function emitReturnTypeConversion(
-	identifier: string,
+	expr: PartialCompilationResult,
 	returnType: ReturnType,
-	generatedCodeType: GeneratedCodeType,
-	astType: ValueType
+	astType: SequenceType,
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
 ): PartialCompilationResult {
 	switch (returnType) {
 		case ReturnType.FIRST_NODE:
-			return emitEvaluationToFirstNode(identifier, generatedCodeType);
+			return emitConversionToFirstNode(expr, astType, contextItemExpr, context);
 		case ReturnType.NODES:
-			return emitEvaluationToNodes(identifier, generatedCodeType);
+			return emitConversionToNodes(expr, astType, contextItemExpr, context);
 		case ReturnType.BOOLEAN:
-			return emitEvaluationToBoolean(identifier, generatedCodeType);
+			return emitEffectiveBooleanValue(expr, astType, contextItemExpr, context);
 		case ReturnType.STRING:
-			return emitEvaluationToString(identifier, generatedCodeType, astType);
+			return emitConversionToString(expr, astType, contextItemExpr, context);
 		default:
 			return rejectAst(`Unsupported: the return type '${returnType}'.`);
 	}
@@ -116,21 +38,15 @@ function emitReturnTypeConversion(
 
 function wrapCompiledCode(code: string, shouldUseContextItem: boolean): string {
 	let finalCode = `
-	return (contextItem, domFacade, runtimeLib) => {
+	return (contextItem, domFacade, runtimeLib, options) => {
 		const {
-			DONE_TOKEN,
-			ValueType,
 			XPDY0002,
-			determinePredicateTruthValue,
-			isSubtypeOf,
-			ready,
-			atomize,
 		} = runtimeLib;`;
 
 	if (shouldUseContextItem) {
 		finalCode += `
 		if (!contextItem) {
-			throw XPDY0002("Context is needed to evaluate the given path expression.");
+			throw errXPDY0002("Context is needed to evaluate the given path expression.");
 		}
 
 		if (!contextItem.nodeType) {
@@ -149,7 +65,7 @@ const compiledXPathIdentifier = 'compiledXPathExpression';
 function compileAstToJavaScript(
 	xPathAst: IAST,
 	returnType: ReturnType,
-	staticContext: CodeGenContext
+	context: CodeGenContext
 ): JavaScriptCompiledXPathResult {
 	const mainModule = astHelper.getFirstChild(xPathAst, 'mainModule');
 	if (!mainModule) {
@@ -163,38 +79,30 @@ function compileAstToJavaScript(
 
 	const queryBodyContents = astHelper.followPath(mainModule, ['queryBody', '*']);
 
-	staticContext.emitBaseExpr = emitBaseExpr;
-
-	const [compiledBaseExpr, _bucket] = emitBaseExpr(
+	const contextItemExpr = context.getVarInScope('contextItem');
+	const [compiledBaseExpr, _bucket] = context.emitBaseExpr(
 		queryBodyContents,
-		compiledXPathIdentifier,
-		staticContext
+		contextItemExpr,
+		context
 	);
-	if (compiledBaseExpr.isAstAccepted === false) {
-		return compiledBaseExpr;
-	}
-
-	const emittedVariables = compiledBaseExpr.variables
-		? compiledBaseExpr.variables.join('\n')
-		: '';
 
 	const queryType = astHelper.getAttribute(queryBodyContents, 'type');
-
-	const returnTypeConversionCode = emitReturnTypeConversion(
-		compiledXPathIdentifier,
+	const compiledReturnValue = emitReturnTypeConversion(
+		compiledBaseExpr,
 		returnType,
-		compiledBaseExpr.generatedCodeType,
-		queryType ? queryType.type : undefined
+		queryType,
+		contextItemExpr,
+		context
 	);
 
-	if (returnTypeConversionCode.isAstAccepted === false) {
-		return returnTypeConversionCode;
+	// Convert partial compilation into a fully-compiled expression
+	if (!compiledReturnValue.isAstAccepted) {
+		return compiledReturnValue;
 	}
-
-	const code = emittedVariables + compiledBaseExpr.code + returnTypeConversionCode.code;
-
+	const code = `
+		${compiledReturnValue.variables.join('\n')}
+		return ${compiledReturnValue.code};`;
 	const requiresContext = checkForContextItemInExpression(xPathAst);
-
 	const wrappedCode = wrapCompiledCode(code, requiresContext);
 
 	return acceptFullyCompiledAst(wrappedCode);
diff --git a/src/jsCodegen/compileXPathToJavaScript.ts b/src/jsCodegen/compileXPathToJavaScript.ts
index 17a609a5..7bfb5df2 100644
--- a/src/jsCodegen/compileXPathToJavaScript.ts
+++ b/src/jsCodegen/compileXPathToJavaScript.ts
@@ -64,9 +64,15 @@ function compileXPathToJavaScript(
 		return rejectAst(`Unsupported: XQuery Prologs are not supported.`);
 	}
 
-	const codegenContext: CodeGenContext = {
-		resolveNamespace: options['namespaceResolver'] || createDefaultNamespaceResolver(null),
-	};
+	const defaultFunctionNamespaceUri =
+		options['defaultFunctionNamespaceURI'] === undefined
+			? BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI
+			: options['defaultFunctionNamespaceURI'];
+
+	const codegenContext = new CodeGenContext(
+		options['namespaceResolver'] || createDefaultNamespaceResolver(null),
+		defaultFunctionNamespaceUri
+	);
 
 	annotateAst(
 		ast,
@@ -75,9 +81,7 @@ function compileXPathToJavaScript(
 				new ExecutionSpecificStaticContext(
 					codegenContext.resolveNamespace,
 					{},
-					options['defaultFunctionNamespaceURI'] === undefined
-						? BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI
-						: options['defaultFunctionNamespaceURI'],
+					defaultFunctionNamespaceUri,
 					options['functionNameResolver'] ||
 						createDefaultFunctionNameResolver(
 							BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI
diff --git a/src/jsCodegen/emitAxis.ts b/src/jsCodegen/emitAxis.ts
new file mode 100644
index 00000000..ea19a8db
--- /dev/null
+++ b/src/jsCodegen/emitAxis.ts
@@ -0,0 +1,230 @@
+import { NODE_TYPES } from '../domFacade/ConcreteNode';
+import { Bucket, intersectBuckets } from '../expressions/util/Bucket';
+import astHelper, { IAST } from '../parsing/astHelper';
+import { emitAnd, mapPartialCompilationResult } from './emitHelpers';
+import {
+	acceptAst,
+	GeneratedCodeBaseType,
+	PartialCompilationResult,
+	rejectAst,
+} from './JavaScriptCompiledXPath';
+
+export const axisAstNodes = {
+	ATTRIBUTE: 'attribute',
+	CHILD: 'child',
+	PARENT: 'parent',
+	SELF: 'self',
+};
+
+function emitMultipleNodesAxis(
+	iterationCode: PartialCompilationResult,
+	conditionExpr: PartialCompilationResult,
+	nestedCode: PartialCompilationResult
+): PartialCompilationResult {
+	return mapPartialCompilationResult(iterationCode, (iterationCode) =>
+		mapPartialCompilationResult(conditionExpr, (conditionExpr) =>
+			mapPartialCompilationResult(nestedCode, (nestedCode) =>
+				acceptAst(
+					`for (${iterationCode.code}) {
+						${conditionExpr.variables.join('\n')}
+						if (!(${conditionExpr.code})) {
+							continue;
+						}
+						${nestedCode.variables.join('\n')}
+						${nestedCode.code}
+					}`,
+					{ type: GeneratedCodeBaseType.Statement },
+					iterationCode.variables
+				)
+			)
+		)
+	);
+}
+
+// The child axis contains the children of the context node.
+// https://www.w3.org/TR/xpath-31/#doc-xpath31-ForwardAxis
+//
+// returns node()s.
+// https://www.w3.org/TR/xpath-datamodel-31/#dm-children
+function emitChildAxis(
+	conditionExpr: PartialCompilationResult,
+	conditionBucket: Bucket,
+	nestedCode: PartialCompilationResult,
+	stepContextItemExpr: PartialCompilationResult,
+	contextItemExpr: PartialCompilationResult
+): PartialCompilationResult {
+	const bucketArg = conditionBucket ? `, "${conditionBucket}"` : '';
+
+	const iterationCode = mapPartialCompilationResult(stepContextItemExpr, (stepContextItemExpr) =>
+		mapPartialCompilationResult(contextItemExpr, (contextItemExpr) =>
+			acceptAst(
+				`let ${stepContextItemExpr.code} = domFacade.getFirstChild(${contextItemExpr.code}${bucketArg});
+							${stepContextItemExpr.code};
+							${stepContextItemExpr.code} = domFacade.getNextSibling(${stepContextItemExpr.code}${bucketArg})`,
+				{ type: GeneratedCodeBaseType.Statement },
+				[...stepContextItemExpr.variables, ...contextItemExpr.variables]
+			)
+		)
+	);
+
+	return emitMultipleNodesAxis(iterationCode, conditionExpr, nestedCode);
+}
+
+// the attribute axis contains the attributes of the context node,
+function emitAttributeAxis(
+	conditionExpr: PartialCompilationResult,
+	conditionBucket: Bucket,
+	nestedCode: PartialCompilationResult,
+	stepContextItemExpr: PartialCompilationResult,
+	contextItemExpr: PartialCompilationResult
+): PartialCompilationResult {
+	// Can't get anything but attributes here
+	const bucket = intersectBuckets(conditionBucket, 'type-2');
+	// Only element nodes can have attributes
+	const nodesExpr = mapPartialCompilationResult(contextItemExpr, (contextItemExpr) =>
+		acceptAst(
+			`(${contextItemExpr.code} && ${contextItemExpr.code}.nodeType === /*ELEMENT_NODE*/ ${
+				NODE_TYPES.ELEMENT_NODE
+			} ? domFacade.getAllAttributes(${contextItemExpr.code}${
+				bucket ? `, "${bucket}"` : ''
+			}) : [])`,
+			{ type: GeneratedCodeBaseType.Value },
+			contextItemExpr.variables
+		)
+	);
+	const iterationCode = mapPartialCompilationResult(stepContextItemExpr, (stepContextItemExpr) =>
+		mapPartialCompilationResult(nodesExpr, (nodesExpr) =>
+			acceptAst(
+				`const ${stepContextItemExpr.code} of ${nodesExpr.code}`,
+				{ type: GeneratedCodeBaseType.Statement },
+				[...stepContextItemExpr.variables, ...nodesExpr.variables]
+			)
+		)
+	);
+
+	return emitMultipleNodesAxis(iterationCode, conditionExpr, nestedCode);
+}
+
+// self::para selects the context node if it is a para element, and otherwise
+// returns an empty sequence
+function emitSelfAxis(
+	conditionExpr: PartialCompilationResult,
+	conditionBucket: Bucket,
+	nestedCode: PartialCompilationResult,
+	stepContextItemExpr: PartialCompilationResult,
+	contextItemExpr: PartialCompilationResult
+): PartialCompilationResult {
+	return emitSingleNodeAxis(stepContextItemExpr, contextItemExpr, conditionExpr, nestedCode);
+}
+
+// parent::node() selects the parent of the context node. If the context node is
+// an attribute node, this expression returns the element node (if any) to which
+// the attribute node is attached.
+function emitParentAxis(
+	conditionExpr: PartialCompilationResult,
+	conditionBucket: Bucket,
+	nestedCode: PartialCompilationResult,
+	stepContextItemExpr: PartialCompilationResult,
+	contextItemExpr: PartialCompilationResult
+): PartialCompilationResult {
+	const bucketArg = conditionBucket ? `, "${conditionBucket}"` : '';
+	const parentExpr = mapPartialCompilationResult(contextItemExpr, (contextItemExpr) =>
+		acceptAst(
+			`domFacade.getParentNode(${contextItemExpr.code}${bucketArg})`,
+			{ type: GeneratedCodeBaseType.Value },
+			contextItemExpr.variables
+		)
+	);
+
+	return emitSingleNodeAxis(stepContextItemExpr, parentExpr, conditionExpr, nestedCode);
+}
+
+// Emit code for an axis made up of exactly one node, that should only be
+// returned once.
+function emitSingleNodeAxis(
+	stepContextItemExpr: PartialCompilationResult,
+	contextItemExpr: PartialCompilationResult,
+	conditionExpr: PartialCompilationResult,
+	nestedCode: PartialCompilationResult
+): PartialCompilationResult {
+	const nullCheck = stepContextItemExpr;
+	const conditionsWithNullCheck = emitAnd(nullCheck, conditionExpr);
+	return mapPartialCompilationResult(stepContextItemExpr, (stepContextItemExpr) =>
+		mapPartialCompilationResult(contextItemExpr, (contextItemExpr) =>
+			mapPartialCompilationResult(conditionsWithNullCheck, (conditionsWithNullCheck) =>
+				mapPartialCompilationResult(nestedCode, (nestedCode) =>
+					acceptAst(
+						`const ${stepContextItemExpr.code} = ${contextItemExpr.code};
+						${conditionsWithNullCheck.variables.join('\n')}
+						if (${conditionsWithNullCheck.code}) {
+							${nestedCode.variables.join('\n')}
+							${nestedCode.code}
+						}`,
+						{ type: GeneratedCodeBaseType.Statement },
+						[...stepContextItemExpr.variables, ...contextItemExpr.variables]
+					)
+				)
+			)
+		)
+	);
+}
+
+export default function emitAxis(
+	ast: IAST,
+	conditionExpr: PartialCompilationResult,
+	conditionBucket: Bucket,
+	nestedCode: PartialCompilationResult,
+	stepContextItemExpr: PartialCompilationResult,
+	contextItemExpr: PartialCompilationResult
+): [PartialCompilationResult, Bucket] {
+	const axisName = astHelper.getTextContent(ast);
+
+	switch (axisName) {
+		case axisAstNodes.ATTRIBUTE:
+			return [
+				emitAttributeAxis(
+					conditionExpr,
+					conditionBucket,
+					nestedCode,
+					stepContextItemExpr,
+					contextItemExpr
+				),
+				'type-1',
+			];
+		case axisAstNodes.CHILD:
+			return [
+				emitChildAxis(
+					conditionExpr,
+					conditionBucket,
+					nestedCode,
+					stepContextItemExpr,
+					contextItemExpr
+				),
+				null,
+			];
+		case axisAstNodes.PARENT:
+			return [
+				emitParentAxis(
+					conditionExpr,
+					conditionBucket,
+					nestedCode,
+					stepContextItemExpr,
+					contextItemExpr
+				),
+				null,
+			];
+		case axisAstNodes.SELF:
+			return [
+				emitSelfAxis(
+					conditionExpr,
+					conditionBucket,
+					nestedCode,
+					stepContextItemExpr,
+					contextItemExpr
+				),
+				conditionBucket,
+			];
+		default:
+			return [rejectAst(`Unsupported: the ${axisName} axis`), null];
+	}
+}
diff --git a/src/jsCodegen/emitBaseExpr.ts b/src/jsCodegen/emitBaseExpr.ts
new file mode 100644
index 00000000..ee678b4c
--- /dev/null
+++ b/src/jsCodegen/emitBaseExpr.ts
@@ -0,0 +1,62 @@
+import { Bucket } from '../expressions/util/Bucket';
+import { IAST } from '../parsing/astHelper';
+import { CodeGenContext } from './CodeGenContext';
+import { emitCompareExpr } from './emitCompare';
+import { emitFunctionCallExpr } from './emitFunctionCallExpr';
+import { emitStringConstantExpr } from './emitLiterals';
+import { emitAndExpr, emitOrExpr } from './emitLogicalExpr';
+import { emitPathExpr } from './emitPathExpr';
+import { PartialCompilationResult, rejectAst } from './JavaScriptCompiledXPath';
+
+/**
+ * Compile AST to base expression wrapped in a function named as the given identifier.
+ *
+ * @param ast The AST node to compile into a function.
+ * @param identifier The function identifier.
+ * @param staticContext Static context parameter to retrieve context-dependent information.
+ * @returns If the AST node's expression is supported for compilation, a wrapped JavaScript function.
+ * If unsupported, a wrapped error message.
+ */
+export function emitBaseExpr(
+	ast: IAST,
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
+): [PartialCompilationResult, Bucket] {
+	const name = ast[0];
+
+	switch (name) {
+		case 'contextItemExpr':
+			return [contextItemExpr, null];
+		case 'pathExpr':
+			return emitPathExpr(ast, contextItemExpr, context);
+		case 'andOp':
+			return emitAndExpr(ast, contextItemExpr, context);
+		case 'orOp':
+			return emitOrExpr(ast, contextItemExpr, context);
+		case 'stringConstantExpr':
+			return emitStringConstantExpr(ast);
+		// generalCompare
+		case 'equalOp':
+		case 'notEqualOp':
+		case 'lessThanOrEqualOp':
+		case 'lessThanOp':
+		case 'greaterThanOrEqualOp':
+		case 'greaterThanOp':
+		// valueCompare
+		case 'eqOp':
+		case 'neOp':
+		case 'ltOp':
+		case 'leOp':
+		case 'gtOp':
+		case 'geOp':
+		case 'isOp':
+		// nodeCompare
+		case 'nodeBeforeOp':
+		case 'nodeAfterOp':
+			return [emitCompareExpr(ast, name, contextItemExpr, context), null];
+		case 'functionCallExpr':
+			return [emitFunctionCallExpr(ast, contextItemExpr, context), null];
+		default:
+			return [rejectAst(`Unsupported: the base expression '${name}'.`), null];
+	}
+}
diff --git a/src/jsCodegen/emitBaseExpression.ts b/src/jsCodegen/emitBaseExpression.ts
deleted file mode 100644
index 791ab0f7..00000000
--- a/src/jsCodegen/emitBaseExpression.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { Bucket } from '../expressions/util/Bucket';
-import { IAST } from '../parsing/astHelper';
-import { CodeGenContext } from './CodeGenContext';
-import { emitCompareExpr } from './emitCompare';
-import { emitFunctionCallExpr } from './emitFunctionCallExpr';
-import { emitStringLiteralExpression } from './emitLiterals';
-import { emitAndExpr, emitOrExpr } from './emitLogicalExpr';
-import { baseExprAstNodes } from './emitOperand';
-import { emitPathExpr } from './emitPathExpr';
-import { FunctionIdentifier, PartialCompilationResult, rejectAst } from './JavaScriptCompiledXPath';
-
-/**
- * Compile AST to base expression wrapped in a function named as the given identifier.
- *
- * @param ast The AST node to compile into a function.
- * @param identifier The function identifier.
- * @param staticContext Static context parameter to retrieve context-dependent information.
- * @returns If the AST node's expression is supported for compilation, a wrapped JavaScript function.
- * If unsupported, a wrapped error message.
- */
-export function emitBaseExpr(
-	ast: IAST,
-	identifier: FunctionIdentifier,
-	staticContext: CodeGenContext
-): [PartialCompilationResult, Bucket] {
-	const name = ast[0];
-
-	switch (name) {
-		case baseExprAstNodes.PATH_EXPR:
-			return emitPathExpr(ast, identifier, staticContext);
-		case baseExprAstNodes.AND_OP:
-			return emitAndExpr(ast, identifier, staticContext);
-		case baseExprAstNodes.OR_OP:
-			return emitOrExpr(ast, identifier, staticContext);
-		case baseExprAstNodes.STRING_LIT_EXPR:
-			return emitStringLiteralExpression(ast, identifier);
-		// generalCompare
-		case baseExprAstNodes.EQUAL_OP:
-		case baseExprAstNodes.NOT_EQUAL_OP:
-		case baseExprAstNodes.LESS_THAN_OR_EQUAL_OP:
-		case baseExprAstNodes.LESS_THAN_OP:
-		case baseExprAstNodes.GREATER_THAN_OR_EQUAL_OP:
-		case baseExprAstNodes.GREATER_THAN_OP:
-		// valueCompare
-		case baseExprAstNodes.EQ_OP:
-		case baseExprAstNodes.NE_OP:
-		case baseExprAstNodes.LT_OP:
-		case baseExprAstNodes.LE_OP:
-		case baseExprAstNodes.GT_OP:
-		case baseExprAstNodes.GE_OP:
-		case baseExprAstNodes.IS_OP:
-		// nodeCompare
-		case baseExprAstNodes.NODE_BEFORE_OP:
-		case baseExprAstNodes.NODE_AFTER_OP:
-			return [emitCompareExpr(ast, identifier, staticContext, name), null];
-		case baseExprAstNodes.FUNCTION_CALL_EXPR:
-			return [emitFunctionCallExpr(ast, identifier, staticContext), null];
-		default:
-			return [rejectAst(`Unsupported: the base expression '${name}'.`), null];
-	}
-}
diff --git a/src/jsCodegen/emitCompare.ts b/src/jsCodegen/emitCompare.ts
index 79094fa7..989c9001 100644
--- a/src/jsCodegen/emitCompare.ts
+++ b/src/jsCodegen/emitCompare.ts
@@ -1,3 +1,4 @@
+import { doesTypeAllowEmpty } from '../expressions/dataTypes/typeHelpers';
 import {
 	SequenceMultiplicity,
 	SequenceType,
@@ -6,125 +7,217 @@ import {
 } from '../expressions/dataTypes/Value';
 import astHelper, { IAST } from '../parsing/astHelper';
 import { CodeGenContext } from './CodeGenContext';
+import {
+	emitAtomizedValue,
+	emitConversionToValue,
+	mapPartialCompilationResult,
+} from './emitHelpers';
 import { emitOperand } from './emitOperand';
 import {
 	acceptAst,
-	FunctionIdentifier,
 	GeneratedCodeBaseType,
-	getCompiledValueCode,
 	PartialCompilationResult,
 	rejectAst,
 } from './JavaScriptCompiledXPath';
 
-export function getDataFromOperandCode(
-	identifier: string,
-	generatedType: GeneratedCodeBaseType,
-	valueType: ValueType
-): string {
-	let code = identifier;
-	// If the generated code returns an iterator, get the first value
-	if (generatedType === GeneratedCodeBaseType.Iterator) {
-		code = `${code}.next().value`;
-	}
-
-	// If the type is an attribute, get the value. Though: if the attribute is absent, return null
-	// instead.
-	if (valueType === ValueType.ATTRIBUTE) {
-		code = `(function () { const attr = ${code}; return attr ? domFacade.getData(attr) : null})()`;
-	}
+/**
+ *  A map to translate from general compare to value compare
+ */
+const OPERATOR_TRANSLATION: { [s: string]: string } = {
+	['equalOp']: 'eqOp',
+	['notEqualOp']: 'neOp',
+	['lessThanOrEqualOp']: 'leOp',
+	['lessThanOp']: 'ltOp',
+	['greaterThanOrEqualOp']: 'geOp',
+	['greaterThanOp']: 'gtOp',
+};
 
-	return code;
-}
+const OPERATOR_SWAP_TABLE: { [s: string]: string } = {
+	['eqOp']: 'eqOp',
+	['neOp']: 'neOp',
+	['leOp']: 'geOp',
+	['ltOp']: 'gtOp',
+	['geOp']: 'leOp',
+	['gtOp']: 'ltOp',
+};
 
 /**
  * Generates javascript code for a value compare expression.
- *
- * @param ast The ast of the value compare expression
- * @param compareType The type of comparison we're executing
- * @param firstExpr The generated code of the first expression
- * @param secondExpr The generated code of the second expression
- * @param identifier The identifier of the result
- * @param staticContext The code generation context
- * @returns Generated code of the value compare
  */
-export function emitValueCompare(
+function emitValueCompare(
 	ast: IAST,
 	compareType: string,
-	firstExpr: PartialCompilationResult,
-	secondExpr: PartialCompilationResult,
-	identifier: string,
-	_staticContext: CodeGenContext
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
 ): PartialCompilationResult {
-	const leftType = astHelper.getAttribute(
-		astHelper.getFirstChild(ast, 'firstOperand')[1] as IAST,
+	const firstType = astHelper.getAttribute(
+		astHelper.followPath(ast, ['firstOperand', '*']),
 		'type'
 	);
-	const rightType = astHelper.getAttribute(
-		astHelper.getFirstChild(ast, 'secondOperand')[1] as IAST,
+	const secondType = astHelper.getAttribute(
+		astHelper.followPath(ast, ['secondOperand', '*']),
 		'type'
 	);
-
 	// If we don't have the types of both operands, we cannot generate the correct code
-	if (!leftType || !rightType) {
-		return rejectAst('Left or right type of compare are not found, annotation failed.');
+	if (!firstType || !secondType) {
+		return rejectAst('Can not generate code for value compare without both types');
 	}
 
 	// Check if both operands are supported
 	const supportedTypes = [ValueType.ATTRIBUTE, ValueType.XSSTRING];
-	if (!supportedTypes.includes(leftType.type) || !supportedTypes.includes(rightType.type)) {
+	if (!supportedTypes.includes(firstType.type) || !supportedTypes.includes(secondType.type)) {
 		return rejectAst(
 			`Unsupported types in compare: [${valueTypeToString(
-				leftType.type
-			)}, ${valueTypeToString(rightType.type)}]`
+				firstType.type
+			)}, ${valueTypeToString(secondType.type)}]`
 		);
 	}
 
-	// If one of the expressions didn't get accepted, return
-	if (!firstExpr.isAstAccepted) {
-		return firstExpr;
+	// Make sure we support the comparison type
+	const compareOperators = new Map<string, string>([
+		['eqOp', '==='],
+		['neOp', '!=='],
+	]);
+	if (!compareOperators.has(compareType)) {
+		return rejectAst(compareType + ' not yet implemented');
 	}
-	if (!secondExpr.isAstAccepted) {
-		return secondExpr;
+	// Get the correct operator
+	const operator = compareOperators.get(compareType);
+
+	// Evaluate both sides to values and atomize the result
+	// TODO: error if more than one item in each sequence
+	const [firstExpr, _firstBucket] = emitOperand(ast, 'firstOperand', contextItemExpr, context);
+	const firstAsValue = emitConversionToValue(firstExpr, contextItemExpr, context);
+	const firstAtomized = emitAtomizedValue(firstAsValue, firstType, context);
+	return mapPartialCompilationResult(
+		context.getIdentifierFor(firstAtomized, 'first'),
+		(firstAtomized) => {
+			const [secondExpr, _secondBucket] = emitOperand(
+				ast,
+				'secondOperand',
+				contextItemExpr,
+				context
+			);
+			const secondAsValue = emitConversionToValue(secondExpr, contextItemExpr, context);
+			const secondAtomized = emitAtomizedValue(secondAsValue, secondType, context);
+			return mapPartialCompilationResult(
+				context.getIdentifierFor(secondAtomized, 'second'),
+				(secondAtomized) => {
+					const nullChecks: string[] = [];
+					if (doesTypeAllowEmpty(firstType)) {
+						nullChecks.push(`${firstAtomized.code} === null`);
+					}
+					if (doesTypeAllowEmpty(secondType)) {
+						nullChecks.push(`${secondAtomized.code} === null`);
+					}
+					return acceptAst(
+						`(${nullChecks.length ? `${nullChecks.join(' || ')} ? null : ` : ''}${
+							firstAtomized.code
+						} ${operator} ${secondAtomized.code})`,
+						{ type: GeneratedCodeBaseType.Value },
+						[...firstAtomized.variables, ...secondAtomized.variables]
+					);
+				}
+			);
+		}
+	);
+}
+
+function emitSimplifiedGeneralCompare(
+	ast: IAST,
+	singleItemOperandName: 'firstOperand' | 'secondOperand',
+	multipleItemOperandName: 'firstOperand' | 'secondOperand',
+	compareType: string,
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
+): PartialCompilationResult {
+	const singleType = astHelper.getAttribute(
+		astHelper.followPath(ast, [singleItemOperandName, '*']),
+		'type'
+	);
+	const multipleType = astHelper.getAttribute(
+		astHelper.followPath(ast, [multipleItemOperandName, '*']),
+		'type'
+	);
+	// If we don't have the types of both operands, we cannot generate the correct code
+	if (!singleType || !multipleType) {
+		return rejectAst('Can not generate code for general compare without both types');
 	}
 
-	// Make sure both child expression got annotated
-	if (!leftType || !rightType) {
-		return rejectAst("Operands in compare weren't annotated");
+	// Check if both operands are supported
+	const supportedTypes = [ValueType.ATTRIBUTE, ValueType.XSSTRING];
+	if (!supportedTypes.includes(singleType.type) || !supportedTypes.includes(multipleType.type)) {
+		return rejectAst(
+			`Unsupported types in compare: [${valueTypeToString(
+				singleType.type
+			)}, ${valueTypeToString(multipleType.type)}]`
+		);
 	}
 
+	// Make sure we support the comparison type
 	const compareOperators = new Map<string, string>([
 		['eqOp', '==='],
 		['neOp', '!=='],
 	]);
-
-	// Make sure we support the comparison type
 	if (!compareOperators.has(compareType)) {
 		return rejectAst(compareType + ' not yet implemented');
 	}
-
 	// Get the correct operator
 	const operator = compareOperators.get(compareType);
 
-	// Generate the code to get the value from both operands
-	const leftGenerated = getCompiledValueCode(firstExpr.code, firstExpr.generatedCodeType);
-	const rightGenerated = getCompiledValueCode(secondExpr.code, secondExpr.generatedCodeType);
-
-	const leftCode = getDataFromOperandCode(leftGenerated[0], leftGenerated[1].type, leftType.type);
-	const rightCode = getDataFromOperandCode(
-		rightGenerated[0],
-		rightGenerated[1].type,
-		rightType.type
+	// Evaluate our single-value side
+	// TODO: error if more than one item in the sequence
+	const [singleExpr, _singleBucket] = emitOperand(
+		ast,
+		singleItemOperandName,
+		contextItemExpr,
+		context
 	);
-
-	return acceptAst(
-		`function ${identifier}(contextItem) {
-				${firstExpr.variables.join('\n')}
-			 	${secondExpr.variables.join('\n')}
-	 	     	return ${leftCode} ${operator} ${rightCode};
-		}`,
-		{
-			type: GeneratedCodeBaseType.Function,
-			returnType: { type: GeneratedCodeBaseType.Value },
+	const singleAsValue = emitConversionToValue(singleExpr, contextItemExpr, context);
+	const singleAtomized = emitAtomizedValue(singleAsValue, singleType, context);
+	return mapPartialCompilationResult(
+		context.getIdentifierFor(singleAtomized, 'single'),
+		(singleAtomized) => {
+			const [multipleExpr, _multipleBucket] = emitOperand(
+				ast,
+				multipleItemOperandName,
+				contextItemExpr,
+				context
+			);
+			return mapPartialCompilationResult(
+				context.getIdentifierFor(multipleExpr, 'multiple'),
+				(multipleExpr) => {
+					if (multipleExpr.generatedCodeType.type !== GeneratedCodeBaseType.Generator) {
+						return rejectAst(
+							'can only generate general compare for a single value and a generator'
+						);
+					}
+					const loopVar = context.getNewIdentifier('n');
+					const atomizedLoopVar = emitAtomizedValue(loopVar, multipleType, context);
+					return mapPartialCompilationResult(contextItemExpr, (contextItemExpr) =>
+						mapPartialCompilationResult(atomizedLoopVar, (atomizedLoopVar) =>
+							acceptAst(
+								`(function () {
+									for (const ${loopVar.code} of ${multipleExpr.code}(${contextItemExpr.code})) {
+										${atomizedLoopVar.variables.join('\n')}
+										if (${atomizedLoopVar.code} ${operator} ${singleAtomized.code}) {
+											return true;
+										}
+									}
+									return false;
+								})()`,
+								{ type: GeneratedCodeBaseType.Value },
+								[
+									...singleAtomized.variables,
+									...loopVar.variables,
+									...contextItemExpr.variables,
+									...multipleExpr.variables,
+								]
+							)
+						)
+					);
+				}
+			);
 		}
 	);
 }
@@ -140,16 +233,20 @@ export function emitValueCompare(
  * @param staticContext The code generation context
  * @returns Generated code of the general compare
  */
-export function emitGeneralCompare(
+function emitGeneralCompare(
 	ast: IAST,
 	compareType: string,
-	firstExpr: PartialCompilationResult,
-	secondExpr: PartialCompilationResult,
-	identifier: string,
-	staticContext: CodeGenContext
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
 ): PartialCompilationResult {
-	const firstType: SequenceType = astHelper.getAttribute(ast, 'type');
-	const secondType: SequenceType = astHelper.getAttribute(ast, 'type');
+	const firstType: SequenceType = astHelper.getAttribute(
+		astHelper.followPath(ast, ['firstOperand', '*']),
+		'type'
+	);
+	const secondType: SequenceType = astHelper.getAttribute(
+		astHelper.followPath(ast, ['secondOperand', '*']),
+		'type'
+	);
 	if (!firstType || !secondType) {
 		return rejectAst('types of compare are not known');
 	}
@@ -157,17 +254,30 @@ export function emitGeneralCompare(
 		firstType.mult === SequenceMultiplicity.EXACTLY_ONE &&
 		secondType.mult === SequenceMultiplicity.EXACTLY_ONE
 	) {
-		return emitValueCompare(
+		return emitValueCompare(ast, OPERATOR_TRANSLATION[compareType], contextItemExpr, context);
+	}
+	if (firstType.mult === SequenceMultiplicity.EXACTLY_ONE) {
+		return emitSimplifiedGeneralCompare(
 			ast,
+			'firstOperand',
+			'secondOperand',
 			OPERATOR_TRANSLATION[compareType],
-			firstExpr,
-			secondExpr,
-			identifier,
-			staticContext
+			contextItemExpr,
+			context
+		);
+	}
+	if (secondType.mult === SequenceMultiplicity.EXACTLY_ONE) {
+		return emitSimplifiedGeneralCompare(
+			ast,
+			'secondOperand',
+			'firstOperand',
+			OPERATOR_SWAP_TABLE[OPERATOR_TRANSLATION[compareType]],
+			contextItemExpr,
+			context
 		);
-	} else {
-		return rejectAst('generalCompare with sequences is still in development');
 	}
+
+	return rejectAst('General comparison for sequences is not implemented');
 }
 
 /**
@@ -182,25 +292,10 @@ export function emitGeneralCompare(
  */
 export function emitCompareExpr(
 	ast: IAST,
-	identifier: FunctionIdentifier,
-	staticContext: CodeGenContext,
-	compareType: string
+	compareType: string,
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
 ): PartialCompilationResult {
-	const [firstExpr, _firstBucket] = emitOperand(ast, identifier, 'firstOperand', staticContext);
-	if (!firstExpr.isAstAccepted) {
-		return firstExpr;
-	}
-
-	const [secondExpr, _secondBucket] = emitOperand(
-		ast,
-		identifier,
-		'secondOperand',
-		staticContext
-	);
-	if (!secondExpr.isAstAccepted) {
-		return secondExpr;
-	}
-
 	switch (compareType) {
 		// valueCompare
 		case 'eqOp':
@@ -210,14 +305,7 @@ export function emitCompareExpr(
 		case 'gtOp':
 		case 'geOp':
 		case 'isOp':
-			return emitValueCompare(
-				ast,
-				compareType,
-				firstExpr,
-				secondExpr,
-				identifier,
-				staticContext
-			);
+			return emitValueCompare(ast, compareType, contextItemExpr, context);
 		// generalCompare
 		case 'equalOp':
 		case 'notEqualOp':
@@ -225,30 +313,11 @@ export function emitCompareExpr(
 		case 'lessThanOp':
 		case 'greaterThanOrEqualOp':
 		case 'greaterThanOp':
-			return emitGeneralCompare(
-				ast,
-				compareType,
-				firstExpr,
-				secondExpr,
-				identifier,
-				staticContext
-			);
+			return emitGeneralCompare(ast, compareType, contextItemExpr, context);
 		// nodeCompare
 		case 'nodeBeforeOp':
 		case 'nodeAfterOp':
 		default:
-			return rejectAst('Unsupported compare type');
+			return rejectAst(`Unsupported compare type: ${compareType}`);
 	}
 }
-
-/**
- *  A map to translate from general compare to value compare
- */
-const OPERATOR_TRANSLATION: { [s: string]: string } = {
-	['equalOp']: 'eqOp',
-	['notEqualOp']: 'neOp',
-	['lessThanOrEqualOp']: 'leOp',
-	['lessThanOp']: 'ltOp',
-	['greaterThanOrEqualOp']: 'geOp',
-	['greaterThanOp']: 'gtOp',
-};
diff --git a/src/jsCodegen/emitFunctionCallExpr.ts b/src/jsCodegen/emitFunctionCallExpr.ts
index 3cae6ee1..61e9881f 100644
--- a/src/jsCodegen/emitFunctionCallExpr.ts
+++ b/src/jsCodegen/emitFunctionCallExpr.ts
@@ -1,10 +1,27 @@
+import isSubtypeOf from '../expressions/dataTypes/isSubtypeOf';
+import { doesTypeAllowMultiple } from '../expressions/dataTypes/typeHelpers';
+import {
+	EllipsisType,
+	ParameterType,
+	SequenceType,
+	ValueType,
+	valueTypeToString,
+} from '../expressions/dataTypes/Value';
+import { getFunctionByArity } from '../expressions/functions/functionRegistry';
+import { BUILT_IN_NAMESPACE_URIS } from '../expressions/staticallyKnownNamespaces';
 import astHelper, { IAST } from '../parsing/astHelper';
 import { CodeGenContext } from './CodeGenContext';
+import {
+	emitConversionToFirstNode,
+	emitConversionToString,
+	emitConversionToValue,
+	emitEffectiveBooleanValue,
+	mapPartialCompilationResult,
+} from './emitHelpers';
+import escapeJavaScriptString from './escapeJavaScriptString';
 import {
 	acceptAst,
-	FunctionIdentifier,
 	GeneratedCodeBaseType,
-	getCompiledValueCode,
 	PartialCompilationResult,
 	rejectAst,
 } from './JavaScriptCompiledXPath';
@@ -13,121 +30,286 @@ const supportedFunctions: Record<
 	string,
 	(
 		ast: IAST,
-		identifier: FunctionIdentifier,
-		staticContext: CodeGenContext
+		contextItemExpr: PartialCompilationResult,
+		context: CodeGenContext
 	) => PartialCompilationResult
 > = {
-	'local-name': emitLocalNameFunction,
-	name: emitNameFunction,
+	'local-name#0': emitLocalNameFunction,
+	'local-name#1': emitLocalNameFunction,
+	'name#0': emitNameFunction,
+	'name#1': emitNameFunction,
+	'not#1': emitNotFunction,
 };
 
-function emitArgument(
-	ast: IAST,
-	staticContext: CodeGenContext,
-	identifier: FunctionIdentifier
+// Built-in function allow list
+// Namespaces not mentioned here are assumed to be custom functions, which are more limited in what
+// they require (no static / dynamic context) and should be safe if their return and argument types
+// are supported.
+const supportedBuiltinFunctions: Record<string, string[]> = {
+	[BUILT_IN_NAMESPACE_URIS.FONTOXPATH_NAMESPACE_URI]: ['version#0'],
+	['']: ['true#0', 'false#0', 'root#1', 'path#1'],
+};
+
+function emitFunctionArgumentConversion(
+	argAst: IAST,
+	requestedType: SequenceType,
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
+): PartialCompilationResult {
+	const [argExpr, _] = context.emitBaseExpr(argAst, contextItemExpr, context);
+	const actualType = astHelper.getAttribute(argAst, 'type');
+	if (doesTypeAllowMultiple(requestedType)) {
+		return rejectAst('Not supported: sequence arguments with multiple items');
+	}
+
+	// TODO: emit runtime check that the actual sequence doesn't contain more than one item
+
+	if (isSubtypeOf(requestedType.type, ValueType.NODE)) {
+		return emitConversionToFirstNode(argExpr, actualType, contextItemExpr, context);
+	}
+
+	switch (requestedType.type) {
+		case ValueType.ITEM:
+			return emitConversionToValue(argExpr, contextItemExpr, context);
+
+		case ValueType.XSBOOLEAN:
+			return emitEffectiveBooleanValue(argExpr, actualType, contextItemExpr, context);
+
+		case ValueType.XSSTRING:
+			return emitConversionToString(argExpr, actualType, contextItemExpr, context);
+	}
+
+	return rejectAst(
+		`Argument types not supported: ${
+			actualType ? valueTypeToString(actualType.type) : 'unknown'
+		} -> ${valueTypeToString(requestedType.type)}`
+	);
+}
+
+function emitFunctionArgumentsConversion(
+	argAsts: IAST[],
+	argTypes: ParameterType[],
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
 ): PartialCompilationResult {
-	const [baseExpr, _bucket] = staticContext.emitBaseExpr(ast, identifier, staticContext);
+	if (
+		argAsts.length !== argTypes.length ||
+		argTypes.some((type) => type === EllipsisType.ELLIPSIS)
+	) {
+		return rejectAst('Not supported: variadic function or mismatch in argument count');
+	}
 
-	if (!baseExpr.isAstAccepted) {
-		return baseExpr;
+	if (argAsts.length === 0) {
+		return acceptAst('', { type: GeneratedCodeBaseType.Value }, []);
+	}
+	const [argAst, ...restArgAsts] = argAsts;
+	const [type, ...restTypes] = argTypes;
+	const argExpr = context.getIdentifierFor(
+		emitFunctionArgumentConversion(
+			argAst,
+			// We checked for ELLIPSIS earlier, so all argument types are normal SequenceTypes
+			type as SequenceType,
+			contextItemExpr,
+			context
+		),
+		'arg'
+	);
+	if (restArgAsts.length === 0) {
+		return argExpr;
 	}
 
-	return acceptAst(`${identifier}`, baseExpr.generatedCodeType, [baseExpr.code]);
+	// Recurse and combine all arguments
+	return mapPartialCompilationResult(argExpr, (argExpr) => {
+		const argsExpr = emitFunctionArgumentsConversion(
+			restArgAsts,
+			restTypes,
+			contextItemExpr,
+			context
+		);
+		return mapPartialCompilationResult(argsExpr, (argsExpr) =>
+			acceptAst(`${argExpr.code}, ${argsExpr.code}`, { type: GeneratedCodeBaseType.Value }, [
+				...argExpr.variables,
+				...argsExpr.variables,
+			])
+		);
+	});
+}
+
+function emitFunctionReturnTypeConversion(
+	callFunctionExpr: PartialCompilationResult,
+	returnType: SequenceType,
+	context: CodeGenContext
+) {
+	return mapPartialCompilationResult(
+		context.getIdentifierFor(callFunctionExpr),
+		(callFunctionExpr) => {
+			// We support (zero or) one string, node or boolean
+			if (
+				!doesTypeAllowMultiple(returnType) &&
+				([ValueType.XSBOOLEAN, ValueType.XSSTRING].includes(returnType.type) ||
+					isSubtypeOf(returnType.type, ValueType.NODE))
+			) {
+				// Singleton string or empty sequence, handled by runtimeLib
+				return callFunctionExpr;
+			}
+			return rejectAst(
+				`Function return type ${valueTypeToString(returnType.type)} not supported`
+			);
+		}
+	);
 }
 
 export function emitFunctionCallExpr(
 	ast: IAST,
-	identifier: FunctionIdentifier,
-	staticContext: CodeGenContext
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
 ): PartialCompilationResult {
-	const functionName = astHelper.getTextContent(astHelper.getFirstChild(ast, 'functionName'));
+	const { localName, namespaceURI } = astHelper.getQName(
+		astHelper.getFirstChild(ast, 'functionName')
+	);
 
-	if (supportedFunctions[functionName] !== undefined) {
-		return supportedFunctions[functionName](ast, identifier, staticContext);
+	const argAsts = astHelper.getChildren(astHelper.getFirstChild(ast, 'arguments'), '*');
+	const arity = argAsts.length;
+	const functionNameAndArity = `${localName}#${arity}`;
+
+	// First check if we have a codegen-specific implementation
+	const isDefaultFunctionNamespace = namespaceURI === context.defaultFunctionNamespaceUri;
+	if (isDefaultFunctionNamespace) {
+		const emitFunctionCall = supportedFunctions[functionNameAndArity];
+		if (emitFunctionCall !== undefined) {
+			return emitFunctionCall(ast, contextItemExpr, context);
+		}
 	}
 
-	return rejectAst(`Unsupported function: ${functionName}`);
-}
+	// Check allowlist
+	const allowlist = supportedBuiltinFunctions[isDefaultFunctionNamespace ? '' : namespaceURI];
+	if (allowlist) {
+		// Function is built-in and must be allowed explicitly to be considered safe for codegen
+		if (!allowlist.includes(functionNameAndArity)) {
+			return rejectAst(
+				`Not supported: built-in function not on allow list: ${functionNameAndArity}`
+			);
+		}
+	}
+
+	// Then check registered functions
+	const functionProperties = getFunctionByArity(namespaceURI, localName, arity);
+	if (!functionProperties) {
+		return rejectAst(`Unknown function / arity: ${functionNameAndArity}`);
+	}
 
-const contextItemCheck = `if (contextItem === undefined || contextItem === null) {
-	throw new Error('XPDY0002: The function which was called depends on dynamic context, which is absent.');
-}`;
+	// Should never happen, but let's check anyway
+	if (functionProperties.isUpdating) {
+		return rejectAst('Not supported: updating functions');
+	}
+
+	const argsExpr = emitFunctionArgumentsConversion(
+		argAsts,
+		functionProperties.argumentTypes,
+		contextItemExpr,
+		context
+	);
 
-function createLocalNameGetter(itemName: string) {
-	return `(${itemName}.localName || ${itemName}.target || '')`;
+	const callFunction = mapPartialCompilationResult(argsExpr, (argsExpr) =>
+		acceptAst(
+			`runtimeLib.callFunction(domFacade, ${escapeJavaScriptString(
+				namespaceURI
+			)}, ${escapeJavaScriptString(localName)}, [${argsExpr.code}], options)`,
+			{ type: GeneratedCodeBaseType.Value },
+			argsExpr.variables
+		)
+	);
+	return emitFunctionReturnTypeConversion(callFunction, functionProperties.returnType, context);
 }
 
-function createNameGetter(itemName: string) {
-	return `(((${itemName}.prefix || '').length !== 0 ? ${itemName}.prefix + ':' : '')
-		+ (${itemName}.localName || ${itemName}.target || ''))`;
+function emitContextItemCheck(
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
+): PartialCompilationResult {
+	return mapPartialCompilationResult(
+		context.getIdentifierFor(contextItemExpr, 'contextItem'),
+		(contextItemExpr) =>
+			acceptAst(contextItemExpr.code, { type: GeneratedCodeBaseType.Value }, [
+				...contextItemExpr.variables,
+				`if (${contextItemExpr.code} === undefined || ${contextItemExpr.code} === null) {
+					throw errXPDY0002('The function which was called depends on dynamic context, which is absent.');
+				}`,
+			])
+	);
 }
 
 function emitSpecificNameFunction(
 	ast: IAST,
-	identifier: FunctionIdentifier,
-	staticContext: CodeGenContext,
-	nameGetterFunction: (itemName: string) => string,
-	functionName: string
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext,
+	nameGetterFunction: (itemName: string) => string
 ): PartialCompilationResult {
-	const argsAst = astHelper.getFirstChild(ast, 'arguments');
-	let code = '';
-	if (argsAst.length === 1) {
-		// No args
-		code = `function ${identifier}(contextItem) {
-			${contextItemCheck}
-			return ${nameGetterFunction('contextItem')};
-		}`;
-	} else if (argsAst.length === 2) {
-		// One arg
-		const innerAst = argsAst[1] as IAST;
-		const emittedArg = emitArgument(innerAst, staticContext, 'arg');
-		// Special handling if context item is passed in
-		if (innerAst[0] === 'contextItemExpr') {
-			code = `function ${identifier}(contextItem) {
-				${contextItemCheck}
-				return ${nameGetterFunction('contextItem')};
-			}`;
-		} else {
-			if (!emittedArg.isAstAccepted) {
-				return emittedArg;
-			}
-
-			code = `function ${identifier}(contextItem) {
-				${emittedArg.variables}
-				const value = ${getCompiledValueCode(emittedArg.code, emittedArg.generatedCodeType)[0]};
-				const childElement = value.next().value;
-				return (value.done ? "" : ${nameGetterFunction('childElement')});
-			}`;
-		}
+	const argumentAst = astHelper.followPath(ast, ['arguments', '*']);
+	let argExpr: PartialCompilationResult;
+	// Handle explicit context item arg as the no-arguments version
+	if (!argumentAst || argumentAst[0] === 'contextItemExpr') {
+		// No args or '.' as arg - use the context item
+		argExpr = emitContextItemCheck(contextItemExpr, context);
 	} else {
-		// Two or more args
-		return rejectAst(`Incorrect arg count for ${functionName} function`);
+		// One arg, should be a node
+		const argType = astHelper.getAttribute(argumentAst, 'type');
+		if (!argType || !isSubtypeOf(argType.type, ValueType.NODE)) {
+			return rejectAst('name function only implemented if arg is a node');
+		}
+
+		const [expr, _] = context.emitBaseExpr(argumentAst, contextItemExpr, context);
+		argExpr = expr;
 	}
 
-	return acceptAst(code, {
-		type: GeneratedCodeBaseType.Function,
-		returnType: { type: GeneratedCodeBaseType.Value },
-	});
+	// TODO: this (and any other function call) should check multiplicity of the actual argument
+	const argAsValue = emitConversionToValue(argExpr, contextItemExpr, context);
+	return mapPartialCompilationResult(context.getIdentifierFor(argAsValue, 'arg'), (argAsValue) =>
+		acceptAst(
+			`(${argAsValue.code} ? ${nameGetterFunction(argAsValue.code)} : '')`,
+			{ type: GeneratedCodeBaseType.Value },
+			argAsValue.variables
+		)
+	);
 }
 
 function emitNameFunction(
 	ast: IAST,
-	identifier: FunctionIdentifier,
-	staticContext: CodeGenContext
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
 ): PartialCompilationResult {
-	return emitSpecificNameFunction(ast, identifier, staticContext, createNameGetter, 'name()');
+	return emitSpecificNameFunction(
+		ast,
+		contextItemExpr,
+		context,
+		(identifier) =>
+			`(((${identifier}.prefix || '').length !== 0 ? ${identifier}.prefix + ':' : '')
+		+ (${identifier}.localName || ${identifier}.target || ''))`
+	);
 }
 
 function emitLocalNameFunction(
 	ast: IAST,
-	identifier: FunctionIdentifier,
-	staticContext: CodeGenContext
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
 ): PartialCompilationResult {
 	return emitSpecificNameFunction(
 		ast,
-		identifier,
-		staticContext,
-		createLocalNameGetter,
-		'local-name()'
+		contextItemExpr,
+		context,
+		(identifier) => `(${identifier}.localName || ${identifier}.target || '')`
+	);
+}
+
+function emitNotFunction(
+	ast: IAST,
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
+): PartialCompilationResult {
+	const argumentAst = astHelper.followPath(ast, ['arguments', '*']);
+	const argType = astHelper.getAttribute(argumentAst, 'type');
+	const [argExpr, _] = context.emitBaseExpr(argumentAst, contextItemExpr, context);
+	const argAsBool = emitEffectiveBooleanValue(argExpr, argType, contextItemExpr, context);
+	return mapPartialCompilationResult(argAsBool, (argAsBool) =>
+		acceptAst(`!${argAsBool.code}`, { type: GeneratedCodeBaseType.Value }, argAsBool.variables)
 	);
 }
diff --git a/src/jsCodegen/emitHelpers.ts b/src/jsCodegen/emitHelpers.ts
new file mode 100644
index 00000000..6c487a5f
--- /dev/null
+++ b/src/jsCodegen/emitHelpers.ts
@@ -0,0 +1,240 @@
+import isSubtypeOf from '../expressions/dataTypes/isSubtypeOf';
+import { doesTypeAllowEmpty } from '../expressions/dataTypes/typeHelpers';
+import { SequenceMultiplicity, SequenceType, ValueType } from '../expressions/dataTypes/Value';
+import { Bucket } from '../expressions/util/Bucket';
+import { CodeGenContext } from './CodeGenContext';
+import {
+	acceptAst,
+	GeneratedCodeBaseType,
+	PartialCompilationResult,
+	PartiallyCompiledAstAccepted,
+	rejectAst,
+} from './JavaScriptCompiledXPath';
+
+export function mapPartialCompilationResult(
+	expr: PartialCompilationResult,
+	cb: (expr: PartiallyCompiledAstAccepted) => PartialCompilationResult
+): PartialCompilationResult {
+	if (!expr.isAstAccepted) {
+		return expr;
+	}
+
+	return cb(expr);
+}
+
+export function mapPartialCompilationResultAndBucket(
+	expr: PartialCompilationResult,
+	cb: (expr: PartiallyCompiledAstAccepted) => [PartialCompilationResult, Bucket]
+): [PartialCompilationResult, Bucket] {
+	if (!expr.isAstAccepted) {
+		return [expr, null];
+	}
+
+	return cb(expr);
+}
+
+export function emitConversionToValue(
+	expr: PartialCompilationResult,
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
+): PartialCompilationResult {
+	return mapPartialCompilationResult(expr, (expr) => {
+		switch (expr.generatedCodeType.type) {
+			case GeneratedCodeBaseType.Value:
+				return expr;
+
+			case GeneratedCodeBaseType.Generator: {
+				// Ensure we have an identifier for the generator in case this is used multiple times
+				return mapPartialCompilationResult(
+					context.getIdentifierFor(expr, 'nodes'),
+					(expr) =>
+						mapPartialCompilationResult(
+							context.getIdentifierFor(contextItemExpr, 'contextItem'),
+							(contextItemExpr) =>
+								acceptAst(
+									`(function () {
+							const { done, value } = ${expr.code}(${contextItemExpr.code}).next();
+							return done ? null : value;
+						})()`,
+									{ type: GeneratedCodeBaseType.Value },
+									[...expr.variables, ...contextItemExpr.variables]
+								)
+						)
+				);
+			}
+
+			default:
+				throw new Error(
+					`invalid generated code type to convert to value: ${expr.generatedCodeType.type}`
+				);
+		}
+	});
+}
+
+export function emitEffectiveBooleanValue(
+	expr: PartialCompilationResult,
+	astType: SequenceType | null,
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
+): PartialCompilationResult {
+	// TODO: a sequence of more than one non-node should return an error, but we don't generate such
+	// sequences yet
+	const value = emitConversionToValue(expr, contextItemExpr, context);
+	if (
+		astType &&
+		astType.type === ValueType.XSBOOLEAN &&
+		astType.mult === SequenceMultiplicity.EXACTLY_ONE
+	) {
+		// No need to coerce if this is already a boolean
+		return value;
+	}
+	return mapPartialCompilationResult(value, (expr) => {
+		return acceptAst(`!!${expr.code}`, { type: GeneratedCodeBaseType.Value }, expr.variables);
+	});
+}
+
+export function emitAtomizedValue(
+	expr: PartialCompilationResult,
+	astType: SequenceType | null,
+	context: CodeGenContext
+): PartialCompilationResult {
+	if (!astType) {
+		return rejectAst('Can not atomize value if type was not annotated');
+	}
+	if (expr.isAstAccepted && expr.generatedCodeType.type !== GeneratedCodeBaseType.Value) {
+		return rejectAst('Atomization only implemented for single value');
+	}
+
+	if (isSubtypeOf(astType.type, ValueType.XSSTRING)) {
+		return expr;
+	}
+
+	if (isSubtypeOf(astType.type, ValueType.ATTRIBUTE)) {
+		return mapPartialCompilationResult(context.getIdentifierFor(expr, 'attr'), (expr) =>
+			acceptAst(
+				`(${expr.code} ? domFacade.getData(${expr.code}) : null)`,
+				{ type: GeneratedCodeBaseType.Value },
+				expr.variables
+			)
+		);
+	}
+	return rejectAst('Atomization only implemented for string and attribute');
+}
+
+export function emitConversionToString(
+	expr: PartialCompilationResult,
+	astType: SequenceType,
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
+): PartialCompilationResult {
+	const value = emitConversionToValue(expr, contextItemExpr, context);
+	const atomized = emitAtomizedValue(value, astType, context);
+	if (!doesTypeAllowEmpty(astType)) {
+		return atomized;
+	}
+	return mapPartialCompilationResult(atomized, (atomized) =>
+		acceptAst(
+			`${atomized.code} ?? ''`,
+			{ type: GeneratedCodeBaseType.Value },
+			atomized.variables
+		)
+	);
+}
+
+function emitNodeCheck(
+	expr: PartialCompilationResult,
+	astType: SequenceType | null,
+	context: CodeGenContext
+): PartialCompilationResult {
+	return mapPartialCompilationResult(context.getIdentifierFor(expr, 'node'), (expr) => {
+		// Accept generators as-is (these currently always yield nodes)
+		if (expr.generatedCodeType.type === GeneratedCodeBaseType.Generator) {
+			return expr;
+		}
+
+		if (astType && !isSubtypeOf(astType.type, ValueType.NODE)) {
+			return rejectAst('Can not evaluate to node if expression does not result in nodes');
+		}
+
+		// Emit a type check
+		return acceptAst(
+			`(function () {
+				if (${expr.code} !== null && !${expr.code}.nodeType) {
+					throw new Error('XPDY0050: The result of the expression was not a node');
+				}
+				return ${expr.code};
+			})()`,
+			{ type: GeneratedCodeBaseType.Value },
+			expr.variables
+		);
+	});
+}
+
+export function emitConversionToFirstNode(
+	expr: PartialCompilationResult,
+	astType: SequenceType | null,
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
+): PartialCompilationResult {
+	const exprAsValue = emitConversionToValue(expr, contextItemExpr, context);
+	return emitNodeCheck(exprAsValue, astType, context);
+}
+
+export function emitConversionToNodes(
+	expr: PartialCompilationResult,
+	astType: SequenceType,
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
+): PartialCompilationResult {
+	return mapPartialCompilationResult(expr, (expr) => {
+		switch (expr.generatedCodeType.type) {
+			case GeneratedCodeBaseType.Generator:
+				return mapPartialCompilationResult(
+					context.getIdentifierFor(expr, 'nodes'),
+					(expr) =>
+						mapPartialCompilationResult(
+							context.getIdentifierFor(contextItemExpr, 'contextItem'),
+							(contextItemExpr) =>
+								acceptAst(
+									`Array.from(${expr.code}(${contextItemExpr.code}))`,
+									{ type: GeneratedCodeBaseType.Value },
+									[...expr.variables, ...contextItemExpr.variables]
+								)
+						)
+				);
+			case GeneratedCodeBaseType.Value:
+				return mapPartialCompilationResult(
+					context.getIdentifierFor(emitNodeCheck(expr, astType, context), 'node'),
+					(expr) =>
+						acceptAst(
+							`(${expr.code} === null ? [] : [${expr.code}])`,
+							{ type: GeneratedCodeBaseType.Value },
+							expr.variables
+						)
+				);
+			default:
+				return rejectAst('Unsupported code type to evaluate to nodes');
+		}
+	});
+}
+
+export function emitAnd(
+	condition1: PartialCompilationResult,
+	condition2: PartialCompilationResult
+): PartialCompilationResult {
+	return mapPartialCompilationResult(condition1, (condition1) =>
+		mapPartialCompilationResult(condition2, (condition2) => {
+			if (
+				condition1.generatedCodeType.type !== GeneratedCodeBaseType.Value ||
+				condition2.generatedCodeType.type !== GeneratedCodeBaseType.Value
+			) {
+				throw new Error('can only use emitAnd with value expressions');
+			}
+			return acceptAst(
+				`${condition1.code} && ${condition2.code}`,
+				{ type: GeneratedCodeBaseType.Value },
+				[...condition1.variables, ...condition2.variables]
+			);
+		})
+	);
+}
diff --git a/src/jsCodegen/emitLiterals.ts b/src/jsCodegen/emitLiterals.ts
index 5c4263db..56eaf910 100644
--- a/src/jsCodegen/emitLiterals.ts
+++ b/src/jsCodegen/emitLiterals.ts
@@ -3,7 +3,6 @@ import astHelper, { IAST } from '../parsing/astHelper';
 import escapeJavaScriptString from './escapeJavaScriptString';
 import {
 	acceptAst,
-	FunctionIdentifier,
 	GeneratedCodeBaseType,
 	PartialCompilationResult,
 } from './JavaScriptCompiledXPath';
@@ -17,15 +16,9 @@ import {
  * @param identifier The function wrapper identifier
  * @returns Wrapped string literal function
  */
-export function emitStringLiteralExpression(
-	ast: IAST,
-	identifier: FunctionIdentifier
-): [PartialCompilationResult, Bucket] {
+export function emitStringConstantExpr(ast: IAST): [PartialCompilationResult, Bucket] {
 	// Note: default the value to the emptyy string. The XQueryX roundtrip may omit them
 	let text = (astHelper.getFirstChild(ast, 'value')[1] as string) || '';
 	text = escapeJavaScriptString(text);
-	return [
-		acceptAst(`const ${identifier} = ${text};`, { type: GeneratedCodeBaseType.Variable }),
-		null,
-	];
+	return [acceptAst(text, { type: GeneratedCodeBaseType.Value }, []), null];
 }
diff --git a/src/jsCodegen/emitLogicalExpr.ts b/src/jsCodegen/emitLogicalExpr.ts
index e72cfd1b..a4affc5a 100644
--- a/src/jsCodegen/emitLogicalExpr.ts
+++ b/src/jsCodegen/emitLogicalExpr.ts
@@ -1,11 +1,10 @@
-import { ValueType } from '../expressions/dataTypes/Value';
 import { Bucket, intersectBuckets } from '../expressions/util/Bucket';
-import { IAST } from '../parsing/astHelper';
+import astHelper, { IAST } from '../parsing/astHelper';
 import { CodeGenContext } from './CodeGenContext';
+import { emitEffectiveBooleanValue, mapPartialCompilationResult } from './emitHelpers';
 import { emitOperand } from './emitOperand';
 import {
 	acceptAst,
-	FunctionIdentifier,
 	GeneratedCodeBaseType,
 	PartialCompilationResult,
 } from './JavaScriptCompiledXPath';
@@ -14,36 +13,26 @@ import {
  * Helper function to compile an and expressions to a JavaScript function.
  *
  * https://www.w3.org/TR/xpath-31/#doc-xpath31-AndExpr
- *
- * @param ast Logical expression AST node.
- * @param identifier Function identifier for the emitted function
- * @param staticContext Static context parameter to retrieve context-dependent information.
- * @returns Wrapped and expression.
  */
 export function emitAndExpr(
 	ast: IAST,
-	identifier: FunctionIdentifier,
-	staticContext: CodeGenContext
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
 ): [PartialCompilationResult, Bucket] {
-	return emitLogicalExpr(ast, identifier, staticContext, '&&');
+	return emitLogicalExpr(ast, '&&', contextItemExpr, context);
 }
 
 /**
  * Helper function to compile an or expressions to a JavaScript function.
  *
  * https://www.w3.org/TR/xpath-31/#doc-xpath31-OrExpr
- *
- * @param ast Logical expression AST node.
- * @param identifier Function identifier for the emitted function
- * @param staticContext Static context parameter to retrieve context-dependent information.
- * @returns Wrapped or expression.
  */
 export function emitOrExpr(
 	ast: IAST,
-	identifier: FunctionIdentifier,
-	staticContext: CodeGenContext
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
 ): [PartialCompilationResult, Bucket] {
-	return emitLogicalExpr(ast, identifier, staticContext, '||');
+	return emitLogicalExpr(ast, '||', contextItemExpr, context);
 }
 
 /**
@@ -59,46 +48,43 @@ export function emitOrExpr(
  */
 function emitLogicalExpr(
 	ast: IAST,
-	identifier: FunctionIdentifier,
-	staticContext: CodeGenContext,
-	logicalExprOperator: '&&' | '||'
+	logicalExprOperator: '&&' | '||',
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
 ): [PartialCompilationResult, Bucket] {
-	const [firstExpr, firstBucket] = emitOperand(
-		ast,
-		identifier,
-		'firstOperand',
-		staticContext,
-		ValueType.XSBOOLEAN
+	const [firstExpr, firstBucket] = emitOperand(ast, 'firstOperand', contextItemExpr, context);
+	const firstType = astHelper.getAttribute(
+		astHelper.followPath(ast, ['firstOperand', '*']),
+		'type'
 	);
-	if (!firstExpr.isAstAccepted) {
-		return [firstExpr, null];
-	}
+	const firstAsBool = emitEffectiveBooleanValue(firstExpr, firstType, contextItemExpr, context);
+	const [secondExpr, secondBucket] = emitOperand(ast, 'secondOperand', contextItemExpr, context);
+	const logicalOpExpr = mapPartialCompilationResult(firstAsBool, (firstAsBool) => {
+		const secondType = astHelper.getAttribute(
+			astHelper.followPath(ast, ['firstOperand', '*']),
+			'type'
+		);
+		const secondAsBool = emitEffectiveBooleanValue(
+			secondExpr,
+			secondType,
+			contextItemExpr,
+			context
+		);
+		return mapPartialCompilationResult(secondAsBool, (secondAsBool) =>
+			acceptAst(
+				`(${firstAsBool.code} ${logicalExprOperator} ${secondAsBool.code})`,
+				{ type: GeneratedCodeBaseType.Value },
+				[...firstAsBool.variables, ...secondAsBool.variables]
+			)
+		);
+	});
 
-	const [secondExpr, secondBucket] = emitOperand(
-		ast,
-		identifier,
-		'secondOperand',
-		staticContext,
-		ValueType.XSBOOLEAN
-	);
-	if (!secondExpr.isAstAccepted) {
-		return [secondExpr, null];
-	}
-
-	const logicalOpCode = `
-	function ${identifier}(contextItem) {
-		${firstExpr.variables.join('\n')}
-		${secondExpr.variables.join('\n')}
-		return ${firstExpr.code} ${logicalExprOperator} ${secondExpr.code};
-	}
-	`;
-	return [
-		acceptAst(logicalOpCode, {
-			type: GeneratedCodeBaseType.Function,
-			returnType: { type: GeneratedCodeBaseType.Value },
-		}),
+	const bucket =
 		logicalExprOperator === '&&'
 			? intersectBuckets(firstBucket, secondBucket)
-			: firstBucket || secondBucket,
-	];
+			: firstBucket === secondBucket
+			? firstBucket
+			: null;
+
+	return [logicalOpExpr, bucket];
 }
diff --git a/src/jsCodegen/emitOperand.ts b/src/jsCodegen/emitOperand.ts
index 9695c153..9c88ee1e 100644
--- a/src/jsCodegen/emitOperand.ts
+++ b/src/jsCodegen/emitOperand.ts
@@ -1,39 +1,7 @@
-import { ValueType } from '../expressions/dataTypes/Value';
 import { Bucket } from '../expressions/util/Bucket';
 import astHelper, { IAST } from '../parsing/astHelper';
 import { CodeGenContext } from './CodeGenContext';
-import {
-	acceptAst,
-	FunctionIdentifier,
-	PartialCompilationResult,
-	rejectAst,
-} from './JavaScriptCompiledXPath';
-import { determinePredicateTruthValue } from './runtimeLib';
-
-export const baseExprAstNodes = {
-	PATH_EXPR: 'pathExpr',
-	AND_OP: 'andOp',
-	OR_OP: 'orOp',
-	STRING_LIT_EXPR: 'stringConstantExpr',
-	EQUAL_OP: 'equalOp',
-	NOT_EQUAL_OP: 'notEqualOp',
-	LESS_THAN_OR_EQUAL_OP: 'lessThanOrEqualOp',
-	LESS_THAN_OP: 'lessThanOp',
-	GREATER_THAN_OR_EQUAL_OP: 'greaterThanOrEqualOp',
-	GREATER_THAN_OP: 'greaterThanOp',
-	EQ_OP: 'eqOp',
-	NE_OP: 'neOp',
-	LT_OP: 'ltOp',
-	LE_OP: 'leOp',
-	GT_OP: 'gtOp',
-	GE_OP: 'geOp',
-	IS_OP: 'isOp',
-	NODE_BEFORE_OP: 'nodeBeforeOp',
-	NODE_AFTER_OP: 'nodeAfterOp',
-	FUNCTION_CALL_EXPR: 'functionCallExpr',
-};
-
-export const baseExpressions = Object.values(baseExprAstNodes);
+import { PartialCompilationResult, rejectAst } from './JavaScriptCompiledXPath';
 
 /**
  * Retrieves the first or second operand for an operator AST node and wraps
@@ -47,40 +15,14 @@ export const baseExpressions = Object.values(baseExprAstNodes);
  */
 export function emitOperand(
 	ast: IAST,
-	identifier: FunctionIdentifier,
 	operandKind: 'firstOperand' | 'secondOperand',
-	staticContext: CodeGenContext,
-	targetType?: ValueType
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
 ): [PartialCompilationResult, Bucket] {
-	const operand = astHelper.getFirstChild(ast, operandKind);
-	const exprAst = astHelper.getFirstChild(operand, baseExpressions);
+	const exprAst = astHelper.followPath(ast, [operandKind, '*']);
 	if (!exprAst) {
-		return [rejectAst('Unsupported: a base expression used with an operand.'), null];
-	}
-
-	const baseExprIdentifier = identifier + operandKind;
-
-	const [baseExpr, bucket] = staticContext.emitBaseExpr(
-		exprAst,
-		baseExprIdentifier,
-		staticContext
-	);
-	if (!baseExpr.isAstAccepted) {
-		return [baseExpr, null];
+		return [rejectAst(`${operandKind} expression not found`), null];
 	}
 
-	if (targetType === ValueType.XSBOOLEAN) {
-		return [
-			determinePredicateTruthValue(
-				baseExprIdentifier,
-				baseExpr.code,
-				baseExpr.generatedCodeType
-			),
-			null,
-		];
-	}
-	return [
-		acceptAst(`${baseExprIdentifier}`, baseExpr.generatedCodeType, [baseExpr.code]),
-		bucket,
-	];
+	return context.emitBaseExpr(exprAst, contextItemExpr, context);
 }
diff --git a/src/jsCodegen/emitPathExpr.ts b/src/jsCodegen/emitPathExpr.ts
index b527d83d..8ff0082a 100644
--- a/src/jsCodegen/emitPathExpr.ts
+++ b/src/jsCodegen/emitPathExpr.ts
@@ -2,191 +2,309 @@ import { NODE_TYPES } from '../domFacade/ConcreteNode';
 import { Bucket, intersectBuckets } from '../expressions/util/Bucket';
 import astHelper, { IAST } from '../parsing/astHelper';
 import { CodeGenContext } from './CodeGenContext';
-import emitStep from './emitStep';
+import emitAxis, { axisAstNodes } from './emitAxis';
+import {
+	emitAnd,
+	emitEffectiveBooleanValue,
+	mapPartialCompilationResult,
+	mapPartialCompilationResultAndBucket,
+} from './emitHelpers';
 import emitTest, { tests } from './emitTest';
 import {
 	acceptAst,
-	FunctionIdentifier,
 	GeneratedCodeBaseType,
 	PartialCompilationResult,
 	rejectAst,
 } from './JavaScriptCompiledXPath';
-import { determinePredicateTruthValue } from './runtimeLib';
+
+function emitPredicate(
+	predicateAst: IAST,
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
+): [PartialCompilationResult, Bucket] {
+	const [expr, bucket] = context.emitBaseExpr(predicateAst, contextItemExpr, context);
+	const type = astHelper.getAttribute(predicateAst, 'type');
+	const asBool = emitEffectiveBooleanValue(expr, type, contextItemExpr, context);
+	return [asBool, bucket];
+}
 
 /**
  * Determines for every path step if it should emit a node or not.
- *
- * @param predicatesAst AST node for the predicate.
- * @param nestLevel The nest level within the path expression.
- * @param staticContext Static context parameter to retrieve context-dependent information.
- * @returns JavaScript code of the steps predicates.
  */
 function emitPredicates(
-	predicatesAst: IAST,
-	nestLevel: number,
-	staticContext: CodeGenContext
-): [PartialCompilationResult, Bucket] {
-	let evaluatePredicateConditionCode = '';
-	const functionDeclarations: string[] = [];
-
-	if (!predicatesAst) {
-		return [acceptAst(``, { type: GeneratedCodeBaseType.None }, functionDeclarations), null];
-	}
-
-	const children = astHelper.getChildren(predicatesAst, '*');
-	let intersectingPredicateBucket = null;
-	for (let i = 0; i < children.length; i++) {
-		const predicate = children[i];
-		const predicateFunctionIdentifier = `step${nestLevel}_predicate${i}`;
-
-		const [compiledPredicate, bucket] = staticContext.emitBaseExpr(
-			predicate,
-			predicateFunctionIdentifier,
-			staticContext
-		);
-
-		if (!compiledPredicate.isAstAccepted) {
-			return [compiledPredicate, null];
-		}
-		intersectingPredicateBucket = intersectBuckets(intersectingPredicateBucket, bucket);
-
-		// Prepare condition used to determine if an axis should
-		// return a node.
-		const predicateFunctionCall = determinePredicateTruthValue(
-			predicateFunctionIdentifier,
-			'',
-			compiledPredicate.generatedCodeType,
-			`contextItem${nestLevel}`
-		);
-
-		if (!predicateFunctionCall.isAstAccepted) {
-			return [predicateFunctionCall, null];
-		}
-
-		if (i === 0) {
-			evaluatePredicateConditionCode += predicateFunctionCall.code;
-		} else {
-			evaluatePredicateConditionCode = `${evaluatePredicateConditionCode} && ${predicateFunctionCall.code}`;
-		}
+	predicatesAst: IAST | null,
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
+): [PartialCompilationResult | null, Bucket] {
+	const predicateAsts = predicatesAst ? astHelper.getChildren(predicatesAst, '*') : [];
+	const [combinedPredicatesExpr, bucket] = predicateAsts.reduce<
+		[PartialCompilationResult | null, Bucket]
+	>(
+		([previousExpr, previousBucket], predicateAst) => {
+			if (!previousExpr) {
+				return emitPredicate(predicateAst, contextItemExpr, context);
+			}
+			let combinedBucket = previousBucket;
+			return mapPartialCompilationResultAndBucket(previousExpr, (previousExpr) => {
+				const [predicateExpr, bucket] = emitPredicate(
+					predicateAst,
+					contextItemExpr,
+					context
+				);
+				combinedBucket = intersectBuckets(previousBucket, bucket);
+				return [
+					mapPartialCompilationResult(predicateExpr, (predicateExpr) =>
+						acceptAst(
+							`${previousExpr.code} && ${predicateExpr.code}`,
+							{ type: GeneratedCodeBaseType.Value },
+							[...previousExpr.variables, ...predicateExpr.variables]
+						)
+					),
+					combinedBucket,
+				];
+			});
+		},
+		[null, null]
+	);
 
-		functionDeclarations.push(compiledPredicate.code);
-	}
 	return [
-		acceptAst(
-			evaluatePredicateConditionCode,
-			{ type: GeneratedCodeBaseType.Value },
-			functionDeclarations
-		),
-		intersectingPredicateBucket,
+		// Make predicate variables evaluate lazily so the test is always checked first
+		combinedPredicatesExpr
+			? mapPartialCompilationResult(combinedPredicatesExpr, (combinedPredicatesExpr) =>
+					acceptAst(
+						`(function () {
+							${combinedPredicatesExpr.variables.join('\n')}
+							return ${combinedPredicatesExpr.code};
+						})()`,
+						{ type: GeneratedCodeBaseType.Value },
+						[]
+					)
+			  )
+			: null,
+		bucket,
 	];
 }
 
 /**
  * Takes the step AST's of a path expression and turns it into runnable JavaScript code.
- *
- * @param stepsAst AST nodes of the path expression steps
- * @param staticContext Static context parameter to retrieve context-dependent information.
- * @returns JavaScript code of the path expression steps.
  */
 function emitSteps(
-	stepsAst: IAST[],
-	staticContext: CodeGenContext
+	stepAsts: IAST[],
+	contextItemCanBeAttribute: boolean,
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
 ): [PartialCompilationResult, Bucket] {
-	if (stepsAst.length === 0) {
+	if (stepAsts.length === 0) {
 		return [
-			acceptAst(
-				`
-			if (!hasReturned) {
-				hasReturned = true;
-				return ready(contextItem);
-			}
-			`,
-				{ type: GeneratedCodeBaseType.Statement },
-				['let hasReturned = false;']
+			mapPartialCompilationResult(contextItemExpr, (contextItemExpr) =>
+				acceptAst(
+					`yield ${contextItemExpr.code};`,
+					{ type: GeneratedCodeBaseType.Statement },
+					contextItemExpr.variables
+				)
 			),
 			null,
 		];
 	}
 
-	let emittedCode = '';
-	const emittedVariables: string[] = [];
-	let intersectingBucket: Bucket = null;
-	for (let i = stepsAst.length - 1; i >= 0; i--) {
-		const step = stepsAst[i];
-		const nestLevel = i + 1;
+	// A step is constructed as follows:
+	// for (stepContextItemExpr of axis) {
+	//     test code for step, continue if no match
+	//     predicates for step, continue if no match
+	//     nested steps
+	// }
+	// This recurses over steps to build that nested structure. Once there are no more nested steps
+	// this hits the case above which outputs the single yield statement.
 
-		const predicatesAst = astHelper.getFirstChild(step, 'predicates');
-		const [emittedPredicates, predicateBucket] = emitPredicates(
-			predicatesAst,
-			nestLevel,
-			staticContext
-		);
-		if (!emittedPredicates.isAstAccepted) {
-			return [emittedPredicates, null];
+	const [stepAst, ...restStepAsts] = stepAsts;
+
+	const lookupAsts = astHelper.getChildren(stepAst, 'lookup');
+	if (lookupAsts.length > 0) {
+		return [rejectAst('Unsupported: lookups'), null];
+	}
+
+	// Every step introduces a context item for its test and predicates
+	const stepContextItemExpr = context.getNewIdentifier('contextItem');
+
+	const predicatesAst = astHelper.getFirstChild(stepAst, 'predicates');
+	const [predicatesExpr, predicatesBucket] = emitPredicates(
+		predicatesAst,
+		stepContextItemExpr,
+		context
+	);
+
+	const axisAst = astHelper.getFirstChild(stepAst, 'xpathAxis');
+	if (axisAst) {
+		const testAst = astHelper.getFirstChild(stepAst, tests);
+		if (!testAst) {
+			return [rejectAst('Unsupported test in step'), null];
 		}
 
-		const axisAst = astHelper.getFirstChild(step, 'xpathAxis');
-		if (axisAst) {
-			const emittedStepsCode = emittedCode;
-			const testAst = astHelper.getFirstChild(step, tests);
-			if (!testAst) {
-				return [rejectAst(`Unsupported: the test in the '${step}' step.`), null];
-			}
+		// TODO: may be better to annotate steps instead to narrow down types?
+		const axisType = astHelper.getTextContent(axisAst);
+		const stepContextItemCanBeAttribute =
+			axisType === 'attribute' || (axisType === 'self' && contextItemCanBeAttribute);
+		const [testExpr, testBucket] = emitTest(
+			testAst,
+			stepContextItemCanBeAttribute,
+			stepContextItemExpr,
+			context
+		);
 
-			// Only the innermost nested step returns a value.
-			const nestedCode =
-				i === stepsAst.length - 1
-					? `i${nestLevel}++;
-					   return ready(contextItem${nestLevel});`
-					: `${emittedStepsCode}
-					   i${nestLevel}++;`;
-
-			const [emittedTest, testBucket] = emitTest(
-				testAst,
-				`contextItem${nestLevel}`,
-				staticContext
-			);
-			if (!emittedTest.isAstAccepted) {
-				return [emittedTest, null];
-			}
+		const combinedConditionExpr =
+			predicatesExpr === null ? testExpr : emitAnd(testExpr, predicatesExpr);
 
-			const intersectingBucketForStep = intersectBuckets(testBucket, predicateBucket);
-
-			const [emittedStep, stepBucket] = emitStep(
-				axisAst,
-				emittedTest.code,
-				emittedPredicates.code,
-				nestLevel,
-				nestedCode,
-				intersectingBucketForStep
-			);
-			if (!emittedStep.isAstAccepted) {
-				return [emittedStep, null];
-			}
+		const combinedConditionBucket = intersectBuckets(testBucket, predicatesBucket);
 
-			if (i === 0) {
-				// For the first iteration, the bucket of the steps is actually the bucket for the whole thing
-				intersectingBucket = stepBucket;
-			}
+		const [nestedStepsCode, _] = emitSteps(
+			restStepAsts,
+			stepContextItemCanBeAttribute,
+			stepContextItemExpr,
+			context
+		);
 
-			emittedVariables.push(...emittedStep.variables, ...emittedPredicates.variables);
-			emittedCode = emittedStep.code;
-		} else {
-			return [rejectAst('Unsupported: filter expressions.'), null];
-		}
+		return emitAxis(
+			axisAst,
+			combinedConditionExpr,
+			combinedConditionBucket,
+			nestedStepsCode,
+			stepContextItemExpr,
+			contextItemExpr
+		);
+	}
 
-		const lookups = astHelper.getChildren(step, 'lookup');
-		if (lookups.length > 0) {
-			return [rejectAst('Unsupported: lookups.'), null];
-		}
+	// step may contain a filterExpr instead
+	const filterExprAst = astHelper.followPath(stepAst, ['filterExpr', '*']);
+	if (!filterExprAst) {
+		return [rejectAst('Unsupported: unknown step type'), null];
 	}
-	const contextDeclaration = 'const contextItem0 = contextItem;';
-	emittedCode = contextDeclaration + emittedCode;
 
-	return [
-		acceptAst(emittedCode, { type: GeneratedCodeBaseType.Statement }, emittedVariables),
-		intersectingBucket,
-	];
+	// Compile the expression
+	const [filterExpr, filterBucket] = context.emitBaseExpr(
+		filterExprAst,
+		contextItemExpr,
+		context
+	);
+	// Assign it to the stepContextItem variable
+	const filterExprAsStepContextItemExpr = mapPartialCompilationResult(filterExpr, (filterExpr) =>
+		acceptAst(
+			`const ${stepContextItemExpr.code} = ${filterExpr.code};`,
+			{ type: GeneratedCodeBaseType.Statement },
+			[...stepContextItemExpr.variables, ...filterExpr.variables]
+		)
+	);
+
+	// If there are following steps, the result of the expression must be a node
+	const filterExprWithCheck =
+		restStepAsts.length === 0
+			? filterExprAsStepContextItemExpr
+			: mapPartialCompilationResult(
+					filterExprAsStepContextItemExpr,
+					(filterExprAsStepContextItemExpr) =>
+						acceptAst(
+							`${filterExprAsStepContextItemExpr.code}
+							if (${stepContextItemExpr.code} !== null && !${stepContextItemExpr.code}.nodeType) {
+								throw new Error('XPTY0019: The result of E1 in a path expression E1/E2 should evaluate to a sequence of nodes.');
+							}`,
+							{ type: GeneratedCodeBaseType.Statement },
+							filterExprAsStepContextItemExpr.variables
+						)
+			  );
+
+	return mapPartialCompilationResultAndBucket(filterExprWithCheck, (filterExprWithCheck) => {
+		// Compile nested steps
+		const [nestedStepsCode, _] = emitSteps(restStepAsts, true, stepContextItemExpr, context);
+
+		const nestedCode =
+			predicatesExpr === null
+				? nestedStepsCode
+				: mapPartialCompilationResult(predicatesExpr, (predicatesExpr) =>
+						mapPartialCompilationResult(nestedStepsCode, (nestedStepsCode) =>
+							acceptAst(
+								`if (${predicatesExpr.code}) {
+									${nestedStepsCode.variables.join('\n')}
+									${nestedStepsCode.code}
+								}`,
+								{ type: GeneratedCodeBaseType.Statement },
+								predicatesExpr.variables
+							)
+						)
+				  );
+
+		// Combine
+		return [
+			mapPartialCompilationResult(nestedCode, (nestedCode) =>
+				acceptAst(
+					`${filterExprWithCheck.code}
+					${nestedCode.variables.join('\n')}
+					${nestedCode.code}`,
+					{ type: GeneratedCodeBaseType.Statement },
+					filterExprWithCheck.variables
+				)
+			),
+			filterBucket,
+		];
+	});
+}
+
+function emitRootExpr(
+	contextItemExpr: PartialCompilationResult,
+	_context: CodeGenContext
+): PartialCompilationResult {
+	return mapPartialCompilationResult(contextItemExpr, (contextItemExpr) =>
+		acceptAst(
+			`(function () {
+				let n = ${contextItemExpr.code};
+				while (n.nodeType !== /*DOCUMENT_NODE*/${NODE_TYPES.DOCUMENT_NODE}) {
+					n = domFacade.getParentNode(n);
+					if (n === null) {
+						throw new Error('XPDY0050: the root node of the context node is not a document node.');
+					}
+				}
+				return n;
+			})()`,
+			{ type: GeneratedCodeBaseType.Value },
+			contextItemExpr.variables
+		)
+	);
+}
+
+function emitSingleSelfPathExpr(
+	stepAst: IAST,
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
+): [PartialCompilationResult, Bucket] {
+	return mapPartialCompilationResultAndBucket(contextItemExpr, (contextItemExpr) => {
+		const lookupAsts = astHelper.getChildren(stepAst, 'lookup');
+		if (lookupAsts.length > 0) {
+			return [rejectAst('Unsupported: lookups'), null];
+		}
+		const predicatesAst = astHelper.getFirstChild(stepAst, 'predicates');
+		const [predicatesExpr, predicatesBucket] = emitPredicates(
+			predicatesAst,
+			contextItemExpr,
+			context
+		);
+		const testAst = astHelper.getFirstChild(stepAst, tests);
+		if (!testAst) {
+			return [rejectAst('Unsupported test in step'), null];
+		}
+		const [testExpr, testBucket] = emitTest(testAst, true, contextItemExpr, context);
+		const combinedConditionExpr =
+			predicatesExpr === null ? testExpr : emitAnd(testExpr, predicatesExpr);
+		const combinedConditionBucket = intersectBuckets(testBucket, predicatesBucket);
+
+		return [
+			mapPartialCompilationResult(combinedConditionExpr, (combinedConditionExpr) =>
+				acceptAst(
+					`((${combinedConditionExpr.code}) ? ${contextItemExpr.code} : null)`,
+					{ type: GeneratedCodeBaseType.Value },
+					[...contextItemExpr.variables, ...combinedConditionExpr.variables]
+				)
+			),
+			combinedConditionBucket,
+		];
+	});
 }
 
 /**
@@ -195,58 +313,39 @@ function emitSteps(
  * consist of a series of one or more steps.
  *
  * https://www.w3.org/TR/xpath-31/#doc-xpath31-PathExpr
- *
- * @param ast AST node of the path expression
- * @param identifier Function identifier for the emitted code.
- * @param staticContext Static context parameter to retrieve context-dependent information.
- * @returns JavaScript code of the path expression AST node.
  */
 export function emitPathExpr(
 	ast: IAST,
-	identifier: FunctionIdentifier,
-	staticContext: CodeGenContext
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
 ): [PartialCompilationResult, Bucket] {
-	// Find the root node from the context.
-	const isAbsolute = astHelper.getFirstChild(ast, 'rootExpr');
-	let absoluteCode = '';
-	if (isAbsolute) {
-		absoluteCode = `
-		let documentNode = contextItem;
-		while (documentNode.nodeType !== /*DOCUMENT_NODE*/${NODE_TYPES.DOCUMENT_NODE}) {
-			documentNode = domFacade.getParentNode(documentNode);
-			if (documentNode === null) {
-				throw new Error('XPDY0050: the root node of the context node is not a document node.');
-			}
+	// Optimized code for single-self-axis paths, which are used a lot in selectors and don't need
+	// a generator as they only test the context node
+	const stepAsts = astHelper.getChildren(ast, 'stepExpr');
+	if (stepAsts.length === 1) {
+		const axisAst = astHelper.getFirstChild(stepAsts[0], 'xpathAxis');
+		if (axisAst && astHelper.getTextContent(axisAst) === axisAstNodes.SELF) {
+			return emitSingleSelfPathExpr(stepAsts[0], contextItemExpr, context);
 		}
-		contextItem = documentNode;
-		`;
 	}
 
-	const [emittedSteps, bucket] = emitSteps(astHelper.getChildren(ast, 'stepExpr'), staticContext);
-	if (!emittedSteps.isAstAccepted) {
-		return [emittedSteps, null];
-	}
-
-	const pathExprCode = `
-	function ${identifier}(contextItem) {
-		${absoluteCode}
-		${emittedSteps.variables.join('\n')}
-		const next = () => {
-			${emittedSteps.code}
-			return DONE_TOKEN;
-		};
-		return {
-			next,
-			[Symbol.iterator]() { return this; }
-		};
-	}
-	`;
-
-	return [
-		acceptAst(pathExprCode, {
-			type: GeneratedCodeBaseType.Function,
-			returnType: { type: GeneratedCodeBaseType.Iterator },
-		}),
-		bucket,
-	];
+	// Other paths compile into a generator which is parameterized over contextItemExpr
+	const contextItemArgExpr = context.getNewIdentifier('contextItem');
+	// Find the root node from the context.
+	const isAbsolute = astHelper.getFirstChild(ast, 'rootExpr');
+	const rootExpr = isAbsolute
+		? context.getIdentifierFor(emitRootExpr(contextItemArgExpr, context), 'root')
+		: contextItemArgExpr;
+	const [stepsCode, bucket] = emitSteps(stepAsts, !isAbsolute, rootExpr, context);
+	const generatorExpr = mapPartialCompilationResult(stepsCode, (stepsCode) =>
+		acceptAst(
+			`(function* (${contextItemArgExpr.code}) {
+			${stepsCode.variables.join('\n')}
+			${stepsCode.code}
+		})`,
+			{ type: GeneratedCodeBaseType.Generator },
+			[]
+		)
+	);
+	return [generatorExpr, bucket];
 }
diff --git a/src/jsCodegen/emitStep.ts b/src/jsCodegen/emitStep.ts
deleted file mode 100644
index 1e55cb40..00000000
--- a/src/jsCodegen/emitStep.ts
+++ /dev/null
@@ -1,200 +0,0 @@
-import { NODE_TYPES } from '../domFacade/ConcreteNode';
-import { Bucket } from '../expressions/util/Bucket';
-import astHelper, { IAST } from '../parsing/astHelper';
-import {
-	acceptAst,
-	ContextItemIdentifier,
-	GeneratedCodeBaseType,
-	PartialCompilationResult,
-	rejectAst,
-} from './JavaScriptCompiledXPath';
-
-const axisAstNodes = {
-	ATTRIBUTE: 'attribute',
-	CHILD: 'child',
-	PARENT: 'parent',
-	SELF: 'self',
-};
-
-// The child axis contains the children of the context node.
-// https://www.w3.org/TR/xpath-31/#doc-xpath31-ForwardAxis
-//
-// returns node()'s.
-// https://www.w3.org/TR/xpath-datamodel-31/#dm-children
-const childAxisContextNodesIdentifier = 'childNodes';
-function emitChildAxis(
-	test: string,
-	predicates: string,
-	nestLevel: number,
-	nestedCode: string,
-	bucket: Bucket | null
-): PartialCompilationResult {
-	const contextNodesCode = `const ${childAxisContextNodesIdentifier}${nestLevel} = domFacade.getChildNodes(contextItem${
-		nestLevel - 1
-	} ${bucket ? `, "${bucket}"` : ''});`;
-
-	return emitMultipleNodeAxis(
-		test,
-		predicates,
-		nestLevel,
-		nestedCode,
-		childAxisContextNodesIdentifier,
-		contextNodesCode
-	);
-}
-
-// the attribute axis contains the attributes of the context node,
-const attributeAxisContextNodesIdentifier = 'attributeNodes';
-function emitAttributeAxis(
-	test: string,
-	predicates: string,
-	nestLevel: number,
-	nestedCode: string,
-	bucket: Bucket | null
-): PartialCompilationResult {
-	// Only element nodes can have attributes.
-	const contextNodesCode = `
-	let ${attributeAxisContextNodesIdentifier}${nestLevel};
-	if (contextItem && contextItem${nestLevel - 1}.nodeType === /*ELEMENT_NODE*/ ${
-		NODE_TYPES.ELEMENT_NODE
-	}) {
-		${attributeAxisContextNodesIdentifier}${nestLevel} =  domFacade.getAllAttributes(contextItem${
-		nestLevel - 1
-	} ${bucket ? `, "${bucket}"` : ''});
-	}
-	`;
-
-	return emitMultipleNodeAxis(
-		test,
-		predicates,
-		nestLevel,
-		nestedCode,
-		attributeAxisContextNodesIdentifier,
-		contextNodesCode
-	);
-}
-
-// self::para selects the context node if it is a para element, and otherwise
-// returns an empty sequence
-function emitSelfAxis(
-	test: string,
-	predicates: string,
-	nestLevel: number,
-	nestedCode: string
-): PartialCompilationResult {
-	const contextNodeCode = `const contextItem${nestLevel} = contextItem${nestLevel - 1};`;
-
-	return emitSingleNodeAxis(test, predicates, nestLevel, nestedCode, contextNodeCode);
-}
-
-// parent::node() selects the parent of the context node. If the context node is
-// an attribute node, this expression returns the element node (if any) to which
-// the attribute node is attached.
-function emitParentAxis(
-	test: string,
-	predicates: string,
-	nestLevel: number,
-	nestedCode: string,
-	bucket: Bucket | null
-): PartialCompilationResult {
-	const contextNodeCode = `
-	const contextItem${nestLevel} = domFacade.getParentNode(contextItem${nestLevel - 1} ${
-		bucket ? `, "${bucket}"` : ''
-	});
-	`;
-
-	return emitSingleNodeAxis(test, predicates, nestLevel, nestedCode, contextNodeCode);
-}
-
-function formatConditions(...conditions: string[]): string {
-	return conditions.filter((c) => c !== '').join(' && ');
-}
-
-function emitMultipleNodeAxis(
-	test: string,
-	predicates: string,
-	nestLevel: number,
-	nestedCode: string,
-	contextItemIdentifier: ContextItemIdentifier,
-	contextNodeCode: string
-): PartialCompilationResult {
-	const whileConditions = formatConditions(
-		`${contextItemIdentifier}${nestLevel}`,
-		`i${nestLevel} < ${contextItemIdentifier}${nestLevel}.length`
-	);
-
-	const indexReset = nestLevel !== 1 ? `i${nestLevel} = 0;` : ``;
-	const nullCheckCode = `contextItem${nestLevel}`;
-
-	const conditions = formatConditions(nullCheckCode, test, predicates);
-
-	const axisCode = `
-	${contextNodeCode}
-	while (${whileConditions}) {
-		const contextItem${nestLevel} = ${contextItemIdentifier}${nestLevel}[i${nestLevel}];
-		if (!(${conditions})) {
-			i${nestLevel}++;
-			continue;
-		}
-		${nestedCode}
-	}
-	${indexReset}
-	`;
-
-	return acceptAst(axisCode, { type: GeneratedCodeBaseType.Statement }, [
-		`let i${nestLevel} = 0;`,
-	]);
-}
-
-// Emit code for an axis made up of exactly one node, that should only be
-// returned once.
-function emitSingleNodeAxis(
-	test: string,
-	predicates: string,
-	nestLevel: number,
-	nestedCode: string,
-	contextNodeCode: string
-): PartialCompilationResult {
-	const isFirstPassCode = `i${nestLevel} === 0`;
-
-	const nullCheckCode = `contextItem${nestLevel}`;
-
-	const conditions = formatConditions(isFirstPassCode, nullCheckCode, test, predicates);
-
-	return acceptAst(
-		`
-		${contextNodeCode}
-		if (${conditions}) {
-			${nestedCode}
-		}
-		`,
-		{ type: GeneratedCodeBaseType.Statement },
-		[`let i${nestLevel} = 0;`]
-	);
-}
-
-function emitStep(
-	ast: IAST,
-	test: string,
-	predicates: string,
-	nestLevel: number,
-	nestedCode: string,
-	bucket: Bucket | null
-): [PartialCompilationResult, Bucket] {
-	const axisName = astHelper.getTextContent(ast);
-
-	switch (axisName) {
-		case axisAstNodes.ATTRIBUTE:
-			return [emitAttributeAxis(test, predicates, nestLevel, nestedCode, bucket), 'type-1'];
-		case axisAstNodes.CHILD:
-			return [emitChildAxis(test, predicates, nestLevel, nestedCode, bucket), null];
-		case axisAstNodes.PARENT:
-			return [emitParentAxis(test, predicates, nestLevel, nestedCode, bucket), null];
-		case axisAstNodes.SELF:
-			return [emitSelfAxis(test, predicates, nestLevel, nestedCode), bucket];
-		default:
-			return [rejectAst(`Unsupported: the ${axisName} axis`), null];
-	}
-}
-
-export default emitStep;
diff --git a/src/jsCodegen/executeJavaScriptCompiledXPath.ts b/src/jsCodegen/executeJavaScriptCompiledXPath.ts
index 61ce9456..7c3e09b0 100644
--- a/src/jsCodegen/executeJavaScriptCompiledXPath.ts
+++ b/src/jsCodegen/executeJavaScriptCompiledXPath.ts
@@ -2,6 +2,7 @@ import DomFacade from '../domFacade/DomFacade';
 import ExternalDomFacade from '../domFacade/ExternalDomFacade';
 import IDomFacade from '../domFacade/IDomFacade';
 import { IReturnTypes, ReturnType } from '../parsing/convertXDMReturnValue';
+import { Options } from '../types/Options';
 import { Node } from '../types/Types';
 import runtimeLib from './runtimeLib';
 
@@ -24,7 +25,8 @@ export type CompiledXPathFunction<
 > = () => (
 	contextItem: unknown,
 	domFacade: unknown,
-	runtimeLib: unknown
+	runtimeLib: unknown,
+	options: unknown
 ) => IReturnTypes<TNode>[TReturnType];
 
 /**
@@ -43,18 +45,12 @@ export type CompiledXPathFunction<
 const executeJavaScriptCompiledXPath = <TNode extends Node, TReturnType extends ReturnType>(
 	compiledXPathFunction: CompiledXPathFunction<TNode, TReturnType>,
 	contextItem?: any | null,
-	domFacade?: IDomFacade | null
+	domFacade?: IDomFacade | null,
+	options?: Options | null
 ): IReturnTypes<TNode>[TReturnType] => {
 	domFacade = !domFacade ? new ExternalDomFacade() : domFacade;
 
-	// if (!contextItem) {
-	// 	throw XPDY0002('Context is needed to evaluate the given path expression.');
-	// }
-	// if (!contextItem.nodeType) {
-	// 	throw new Error('Context item must be subtype of node().');
-	// }
-
-	return compiledXPathFunction()(contextItem, domFacade, runtimeLib);
+	return compiledXPathFunction()(contextItem ?? null, domFacade, runtimeLib, options);
 };
 
 export default executeJavaScriptCompiledXPath;
diff --git a/src/jsCodegen/runtimeLib.ts b/src/jsCodegen/runtimeLib.ts
index 8945b754..7ca30046 100644
--- a/src/jsCodegen/runtimeLib.ts
+++ b/src/jsCodegen/runtimeLib.ts
@@ -1,66 +1,77 @@
-import isSubtypeOf from '../expressions/dataTypes/isSubtypeOf';
-import getEffectiveBooleanValue from '../expressions/dataTypes/Sequences/getEffectiveBooleanValue';
-import { DONE_TOKEN, ready } from '../expressions/util/iterators';
-import {
-	acceptAst,
-	GeneratedCodeBaseType,
-	GeneratedCodeType,
-	PartialCompilationResult,
-} from './JavaScriptCompiledXPath';
-
-export function determinePredicateTruthValue(
-	identifier: string,
-	code: string,
-	type: GeneratedCodeType,
-	contextItem: string = 'contextItem'
-): PartialCompilationResult {
-	switch (type.type) {
-		case GeneratedCodeBaseType.Value:
-		case GeneratedCodeBaseType.Variable:
-			return acceptAst(`!!${identifier}`, { type: GeneratedCodeBaseType.Value }, [code]);
-		case GeneratedCodeBaseType.Function: {
-			const result = determinePredicateTruthValue(
-				`${identifier}(${contextItem})`,
-				'',
-				type.returnType,
-				contextItem
-			);
-			if (!result.isAstAccepted) {
-				return result;
-			}
-			return acceptAst(`!!${result.code}`, result.generatedCodeType, [code]);
-		}
-		case GeneratedCodeBaseType.Iterator: {
-			return acceptAst(
-				`(() => {
-					const result = ${identifier}.next();
-					return result.done ? false : !!result.value;
-				})()`,
-				{ type: GeneratedCodeBaseType.Value }
-			);
-		}
-		case GeneratedCodeBaseType.None:
-			throw new Error('Trying to get value of generated code with type None');
-		default:
-			throw new Error(
-				'Unreachable! Trying to get compiled value of unsupported GeneratedCodeType.'
-			);
-	}
-}
+import DomFacade from '../domFacade/DomFacade';
+import IDomFacade from '../domFacade/IDomFacade';
+import { adaptJavaScriptValueToSequence } from '../expressions/adaptJavaScriptValueToXPathValue';
+import sequenceFactory from '../expressions/dataTypes/sequenceFactory';
+import { SequenceMultiplicity, SequenceType, ValueType } from '../expressions/dataTypes/Value';
+import DynamicContext from '../expressions/DynamicContext';
+import ExecutionParameters from '../expressions/ExecutionParameters';
+import { getFunctionByArity } from '../expressions/functions/functionRegistry';
+import { errXPDY0002 } from '../expressions/XPathErrors';
+import { adaptXPathValueToJavascriptValue } from '../registerCustomXPathFunction';
+import { UntypedExternalValue } from '../types/createTypedValueFactory';
+import { Options } from '../types/Options';
 
 // Make sure Closure Compiler does not change property names.
 declare interface IRuntimeLib {
-	DONE_TOKEN: typeof DONE_TOKEN;
-	getEffectiveBooleanValue: typeof getEffectiveBooleanValue;
-	isSubtypeOf: typeof isSubtypeOf;
-	ready: typeof ready;
+	callFunction: (
+		domFacade: IDomFacade,
+		namespaceURI: string,
+		localName: string,
+		args: UntypedExternalValue[],
+		options: Options | null
+	) => unknown;
+	errXPDY0002: typeof errXPDY0002;
 }
 
 const runtimeLib: IRuntimeLib = {
-	DONE_TOKEN,
-	getEffectiveBooleanValue,
-	isSubtypeOf,
-	ready,
+	callFunction(domFacade, namespaceURI, localName, args, options): unknown {
+		const functionProperties = getFunctionByArity(namespaceURI, localName, args.length);
+		if (!functionProperties) {
+			throw new Error('function not found for codegen function call');
+		}
+		// TODO: pass context item if the function uses it - can we annotate them?
+		// (custom functions and functions on allow list don't currently use this)
+		const dynamicContext = new DynamicContext({
+			contextItem: null,
+			contextItemIndex: 0,
+			contextSequence: sequenceFactory.empty(),
+			variableBindings: {},
+		});
+		const wrappedDomFacade = new DomFacade(domFacade);
+		const currentContext = options ? options['currentContext'] : null;
+		const executionParameters = new ExecutionParameters(
+			false,
+			false,
+			wrappedDomFacade,
+			null,
+			null,
+			currentContext,
+			null
+		);
+		const ret = functionProperties.callFunction(
+			dynamicContext,
+			executionParameters,
+			null,
+			// Assume types are pre-checked and converted by the calling codegen code - any type
+			// checking should happen during codegen. Sequences > 1 are not supported.
+			...args.map((v, i) =>
+				adaptJavaScriptValueToSequence(
+					wrappedDomFacade,
+					v,
+					functionProperties.argumentTypes[i] as SequenceType
+				)
+			)
+		);
+		return adaptXPathValueToJavascriptValue(
+			ret,
+			{
+				type: ValueType.ITEM,
+				mult: SequenceMultiplicity.ZERO_OR_ONE,
+			},
+			executionParameters
+		);
+	},
+	errXPDY0002,
 };
 
 export default runtimeLib;
diff --git a/src/registerCustomXPathFunction.ts b/src/registerCustomXPathFunction.ts
index 77ac4dcc..39c73d4e 100644
--- a/src/registerCustomXPathFunction.ts
+++ b/src/registerCustomXPathFunction.ts
@@ -62,7 +62,7 @@ class CustomXPathFunctionError extends Error {
 	}
 }
 
-function adaptXPathValueToJavascriptValue(
+export function adaptXPathValueToJavascriptValue(
 	valueSequence: ISequence,
 	sequenceType: SequenceType,
 	executionParameters: ExecutionParameters
diff --git a/src/typeInference/annotateCompareOperator.ts b/src/typeInference/annotateCompareOperator.ts
index 96199dfb..f344470d 100644
--- a/src/typeInference/annotateCompareOperator.ts
+++ b/src/typeInference/annotateCompareOperator.ts
@@ -1,3 +1,4 @@
+import { doesTypeAllowEmpty } from '../expressions/dataTypes/typeHelpers';
 import { SequenceMultiplicity, SequenceType, ValueType } from '../expressions/dataTypes/Value';
 import astHelper, { IAST } from '../parsing/astHelper';
 
@@ -29,9 +30,21 @@ export function annotateGeneralCompare(ast: IAST): SequenceType {
  * of the input, that's what a comparison will return.
  */
 export function annotateValueCompare(ast: IAST): SequenceType {
+	// A value comparison only returns an empty sequence if either of its arguments is empty
+	const firstOperandType = astHelper.getAttribute(
+		astHelper.followPath(ast, ['firstOperand', '*']),
+		'type'
+	);
+	const secondOperandType = astHelper.getAttribute(
+		astHelper.followPath(ast, ['secondOperand', '*']),
+		'type'
+	);
 	const seqType = {
 		type: ValueType.XSBOOLEAN,
-		mult: SequenceMultiplicity.EXACTLY_ONE,
+		mult:
+			doesTypeAllowEmpty(firstOperandType) || doesTypeAllowEmpty(secondOperandType)
+				? SequenceMultiplicity.ZERO_OR_ONE
+				: SequenceMultiplicity.EXACTLY_ONE,
 	};
 
 	astHelper.insertAttribute(ast, 'type', seqType);
@@ -48,9 +61,21 @@ export function annotateValueCompare(ast: IAST): SequenceType {
  * of the input, that's what a comparison will return.
  */
 export function annotateNodeCompare(ast: IAST): SequenceType {
+	// A node comparison only returns an empty sequence if either of its arguments is empty
+	const firstOperandType = astHelper.getAttribute(
+		astHelper.followPath(ast, ['firstOperand', '*']),
+		'type'
+	);
+	const secondOperandType = astHelper.getAttribute(
+		astHelper.followPath(ast, ['secondOperand', '*']),
+		'type'
+	);
 	const seqType = {
 		type: ValueType.XSBOOLEAN,
-		mult: SequenceMultiplicity.EXACTLY_ONE,
+		mult:
+			doesTypeAllowEmpty(firstOperandType) || doesTypeAllowEmpty(secondOperandType)
+				? SequenceMultiplicity.ZERO_OR_ONE
+				: SequenceMultiplicity.EXACTLY_ONE,
 	};
 
 	astHelper.insertAttribute(ast, 'type', seqType);
diff --git a/tslint.json b/tslint.json
index bdd6d23d..80d7cd64 100644
--- a/tslint.json
+++ b/tslint.json
@@ -16,6 +16,7 @@
 			}
 		],
 		"no-bitwise": false,
+		"no-shadowed-variable": false,
 		"no-string-literal": false,
 		"no-unnecessary-qualifier": true,
 		"no-unnecessary-type-assertion": true,


Test patch:
diff --git a/src/jsCodegen/emitTest.ts b/src/jsCodegen/emitTest.ts
index 41fc9037..3c46a8b7 100644
--- a/src/jsCodegen/emitTest.ts
+++ b/src/jsCodegen/emitTest.ts
@@ -3,10 +3,14 @@ import QName from '../expressions/dataTypes/valueTypes/QName';
 import { Bucket } from '../expressions/util/Bucket';
 import astHelper, { IAST } from '../parsing/astHelper';
 import { CodeGenContext } from './CodeGenContext';
+import {
+	emitAnd,
+	mapPartialCompilationResult,
+	mapPartialCompilationResultAndBucket,
+} from './emitHelpers';
 import escapeJavaScriptString from './escapeJavaScriptString';
 import {
 	acceptAst,
-	ContextItemIdentifier,
 	GeneratedCodeBaseType,
 	PartialCompilationResult,
 	rejectAst,
@@ -17,6 +21,7 @@ const testAstNodes = {
 	ELEMENT_TEST: 'elementTest',
 	NAME_TEST: 'nameTest',
 	WILDCARD: 'Wildcard',
+	ANY_KIND_TEST: 'anyKindTest',
 };
 
 export const tests = Object.values(testAstNodes);
@@ -25,13 +30,20 @@ export const tests = Object.values(testAstNodes);
 // https://www.w3.org/TR/xpath-31/#doc-xpath31-TextTest
 function emitTextTest(
 	_ast: IAST,
-	identifier: ContextItemIdentifier
+	contextItemExpr: PartialCompilationResult,
+	_context: CodeGenContext
 ): [PartialCompilationResult, Bucket | null] {
 	return [
-		acceptAst(`${identifier}.nodeType === /*TEXT_NODE*/ ${NODE_TYPES.TEXT_NODE}`, {
-			type: GeneratedCodeBaseType.Value,
-		}),
-		null,
+		mapPartialCompilationResult(contextItemExpr, (contextItemExpr) =>
+			acceptAst(
+				`${contextItemExpr.code}.nodeType === /*TEXT_NODE*/ ${NODE_TYPES.TEXT_NODE}`,
+				{
+					type: GeneratedCodeBaseType.Value,
+				},
+				[]
+			)
+		),
+		'type-3',
 	];
 }
 
@@ -52,90 +64,132 @@ function resolveNamespaceURI(qName: QName, staticContext: CodeGenContext) {
 }
 
 function emitNameTestFromQName(
-	identifier: ContextItemIdentifier,
 	qName: QName,
-	staticContext: CodeGenContext
+	canBeAttribute: boolean,
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
 ): [PartialCompilationResult, Bucket | null] {
-	resolveNamespaceURI(qName, staticContext);
+	resolveNamespaceURI(qName, context);
 	const { prefix, namespaceURI, localName } = qName;
 
-	const isElementOrAttributeCode = `${identifier}.nodeType
-		&& (${identifier}.nodeType === /*ELEMENT_NODE*/ ${NODE_TYPES.ELEMENT_NODE}
-		|| ${identifier}.nodeType === /*ATTRIBUTE_NODE*/ ${NODE_TYPES.ATTRIBUTE_NODE})`;
+	return mapPartialCompilationResultAndBucket(contextItemExpr, (contextItemExpr) => {
+		const isCorrectNodeTypeExpr = canBeAttribute
+			? acceptAst(
+					`${contextItemExpr.code}.nodeType
+						&& (${contextItemExpr.code}.nodeType === /*ELEMENT_NODE*/ ${NODE_TYPES.ELEMENT_NODE}
+						|| ${contextItemExpr.code}.nodeType === /*ATTRIBUTE_NODE*/ ${NODE_TYPES.ATTRIBUTE_NODE})`,
+					{ type: GeneratedCodeBaseType.Value },
+					[]
+			  )
+			: acceptAst(
+					`${contextItemExpr.code}.nodeType
+						&& ${contextItemExpr.code}.nodeType === /*ELEMENT_NODE*/ ${NODE_TYPES.ELEMENT_NODE}`,
+					{ type: GeneratedCodeBaseType.Value },
+					[]
+			  );
 
-	// Simple cases.
-	if (prefix === '*') {
-		if (localName === '*') {
+		// Simple cases.
+		if (prefix === '*') {
+			if (localName === '*') {
+				return [isCorrectNodeTypeExpr, canBeAttribute ? 'type-1-or-type-2' : 'type-1'];
+			}
 			return [
-				acceptAst(isElementOrAttributeCode, { type: GeneratedCodeBaseType.Value }),
-				null,
+				emitAnd(
+					isCorrectNodeTypeExpr,
+					acceptAst(
+						`${contextItemExpr.code}.localName === ${escapeJavaScriptString(
+							localName
+						)}`,
+						{ type: GeneratedCodeBaseType.Value },
+						[]
+					)
+				),
+				`name-${localName}`,
 			];
 		}
-		return [
-			acceptAst(
-				`${isElementOrAttributeCode} && ${identifier}.localName === ${escapeJavaScriptString(
-					localName
-				)}`,
-				{ type: GeneratedCodeBaseType.Value }
-			),
-			`name-${localName}`,
-		];
-	}
 
-	// Return condition comparing localName and namespaceURI against the context
-	// item.
-	const matchesLocalNameCode =
-		localName !== '*'
-			? `${isElementOrAttributeCode} && ${identifier}.localName === ${escapeJavaScriptString(
-					localName
-			  )} && `
-			: '';
-
-	let resolveNamespaceURICode: string;
-	if (prefix === '') {
-		// The prefix was empty: attributes should always resolve the empty prefix to the null namespace
-		resolveNamespaceURICode = `${identifier}.nodeType
-&& ${identifier}.nodeType === /*ELEMENT_NODE*/ ${
-			NODE_TYPES.ELEMENT_NODE
-		} ? ${escapeJavaScriptString(namespaceURI)} : null`;
-	} else {
-		// There was a prefix, or the namespace was already resolved. This applies to elements and attributes
-		resolveNamespaceURICode = escapeJavaScriptString(namespaceURI);
-	}
-	const matchesNamespaceCode = `(${identifier}.namespaceURI || null) === ((${resolveNamespaceURICode}) || null)`;
+		// Return condition comparing localName and namespaceURI against the context item.
+		const matchesLocalNameExpr =
+			localName === '*'
+				? isCorrectNodeTypeExpr
+				: emitAnd(
+						isCorrectNodeTypeExpr,
+						acceptAst(
+							`${contextItemExpr.code}.localName === ${escapeJavaScriptString(
+								localName
+							)}`,
+							{ type: GeneratedCodeBaseType.Value },
+							[]
+						)
+				  );
 
-	return [
-		acceptAst(`${matchesLocalNameCode}${matchesNamespaceCode}`, {
-			type: GeneratedCodeBaseType.Value,
-		}),
-		`name-${localName}`,
-	];
+		const namespaceUriExpr = acceptAst(
+			escapeJavaScriptString(namespaceURI),
+			{ type: GeneratedCodeBaseType.Value },
+			[]
+		);
+		const nodeNamespaceExpr =
+			prefix === '' && canBeAttribute
+				? // The prefix was empty: attributes should always resolve the empty prefix to the null namespace
+				  mapPartialCompilationResult(namespaceUriExpr, (namespaceUriExpr) =>
+						acceptAst(
+							`${contextItemExpr.code}.nodeType === /*ELEMENT_NODE*/ ${NODE_TYPES.ELEMENT_NODE} ? ${namespaceUriExpr.code} : null`,
+							{ type: GeneratedCodeBaseType.Value },
+							namespaceUriExpr.variables
+						)
+				  )
+				: namespaceUriExpr;
+		const matchesNamespaceExpr = mapPartialCompilationResult(
+			nodeNamespaceExpr,
+			(nodeNamespaceExpr) =>
+				acceptAst(
+					`(${contextItemExpr.code}.namespaceURI || null) === ((${nodeNamespaceExpr.code}) || null)`,
+					{ type: GeneratedCodeBaseType.Value },
+					nodeNamespaceExpr.variables
+				)
+		);
+
+		return [emitAnd(matchesLocalNameExpr, matchesNamespaceExpr), `name-${localName}`];
+	});
 }
 
 // element() and element(*) match any single element node, regardless of its name or type annotation.
 // https://www.w3.org/TR/xpath-31/#doc-xpath31-ElementTest
 function emitElementTest(
 	ast: IAST,
-	identifier: ContextItemIdentifier,
-	staticContext: CodeGenContext
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
 ): [PartialCompilationResult, Bucket | null] {
 	const elementName = astHelper.getFirstChild(ast, 'elementName');
 	const star = elementName && astHelper.getFirstChild(elementName, 'star');
-	const isElementCode = `${identifier}.nodeType === /*ELEMENT_NODE*/ ${NODE_TYPES.ELEMENT_NODE}`;
 
 	if (elementName === null || star) {
-		return [acceptAst(isElementCode, { type: GeneratedCodeBaseType.Value }), null];
+		return [
+			mapPartialCompilationResult(contextItemExpr, (contextItemExpr) =>
+				acceptAst(
+					`${contextItemExpr.code}.nodeType === /*ELEMENT_NODE*/ ${NODE_TYPES.ELEMENT_NODE}`,
+					{ type: GeneratedCodeBaseType.Value },
+					[]
+				)
+			),
+			'type-1',
+		];
 	}
 
 	const qName = astHelper.getQName(astHelper.getFirstChild(elementName, 'QName'));
 
-	return emitNameTestFromQName(identifier, qName, staticContext);
+	return emitNameTestFromQName(qName, false, contextItemExpr, context);
 }
 
 // A node test that consists only of an EQName or a Wildcard is called a name test.
 // https://www.w3.org/TR/xpath-31/#doc-xpath31-NameTest
-function emitNameTest(ast: IAST, identifier: ContextItemIdentifier, staticContext: CodeGenContext) {
-	return emitNameTestFromQName(identifier, astHelper.getQName(ast), staticContext);
+function emitNameTest(
+	ast: IAST,
+	canBeAttribute: boolean,
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
+) {
+	return emitNameTestFromQName(astHelper.getQName(ast), canBeAttribute, contextItemExpr, context);
 }
 
 // select all element children of the context node
@@ -143,31 +197,34 @@ function emitNameTest(ast: IAST, identifier: ContextItemIdentifier, staticContex
 // https://www.w3.org/TR/xpath-31/#doc-xpath31-Wildcard
 function emitWildcard(
 	ast: IAST,
-	identifier: ContextItemIdentifier,
-	staticContext: CodeGenContext
+	canBeAttribute: boolean,
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
 ): [PartialCompilationResult, Bucket | null] {
 	if (!astHelper.getFirstChild(ast, 'star')) {
 		return emitNameTestFromQName(
-			identifier,
 			{
 				localName: '*',
 				namespaceURI: null,
 				prefix: '*',
 			},
-			staticContext
+			canBeAttribute,
+			contextItemExpr,
+			context
 		);
 	}
 
 	const uri = astHelper.getFirstChild(ast, 'uri');
 	if (uri !== null) {
 		return emitNameTestFromQName(
-			identifier,
 			{
 				localName: '*',
 				namespaceURI: astHelper.getTextContent(uri),
 				prefix: '',
 			},
-			staticContext
+			canBeAttribute,
+			contextItemExpr,
+			context
 		);
 	}
 
@@ -176,47 +233,71 @@ function emitWildcard(
 	if (astHelper.getFirstChild(ast, '*')[0] === 'star') {
 		// The prefix is 'starred'
 		return emitNameTestFromQName(
-			identifier,
 			{
 				localName: astHelper.getTextContent(ncName),
 				namespaceURI: null,
 				prefix: '*',
 			},
-			staticContext
+			canBeAttribute,
+			contextItemExpr,
+			context
 		);
 	}
 
 	// The localName is 'starred'
 	return emitNameTestFromQName(
-		identifier,
 		{
 			localName: '*',
 			namespaceURI: null,
 			prefix: astHelper.getTextContent(ncName),
 		},
-		staticContext
+		canBeAttribute,
+		contextItemExpr,
+		context
 	);
 }
 
+// node() matches any node.
+// https://www.w3.org/TR/xpath-31/#doc-xpath31-AnyKindTest
+function emitAnyKindTest(
+	_ast: IAST,
+	contextItemExpr: PartialCompilationResult,
+	_context: CodeGenContext
+): [PartialCompilationResult, Bucket | null] {
+	return [
+		mapPartialCompilationResult(contextItemExpr, (contextItemExpr) =>
+			acceptAst(
+				`!!${contextItemExpr.code}.nodeType`,
+				{
+					type: GeneratedCodeBaseType.Value,
+				},
+				[]
+			)
+		),
+		null,
+	];
+}
+
 export default function emitTest(
 	ast: IAST,
-	identifier: ContextItemIdentifier,
-	staticContext: CodeGenContext
+	canBeAttribute: boolean,
+	contextItemExpr: PartialCompilationResult,
+	context: CodeGenContext
 ): [PartialCompilationResult, Bucket | null] {
-	// emitTest returns a tuple of the generated code and an optional bucket.
-
 	const test = ast[0];
 
 	switch (test) {
 		case testAstNodes.ELEMENT_TEST:
-			return emitElementTest(ast, identifier, staticContext);
+			return emitElementTest(ast, contextItemExpr, context);
 		case testAstNodes.TEXT_TEST:
-			return emitTextTest(ast, identifier);
+			return emitTextTest(ast, contextItemExpr, context);
 		case testAstNodes.NAME_TEST:
-			return emitNameTest(ast, identifier, staticContext);
+			return emitNameTest(ast, canBeAttribute, contextItemExpr, context);
 		case testAstNodes.WILDCARD:
-			return emitWildcard(ast, identifier, staticContext);
+			return emitWildcard(ast, canBeAttribute, contextItemExpr, context);
+		case testAstNodes.ANY_KIND_TEST:
+			return emitAnyKindTest(ast, contextItemExpr, context);
 		default:
-			return [rejectAst(`Unsupported: the test '${test}'.`), null];
+			return [rejectAst(`Test not implemented: '${test}`), null];
 	}
 }
diff --git a/test/specs/annotation.tests.ts b/test/specs/annotation.tests.ts
index a7582342..ccdfaa7b 100644
--- a/test/specs/annotation.tests.ts
+++ b/test/specs/annotation.tests.ts
@@ -460,7 +460,7 @@ describe('annotating varRef', () => {
  * @param context the context in which the variables are inserted
  * @param variables
  */
-function insertVariables(context: AnnotationContext, variables: Array<[string, SequenceType]>) {
+function insertVariables(context: AnnotationContext, variables: [string, SequenceType][]) {
 	variables.forEach((element) => {
 		context.insertVariable(element[0], element[1]);
 	});
@@ -473,6 +473,6 @@ function insertVariables(context: AnnotationContext, variables: Array<[string, S
 // describe('Annotating ', () => {
 // 	it('',
 // 		() => {
-//			// assertValueType('', ValueType. , undefined)
+// 			// assertValueType('', ValueType. , undefined)
 // 		});
 // });
diff --git a/test/specs/expressions/jsCodegen/compare.tests.ts b/test/specs/expressions/jsCodegen/compare.tests.ts
index 657aa848..55648a35 100644
--- a/test/specs/expressions/jsCodegen/compare.tests.ts
+++ b/test/specs/expressions/jsCodegen/compare.tests.ts
@@ -13,11 +13,11 @@ describe('compare tests', () => {
 
 	it('does not generate compare function for nodes', () => {
 		const node = documentNode.documentElement;
-		const query = '@attr = true()';
+		const query = '@attr << @attr';
 
 		chai.assert.throws(
 			() => evaluateXPathWithJsCodegen(query, node, null, ReturnType.BOOLEAN),
-			'Unsupported function: true'
+			'Unsupported compare type'
 		);
 
 		chai.assert.equal(evaluateXPathToBoolean(query, node), true);
diff --git a/test/specs/expressions/jsCodegen/string.tests.ts b/test/specs/expressions/jsCodegen/string.tests.ts
index bc498366..00e586a5 100644
--- a/test/specs/expressions/jsCodegen/string.tests.ts
+++ b/test/specs/expressions/jsCodegen/string.tests.ts
@@ -8,9 +8,9 @@ import {
 	parseScript,
 	ReturnType,
 } from 'fontoxpath';
+import escapeJavaScriptString from 'fontoxpath/jsCodegen/escapeJavaScriptString';
 import * as slimdom from 'slimdom';
 import jsonMlMapper from 'test-helpers/jsonMlMapper';
-import escapeJavaScriptString from 'fontoxpath/jsCodegen/escapeJavaScriptString';
 import evaluateXPathWithJsCodegen from '../../parsing/jsCodegen/evaluateXPathWithJsCodegen';
 
 describe('string tests', () => {
diff --git a/test/specs/parsing/functions/functions.node.tests.ts b/test/specs/parsing/functions/functions.node.tests.ts
index cab625c2..376899f0 100644
--- a/test/specs/parsing/functions/functions.node.tests.ts
+++ b/test/specs/parsing/functions/functions.node.tests.ts
@@ -67,8 +67,8 @@ describe('functions over nodes', () => {
 	});
 
 	describe('name()', () => {
-		it('returns an empty sequence if $arg is an empty sequence', () =>
-			chai.assert.isEmpty(evaluateXPathToStrings('name(())', documentNode)));
+		it('returns an empty string if $arg is an empty sequence', () =>
+			chai.assert.deepEqual(evaluateXPathToStrings('name(())', documentNode), ['']));
 
 		it('it defaults to the context item when the argument is omitted', () => {
 			jsonMlMapper.parse(['someElement'], documentNode);
diff --git a/test/specs/parsing/jsCodegen/astRejection.tests.ts b/test/specs/parsing/jsCodegen/astRejection.tests.ts
index 80213e82..a8b39c78 100644
--- a/test/specs/parsing/jsCodegen/astRejection.tests.ts
+++ b/test/specs/parsing/jsCodegen/astRejection.tests.ts
@@ -136,7 +136,7 @@ describe("rejecting unsupported AST's (js-codegen)", () => {
 		);
 	});
 
-	it('rejects unsupported base expression (functionCallExpr)', () => {
+	it('rejects unsupported function (boolean)', () => {
 		chai.assert.throws(
 			() =>
 				evaluateXPathWithJsCodegen(
@@ -145,7 +145,7 @@ describe("rejecting unsupported AST's (js-codegen)", () => {
 					null,
 					ReturnType.BOOLEAN
 				),
-			'Unsupported'
+			'Not supported'
 		);
 	});
 
diff --git a/test/specs/parsing/jsCodegen/functionCall.tests.ts b/test/specs/parsing/jsCodegen/functionCall.tests.ts
new file mode 100644
index 00000000..ae7378dd
--- /dev/null
+++ b/test/specs/parsing/jsCodegen/functionCall.tests.ts
@@ -0,0 +1,99 @@
+import * as chai from 'chai';
+import * as slimdom from 'slimdom';
+
+import jsonMlMapper from 'test-helpers/jsonMlMapper';
+
+import { domFacade, registerCustomXPathFunction, ReturnType } from 'fontoxpath';
+import evaluateXPathWithJsCodegen from './evaluateXPathWithJsCodegen';
+
+describe('function calls', () => {
+	let documentNode: slimdom.Document;
+
+	before(() => {
+		registerCustomXPathFunction(
+			{ localName: 'functionCallTest', namespaceURI: 'test' },
+			['xs:string', 'xs:boolean?', 'node()'],
+			'node()?',
+			({ domFacade }, str, bool, node) => (bool ? domFacade.getFirstChild(node) : null)
+		);
+	});
+
+	beforeEach(() => {
+		documentNode = new slimdom.Document();
+		jsonMlMapper.parse(
+			[
+				'xml',
+				{ id: 'yes' },
+				['title', 'Tips'],
+				[
+					'tips',
+					['tip', 'Make it work'],
+					['tip', 'Make it right'],
+					['tip', { id: 'last' }, 'Make it fast'],
+				],
+			],
+			documentNode
+		);
+	});
+
+	it('can call selected built-in functions through the runtimeLib', () => {
+		chai.assert.equal(
+			evaluateXPathWithJsCodegen(
+				'fontoxpath:version()',
+				documentNode,
+				null,
+				ReturnType.STRING
+			),
+			'devbuild'
+		);
+	});
+
+	it('can codegen specific functions directly', () => {
+		chai.assert.equal(
+			evaluateXPathWithJsCodegen('not(name(/bla))', documentNode, null, ReturnType.BOOLEAN),
+			true
+		);
+	});
+
+	it('can call custom functions through the runtimeLib', () => {
+		chai.assert.equal(
+			evaluateXPathWithJsCodegen(
+				'Q{test}functionCallTest("a", true(), root(.))',
+				documentNode.firstChild.lastChild,
+				null,
+				ReturnType.FIRST_NODE
+			),
+			documentNode.firstChild
+		);
+		chai.assert.equal(
+			evaluateXPathWithJsCodegen(
+				'Q{test}functionCallTest("a", xml/nope, .)',
+				documentNode.firstChild.lastChild,
+				null,
+				ReturnType.FIRST_NODE
+			),
+			null
+		);
+	});
+
+	it('can call functions at the end of a path', () => {
+		chai.assert.equal(
+			evaluateXPathWithJsCodegen(
+				'xml/tips/tip[@id="last"]/local-name()',
+				documentNode,
+				null,
+				ReturnType.STRING
+			),
+			'tip'
+		);
+		chai.assert.equal(
+			evaluateXPathWithJsCodegen(
+				'xml/tips/tip[@id="last"]/path(.)',
+				documentNode,
+				null,
+				ReturnType.STRING
+			),
+			'/Q{}xml[1]/Q{}tips[1]/Q{}tip[3]'
+		);
+	});
+});
diff --git a/test/specs/parsing/jsCodegen/nodeTests.tests.ts b/test/specs/parsing/jsCodegen/nodeTests.tests.ts
index 7df100a7..427da86c 100644
--- a/test/specs/parsing/jsCodegen/nodeTests.tests.ts
+++ b/test/specs/parsing/jsCodegen/nodeTests.tests.ts
@@ -10,7 +10,16 @@ describe('node tests', () => {
 	let documentNode: slimdom.Document;
 	beforeEach(() => {
 		documentNode = new slimdom.Document();
-		jsonMlMapper.parse(['xml', 'Hello'], documentNode);
+		jsonMlMapper.parse(['xml', 'Hello', ['!', 'comment']], documentNode);
+	});
+
+	it('selects any nodes', () => {
+		const textNode = documentNode.documentElement.firstChild;
+		const comment = documentNode.documentElement.lastChild;
+		chai.assert.sameOrderedMembers(
+			evaluateXPathWithJsCodegen('/xml/node()', documentNode, null, ReturnType.NODES),
+			[textNode, comment]
+		);
 	});
 
 	it('selects text nodes', () => {
diff --git a/test/specs/parsing/jsCodegen/predicates.tests.ts b/test/specs/parsing/jsCodegen/predicates.tests.ts
index 06ff6260..9ccb2939 100644
--- a/test/specs/parsing/jsCodegen/predicates.tests.ts
+++ b/test/specs/parsing/jsCodegen/predicates.tests.ts
@@ -81,4 +81,12 @@ describe('predicates', () => {
 		);
 		chai.assert.equal(results.length, 2);
 	});
+
+	it('can apply predicates to a filterExpr', () => {
+		const results = evaluateXPathWithJsCodegen('.[xml]', documentNode, null, ReturnType.NODES);
+		chai.assert.equal(results.length, 1);
+
+		const results2 = evaluateXPathWithJsCodegen('.[bla]', documentNode, null, ReturnType.NODES);
+		chai.assert.equal(results2.length, 0);
+	});
 });
diff --git a/test/testCodegenFacade.ts b/test/testCodegenFacade.ts
index 9bc1c58d..2ff258d6 100644
--- a/test/testCodegenFacade.ts
+++ b/test/testCodegenFacade.ts
@@ -88,7 +88,7 @@ const evaluateXPathWithJsCodegen = <
 		const evalFunction = new Function(compiledXPathResult.code) as CompiledXPathFunction;
 
 		codegenCases++;
-		return executeJavaScriptCompiledXPath(evalFunction, contextItem, localDomFacade);
+		return executeJavaScriptCompiledXPath(evalFunction, contextItem, localDomFacade, options);
 	} else {
 		// The query AST was not eligible to be converted to Javascript
 		return evaluateXPath(query, contextItem, localDomFacade, null, returnType, options);
diff --git a/test/xQueryXUtils.ts b/test/xQueryXUtils.ts
index 895b2ccf..c9a14659 100644
--- a/test/xQueryXUtils.ts
+++ b/test/xQueryXUtils.ts
@@ -6,7 +6,7 @@ import {
 	Language,
 	parseScript,
 } from 'fontoxpath';
-import { Document, Element, Text, parseXmlDocument, XMLSerializer } from 'slimdom';
+import { Document, Element, parseXmlDocument, Text, XMLSerializer } from 'slimdom';
 
 function removeInsignificantWhitespace(root: Document) {
 	const nonSignificantWhitespace = evaluateXPathToNodes<Text>('//*/text()', root, null, null, {