Repository: FontoXML/fontoxpath
Pull request #550 (2022-10-14 12:45:57)

Use the errXPDY0002 function everywhere that error is thrown

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

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

Status | Path | Size | Limits
:------------: | ------------ | :------------: | :------------:
:white_check_mark: | fontoxpath.js<br/> | 82.26KB (+32B +0.04%) | -
:white_check_mark: | fontoxpath.esm.js<br/> | 82.38KB (+30B +0.04%) | -
</details>

  
  


Total files change +62B +0.04%

Final result: :white_check_mark:

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

---
<p align="center"><a href="https://app.bundlemon.dev/projects/609024effb87a20009a6b096/reports?branch=errXPDY0002-usage&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/53280565/badge)](https://coveralls.io/builds/53280565)

Coverage increased (+0.004%) to 91.197% when pulling **9d897face89fdf181e139e01a10b51172ea679cc on errXPDY0002-usage** into **06b88edba9fff3cba7ba7d902cdb993770f49ca3 on master**.


Patch:
diff --git a/package-lock.json b/package-lock.json
index 5811f849..c9975261 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,11 +1,11 @@
 {
 	"name": "fontoxpath",
-	"version": "3.27.1",
+	"version": "3.28.1",
 	"lockfileVersion": 2,
 	"requires": true,
 	"packages": {
 		"": {
-			"version": "3.27.1",
+			"version": "3.28.1",
 			"license": "MIT",
 			"dependencies": {
 				"prsc": "4.0.0",
diff --git a/package.json b/package.json
index 5c9434da..becccbd1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "fontoxpath",
-	"version": "3.27.1",
+	"version": "3.28.1",
 	"description": "A minimalistic XPath 3.1 engine in JavaScript",
 	"main": "dist/fontoxpath.js",
 	"module": "dist/fontoxpath.esm.js",
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/axes/validateContextNode.ts b/src/expressions/axes/validateContextNode.ts
index 6f65dba8..30765ad1 100644
--- a/src/expressions/axes/validateContextNode.ts
+++ b/src/expressions/axes/validateContextNode.ts
@@ -1,10 +1,11 @@
 import { NodePointer } from '../../domClone/Pointer';
 import isSubtypeOf from '../dataTypes/isSubtypeOf';
 import Value, { ValueType } from '../dataTypes/Value';
+import { errXPDY0002 } from '../XPathErrors';
 
 export default function validateContextNode(value: Value): NodePointer {
 	if (value === null) {
-		throw new Error('XPDY0002: context is absent, it needs to be present to use axes.');
+		throw errXPDY0002('context is absent, it needs to be present to use axes.');
 	}
 	if (!isSubtypeOf(value.type, ValueType.NODE)) {
 		throw new Error('XPTY0020: Axes can only be applied to nodes.');
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_context.ts b/src/expressions/functions/builtInFunctions_context.ts
index f50e5207..1c23a488 100644
--- a/src/expressions/functions/builtInFunctions_context.ts
+++ b/src/expressions/functions/builtInFunctions_context.ts
@@ -3,14 +3,13 @@ import sequenceFactory from '../dataTypes/sequenceFactory';
 import { SequenceMultiplicity, ValueType } from '../dataTypes/Value';
 import { BUILT_IN_NAMESPACE_URIS } from '../staticallyKnownNamespaces';
 import { DONE_TOKEN, ready } from '../util/iterators';
+import { errXPDY0002 } from '../XPathErrors';
 import { BuiltinDeclarationType } from './builtInFunctions';
 import FunctionDefinitionType from './FunctionDefinitionType';
 
 const fnLast: FunctionDefinitionType = (dynamicContext) => {
 	if (dynamicContext.contextItem === null) {
-		throw new Error(
-			'XPDY0002: The fn:last() function depends on dynamic context, which is absent.'
-		);
+		throw errXPDY0002('The fn:last() function depends on dynamic context, which is absent.');
 	}
 
 	let done = false;
@@ -31,8 +30,8 @@ const fnLast: FunctionDefinitionType = (dynamicContext) => {
 
 const fnPosition: FunctionDefinitionType = (dynamicContext) => {
 	if (dynamicContext.contextItem === null) {
-		throw new Error(
-			'XPDY0002: The fn:position() function depends on dynamic context, which is absent.'
+		throw errXPDY0002(
+			'The fn:position() function depends on dynamic context, which is absent.'
 		);
 	}
 	// Note: +1 because XPath is one-based
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..714599b7 100644
--- a/src/expressions/functions/builtInFunctions_node.ts
+++ b/src/expressions/functions/builtInFunctions_node.ts
@@ -26,6 +26,7 @@ import { BUILT_IN_NAMESPACE_URIS } from '../staticallyKnownNamespaces';
 import StaticContext from '../StaticContext';
 import { IterationHint } from '../util/iterators';
 import zipSingleton from '../util/zipSingleton';
+import { errXPDY0002 } from '../XPathErrors';
 import { BuiltinDeclarationType } from './builtInFunctions';
 import builtinStringFunctions from './builtInFunctions_string';
 import FunctionDefinitionType from './FunctionDefinitionType';
@@ -33,14 +34,15 @@ import FunctionDefinitionType from './FunctionDefinitionType';
 const fnString = builtinStringFunctions.functions.string;
 
 function contextItemAsFirstArgument(
+	functionName: string,
 	fn: FunctionDefinitionType,
 	dynamicContext: DynamicContext,
 	executionParameters: ExecutionParameters,
 	staticContext: StaticContext
 ) {
 	if (dynamicContext.contextItem === null) {
-		throw new Error(
-			'XPDY0002: The function which was called depends on dynamic context, which is absent.'
+		throw errXPDY0002(
+			`The function ${functionName} depends on dynamic context, which is absent.`
 		);
 	}
 	return fn(
@@ -108,7 +110,7 @@ const fnName: FunctionDefinitionType = (
 				staticContext,
 				fnNodeName(dynamicContext, executionParameters, staticContext, sequence)
 			),
-		empty: () => sequenceFactory.empty(),
+		empty: () => sequenceFactory.singleton(createAtomicValue('', ValueType.XSSTRING)),
 	});
 };
 
@@ -403,12 +405,12 @@ 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 },
 	},
 
 	{
 		argumentTypes: [],
-		callFunction: contextItemAsFirstArgument.bind(null, fnName),
+		callFunction: contextItemAsFirstArgument.bind(null, 'name', fnName),
 		localName: 'name',
 		namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
 		returnType: { type: ValueType.XSSTRING, mult: SequenceMultiplicity.EXACTLY_ONE },
@@ -424,7 +426,7 @@ const declarations: BuiltinDeclarationType[] = [
 
 	{
 		argumentTypes: [],
-		callFunction: contextItemAsFirstArgument.bind(null, fnNamespaceURI),
+		callFunction: contextItemAsFirstArgument.bind(null, 'namespace-uri', fnNamespaceURI),
 		localName: 'namespace-uri',
 		namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
 		returnType: { type: ValueType.XSANYURI, mult: SequenceMultiplicity.EXACTLY_ONE },
@@ -459,7 +461,7 @@ const declarations: BuiltinDeclarationType[] = [
 
 	{
 		argumentTypes: [],
-		callFunction: contextItemAsFirstArgument.bind(null, fnHasChildren),
+		callFunction: contextItemAsFirstArgument.bind(null, 'has-children', fnHasChildren),
 		localName: 'has-children',
 		namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
 		returnType: { type: ValueType.XSBOOLEAN, mult: SequenceMultiplicity.EXACTLY_ONE },
@@ -475,7 +477,7 @@ const declarations: BuiltinDeclarationType[] = [
 
 	{
 		argumentTypes: [],
-		callFunction: contextItemAsFirstArgument.bind(null, fnPath),
+		callFunction: contextItemAsFirstArgument.bind(null, 'path', fnPath),
 		localName: 'path',
 		namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
 		returnType: { type: ValueType.XSSTRING, mult: SequenceMultiplicity.ZERO_OR_ONE },
@@ -491,7 +493,7 @@ const declarations: BuiltinDeclarationType[] = [
 
 	{
 		argumentTypes: [],
-		callFunction: contextItemAsFirstArgument.bind(null, fnNodeName),
+		callFunction: contextItemAsFirstArgument.bind(null, 'node-name', fnNodeName),
 		localName: 'node-name',
 		namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
 		returnType: { type: ValueType.XSQNAME, mult: SequenceMultiplicity.ZERO_OR_ONE },
@@ -507,7 +509,7 @@ const declarations: BuiltinDeclarationType[] = [
 
 	{
 		argumentTypes: [],
-		callFunction: contextItemAsFirstArgument.bind(null, fnLocalName),
+		callFunction: contextItemAsFirstArgument.bind(null, 'local-name', fnLocalName),
 		localName: 'local-name',
 		namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
 		returnType: { type: ValueType.XSSTRING, mult: SequenceMultiplicity.EXACTLY_ONE },
@@ -523,7 +525,7 @@ const declarations: BuiltinDeclarationType[] = [
 
 	{
 		argumentTypes: [],
-		callFunction: contextItemAsFirstArgument.bind(null, fnRoot),
+		callFunction: contextItemAsFirstArgument.bind(null, 'root', fnRoot),
 		localName: 'root',
 		namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
 		returnType: { type: ValueType.NODE, mult: SequenceMultiplicity.ZERO_OR_ONE },
@@ -531,7 +533,7 @@ const declarations: BuiltinDeclarationType[] = [
 
 	{
 		argumentTypes: [],
-		callFunction: contextItemAsFirstArgument.bind(null, fnData),
+		callFunction: contextItemAsFirstArgument.bind(null, 'data', fnData),
 		localName: 'data',
 		namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
 		returnType: { type: ValueType.XSANYATOMICTYPE, mult: SequenceMultiplicity.ZERO_OR_MORE },
diff --git a/src/expressions/functions/builtInFunctions_numeric.ts b/src/expressions/functions/builtInFunctions_numeric.ts
index 0db27353..9e15c61a 100644
--- a/src/expressions/functions/builtInFunctions_numeric.ts
+++ b/src/expressions/functions/builtInFunctions_numeric.ts
@@ -13,6 +13,7 @@ import type ExecutionParameters from '../ExecutionParameters';
 import { BUILT_IN_NAMESPACE_URIS } from '../staticallyKnownNamespaces';
 import type StaticContext from '../StaticContext';
 import { DONE_TOKEN, ready } from '../util/iterators';
+import { errXPDY0002 } from '../XPathErrors';
 import { performFunctionConversion } from './argumentHelper';
 import type { BuiltinDeclarationType } from './builtInFunctions';
 import type FunctionDefinitionType from './FunctionDefinitionType';
@@ -484,7 +485,7 @@ const declarations: BuiltinDeclarationType[] = [
 					false
 				);
 			if (!atomizedContextItem) {
-				throw new Error('XPDY0002: fn:number needs an atomizable context item.');
+				throw errXPDY0002('fn:number needs an atomizable context item.');
 			}
 			return fnNumber(
 				dynamicContext,
diff --git a/src/expressions/functions/builtInFunctions_string.ts b/src/expressions/functions/builtInFunctions_string.ts
index 234d62a4..a0ff2a1c 100644
--- a/src/expressions/functions/builtInFunctions_string.ts
+++ b/src/expressions/functions/builtInFunctions_string.ts
@@ -12,6 +12,7 @@ import { BUILT_IN_NAMESPACE_URIS } from '../staticallyKnownNamespaces';
 import StaticContext from '../StaticContext';
 import { DONE_TOKEN, ready } from '../util/iterators';
 import zipSingleton from '../util/zipSingleton';
+import { errXPDY0002 } from '../XPathErrors';
 import { BuiltinDeclarationType } from './builtInFunctions';
 import builtInNumericFunctions from './builtInFunctions_numeric';
 import FunctionDefinitionType from './FunctionDefinitionType';
@@ -29,8 +30,8 @@ function contextItemAsFirstArgument(
 	staticContext: StaticContext
 ) {
 	if (dynamicContext.contextItem === null) {
-		throw new Error(
-			'XPDY0002: The function which was called depends on dynamic context, which is absent.'
+		throw errXPDY0002(
+			'The function which was called depends on dynamic context, which is absent.'
 		);
 	}
 	return fn(
diff --git a/src/expressions/path/AbsolutePathExpression.ts b/src/expressions/path/AbsolutePathExpression.ts
index 49353a74..6ada9d25 100644
--- a/src/expressions/path/AbsolutePathExpression.ts
+++ b/src/expressions/path/AbsolutePathExpression.ts
@@ -5,6 +5,7 @@ import DynamicContext from '../DynamicContext';
 import ExecutionParameters from '../ExecutionParameters';
 import Expression, { RESULT_ORDERINGS } from '../Expression';
 import Specificity from '../Specificity';
+import { errXPDY0002 } from '../XPathErrors';
 
 class AbsolutePathExpression extends Expression {
 	private _relativePathExpression: Expression;
@@ -25,7 +26,7 @@ class AbsolutePathExpression extends Expression {
 
 	public evaluate(dynamicContext: DynamicContext, executionParameters: ExecutionParameters) {
 		if (dynamicContext.contextItem === null) {
-			throw new Error('XPDY0002: context is absent, it needs to be present to use paths.');
+			throw errXPDY0002('context is absent, it needs to be present to use paths.');
 		}
 		const node = dynamicContext.contextItem.value;
 		const domFacade = executionParameters.domFacade;
diff --git a/src/expressions/path/ContextItemExpression.ts b/src/expressions/path/ContextItemExpression.ts
index b32bc529..6a513c41 100644
--- a/src/expressions/path/ContextItemExpression.ts
+++ b/src/expressions/path/ContextItemExpression.ts
@@ -4,6 +4,7 @@ import DynamicContext from '../DynamicContext';
 import ExecutionParameters from '../ExecutionParameters';
 import Expression, { RESULT_ORDERINGS } from '../Expression';
 import Specificity from '../Specificity';
+import { errXPDY0002 } from '../XPathErrors';
 
 class ContextItemExpression extends Expression {
 	constructor(type: SequenceType) {
@@ -20,9 +21,7 @@ class ContextItemExpression extends Expression {
 
 	public evaluate(dynamicContext: DynamicContext, _executionParameters: ExecutionParameters) {
 		if (dynamicContext.contextItem === null) {
-			throw new Error(
-				'XPDY0002: context is absent, it needs to be present to use the "." operator'
-			);
+			throw errXPDY0002('context is absent, it needs to be present to use the "." operator');
 		}
 		return sequenceFactory.singleton(dynamicContext.contextItem);
 	}
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..820ede25 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,
+			errXPDY0002,
 		} = 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..de4a8081 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, ['secondOperand', '*']),
+			'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..d2d207bd 100644
--- a/src/jsCodegen/emitPathExpr.ts
+++ b/src/jsCodegen/emitPathExpr.ts
@@ -2,251 +2,373 @@ 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 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];
-			}
+	const lookupAsts = astHelper.getChildren(stepAst, 'lookup');
+	if (lookupAsts.length > 0) {
+		return [rejectAst('Unsupported: lookups'), null];
+	}
 
-			// 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];
-			}
+	// Every step introduces a context item for its test and predicates
+	const stepContextItemExpr = context.getNewIdentifier('contextItem');
 
-			const intersectingBucketForStep = intersectBuckets(testBucket, predicateBucket);
+	const predicatesAst = astHelper.getFirstChild(stepAst, 'predicates');
+	const [predicatesExpr, predicatesBucket] = emitPredicates(
+		predicatesAst,
+		stepContextItemExpr,
+		context
+	);
 
-			const [emittedStep, stepBucket] = emitStep(
-				axisAst,
-				emittedTest.code,
-				emittedPredicates.code,
-				nestLevel,
-				nestedCode,
-				intersectingBucketForStep
-			);
-			if (!emittedStep.isAstAccepted) {
-				return [emittedStep, null];
-			}
+	const axisAst = astHelper.getFirstChild(stepAst, 'xpathAxis');
+	if (axisAst) {
+		const testAst = astHelper.getFirstChild(stepAst, tests);
+		if (!testAst) {
+			return [rejectAst('Unsupported test in step'), null];
+		}
 
-			if (i === 0) {
-				// For the first iteration, the bucket of the steps is actually the bucket for the whole thing
-				intersectingBucket = stepBucket;
-			}
+		// 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
+		);
 
-			emittedVariables.push(...emittedStep.variables, ...emittedPredicates.variables);
-			emittedCode = emittedStep.code;
-		} else {
-			return [rejectAst('Unsupported: filter expressions.'), null];
-		}
+		const combinedConditionExpr =
+			predicatesExpr === null ? testExpr : emitAnd(testExpr, predicatesExpr);
 
-		const lookups = astHelper.getChildren(step, 'lookup');
-		if (lookups.length > 0) {
-			return [rejectAst('Unsupported: lookups.'), null];
-		}
+		const combinedConditionBucket = intersectBuckets(testBucket, predicatesBucket);
+
+		const [nestedStepsCode, _] = emitSteps(
+			restStepAsts,
+			stepContextItemCanBeAttribute,
+			stepContextItemExpr,
+			context
+		);
+
+		return emitAxis(
+			axisAst,
+			combinedConditionExpr,
+			combinedConditionBucket,
+			nestedStepsCode,
+			stepContextItemExpr,
+			contextItemExpr
+		);
+	}
+
+	// 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;
 
+	// Compile the expression
+	const [filterExpr, filterBucket] = context.emitBaseExpr(
+		filterExprAst,
+		contextItemExpr,
+		context
+	);
+	// Assign it to the stepContextItem variable
+	// If the filterExpr is a path, the result could be another generator we should loop over
 	return [
-		acceptAst(emittedCode, { type: GeneratedCodeBaseType.Statement }, emittedVariables),
-		intersectingBucket,
+		mapPartialCompilationResult(filterExpr, (filterExpr) => {
+			// If there are following steps, the result of the expression must be a node
+			const pathTypeCheck =
+				restStepAsts.length === 0
+					? acceptAst(``, { type: GeneratedCodeBaseType.Statement }, [])
+					: acceptAst(
+							`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 },
+							[]
+					  );
+
+			// 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) => {
+				switch (filterExpr.generatedCodeType.type) {
+					case GeneratedCodeBaseType.Generator:
+						return mapPartialCompilationResult(contextItemExpr, (contextItemExpr) =>
+							acceptAst(
+								`for (const ${stepContextItemExpr.code} of ${filterExpr.code}(${
+									contextItemExpr.code
+								})) {
+									${nestedCode.variables.join('\n')}
+									${nestedCode.code}
+								}`,
+								{ type: GeneratedCodeBaseType.Statement },
+								[
+									...stepContextItemExpr.variables,
+									...filterExpr.variables,
+									...pathTypeCheck.variables,
+								]
+							)
+						);
+
+					case GeneratedCodeBaseType.Value:
+						return acceptAst(
+							`const ${stepContextItemExpr.code} = ${filterExpr.code};
+							${pathTypeCheck.code}
+							if (${stepContextItemExpr.code} !== null) {
+								${nestedCode.variables.join('\n')}
+								${nestedCode.code}
+							}`,
+							{ type: GeneratedCodeBaseType.Statement },
+							[
+								...stepContextItemExpr.variables,
+								...filterExpr.variables,
+								...pathTypeCheck.variables,
+							]
+						);
+					default:
+						return rejectAst('Unsupported generated code type for filterExpr');
+				}
+			});
+		}),
+		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,
+		];
+	});
+}
+
 /**
  * Takes a path expression AST node and turns it into a javascript function.
  * Path expression can be used to locate nodes within trees and they
  * 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/evaluateXPathWithJsCodegen.ts b/test/specs/parsing/jsCodegen/evaluateXPathWithJsCodegen.ts
index 7b0c2130..61599b6e 100644
--- a/test/specs/parsing/jsCodegen/evaluateXPathWithJsCodegen.ts
+++ b/test/specs/parsing/jsCodegen/evaluateXPathWithJsCodegen.ts
@@ -1,6 +1,7 @@
 import {
 	CompiledXPathFunction,
 	compileXPathToJavaScript,
+	EvaluableExpression,
 	executeJavaScriptCompiledXPath,
 	IDomFacade,
 	IReturnTypes,
@@ -8,17 +9,33 @@ import {
 	ReturnType,
 } from '../../../../src/index';
 
-function generateKey(query: string, returnType: ReturnType) {
-	return `${query} ${returnType}`;
+const compiledXPathCache = new Map<EvaluableExpression, Map<ReturnType, CompiledXPathFunction>>();
+
+function getFromCache(
+	query: EvaluableExpression,
+	returnType: ReturnType
+): CompiledXPathFunction | undefined {
+	return compiledXPathCache.get(query)?.get(returnType);
 }
 
-const compiledXPathCache = new Map();
+function addToCache(
+	query: EvaluableExpression,
+	returnType: ReturnType,
+	fn: CompiledXPathFunction
+): void {
+	let byReturnType = compiledXPathCache.get(query);
+	if (byReturnType === undefined) {
+		byReturnType = new Map();
+		compiledXPathCache.set(query, byReturnType);
+	}
+	byReturnType.set(returnType, fn);
+}
 
 const evaluateXPathWithJsCodegen = <
 	TNode extends Node,
 	TReturnType extends keyof IReturnTypes<TNode>
 >(
-	query: string,
+	query: EvaluableExpression,
 	contextItem?: any | null,
 	domFacade?: IDomFacade | null,
 	returnType?: ReturnType,
@@ -26,16 +43,14 @@ const evaluateXPathWithJsCodegen = <
 ): IReturnTypes<TNode>[TReturnType] => {
 	returnType = returnType || (ReturnType.ANY as any);
 
-	const cacheKey = generateKey(query, returnType);
-	const cachedCompiledXPath = compiledXPathCache.get(cacheKey);
-
+	const cachedCompiledXPath = getFromCache(query, returnType);
 	if (!cachedCompiledXPath) {
 		const compiledXPathResult = compileXPathToJavaScript(query, returnType, options);
 		if (compiledXPathResult.isAstAccepted === true) {
 			// tslint:disable-next-line
 			const evalFunction = new Function(compiledXPathResult.code) as CompiledXPathFunction;
 
-			compiledXPathCache.set(cacheKey, evalFunction);
+			addToCache(query, returnType, evalFunction);
 
 			return executeJavaScriptCompiledXPath(evalFunction, contextItem, domFacade);
 		} else {
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/pathExpr.tests.ts b/test/specs/parsing/jsCodegen/pathExpr.tests.ts
index dbfb617d..4b11850c 100644
--- a/test/specs/parsing/jsCodegen/pathExpr.tests.ts
+++ b/test/specs/parsing/jsCodegen/pathExpr.tests.ts
@@ -2,7 +2,7 @@ import * as chai from 'chai';
 import * as slimdom from 'slimdom';
 import jsonMlMapper from 'test-helpers/jsonMlMapper';
 
-import { ReturnType } from 'fontoxpath';
+import { evaluateXPath, Language, parseScript, ReturnType } from 'fontoxpath';
 import evaluateXPathWithJsCodegen from './evaluateXPathWithJsCodegen';
 
 describe('paths (js-codegen)', () => {
@@ -29,4 +29,102 @@ describe('paths (js-codegen)', () => {
 			evaluateXPathWithJsCodegen('/does-not-exist', titleNode, null, ReturnType.BOOLEAN)
 		);
 	});
+
+	it('handles paths with filterExpr', () => {
+		const titleNode = documentNode.firstChild.firstChild;
+		chai.assert.isFalse(
+			evaluateXPathWithJsCodegen('self::title/false()', titleNode, null, ReturnType.BOOLEAN)
+		);
+		// context item is also a filterExpr
+		chai.assert.equal(
+			evaluateXPathWithJsCodegen('self::title/./.', titleNode, null, ReturnType.FIRST_NODE),
+			titleNode
+		);
+		// filterExpr with following steps must result in a node
+		chai.assert.throws(
+			() =>
+				evaluateXPathWithJsCodegen(
+					'false()/self::title',
+					titleNode,
+					null,
+					ReturnType.BOOLEAN
+				),
+			/XPTY0019/
+		);
+	});
+
+	it('handles paths with filterExpr with nested paths (single value)', () => {
+		// Fonto's xq interpolates expressions using AST manipulation, which may result in
+		// having a pathExpr inside of a filterExpr:
+		const outerAst = parseScript(
+			'$a/*',
+			{ language: Language.XQUERY_3_1_LANGUAGE, debug: false },
+			new slimdom.Document()
+		);
+		const innerAst = parseScript(
+			'self::nope',
+			{ language: Language.XQUERY_3_1_LANGUAGE, debug: false },
+			new slimdom.Document()
+		);
+		const actualExpression = evaluateXPath<slimdom.Element, ReturnType.FIRST_NODE>(
+			'descendant::Q{http://www.w3.org/2005/XQueryX}queryBody/*',
+			innerAst,
+			null,
+			{},
+			ReturnType.FIRST_NODE
+		);
+		const expressionToReplace = evaluateXPath<slimdom.Element, ReturnType.FIRST_NODE>(
+			`descendant::Q{http://www.w3.org/2005/XQueryX}varRef`,
+			outerAst,
+			null,
+			{},
+			ReturnType.FIRST_NODE
+		);
+		expressionToReplace.replaceWith(actualExpression);
+
+		chai.assert.equal(
+			evaluateXPathWithJsCodegen(
+				outerAst,
+				documentNode.documentElement,
+				null,
+				ReturnType.FIRST_NODE
+			),
+			null
+		);
+	});
+
+	it('handles paths with filterExpr with nested paths (multiple values)', () => {
+		// Fonto's xq interpolates expressions using AST manipulation, which may result in
+		// having a pathExpr inside of a filterExpr:
+		const outerAst = parseScript(
+			'$a/*',
+			{ language: Language.XQUERY_3_1_LANGUAGE, debug: false },
+			new slimdom.Document()
+		);
+		const innerAst = parseScript(
+			'*',
+			{ language: Language.XQUERY_3_1_LANGUAGE, debug: false },
+			new slimdom.Document()
+		);
+		const actualExpression = evaluateXPath<slimdom.Element, ReturnType.FIRST_NODE>(
+			'descendant::Q{http://www.w3.org/2005/XQueryX}queryBody/*',
+			innerAst,
+			null,
+			{},
+			ReturnType.FIRST_NODE
+		);
+		const expressionToReplace = evaluateXPath<slimdom.Element, ReturnType.FIRST_NODE>(
+			`descendant::Q{http://www.w3.org/2005/XQueryX}varRef`,
+			outerAst,
+			null,
+			{},
+			ReturnType.FIRST_NODE
+		);
+		expressionToReplace.replaceWith(actualExpression);
+
+		chai.assert.equal(
+			evaluateXPathWithJsCodegen(outerAst, documentNode, null, ReturnType.FIRST_NODE),
+			documentNode.documentElement.firstElementChild
+		);
+	});
 });
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/specs/parsing/jsCodegen/returnValues.tests.ts b/test/specs/parsing/jsCodegen/returnValues.tests.ts
index 2298a665..f146e5d8 100644
--- a/test/specs/parsing/jsCodegen/returnValues.tests.ts
+++ b/test/specs/parsing/jsCodegen/returnValues.tests.ts
@@ -44,6 +44,14 @@ describe('return values', () => {
 				ReturnType.BOOLEAN
 			)
 		);
+		chai.assert.isTrue(
+			evaluateXPathWithJsCodegen(
+				`false() or self::xml`,
+				documentNode.documentElement,
+				null,
+				ReturnType.BOOLEAN
+			)
+		);
 	});
 
 	it('evaluates to nodes', () => {
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, {