//@ts-check
//////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////      Helios      /////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////
//
// Author:        Christian Schmitz
// Email:         cschmitz398@gmail.com
// Website:       https://www.hyperion-bt.org
// Repository:    https://github.com/hyperion-bt/helios
// Version:       0.16.7
// Last update:   January 2024
// License type:  BSD-3-Clause
//
//
// About: Helios is a smart contract DSL for Cardano.
//     This Javascript library contains functions to compile Helios sources into Plutus-core.
//     Transactions can also be built using Helios.
//
//
// Dependencies: none
//
//
// Disclaimer: I made Helios available as FOSS so that the Cardano community can test it 
//     extensively. I don't guarantee the library is bug-free, nor do I guarantee
//     backward compatibility with future versions.
//
//
// Example usage:
//     > import * as helios from "helios.js";
//     > console.log(helios.Program.new("spending my_validator ...").compile().serialize());
//     
//
// Documentation: https://www.hyperion-bt.org/helios-book
//
//
// Note: I recommend keeping the Helios library as a single unminified file for optimal 
//     auditability.
//
// 
// License text:
//     Copyright 2023 Christian Schmitz
//     
//     Redistribution and use in source and binary forms, with or without 
//     modification, are permitted provided that the following conditions are met:
//     
//     1. Redistributions of source code must retain the above copyright notice, this 
//     list of conditions and the following disclaimer.
//     
//     2. Redistributions in binary form must reproduce the above copyright notice, 
//     this list of conditions and the following disclaimer in the documentation 
//     and/or other materials provided with the distribution.
//     
//     3. Neither the name of the copyright holder nor the names of its contributors 
//     may be used to endorse or promote products derived from this software without 
//     specific prior written permission.
//     
//     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS â€œAS ISâ€ 
//     AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
//     IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
//     DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 
//     FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
//     DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
//     SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
//     CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
//     OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
//     OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//     
//
//
// Overview of internals:
//     Section 1: Config                     VERSION, TAB, REAL_PRECISION, config
//
//     Section 2: Utilities                  assert, assertDefined, assertClass, assertNonEmpty, 
//                                           assertNumber, reduceNull, reduceNullPairs, eq, 
//                                           assertEq, idiv, ipow2, imask, imod8, bigIntToBytes, 
//                                           bytesToBigInt, leBytesToBigInt, bigIntToLe32Bytes, 
//                                           padZeroes, byteToBitString, hexToBytes, bytesToHex, 
//                                           textToBytes, bytesToText, replaceTabs, BitReader, 
//                                           BitWriter, Source, hl, deprecationWarning
//
//     Section 3: Tokens                     Site, RuntimeError, Token, assertToken, Word, 
//                                           SymbolToken, Group, PrimitiveLiteral, IntLiteral, 
//                                           RealLiteral, BoolLiteral, ByteArrayLiteral, 
//                                           StringLiteral, RE_IR_PARAMETRIC_NAME, TTPP, FTPP, 
//                                           RE_TEMPLATE_NAME, IRParametricName
//
//     Section 4: Cryptography functions     BLAKE2B_DIGEST_SIZE, setBlake2bDigestSize, imod32, 
//                                           irotr, posMod, randomBytes, UINT64_ZERO, 
//                                           uint64FromBytes, uint64FromString, uint64ToBytes, 
//                                           uint64Eq, uint64Not, uint64And, uint64Xor, uint64Add, 
//                                           uint64Rotr, uint64Shiftr, UInt64, encodeBase32Bytes, 
//                                           expandBech32HumanReadablePart, calcBech32Checksum, 
//                                           calcBech32Polymod, hmac, DEFAULT_BASE32_ALPHABET, 
//                                           BECH32_BASE32_ALPHABET, Crypto, ED25519_Q, 
//                                           ED25519_Q38, ED25519_CURVE_ORDER, ED25519_D, 
//                                           ED25519_I, expMod, curveMod, curveInvert, recoverX, 
//                                           encodeCurveInt, decodeCurveInt, getBit, AffinePoint, 
//                                           ExtendedPoint, clamp, nonce, CurvePointImpl, Ed25519, 
//                                           BIP39_DICT_EN
//
//     Section 5: Cbor encoder/decoder       CborData, Cbor
//
//     Section 6: Uplc data types            UPLC_DATA_NODE_MEM_SIZE, UplcData, IntData, 
//                                           ByteArrayData, ListData, MapData, ConstrData
//
//     Section 7: Helios data objects        HeliosData, HInt, Time, Duration, Bool, HString, 
//                                           ByteArray, HList, HMap, Option, Hash, DatumHash, 
//                                           PubKey, PubKeyHash, ScriptHash, MintingPolicyHash, 
//                                           StakingValidatorHash, ValidatorHash, TxId, 
//                                           TxOutputId, Address, AssetClass, Assets, Value
//
//     Section 8: Uplc cost-models           NetworkParams, CostModel, ConstCost, LinearCost, 
//                                           ArgSizeCost, Arg0SizeCost, Arg1SizeCost, 
//                                           Arg2SizeCost, MinArgSizeCost, MaxArgSizeCost, 
//                                           SumArgSizesCost, ArgSizeDiffCost, ArgSizeProdCost, 
//                                           ArgSizeDiagCost
//
//     Section 9: Uplc built-in functions    BUILTIN_PREFIX, SAFE_BUILTIN_SUFFIX, 
//                                           MACRO_BUILTIN_PREFIX, UPLC_BUILTINS, 
//                                           UPLC_MACROS_OFFSET, UPLC_MACROS, dumpCostModels, 
//                                           findUplcBuiltin, isUplcBuiltin
//
//     Section 10: Uplc AST                  UplcValueImpl, UplcType, DEFAULT_UPLC_RTE_CALLBACKS, 
//                                           UplcRte, UplcStack, UplcAny, UplcInt, UplcByteArray, 
//                                           UplcString, UplcUnit, UplcBool, UplcPair, UplcList, 
//                                           UplcDataValue, UplcTerm, UplcVariable, UplcDelay, 
//                                           UplcLambda, UplcCall, UplcConst, UplcForce, 
//                                           UplcError, UplcBuiltin, UplcFrame, ForceFrame, 
//                                           PreCallFrame, CallFrame, UplcTermWithEnv, 
//                                           UplcLambdaWithEnv, UplcDelayWithEnv, UplcAnonValue, 
//                                           AppliedUplcBuiltin
//
//     Section 11: Uplc program              UPLC_VERSION_COMPONENTS, UPLC_VERSION, 
//                                           PLUTUS_SCRIPT_VERSION, UPLC_TAG_WIDTHS, 
//                                           deserializeUplcBytes, deserializeUplc
//
//     Section 12: Tokenization              Tokenizer, tokenize, tokenizeIR
//
//     Section 13: Eval common types         applyTypes, Common, AllType, AnyType, ErrorType, 
//                                           ArgType, FuncType, GenericType, 
//                                           GenericEnumMemberType, VoidType, DataEntity, 
//                                           ErrorEntity, NamedEntity, FuncEntity, TypedEntity, 
//                                           VoidEntity, ModuleNamespace
//
//     Section 14: Eval primitive types      genCommonInstanceMembers, genCommonTypeMembers, 
//                                           genCommonEnumTypeMembers, BoolType, ByteArrayType, 
//                                           IntType, RawDataType, RealType, StringType
//
//     Section 15: Eval builtin typeclasses  GenericParametricType, 
//                                           GenericParametricEnumMemberType, TypeClassImpl, 
//                                           DataTypeClassImpl, AnyTypeClass, DefaultTypeClass, 
//                                           SummableTypeClass, Parameter, ParametricFunc, 
//                                           AppliedType, ParametricType
//
//     Section 16: Eval builtin functions    BuiltinFunc, AssertFunc, ErrorFunc, PrintFunc
//
//     Section 17: Eval container types      IteratorType, TupleType, isDataType, TupleType, 
//                                           getTupleItemTypes, ListType, ListType, MapType, 
//                                           MapType, OptionType, OptionType
//
//     Section 18: Eval time types           DurationType, TimeType, TimeRangeType
//
//     Section 19: Eval hash types           genHashInstanceMembers, genHashTypeMembers, 
//                                           genHashTypeProps, ScriptHashType, scriptHashType, 
//                                           DatumHashType, MintingPolicyHashType, PubKeyType, 
//                                           PubKeyHashType, StakingHashType, 
//                                           StakingHashStakeKeyType, StakingHashValidatorType, 
//                                           StakingValidatorHashType, ValidatorHashType
//
//     Section 20: Eval money types          AssetClassType, ValueType, ValuableTypeClass
//
//     Section 21: Eval tx types             AddressType, DCertType, DCertDelegateType, 
//                                           DCertDeregisterType, DCertRegisterType, 
//                                           DCertRegisterPoolType, DCertRetirePoolType, 
//                                           CredentialType, CredentialPubKeyType, 
//                                           CredentialValidatorType, OutputDatumType, 
//                                           OutputDatumHashType, OutputDatumInlineType, 
//                                           OutputDatumNoneType, MacroType, ScriptsType, 
//                                           ScriptContextType, ContractContextType, WalletType, 
//                                           NetworkType, ScriptPurposeType, 
//                                           ScriptPurposeCertifyingType, 
//                                           ScriptPurposeMintingType, ScriptPurposeTypeRewarding, 
//                                           ScriptPurposeSpendingType, StakingCredentialType, 
//                                           StakingCredentialHashType, StakingCredentialPtrType, 
//                                           StakingPurposeType, StakingPurposeCertifyingType, 
//                                           StakingPurposeRewardingType, TxBuilderType, TxType, 
//                                           TxIdType, TxInputType, TxOutputType, TxOutputIdType
//
//     Section 22: Scopes                    builtinTypes, GlobalScope, Scope, TopScope, 
//                                           ModuleScope
//
//     Section 23: IR definitions            onNotifyRawUsage, setRawUsageNotifier, RE_BUILTIN, 
//                                           RawFunc, makeRawFunctions, ToIRContext, 
//                                           fetchRawGenerics, fetchRawFunctions, 
//                                           wrapWithRawFunctions
//
//     Section 24: Helios AST expressions    Expr, RefExpr, PathExpr, ValuePathExpr, ListTypeExpr, 
//                                           MapTypeExpr, IteratorTypeExpr, OptionTypeExpr, 
//                                           VoidTypeExpr, TupleTypeExpr, FuncArgTypeExpr, 
//                                           FuncTypeExpr, ChainExpr, AssignExpr, VoidExpr, 
//                                           PrimitiveLiteralExpr, LiteralDataExpr, 
//                                           StructLiteralField, StructLiteralExpr, 
//                                           ListLiteralExpr, MapLiteralExpr, NameTypePair, 
//                                           FuncArg, FuncLiteralExpr, ParametricExpr, UnaryExpr, 
//                                           BINARY_SYMBOLS_MAP, BinaryExpr, ParensExpr, 
//                                           CallArgExpr, CallExpr, MemberExpr, IfElseExpr, 
//                                           DestructExpr, SwitchCase, UnconstrDataSwitchCase, 
//                                           SwitchDefault, SwitchExpr, EnumSwitchExpr, 
//                                           DataSwitchExpr
//
//     Section 25: Helios AST statements     Statement, ImportFromStatement, 
//                                           ImportModuleStatement, ConstStatement, TypeParameter, 
//                                           TypeParameters, DataField, DataDefinition, 
//                                           StructStatement, FuncStatement, EnumMember, 
//                                           EnumStatement, ImplDefinition
//
//     Section 26: Helios AST building       AUTOMATIC_METHODS, importPathTranslator, 
//                                           setImportPathTranslator, buildProgramStatements, 
//                                           buildScriptPurpose, buildScript, 
//                                           extractScriptPurposeAndName, buildConstStatement, 
//                                           buildTypeClassExpr, buildTypeParameter, 
//                                           buildTypeParameters, splitDataImpl, 
//                                           buildStructStatement, buildDataFields, 
//                                           buildFuncStatement, buildFuncLiteralExpr, 
//                                           buildFuncArgs, buildEnumStatement, 
//                                           buildImportStatements, buildImportModuleStatement, 
//                                           buildImportFromStatements, buildEnumMember, 
//                                           buildImplDefinition, buildImplMembers, buildTypeExpr, 
//                                           buildParametricTypeExpr, buildListTypeExpr, 
//                                           buildMapTypeExpr, buildOptionTypeExpr, 
//                                           buildIteratorTypeExpr, buildFuncTypeExpr, 
//                                           buildFuncArgTypeExpr, buildFuncRetTypeExpr, 
//                                           buildTypePathExpr, buildTypeRefExpr, 
//                                           buildTupleTypeExpr, buildValueExpr, 
//                                           buildMaybeAssignOrChainExpr, buildDestructExpr, 
//                                           buildDestructExprs, buildAssignLhs, buildPipedExpr, 
//                                           makeBinaryExprBuilder, makeUnaryExprBuilder, 
//                                           buildChainedValueExpr, 
//                                           buildRemainingChainedValueExpr, 
//                                           buildParametricValueExpr, buildCallExpr, 
//                                           buildChainStartValueExpr, buildParensExpr, 
//                                           buildCallArgs, buildCallArgExpr, buildIfElseExpr, 
//                                           buildSwitchExpr, buildSwitchCaseName, 
//                                           buildSwitchCase, buildSwitchCaseNameType, 
//                                           buildMultiArgSwitchCase, buildSingleArgSwitchCase, 
//                                           buildSwitchCaseBody, buildSwitchDefault, 
//                                           buildListLiteralExpr, buildOptionSomeLiteralExpr, 
//                                           buildMapLiteralExpr, buildStructLiteralExpr, 
//                                           buildStructLiteralField, 
//                                           buildStructLiteralNamedField, 
//                                           buildStructLiteralUnnamedField, buildValuePathExpr
//
//     Section 27: IR Context objects        IRScope, IRVariable
//
//     Section 28: IR AST objects            IRNameExpr, IRLiteralExpr, IRFuncExpr, IRCallExpr, 
//                                           IRErrorExpr, loopIRExprs
//
//     Section 29: IR AST build functions    IRExprTagger, buildIRExpr, buildIRFuncExpr
//
//     Section 30: IR pseudo evaluation      IRStack, IRLiteralValue, IRDataValue, 
//                                           collectIRVariables, IRBuiltinValue, IRFuncValue, 
//                                           IRErrorValue, IRAnyValue, IRMultiValue, 
//                                           IRValueCodeMapper, IR_BUILTIN_CALLBACKS, IREvaluator, 
//                                           annotateIR
//
//     Section 31: IR optimization           INLINE_MAX_SIZE, isIdentityFunc, IROptimizerState, 
//                                           IROptimizer
//
//     Section 32: IR Program                IRProgram, IRParametricProgram
//
//     Section 33: Helios program            Module, MainModule, DEFAULT_PROGRAM_CONFIG, 
//                                           RedeemerProgram, DatumRedeemerProgram, 
//                                           GenericProgram, TestingProgram, SpendingProgram, 
//                                           MintingProgram, StakingProgram, EndpointProgram
//
//     Section 34: Native scripts            NativeContext, NativeScript, NativeSig, NativeAll, 
//                                           NativeAny, NativeAtLeast, NativeAfter, NativeBefore
//
//     Section 35: Tx types                  Tx, TxBody, TxWitnesses, TxInput, UTxO, TxRefInput, 
//                                           TxOutput, DCert, DCertRegister, DCertDeregister, 
//                                           DCertDelegate, DCertRegisterPool, DCertRetirePool, 
//                                           StakeAddress, Signature, Ed25519PrivateKey, 
//                                           BIP32_HARDEN, Bip32PrivateKey, RootPrivateKey, 
//                                           Redeemer, SpendingRedeemer, MintingRedeemer, Datum, 
//                                           HashedDatum, InlineDatum, encodeMetadata, 
//                                           decodeMetadata, TxMetadata
//
//     Section 36: Highlighting function     SyntaxCategory, highlight
//
//     Section 37: CoinSelection             CoinSelection
//
//     Section 38: Wallets                   Cip30Wallet, WalletHelper, RemoteWallet
//
//     Section 39: Network                   BlockfrostV0, KoiosV0
//
//     Section 40: Emulator                  rawNetworkEmulatorParams, SimpleWallet, GenesisTx, 
//                                           RegularTx, NetworkEmulator, TxChainWallet, TxChain, 
//                                           NetworkSlice
//
//     Section 41: Fuzzy testing framework   FuzzyTest
//
//     Section 42: Bundling specific functionsjsToUplc, uplcToJs
//
///////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////


////////////////////
// Section 1: Config
////////////////////

/**
 * Current version of the Helios library.
 */
export const VERSION = "0.16.7";

/**
 * A tab used for indenting of the IR.
 * 2 spaces.
 * @internal
 * @type {string}
 */
export const TAB = "  ";

/**
 * A Real in Helios is a fixed point number with REAL_PRECISION precision
 * @internal
 * @type {number}
 */
export const REAL_PRECISION = 6;

/**
 * Mutable global config properties.
 * @namespace
 */
export const config = {
    /**
     * Modify the config properties
     * @param {{
     *   DEBUG?: boolean
     *   STRICT_BABBAGE?: boolean
     *   IS_TESTNET?: boolean
     *   N_DUMMY_INPUTS?: number
     *   AUTO_SET_VALIDITY_RANGE?: boolean
     *   VALIDITY_RANGE_START_OFFSET?: number
     *   VALIDITY_RANGE_END_OFFSET?: number
     *   IGNORE_UNEVALUATED_CONSTANTS?: boolean
     *   CHECK_CASTS?: boolean
     *   MAX_ASSETS_PER_CHANGE_OUTPUT?: number
     * }} props 
     */
    set: (props) => {
        Object.keys(props).forEach(k => {
            config[k] = props[k];
        });
    },

    /**
     * Global debug flag. Currently unused.
     * 
     * Default: `false`.
     * @type {boolean}
     */
    DEBUG: false,

    /**
     * If true, `TxOutput` is serialized using strictly the Babagge cddl format (slightly more verbose).
     * 
     * Default: `false`.
     * @type {boolean}
     */
    STRICT_BABBAGE: false,


    /**
     * If true, `Address` instances are assumed to be for a Testnet when constructing from hashes or raw bytes, otherwise for mainnet.
     * 
     * Default: `true`.
     * @type {boolean}
     */
    IS_TESTNET: true,

    /**
     * Calculating the execution budget during tx building requires knowing all the inputs beforehand,
     * which is very difficult because balancing is done after the budget is calculated.
     * Instead we use at least 1 dummy input, which should act as a representative balancing input.
     * For increased robustness we use 2 dummy inputs, one with Txid 0 and other with TxId ffff...,
     * because eg. there are cases where the TxId is being printed,
     * and a Txid of ffff... would overestimate the fee for that.
     * This value must be '1' or '2'.
     * 
     * Default: 2.
     * @deprecated
     * @type {number}
     */
    N_DUMMY_INPUTS: 2,

    /**
     * The validity time range can be set automatically if a call to tx.time_range in a Helios script is detected.
     * If `false` the validity range is still set automatically if not set manually but a warning is printed.
     * 
     * Default: `false`.
     * @type {boolean}
     */
    AUTO_SET_VALIDITY_RANGE: false,


    /**
     * Lower offset wrt. the current system time when setting a transaction validity range automatically.
     * 
     * Defaut: 90 seconds.
     * @type {number} seconds
     */
    VALIDITY_RANGE_START_OFFSET: 90,

    /**
     * Upper offset wrt. the current system time when setting a transaction validity range automatically.
     * 
     * Default: 300 seconds.
     * @type {number} seconds
     */
    VALIDITY_RANGE_END_OFFSET: 300,

    /**
     * Ignore constants that can't be evaluated during compile-time.
     * 
     * Default: `false`.
     * @type {boolean}
     */
    IGNORE_UNEVALUATED_CONSTANTS: false,

    /**
     * Check that `from_data` casts make sense during runtime, printing a warning if it doesn't. This ony impacts unsimplified UplcPrograms.
     * 
     * Default: `false`.
     * @type {boolean}
     */
    CHECK_CASTS: false,

    /**
     * Maximum number of assets per change output. Used to break up very large asset outputs into multiple outputs.
     * 
     * Default: `undefined` (no limit).
     */
    MAX_ASSETS_PER_CHANGE_OUTPUT: undefined,
}



///////////////////////
// Section 2: Utilities
///////////////////////

/**
 * Needed by transfer() methods
 * @typedef {{
 *   transferByteArrayData: (bytes: number[]) => any,
 *   transferConstrData: (index: number, fields: any[]) => any,
 *   transferIntData: (value: bigint) => any,
 *   transferListData: (items: any[]) => any,
 *   transferMapData: (pairs: [any, any][]) => any,
 *   transferSite: (src: any, startPos: number, endPos: number, codeMapSite: null | any) => any,
 *   transferSource: (raw: string, name: string) => any,
 *   transferUplcBool: (site: any, value: boolean) => any,
 *   transferUplcBuiltin: (site: any, name: string | number) => any,
 *   transferUplcByteArray: (site: any, bytes: number[]) => any,
 *   transferUplcCall: (site: any, a: any, b: any) => any,
 *   transferUplcConst: (value: any) => any,
 *   transferUplcDataValue: (site: any, data: any) => any,
 *   transferUplcDelay: (site: any, expr: any) => any,
 *   transferUplcError: (site: any, msg: string) => any,
 *   transferUplcForce: (site: any, expr: any) => any,
 *   transferUplcInt: (site: any, value: bigint, signed: boolean) => any,
 *   transferUplcLambda: (site: any, rhs: any, name: null | string) => any,
 *   transferUplcList: (site: any, itemType: any, items: any[]) => any,
 *   transferUplcPair: (site: any, first: any, second: any) => any,
 *   transferUplcString: (site: any, value: string) => any,
 *   transferUplcType: (typeBits: string) => any,
 *   transferUplcUnit: (site: any) => any,
 *   transferUplcVariable: (site: any, index: any) => any
 * }} TransferUplcAst
 */

/**
 * Throws an error if 'cond' is false.
 * @internal
 * @param {boolean} cond 
 * @param {string} msg 
 */
export function assert(cond, msg = "unexpected") {
	if (!cond) {
		throw new Error(msg);
	}
}

/**
 * Throws an error if 'obj' is undefined. Returns 'obj' itself (for chained application).
 * @internal
 * @template T
 * @param {T | undefined | null} obj 
 * @param {string} msg 
 * @returns {T}
 */
export function assertDefined(obj, msg = "unexpected undefined value") {
	if (obj === undefined || obj === null ) {
		throw new Error(msg);
	}

	return obj;
}

/**
 * @internal
 * @template Tin, Tout
 * @param {Tin} obj
 * @param {{new(...any): Tout}} C
 * @returns {Tout}
 */
export function assertClass(obj, C, msg = "unexpected class") {
	if (obj instanceof C) {
		return obj;
	} else {
		throw new Error(msg);
	}
}

/**
 * @internal
 * @param {string} str 
 * @param {string} msg 
 * @returns {string}
 */
export function assertNonEmpty(str, msg = "empty string") {
	if (str.length == 0) {
		throw new Error(msg);
	} else {
		return str;
	}
}

/**
 * @internal
 * @param {any} obj 
 * @param {string} msg 
 * @returns {number}
 */
export function assertNumber(obj, msg = "expected a number") {
	if (obj === undefined || obj === null) {
		throw new Error(msg);
	} else if (typeof obj == "number") {
		return obj;
	} else {
		throw new Error(msg);
	}
}

/**
 * @internal
 * @template T
 * @param {(T | null)[]} lst
 * @returns {null | (T[])}
 */
export function reduceNull(lst) {
	/**
	 * @type {T[]}
	 */
	const nonNullLst = [];

	let someNull = false;

	lst.forEach(item => {
		if (item !== null && !someNull) {
			nonNullLst.push(item);
		} else {
			someNull = true;
		}
	});

	if (someNull) {
		return null;
	} else {
		return nonNullLst;
	}
}

/**
 * @internal
 * @template Ta
 * @template Tb
 * @param {[Ta | null, Tb | null][]} pairs
 * @returns {null | [Ta, Tb][]}
 */
export function reduceNullPairs(pairs) {
	/**
	 * @type {[Ta, Tb][]}
	 */
	const nonNullPairs = [];

	let someNull = false;

	pairs.forEach(([a, b]) => {
		if (a === null || b === null) {
			someNull = true;
		} else if (!someNull) {
			nonNullPairs.push([a, b]);
		}
	});

	if (someNull) {
		return null;
	} else {
		return nonNullPairs;
	}
}

/**
 * Compares two objects (deep recursive comparison)
 * @internal
 * @template T
 * @param {T} a 
 * @param {T} b 
 * @returns {boolean}
 */
export function eq(a, b) {
	if (a === undefined || b === undefined) {
		throw new Error("one of the args is undefined");
	} else if (typeof a == "string") {
		return a === b;
	} else if (typeof a == "number") {
		return a === b;
	} else if (typeof a == "boolean") {
		return a === b;
	} else if (typeof a == "bigint") {
		return a === b;
	} else if (a instanceof Array && b instanceof Array) {
		if (a.length != b.length) {
			return false;
		}

		for (let i = 0; i < a.length; i++) {
			if (!eq(a[i], b[i])) {
				return false;
			}
		}

		return true;
	} else {
		throw new Error(`eq not yet implemented for these types: ${typeof a} and ${typeof b}`);
	}
}

/**
 * Throws an error if two object aren't equal (deep comparison).
 * Used by unit tests that are autogenerated from JSDoc inline examples.
 * @internal
 * @template T
 * @param {T} a
 * @param {T} b
 * @param {string} msg
 */
export function assertEq(a, b, msg) {
	if (!eq(a, b)) {
		console.log("lhs:", a);
		console.log("rhs:", b);
		console.log("...")
		throw new Error(msg);
	}
}

/**
 * Divides two integers. Assumes a and b are whole numbers. Rounds down the result.
 * @example
 * idiv(355, 113) == 3
 * @internal
 * @param {number} a
 * @param {number} b 
 */
export function idiv(a, b) {
	return Math.floor(a / b);
	// alternatively: (a - a%b)/b
}

/**
 * 2 to the power 'p' for bigint.
 * @internal
 * @param {bigint} p
 * @returns {bigint}
 */
export function ipow2(p) {
	return (p <= 0n) ? 1n : 2n << (p - 1n);
}

/**
 * Masks bits of 'b' by setting bits outside the range ['i0', 'i1') to 0. 
 * 'b' is an 8 bit integer (i.e. number between 0 and 255).
 * The return value is also an 8 bit integer, shift right by 'i1'.
 
 * @example
 * imask(0b11111111, 1, 4) == 0b0111 // (i.e. 7)
 * @internal
 * @param {number} b 
 * @param {number} i0 
 * @param {number} i1 
 * @returns {number}
 */
export function imask(b, i0, i1) {
	assert(i0 < i1);

	const mask_bits = [
		0b11111111,
		0b01111111,
		0b00111111,
		0b00011111,
		0b00001111,
		0b00000111,
		0b00000011,
		0b00000001,
	];

	return (b & mask_bits[i0]) >> (8 - i1);
}

/**
 * Make sure resulting number fits in uint8
 * @internal
 * @param {number} x
 */
export function imod8(x) {
	return x & 0xff;
}

/**
 * Converts an unbounded integer into a list of uint8 numbers (big endian)
 * Used by the CBOR encoding of data structures, and by Ed25519
 * @internal
 * @param {bigint} x
 * @returns {number[]}
 */
export function bigIntToBytes(x) {
	if (x == 0n) {
		return [0];
	} else {
		/**
		 * @type {number[]}
		 */
		const res = [];

		while (x > 0n) {
			res.unshift(Number(x%256n));

			x = x/256n;
		}

		return res;
	}
}

/**
 * Converts a list of uint8 numbers into an unbounded int (big endian)
 * Used by the CBOR decoding of data structures.
 * @internal
 * @param {number[]} b
 * @return {bigint}
 */
export function bytesToBigInt(b) {
	let s = 1n;
	let total = 0n;

	while (b.length > 0) {
		total += BigInt(assertDefined(b.pop()))*s;

		s *= 256n;
	}

	return total;
}

/**
 * Little Endian 32 bytes
 * @internal
 * @param {number[]} b 
 * @returns {bigint}
 */
export function leBytesToBigInt(b) {
	return bytesToBigInt(b.slice().reverse());
}

/**
 * Little Endian 32 bytes
 * @internal
 * @param {bigint} x 
 * @returns {number[]}
 */
export function bigIntToLe32Bytes(x) {
	const bytes = bigIntToBytes(x).reverse();
			
	while (bytes.length < 32) {
		bytes.push(0);
	}

	return bytes;
}

/**
 * Prepends zeroes to a bit-string so that 'result.length == n'.
 * @example
 * padZeroes("1111", 8) == "00001111"
 * @internal
 * @param {string} bits
 * @param {number} n 
 * @returns {string}
 */
export function padZeroes(bits, n) {
	// padded to multiple of n
	if (bits.length % n != 0) {
		const nPad = n - bits.length % n;

		bits = (new Array(nPad)).fill('0').join('') + bits;
	}

	return bits;
}

/**
 * Converts a 8 bit integer number into a bit string with an optional "0b" prefix.
 * The result is padded with leading zeroes to become 'n' chars long ('2 + n' chars long if you count the "0b" prefix). 
 * @example
 * byteToBitString(7) == "0b00000111"
 * @internal
 * @param {number} b 
 * @param {number} n
 * @param {boolean} prefix
 * @returns {string}
 */
export function byteToBitString(b, n = 8, prefix = true) {
	const s = padZeroes(b.toString(2), n);

	if (prefix) {
		return "0b" + s;
	} else {
		return s;
	}
}

/**
 * Converts a hexadecimal string into a list of bytes.
 * @example
 * hexToBytes("00ff34") == [0, 255, 52] 
 * @param {string} hex 
 * @returns {number[]}
 */
export function hexToBytes(hex) {
	hex = hex.trim();
	
	const bytes = [];

	for (let i = 0; i < hex.length; i += 2) {
		const b = parseInt(hex.slice(i, i + 2), 16);

		assert(!Number.isNaN(b), `invalid hexstring "${hex}"`);
		
		bytes.push(parseInt(hex.slice(i, i + 2), 16));
	}

	return bytes;
}

/**
 * Converts a list of uint8 bytes into its hexadecimal string representation.
 * @example
 * bytesToHex([0, 255, 52]) == "00ff34"
 * @param {number[]} bytes
 * @returns {string}
 */
export function bytesToHex(bytes) {
	const parts = [];

	for (let b of bytes) {
		parts.push(padZeroes(b.toString(16), 2));
	}

	return parts.join('');
}

/**
 * Encodes a string into a list of uint8 bytes using UTF-8 encoding.
 * @example
 * textToBytes("hello world") == [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]
 * @param {string} str 
 * @returns {number[]}
 */
export function textToBytes(str) {
	return Array.from((new TextEncoder()).encode(str));
}

/**
 * Decodes a list of uint8 bytes into a string using UTF-8 encoding.
 * @example
 * bytesToText([104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]) == "hello world"
 * @param {number[]} bytes 
 * @returns {string}
 */
export function bytesToText(bytes) {
	return (new TextDecoder("utf-8", {fatal: true})).decode((new Uint8Array(bytes)).buffer);
}

/**
 * Replaces the tab characters of a string with spaces.
 * This is used to create a prettier IR (which is built-up from many template js strings in this file, which might contain tabs depending on the editor used)
 * @example
 * replaceTabs("\t\t\t") == [TAB, TAB, TAB].join("")
 * @internal
 * @param {string} str 
 * @returns {string}
 */
export function replaceTabs(str) {
	return str.replace(new RegExp("\t", "g"), TAB);
}

/**
 * Read non-byte aligned numbers
 * @internal
 */
export class BitReader {
    /**
     * @type {Uint8Array}
     */
	#view;

    /**
     * @type {number}
     */
	#pos;

    /**
     * @type {boolean}
     */
	#truncate;

	/**
	 * @param {number[]} bytes
	 * @param {boolean} truncate - if true then read last bits as low part of number, if false pad with zero bits
	 */
	constructor(bytes, truncate = true) {
		this.#view = new Uint8Array(bytes);
		this.#pos = 0; // bit position, not byte position
		this.#truncate = truncate;
	}

	/**
     * @internal
	 * @returns {boolean}
	 */
	eof() {
		return idiv(this.#pos, 8) >= this.#view.length;
	}

	/**
	 * Reads a number of bits (<= 8) and returns the result as an unsigned number
     * @internal
	 * @param {number} n - number of bits to read
	 * @returns {number}
	 */
	readBits(n) {
		assert(n <= 8, "reading more than 1 byte");

		let leftShift = 0;
		if (this.#pos + n > this.#view.length * 8) {
			const newN = (this.#view.length*8 - this.#pos);

			if (!this.#truncate) {
				leftShift = n - newN;
			}

			n = newN;
		}

		assert(n > 0, "eof");

		// it is assumed we don't need to be at the byte boundary

		let res = 0;
		let i0 = this.#pos;

		for (let i = this.#pos + 1; i <= this.#pos + n; i++) {
			if (i % 8 == 0) {
				const nPart = i - i0;

				res += imask(this.#view[idiv(i, 8) - 1], i0 % 8, 8) << (n - nPart);

				i0 = i;
			} else if (i == this.#pos + n) {
				res += imask(this.#view[idiv(i, 8)], i0 % 8, i % 8);
			}
		}

		this.#pos += n;
		return res << leftShift;
	}

	/**
	 * Moves position to next byte boundary
     * @internal
	 * @param {boolean} force - if true then move to next byte boundary if already at byte boundary
	 */
	moveToByteBoundary(force = false) {
		if (this.#pos % 8 != 0) {
			let n = 8 - this.#pos % 8;

			void this.readBits(n);
		} else if (force) {
			this.readBits(8);
		}
	}

	/**
	 * Reads 8 bits
     * @internal
	 * @returns {number}
	 */
	readByte() {
		return this.readBits(8);
	}

	/**
	 * Dumps remaining bits we #pos isn't yet at end.
	 * This is intended for debugging use.
     * @internal
	 */
	dumpRemainingBits() {
		if (!this.eof()) {
			console.log("remaining bytes:");
			for (let first = true, i = idiv(this.#pos, 8); i < this.#view.length; first = false, i++) {
				if (first && this.#pos % 8 != 0) {
					console.log(byteToBitString(imask(this.#view[i], this.#pos % 8, 8) << 8 - this.#pos % 7));
				} else {
					console.log(byteToBitString(this.#view[i]));
				}
			}
		} else {
			console.log("eof");
		}
	}
}

/**
 * BitWriter turns a string of '0's and '1's into a list of bytes.
 * Finalization pads the bits using '0*1' if not yet aligned with the byte boundary.
 */
export class BitWriter {
	/**
	 * Concatenated and padded upon finalization
	 * @type {string[]}
	 */
	#parts;

	/**
	 * Number of bits written so far
	 * @type {number}
	 */
	#n;

	constructor() {
		this.#parts = [];
		this.#n = 0;
	}

	/**
     * @internal
	 * @type {number}
	 */
	get length() {
		return this.#n;
	}

	/**
	 * Write a string of '0's and '1's to the BitWriter.
     * @internal
	 * @param {string} bitChars
	 */
	write(bitChars) {
		for (let c of bitChars) {
			if (c != '0' && c != '1') {
				throw new Error("bit string contains invalid chars: " + bitChars);
			}
		}

		this.#parts.push(bitChars);
		this.#n += bitChars.length;
	}

	/**
     * @internal
	 * @param {number} byte
	 */
	writeByte(byte) {
		this.write(padZeroes(byte.toString(2), 8));
	}

	/**
	 * Add padding to the BitWriter in order to align with the byte boundary.
	 * If 'force == true' then 8 bits are added if the BitWriter is already aligned.
     * @internal
	 * @param {boolean} force 
	 */
	padToByteBoundary(force = false) {
		let nPad = 0;
		if (this.#n % 8 != 0) {
			nPad = 8 - this.#n % 8;
		} else if (force) {
			nPad = 8;
		}

		if (nPad != 0) {
			let padding = (new Array(nPad)).fill('0');
			padding[nPad - 1] = '1';

			this.#parts.push(padding.join(''));

			this.#n += nPad;
		}
	}

	/**
	 * Pop n bits of the end
	 * @param {number} n 
	 * @returns {string}
	 */
	pop(n) {
		assert(n <= this.#n, `too many bits to pop, only have ${this.#n} bits, but want ${n}`);
		const n0 = n;

		/**
		 * @type {string[]}
		 */
		const parts = [];

		while (n > 0) {
			const last = assertDefined(this.#parts.pop());

			if (last.length == 0) {
				continue;
			}

			if (last.length <= n) {
				parts.unshift(last);
				n -= last.length;
			} else {
				parts.unshift(last.slice(last.length - n));
				this.#parts.push(last.slice(0, last.length - n));
				n = 0;
			}
		}

		this.#n -= n0;

		const bits = parts.join('');
		assert(bits.length == n0);

		return bits;
	}

	/**
	 * Pads the BitWriter to align with the byte boundary and returns the resulting bytes.
     * @internal
	 * @param {boolean} force - force padding (will add one byte if already aligned)
	 * @returns {number[]}
	 */
	finalize(force = true) {
		this.padToByteBoundary(force);

		let chars = this.#parts.join('');

		let bytes = [];

		for (let i = 0; i < chars.length; i += 8) {
			let byteChars = chars.slice(i, i + 8);
			let byte = parseInt(byteChars, 2);

			bytes.push(byte);
		}

		return bytes;
	}
}

/**
 * Function that generates a random number between 0 and 1
 * @typedef {() => number} NumberGenerator
 */

/**
 * A Source instance wraps a string so we can use it cheaply as a reference inside a Site.
 * Also used by VSCode plugin
 */
export class Source {
	#raw;
	#name;
	#errors; // errors are collected into this object

	/**
	 * @param {string} raw 
	 * @param {string} name
	 */
	constructor(raw, name) {
		this.#raw = assertDefined(raw);
		this.#name = name;
		this.#errors = [];
	}

	/**
	 * @param {TransferUplcAst} other 
	 * @returns {any}
	 */
	transfer(other) {
		// errors don't need to be transfered
		return other.transferSource(
			this.#raw,
			this.#name	
		)
	}

    /**
     * @internal
     * @type {string}
     */
	get raw() {
		return this.#raw;
	}

    /**
     * @internal
     * @type {string}
     */
	get name() {
		return this.#name;
	}

	/**
	 * @type {Error[]}
	 */
	get errors() {
		return this.#errors;
	}

	throwErrors() {
		if (this.#errors.length > 0) {
			throw this.#errors[0];
		}
	}

	/**
	 * Get char from the underlying string.
	 * Should work fine utf-8 runes.
     * @internal
	 * @param {number} pos
	 * @returns {string}
	 */
	getChar(pos) {
		return this.#raw[pos];
	}
	
	/**
	 * Returns word under pos
     * @internal
	 * @param {number} pos 
	 * @returns {?string}
	 */
	getWord(pos) {
		/** @type {string[]} */
		const chars = [];

		/**
		 * @param {string | undefined} c 
		 * @returns {boolean}
		 */
		function isWordChar(c) {
			if (c === undefined) {
				return false;
			} else {
				return (c == '_' || (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'));
			}
		}

		let c = this.#raw[pos];
		while (isWordChar(c)) {
			chars.push(c);
			pos += 1;
			c = this.#raw[pos];
		}

		if (chars.length == 0) {
			return null;
		} else {
			return chars.join("");
		}
	}

    /**
     * @internal
     * @type {number}
     */
	get length() {
		return this.#raw.length;
	}

	/**
	 * Calculates the line number of the line where the given character is located (0-based).
     * @internal
	 * @param {number} pos 
	 * @returns {number}
	 */
	posToLine(pos) {
		let line = 0;
		for (let i = 0; i < pos; i++) {
			if (this.#raw[i] == '\n') {
				line += 1;
			}
		}

		return line;
	}

	/**
	 * Calculates the column and line number where the given character is located (0-based).
     * @internal
	 * @param {number} pos
	 * @returns {[number, number]}
	 */
	// returns [col, line]
	posToLineAndCol(pos) {
		let col = 0;
		let line = 0;
		for (let i = 0; i < pos; i++) {
			if (this.#raw[i] == '\n') {
				col = 0;
				line += 1;
			} else {
				col += 1;
			}
		}

		return [line, col];
	}

	/**
	 * Creates a more human-readable version of the source by prepending the line-numbers to each line.
	 * The line-numbers are at least two digits.
	 * @example
	 * (new Source("hello\nworld")).pretty() == "01  hello\n02  world"
     * @internal
	 * @returns {string}
	 */
	pretty() {
		const lines = this.#raw.split("\n");

		const nLines = lines.length;
		const nDigits = Math.max(Math.ceil(Math.log10(nLines)), 2); // line-number is at least two digits

		for (let i = 0; i < nLines; i++) {
			lines[i] = String(i + 1).padStart(nDigits, '0') + "  " + lines[i];
		}

		return lines.join("\n");
	}
}

/**
 * Template string tag function that doesn't do anything and just returns the template string as a string.
 * Can be used as a marker of Helios sources so that syntax highlighting can work inside JS/TS files.
 * @example
 * hl`hello ${"world"}!` == "hello world!"
 * @param {string[]} a 
 * @param  {...any} b 
 * @returns {string}
 */
export function hl(a, ...b) {
	return a.map((part, i) => {
		if (i < b.length) {
			return part + b[i].toString();
		} else {
			return part;
		}
	}).join("");
}

/**
 * Display a warning message that a certain feature will be deprecated at some point in the future.
 * @internal
 * @param {string} feature
 * @param {string} futureVersion
 * @param {string} alternative
 * @param {string} docUrl
 */
export function deprecationWarning(feature, futureVersion, alternative, docUrl = "") {
	let msg = `${feature} is DEPRECATED, and will be removed from version ${futureVersion} onwards!
${alternative}`;

	if (docUrl != "") {
		msg += `\n(for more information: ${docUrl})`;
	}

	console.warn(msg);
}



////////////////////
// Section 3: Tokens
////////////////////
/**
 * Each Token/Expression/Statement has a Site, which encapsulates a position in a Source
 */
export class Site {
	#src;
	#startPos;
	#endPos;

	/** @type {null | Site} - end of token, exclusive, TODO: replace with endPos */
	#endSite;

	/** @type {null | Site} */
	#codeMapSite;

	/**
	 * @param {Source} src 
	 * @param {number} startPos
	 * @param {number} endPos
	 * @param {Site | null} codeMapSite
	 */
	constructor(src, startPos, endPos = startPos + 1, codeMapSite = null) {
		this.#src = src;
		this.#startPos = startPos;
		this.#endPos = endPos;
		this.#endSite = null;
		this.#codeMapSite = codeMapSite;
	}

	/**
	 * 
	 * @param {TransferUplcAst} other 
	 */
	transfer(other) {
		return other.transferSite(
			this.#src.transfer(other),
			this.#startPos,
			this.#endPos,
			this.#codeMapSite?.transfer(other) ?? null
		)
	}

	static dummy() {
		return new Site(new Source("", ""), 0);
	}

	get src() {
		return this.#src;
	}

	get startPos() {
		return this.#startPos;
	}

	get endPos() {
		return this.#endPos;
	}
	
	get endSite() {
		return this.#endSite;
	}

	/**
	 * @param {Site} other 
	 * @returns {Site}
	 */
	merge(other) {
		return new Site(this.#src, this.#startPos, other.#endPos, this.#codeMapSite ?? other.#codeMapSite);
	}

	/**
	 * @param {?Site} site
	 */
	setEndSite(site) {
		this.#endSite = site;
	}

	/**
	 * @type {?Site} 
	 */
	get codeMapSite() {
		return this.#codeMapSite;
	}

	/**
	 * @param {Site} site 
	 */
	setCodeMapSite(site) {
		this.#codeMapSite = site;
	}

	/**
	 * Returns a SyntaxError
	 * @param {string} info 
	 * @returns {UserError}
	 */
	syntaxError(info = "") {
		return UserError.syntaxError(this.#src, this.#startPos, this.#endPos, info);
	}

	/**
	 * Returns a TypeError
	 * @param {string} info
	 * @returns {UserError}
	 */
	typeError(info = "") {
		return UserError.typeError(this.#src, this.#startPos, this.#endPos, info);
	}

	/**
	 * Returns a ReferenceError
	 * @param {string} info 
	 * @returns {UserError}
	 */
	referenceError(info = "") {
		return UserError.referenceError(this.#src, this.#startPos, this.#endPos, info);
	}

	/**
	 * Calculates the column,line position in 'this.#src'
	 * @returns {[number, number, number, number]} - [startLine, startCol, endLine, endCol]
	 */
	getFilePos() {
		const [startLine, startCol] = this.#src.posToLineAndCol(this.#startPos);

		const [endLine, endCol] = this.#src.posToLineAndCol(this.#endPos);

		return [startLine, startCol, endLine, endCol];
	}

	/**
	 * @returns {string}
	 */
	toString() {
		const [startLine, startCol] = this.#src.posToLineAndCol(this.#startPos);

		return `${this.src.name}:${startLine+1}:${startCol+1}`;
	}
}


/**
 * UserErrors are generated when the user of Helios makes a mistake (eg. a syntax error).
 */
 export class UserError extends Error {
	#src;
	#startPos;
	#endPos;

	/**
	 * @type {Object}
	 */
	#context; // additional context

	/**
	 * @internal
	 * @param {string} msg
	 * @param {Source} src 
	 * @param {number} startPos 
	 * @param {number} endPos
	 */
	constructor(msg, src, startPos, endPos = startPos + 1) {
		super(msg);
		this.#src = src;
		this.#startPos = startPos;
		this.#endPos = endPos;
		this.#context = {};
	}

	/**
	 * @internal
	 * @param {string} type
	 * @param {Source} src 
	 * @param {number} startPos 
	 * @param {number} endPos
	 * @param {string} info 
	 */
	static new(type, src, startPos, endPos, info = "") {
		let [line, col] = src.posToLineAndCol(startPos);

		let msg = `(${src.name}:${line+1}:${col+1}) ${type}`;
		if (info != "") {
			msg += `: ${info}`;
		}

		return new UserError(msg, src, startPos, endPos);
	}

	/**
	 * @internal
	 * @type {Source}
	 */
	get src() {
		return this.#src;
	}

	/**
	 * Filled with CBOR hex representations of Datum, Redeemer and ScriptContext by validation scripts throwing errors during `tx.finalize()`; and Redeemer and ScriptContext by minting scripts throwing errors.
	 * @type {Object}
	 */
	get context() {
		return this.#context;
	}

	/**
	 * Constructs a SyntaxError
	 * @internal
	 * @param {Source} src 
	 * @param {number} startPos 
	 * @param {number} endPos
	 * @param {string} info 
	 * @returns {UserError}
	 */
	static syntaxError(src, startPos, endPos, info = "") {
		const error = UserError.new("SyntaxError", src, startPos, endPos, info);

		src.errors.push(error);

		return error;
	}

	/**
	 * Constructs a TypeError
	 * @internal
	 * @param {Source} src 
	 * @param {number} startPos 
	 * @param {number} endPos
	 * @param {string} info 
	 * @returns {UserError}
	 */
	static typeError(src, startPos, endPos, info = "") {
		const error = UserError.new("TypeError", src, startPos, endPos, info);

		src.errors.push(error);

		return error;
	}

	/**
	 * @param {Error} e 
	 * @returns {boolean}
	 */
	static isTypeError(e) {
		return (e instanceof UserError) && e.message.startsWith("TypeError");
	}

	/**
	 * Constructs a ReferenceError (i.e. name undefined, or name unused)
	 * @internal
	 * @param {Source} src 
	 * @param {number} startPos 
	 * @param {number} endPos
	 * @param {string} info 
	 * @returns {UserError}
	 */
	static referenceError(src, startPos, endPos, info = "") {
		const error = UserError.new("ReferenceError", src, startPos, endPos, info);

		src.errors.push(error);

		return error;
	}

	/**
	 * @param {Error} e 
	 * @returns {boolean}
	 */
	static isReferenceError(e) {
		return (e instanceof UserError) && e.message.startsWith("ReferenceError");
	}

	/**
	 * @internal
	 */
	get data() {
		throw new Error("is error");
	}

	/**
	 * @internal
	 * @type {number}
	 */
	get startPos() {
		return this.#startPos;
	}

	/**
	 * Calculates column/line position in 'this.src'.
	 * @internal
	 * @returns {[number, number, number, number]} - [startLine, startCol, endLine, endCol]
	 */
	getFilePos() {
		const [startLine, startCol] = this.#src.posToLineAndCol(this.#startPos);
		const [endLine, endCol] = this.#src.posToLineAndCol(this.#endPos);

		return [startLine, startCol, endLine, endCol];
	}

	/**
	 * Dumps the error without throwing.
	 * If 'verbose == true' the Source is also pretty printed with line-numbers.
	 * @internal
	 * @param {boolean} verbose 
	 */
	dump(verbose = false) {
		if (verbose) {
			console.error(this.#src.pretty());
		}

		console.error("\n" + this.message);
	}

	/**
	 * Returns the error message (alternative to e.message)
	 * @returns {string}
	 */
	toString() {
		return this.message;
	}

	/**
	 * Catches any UserErrors thrown inside 'fn()`.
	 * Dumps the error
	 * @template T
	 * @param {() => T} fn 
	 * @param {boolean} verbose 
	 * @returns {T | undefined}	
	 */
	static catch(fn, verbose = false) {
		try {
			return fn();
		} catch (error) {
			if (error instanceof UserError) {
				error.dump(verbose);
			} else {
				throw error;
			}
		}
	}
}

/**
 * Used for errors thrown during Uplc evaluation
 */
export class RuntimeError extends Error {
	/**
	 * @type {Object}
	 */
	#context; // additional context

	/**
	 * @internal
	 * @param {string} msg
	 */
	constructor(msg) {
		super(msg);

		this.#context = {};
	}

	get context() {
		return this.#context;
	}
}

/**
 * Token is the base class of all Expressions and Statements
 * @internal
 */
export class Token {
	#site;

	/**
	 * @param {Site} site 
	 */
	constructor(site) {
		this.#site = assertDefined(site); // position in source of start of token
	}

	get site() {
		return this.#site;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		throw new Error("not yet implemented");
	}

	/**
	 * Returns 'true' if 'this' is a literal primitive, a literal struct constructor, or a literal function expression.
	 * @returns {boolean}
	 */
	isLiteral() {
		return false;
	}

	/**
	 * Returns 'true' if 'this' is a Word token.
	 * @param {?(string | string[])} value
	 * @returns {boolean}
	 */
	isWord(value = null) {
		return false;
	}

	/**
	 * @returns {boolean}
	 */
	isKeyword() {
		return false;
	}

	/**
	 * Returns 'true' if 'this' is a Symbol token (eg. '+', '(' etc.)
	 * @param {?(string | string[])} value
	 * @returns {boolean}
	 */
	isSymbol(value = null) {
		return false;
	}

	/**
	 * Returns 'true' if 'this' is a group (eg. '(...)').
	 * @param {?string} value
	 * @param {number | null} nFields
	 * @returns {boolean}
	 */
	isGroup(value, nFields = null) {
		return false;
	}

	/**
	 * Returns a SyntaxError at the current Site.
	 * @param {string} msg 
	 * @returns {UserError}
	 */
	syntaxError(msg) {
		return this.#site.syntaxError(msg);
	}

	/**
	 * Returns a TypeError at the current Site.
	 * @param {string} msg
	 * @returns {UserError}
	 */
	typeError(msg) {
		return this.#site.typeError(msg);
	}

	/**
	 * Returns a ReferenceError at the current Site.
	 * @param {string} msg
	 * @returns {UserError}
	 */
	referenceError(msg) {
		return this.#site.referenceError(msg);
	}

	/**
	 * Throws a SyntaxError if 'this' isn't a Word.
	 * @param {?(string | string[])} value 
	 * @returns {Word | null}
	 */
	assertWord(value = null) {
		if (value !== null) {
			this.syntaxError(`expected \'${value}\', got \'${this.toString()}\'`);
		} else {
			this.syntaxError(`expected word, got ${this.toString()}`);
		}

		return null;
	}

	/**
	 * Throws a SyntaxError if 'this' isn't a Symbol.
	 * @param {?(string | string[])} value 
	 * @returns {SymbolToken | null}
	 */
	assertSymbol(value = null) {
		if (value !== null) {
			this.syntaxError(`expected '${value}', got '${this.toString()}'`);
		} else {
			this.syntaxError(`expected symbol, got '${this.toString()}'`);
		}

		return null;
	}

	/**
	 * Throws a SyntaxError if 'this' isn't a Group.
	 * @param {?string} type 
	 * @param {?number} nFields
	 * @returns {Group | null}
	 */
	assertGroup(type = null, nFields = null) {
		if (type !== null) {
			this.syntaxError(`invalid syntax: expected '${type}...${Group.matchSymbol(type)}'`);
		} else {
			this.syntaxError(`invalid syntax: expected group`);
		}

		return null;
	}
}

/**
 * @internal
 * @param {undefined | null | Token} t
 * @param {Site} site
 * @param {string} msg
 * @returns {null | Token}
 */
export function assertToken(t, site, msg = "expected token") {
	if (!t) {
		site.syntaxError(msg);
		return null;
	} else {
		return t;
	}
}

/**
 * A Word token represents a token that matches /[A-Za-z_][A-Za-z_0-9]/
 * @internal
 */
export class Word extends Token {
	#value;

	/**
	 * @param {Site} site 
	 * @param {string} value 
	 */
	constructor(site, value) {
		super(site);
		this.#value = value;
	}

	/**
	 * @param {string} value 
	 * @returns {Word}
	 */
	static new(value) {
		return new Word(Site.dummy(), value);
	}

	get value() {
		return this.#value;
	}

	/**
	 * @param {?(string | string[])} value 
	 * @returns {boolean}
	 */
	isWord(value = null) {
		if (value !== null) {
			if (value instanceof Array) {
				return value.lastIndexOf(this.#value) != -1;
			} else {
				return value == this.#value;
			}
		} else {
			return true;
		}
	}

	/**
	 * @param {?(string | string[])} value 
	 * @returns {Word}
	 */
	assertWord(value = null) {
		if (!this.isWord(value)) {
			super.assertWord(value);
		}

		return this;
	}

	/**
	 * @returns {Word}
	 */
	assertNotInternal() {
		if (this.#value == "_") {
			throw this.syntaxError("_ is reserved");
		} else if (this.#value.startsWith("__")) {
			throw this.syntaxError("__ prefix is reserved");
		} else if (this.#value.endsWith("__")) {
			throw this.syntaxError("__ suffix is reserved");
		}

		return this;
	}

	/**
	 * @returns {boolean}
	 */
	isKeyword() {
		switch (this.#value) {
			case "const":
			case "func":
			case "struct":
			case "enum":
			case "import":
			case "if":
			case "else":
			case "switch":
			case "self":
				return true;
			default:
				return false;
		}
	}

	/**
	 * @returns {Word | null}
	 */
	assertNotKeyword() {
		this.assertNotInternal();

		if (this.isKeyword()) {
			this.syntaxError(`'${this.#value}' is a reserved word`);
			return null;
		}

		return this;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return this.#value;
	}

	/**
	 * Finds the index of the first Word(value) in a list of tokens
	 * Returns -1 if none found
	 * @param {Token[]} ts 
	 * @param {string | string[]} value 
	 * @returns {number}
	 */
	static find(ts, value) {
		return ts.findIndex(item => item.isWord(value));
	}
}

/**
 * Symbol token represent anything non alphanumeric
 * @internal
 */
export class SymbolToken extends Token {
	#value;

	/**
	 * @param {Site} site
	 * @param {string} value
	 */
	constructor(site, value) {
		super(site);
		this.#value = value;
	}

	get value() {
		return this.#value;
	}

	/**
	 * @param {?(string | string[])} value 
	 * @returns {boolean}
	 */
	isSymbol(value = null) {
		if (value !== null) {
			if (value instanceof Array) {
				return value.lastIndexOf(this.#value) != -1;
			} else {
				return value == this.#value;
			}
		} else {
			return true;
		}
	}

	/**
	 * @param {?(string | string[])} value 
	 * @returns {SymbolToken}
	 */
	assertSymbol(value) {
		if (!this.isSymbol(value)) {
			super.assertSymbol(value);
		}

		return this;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return this.#value;
	}

	/**
	 * Finds the index of the first Symbol(value) in a list of tokens.
	 * Returns -1 if none found.
	 * @param {Token[]} ts
	 * @param {string | string[]} value
	 * @returns {number}
	 */
	static find(ts, value) {
		return ts.findIndex(item => item.isSymbol(value));
	}

	/**
	 * Finds the index of the last Symbol(value) in a list of tokens.
	 * Returns -1 if none found.
	 * @param {Token[]} ts 
	 * @param {string | string[]} value 
	 * @returns {number}
	 */
	static findLast(ts, value) {
		for (let i = ts.length - 1; i >= 0; i--) {
			if (ts[i].isSymbol(value)) {
				return i;
			}
		}

		return -1;
	}
}

/**
 * Group token can '(...)', '[...]' or '{...}' and can contain comma separated fields.
 * @internal
 */
export class Group extends Token {
	#type;
	#fields;
	#firstComma;

	/**
	 * @param {Site} site 
	 * @param {string} type - "(", "[" or "{"
	 * @param {Token[][]} fields 
	 * @param {?SymbolToken} firstComma
	 */
	constructor(site, type, fields, firstComma = null) {
		super(site);
		this.#type = type;
		this.#fields = fields; // list of lists of tokens
		this.#firstComma = firstComma;

		assert(fields.length < 2 || firstComma !== null);
	}

	get fields() {
		return this.#fields.slice(); // copy, so fields_ doesn't get mutated
	}

	/**
	 * @param {?string} type 
	 * @param {number | null} nFields
	 * @returns {boolean}
	 */
	isGroup(type = null, nFields = null) {
		const nFieldsOk = (nFields === null) || (nFields == this.#fields.length);

		if (type !== null) {
			return this.#type == type && nFieldsOk;
		} else {
			return nFieldsOk;
		}
	}

	/**
	 * @param {?string} type 
	 * @param {?number} nFields 
	 * @returns {Group | null}
	 */
	assertGroup(type = null, nFields = null) {
		if (type !== null && this.#type != type) {
			this.syntaxError(`invalid syntax: expected '${type}...${Group.matchSymbol(type)}', got '${this.#type}...${Group.matchSymbol(this.#type)}'`);

			return null;
		} else if (type !== null && nFields !== null && nFields != this.#fields.length) {
			if (this.#fields.length > 1 && nFields <= 1 && this.#firstComma !== null) {
				this.#firstComma.syntaxError(`invalid syntax, unexpected ','`);
			} else {
				this.syntaxError(`invalid syntax: expected '${type}...${Group.matchSymbol(type)}' with ${nFields} field(s), got '${type}...${Group.matchSymbol(type)}' with ${this.#fields.length} fields`);
			}

			return null;
		} else {
			return this;
		}
	}

	/**
	 * @returns {string}
	 */
	toString() {
		let s = this.#type;

		let parts = [];
		for (let f of this.#fields) {
			parts.push(f.map(t => t.toString()).join(" "));
		}

		s += parts.join(", ") + Group.matchSymbol(this.#type);

		return s;
	}

	/**
	 * @param {Token} t 
	 * @returns {boolean}
	 */
	static isOpenSymbol(t) {
		return t.isSymbol("{") || t.isSymbol("[") || t.isSymbol("(");
	}

	/**
	 * @param {Token} t 
	 * @returns {boolean}
	 */
	static isCloseSymbol(t) {
		return t.isSymbol("}") || t.isSymbol("]") || t.isSymbol(")");
	}

	/**
	 * Returns the corresponding closing bracket, parenthesis or brace.
	 * Throws an error if not a group symbol.
	 * @example
	 * Group.matchSymbol("(") == ")"
	 * @param {string | SymbolToken} t
	 * @returns {string}
	 */
	static matchSymbol(t) {
		if (t instanceof SymbolToken) {
			t = t.value;
		}

		if (t == "{") {
			return "}";
		} else if (t == "[") {
			return "]";
		} else if (t == "(") {
			return ")";
		} else if (t == "}") {
			return "{";
		} else if (t == "]") {
			return "[";
		} else if (t == ")") {
			return "(";
		} else {
			throw new Error("not a group symbol");
		}
	}

	/**
	 * Finds the index of first Group(type) in list of tokens
	 * Returns -1 if none found.
	 * @param {Token[]} ts 
	 * @param {string} type 
	 * @returns {number}
	 */
	static find(ts, type) {
		return ts.findIndex(item => item.isGroup(type));
	}
}

/**
 * Base class of literal tokens
 * @internal
 */
export class PrimitiveLiteral extends Token {
	/**
	 * @param {Site} site 
	 */
	constructor(site) {
		super(site);
	}

	/**
	 * @returns {boolean}
	 */
	isLiteral() {
		return true;
	}
}

/**
 * Signed int literal token
 * @internal
 */
export class IntLiteral extends PrimitiveLiteral {
	#value;

	/**
	 * @param {Site} site 
	 * @param {bigint} value 
	 */
	constructor(site, value) {
		super(site);
		this.#value = value;
	}

	get value() {
		return this.#value;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return this.#value.toString();
	}
}

/**
 * Fixed point number literal token
 * @internal
 */
export class RealLiteral extends PrimitiveLiteral {
	#value;

	/**
	 * @param {Site} site 
	 * @param {bigint} value 
	 */
	constructor(site, value) {
		super(site);
		this.#value = value;
	}

	get value() {
		return this.#value;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return this.#value.toString();
	}
}

/**
 * Bool literal token
 * @internal
 */
export class BoolLiteral extends PrimitiveLiteral {
	#value;

	/**
	 * @param {Site} site 
	 * @param {boolean} value 
	 */
	constructor(site, value) {
		super(site);
		this.#value = value;
	}

	get value() {
		return this.#value;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return this.#value ? "true" : "false";
	}
}

/**
 * ByteArray literal token
 * @internal
 */
export class ByteArrayLiteral extends PrimitiveLiteral {
	#bytes;

	/**
	 * @param {Site} site 
	 * @param {number[]} bytes 
	 */
	constructor(site, bytes) {
		super(site);
		this.#bytes = bytes;
	}

	get bytes() {
		return this.#bytes;
	}

	toString() {
		return `#${bytesToHex(this.#bytes)}`;
	}
}

/**
 * String literal token (utf8)
 * @internal
 */
export class StringLiteral extends PrimitiveLiteral {
	#value;

	/**
	 * @param {Site} site 
	 * @param {string} value 
	 */
	constructor(site, value) {
		super(site);
		this.#value = value;
	}

	get value() {
		return this.#value;
	}

	toString() {
		return `"${this.#value.toString()}"`;
	}
}

/**
 * @internal
 * @typedef {[number, Site][]} CodeMap
 */

/**
 * @internal
 * @typedef {Map<string, IR>} IRDefinitions
 */

/**
 * The IR class combines a string of intermediate representation sourcecode with an optional site.
 * The site is used for mapping IR code to the original source code.
 * @internal
 */
 export class IR {
	#content;
	#site;

	/**
	 * @param {string | IR[]} content 
	 * @param {null | Site} site 
	 */
	constructor(content, site = null) {
		assert(!(Array.isArray(content) && content.some(item => item == undefined)), "some items undefined");
		this.#content = content;
		this.#site = site;
	}

    /**
     * @internal
     * @type {string | IR[]}
     */
	get content() {
		return this.#content;
	}

    /**
     * @internal
     * @type {?Site}
     */
	get site() {
		return this.#site;
	}

	/**
	 * Can be used as a template literal tag function
	 * @param {string | TemplateStringsArray | IR[]} content 
	 * @param  {...(Site | string | IR | IR[] | null | number)} args 
	 * @returns {IR}
	 */
	static new(content, ...args) {
		if (typeof content == "string") {
			if (args.length == 0) {
				return new IR(content);
			} else if (args.length == 1 && args[0] instanceof Site) {
				const site = args[0];

				if (site instanceof Site) {
					return new IR(content, site);
				} else {
					throw new Error("unexpected second argument");
				}
			} else {
				throw new Error("unexpected second argument");
			}
		} else if ("raw" in content) {
			const raw = content.raw.slice();

			/**
			 * @type {IR[]}
			 */
			let items = [];
			
			/**
			 * @type {Site | null}
			 */
			let lastSite = null;
			
			if (raw.length > 0 && raw[raw.length - 1] == "" && args.length > 0 && args[args.length -1 ] instanceof Site) {
				raw.pop();
				lastSite = assertClass(args.pop(), Site);
			}

			let s = "";

			for (let c of raw) {
				s += c;

				const a = args.shift();

				if (a instanceof Site) {
					items.push(new IR(s, a));
					s = "";
				} else if (a instanceof IR) {
					if (s != "") {
						items.push(new IR(s));
						s = "";
					}

					items.push(a);
				} else if (Array.isArray(a)) {
					if (s != "") {
						items.push(new IR(s));
						s = "";
					}

					a.forEach(ir => items.push(ir));
				} else if (typeof a == "string" || typeof a == "number") {
					s += a.toString();
				} else if (a === undefined || a === null) {
					if (s != "") {
						items.push(new IR(s));
						s = "";
					}
				} else {
					throw new Error("unexpected second argument");
				}
			}

			assert(args.length == 0);

			if (s != "") {
				items.push(new IR(s));
			}

			return new IR(items, lastSite);
		} else if (Array.isArray(content)) {
			/**
			 * @type {IR[]}
			 */
			let items = [];

			for (let c of content) {
				items.push(c);
			}

			if (args.length == 0) {
				return new IR(items);
			} else if (args.length == 1) {
				const arg = args[0];
				if (arg instanceof Site) {
					return new IR(items, arg);
				} else {
					throw new Error("unexpected second argument");
				}
			} else {
				throw new Error("unexpected second argument");
			}
		} else {
			throw new Error("unexpected first argument");
		}
	}

	/**
	 * @returns {any}
	 */
	dump() {
		if (typeof this.#content == "string") {
			return this.#content;
		} else {
			return this.#content.map(c => c.dump());
		}
	}

	/**
	 * Returns a list containing IR instances that themselves only contain strings
     * @internal
	 * @returns {IR[]}
	 */
	flatten() {
		if (typeof this.#content == "string") {
			return [this];
		} else {
			/**
			 * @type {IR[]}
			 */
			let result = [];

			for (let item of this.#content) {
				result = result.concat(item.flatten());
			}

			return result;
		}
	}

	/**
	 * Intersperse nested IR content with a separator
     * @internal
	 * @param {string} sep
	 * @returns {IR}
	 */
	join(sep) {
		if (typeof this.#content == "string") {
			return this;
		} else {
			/** @type {IR[]} */
			const result = [];

			for (let i = 0; i < this.#content.length; i++) {
				result.push(this.#content[i]);

				if (i < this.#content.length - 1) {
					result.push(new IR(sep))
				}
			}

			return new IR(result);
		}
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return this.flatten().map(p => typeof p.content == "string" ? p.content : "").join("");
	}

    /**
     * @internal
	 * @returns {[string, CodeMap]}
	 */
	generateSource() {
		const parts = this.flatten();

		/** @type {string[]} */
		const partSrcs = [];

		/** @type {CodeMap} */
		const codeMap = [];

		let pos = 0;
		for (let part of parts) {
			const rawPartSrc = part.content;

			if (typeof rawPartSrc == "string") {
				const origSite = part.site;
				if (origSite !== null) {
					/** @type {[number, Site]} */
					const pair = [pos, origSite];

					codeMap.push(pair);
				}

				const partSrc = replaceTabs(rawPartSrc);

				pos += partSrc.length;
				partSrcs.push(partSrc);
			} else {
				throw new Error("expected IR to contain only strings after flatten");
			}
		}

		return [partSrcs.join(""), codeMap];
	}

	/**
	 * @returns {string}
	 */
	pretty() {
		const [src, _] = this.generateSource();

		return (new Source(src, "")).pretty();
	}

	/**
	 * @param {string} str 
	 * @returns {boolean}
	 */
	includes(str) {
		if (typeof this.#content == "string") {
			return this.#content.includes(str);
		} else {
			return this.#content.some(ir => ir.includes(str));
		}
	}

	/**
	 * @param {RegExp} re 
	 * @param {string} newStr
	 * @returns {IR}
	 */
	replace(re, newStr) {
		if (typeof this.#content == "string") {
			return new IR(this.#content.replace(re, newStr), this.#site);
		} else {
			return new IR(this.#content.map(ir => ir.replace(re, newStr), this.#site));
		}
	}

	/**
	 * 
	 * @param {RegExp} re 
	 * @param {(match: string) => void} callback 
	 */
	search(re, callback) {
		if (typeof this.#content == "string") {
			const ms = this.#content.match(re);

			if (ms) {
				for (let m of ms) {
					callback(m);
				}
			}
		} else {
			this.#content.forEach(ir => ir.search(re, callback));
		}
	}

	/**
	 * Wraps 'inner' IR source with some definitions (used for top-level statements and for builtins)
     * @internal
	 * @param {IR} inner 
	 * @param {IRDefinitions} definitions - name -> definition
	 * @returns {IR}
	 */
	static wrapWithDefinitions(inner, definitions) {
		const keys = Array.from(definitions.keys()).reverse();

		let res = inner;
		for (let key of keys) {
			const definition = definitions.get(key);

			if (definition === undefined) {
				throw new Error("unexpected");
			} else {

				res = new IR([new IR("("), new IR(key), new IR(") -> {\n"),
					res, new IR(`\n}(\n${TAB}/*${key}*/\n${TAB}`), definition,
				new IR("\n)")]);
			}
		}

		return res;
	}
}

/**
 * @internal
 */
export const RE_IR_PARAMETRIC_NAME = /[a-zA-Z_][a-zA-Z_0-9]*[[][a-zA-Z_0-9@[\]]*/g;


/**
 * Type type parameter prefix
 * @internal
 */
export const TTPP = "__T";

/**
 * Func type parameter prefix
 * @internal
 */
export const FTPP = "__F";

const RE_TEMPLATE_NAME = new RegExp(`\\b(${TTPP}|${FTPP})[0-9]*\\b`);

/**
 * @internal
 */
export class IRParametricName {
	/**
	 * Base type name
	 * @type {string}
	 */
	#base;

	/**
	 * Type type parameters
	 * Note: nested type names can stay strings
	 * Note: can be empty
	 * @type {string[]}
	 */
	#ttp;

	/**
	 * Function name
	 * @type {string}
	 */
	#fn;

	/**
	 * Function type parameters
	 * Note: can be empty
	 * @type {string[]}
	 */
	#ftp;

	/**
	 * @param {string} base 
	 * @param {string[]} ttp 
	 * @param {string} fn 
	 * @param {string[]} ftp 
	 */
	constructor(base, ttp, fn = "", ftp = []) {
		this.#base = base;
		this.#ttp = ttp;
		this.#fn = fn;
		this.#ftp = ftp;
	}

	/**
	 * @param {string} base 
	 * @param {number} nTtps 
	 * @param {string} fn 
	 * @param {number} nFtps 
	 * @returns 
	 */
	static newTemplate(base, nTtps, fn = "", nFtps = 0) {
		return new IRParametricName(
			base, 
			(new Array(nTtps)).map((_, i) => `${TTPP}${i}`),
			fn,
			(new Array(nFtps)).map((_, i) => `${FTPP}${i}`)
		);
	}

	/**
	 * @type {string[]}
	 */
	get ttp() {
		return this.#ttp;
	}

	/**
	 * @type {string[]}
	 */
	get ftp() {
		return this.#ftp;
	}

	/**
	 * @type {string}
	 */
	get base() {
		return this.#base;
	}

	/**
	 * @type {string}
	 */
	get fn() {
		return this.#fn;
	}

	/**
	 * @param {string[]} ttp 
	 * @param {string[]} ftp 
	 * @returns {IRParametricName}
	 */
	toImplementation(ttp, ftp = []) {
		assert(ttp.length == this.#ttp.length, `expected ${this.#ttp.length} type parameters, got ${ttp.length} (in ${this.toString()})`);
		assert(ftp.length == this.#ftp.length, `expected ${this.#ftp.length} function type parameters, got ${ftp.length} (in ${this.toString()})`);

		return new IRParametricName(this.#base, ttp, this.#fn, ftp);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `${this.#base}${this.#ttp.length > 0 ? `[${this.#ttp.join("@")}]` : ""}${this.#fn}${this.#ftp.length > 0 ? `[${this.#ftp.join("@")}]` : ""}`;
	}

	/**
	 * @param {boolean} emptyParameters
	 * @return {string}
	 */
	toTemplate(emptyParameters = false) {
		if (emptyParameters) {
			return `${this.#base}${this.#ttp.length > 0 ? "[]" : ""}${this.#fn}${this.#ftp.length > 0 ? "[]" : ""}`;
		} else {
			return `${this.#base}${this.#ttp.length > 0 ? `[${this.#ttp.map((_, i) => `${TTPP}${i}`).join("@")}]` : ""}${this.#fn}${this.#ftp.length > 0 ? `[${this.#ftp.map((_, i) => `${FTPP}${i}`).join("@")}]` : ""}`;
		}
	}

	/**
	 * @param {IR} ir
	 * @returns {IR}
	 */
	replaceTemplateNames(ir) {
		this.#ttp.forEach((name, i) => {
			ir = ir.replace(new RegExp(`\\b${TTPP}${i}`, "gm"), name);
		})

		this.#ftp.forEach((name, i) => {
			ir = ir.replace(new RegExp(`\\b${FTPP}${i}`, "gm"), name);
		})

		return ir;
	}

	/**
	 * @example
 	 * IRParametricName.matches("__helios__map[__T0@__T1]__fold[__F2@__F3]") == true
	 * @example
	 * IRParametricName.matches("__helios__int") == false
	 * @example
	 * IRParametricName.matches("__helios__option[__T0]__none__new") == true
	 * @param {string} str 
	 * @returns {boolean}
	 */
	static matches(str) {
		return str.match(RE_IR_PARAMETRIC_NAME) ? true : false;
	}

	/**
	 * @param {string} name 
	 * @returns {boolean}
	 */
	static isTemplate(name) {
		return name.match(RE_TEMPLATE_NAME) ? true : false;
	}

	/**
	 * @example
	 * IRParametricName.parse("__helios__map[__T0@__T1]__fold[__F0@__F1]").toString() == "__helios__map[__T0@__T1]__fold[__F0@__F1]"
	 * @example
	 * IRParametricName.parse("__helios__map[__helios__bytearray@__helios__map[__helios__bytearray@__helios__int]]__fold[__F0@__F1]").toString() == "__helios__map[__helios__bytearray@__helios__map[__helios__bytearray@__helios__int]]__fold[__F0@__F1]"
	 * @example
	 * IRParametricName.parse("__helios__map[__helios__bytearray@__helios__map[__helios__bytearray@__helios__list[__T0]]]__fold[__F0@__F1]").toString() == "__helios__map[__helios__bytearray@__helios__map[__helios__bytearray@__helios__list[__T0]]]__fold[__F0@__F1]"
	 * @param {string} str 
	 * @param {boolean} preferType
	 * @returns {IRParametricName}
	 */
	static parse(str, preferType = false) {
		let pos = 0;

		/**
		 * @returns {string}
		 */
		const eatAlphaNum = () => {
			let c = str.charAt(pos);

			const chars = [];

			while ((c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || c == "_" || (c >= "0" && c <= "9")) {
				chars.push(c);

				pos++;

				c = str.charAt(pos);
			}
			
			return chars.join("");
		}

		/**
		 * @returns {string[]}
		 */
		const eatParams = () => {
			if (pos >= str.length) {
				return [];
			}
			
			let c = str.charAt(pos);

			assert(c == "[", `expected [, got ${c} (in ${str})`);

			const groups = [];
			let chars = [];

			let depth = 1;

			while (depth > 0) {
				pos++;

				c = str.charAt(pos);

				if (c == "[") {
					chars.push(c);
					depth++;
				} else if (c == "]") {
					if (depth > 1) {
						chars.push(c);
					} else {
						if (chars.length > 0) {
							groups.push(chars);	
						}
						chars = [];
					}
					depth--;
				} else if (c == "@") {
					if (depth > 1) {
						chars.push(c);
					} else {
						assert(chars.length > 0, "zero chars in group before @");
						groups.push(chars);
						chars = [];
					}
				} else if ((c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || c == "_" || (c >= "0" && c <= "9")) {
					chars.push(c);
				} else {
					throw new Error(`unexpected char '${c}' in parametric name '${str}'`);
				}
			}

			// final closing bracket
			pos++;

			return groups.map(g => g.join(""));
		}

		/**
		 * 
		 * @param {string} base 
		 * @returns {[string, string]}
		 */
		const uneatFn = (base) => {
			let pos = base.length - 1;

			let c = base.charAt(pos);

			assert(c != "_", "unexpected underscore");

			let underscores = 0;

			while (pos > 0) {
				pos--;
				c = base.charAt(pos);

				if (underscores >= 2) {
					if (c != "_") {
						return [base.slice(0, pos+1), base.slice(pos+1)];
					} else {
						underscores++;
					}
				} else {
					if (c == "_") {
						underscores++;
					} else {
						underscores = 0;
					}
				}
			}

			throw new Error("bad name format");
		}

		let base = eatAlphaNum();

		let ttp = eatParams();
		let fn = "";
		let ftp = [];

		if (pos >= str.length) {
			if (!preferType) {
				[base, fn] = uneatFn(base);
				ftp = ttp;
				ttp = [];
			}
		} else {
			fn = eatAlphaNum();

			if (pos < str.length) {
				ftp = eatParams();
			}
		}

		return new IRParametricName(base, ttp, fn, ftp);
	}
}


////////////////////////////////////
// Section 4: Cryptography functions
////////////////////////////////////
/**
 * Size of default Blake2b digest
 * @internal
 */
var BLAKE2B_DIGEST_SIZE = 32; // bytes

/**
 * Changes the value of BLAKE2B_DIGEST_SIZE 
 *  (because the nodejs crypto module only supports 
 *   blake2b-512 and not blake2b-256, and we want to avoid non-standard dependencies in the 
 *   test-suite)
 * @internal
 * @param {number} s - 32 or 64
 */
export function setBlake2bDigestSize(s) {
    BLAKE2B_DIGEST_SIZE = s;
}

/**
 * Make sure resulting number fits in uint32
 * @internal
 * @param {number} x
 */
function imod32(x) {
    return x >>> 0;
}

/**
 * 32 bit number rotation
 * @internal
 * @param {number} x - originally uint32
 * @param {number} n
 * @returns {number} - originally uint32
 */
function irotr(x, n) {
    return imod32((x >>> n) | (x << (32 - n)));
}

/**
 * Positive modulo operator
 * @internal
 * @param {bigint} x 
 * @param {bigint} n 
 * @returns {bigint}
 */
function posMod(x, n) {
    const res = x % n;

    if (res < 0n) {
        return res + n;
    } else {
        return res;
    }
}

/**
 * @internal
 * @param {NumberGenerator} random 
 * @param {number} n 
 * @returns {number[]}
 */
export function randomBytes(random, n) {
    const key = [];

    for (let i = 0; i < n; i++) {
        key.push(Math.floor(random() * 256) % 256);
    }

    return key;
}

/**
 * TODO: switch to using UInt64Fast instead of UInt64 everywhere
 * First entry: high
 * Second entry: low
 * @typedef {[number, number]} UInt64Fast
 */

const UINT64_ZERO = [0, 0];

/**
 * 
 * @param {number[]} bytes 
 * @param {boolean} littleEndian 
 * @returns {UInt64Fast}
 */
function uint64FromBytes(bytes, littleEndian = true) {
    /** @type {number} */
    let low;

    /** @type {number} */
    let high;

    if (littleEndian) {
        low = (bytes[0] << 0) | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24);
        high = (bytes[4] << 0) | (bytes[5] << 8) | (bytes[6] << 16) | (bytes[7] << 24);
    } else {
        high = (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | (bytes[3] << 0);
        low = (bytes[4] << 24) | (bytes[5] << 16) | (bytes[6] << 8) | (bytes[7] << 0);
    }

    return [imod32(high), imod32(low)];
}

/**
 * @param {string} str
 * @returns {UInt64Fast}
 */
function uint64FromString(str) {
    const high = parseInt(str.slice(0, 8), 16);
    const low = parseInt(str.slice(8, 16), 16);

    return [imod32(high), imod32(low)];
}

/**
 * Returns [low[0], low[1], low[2], low[3], high[0], high[1], high[2], high[3]] if littleEndian==true
 * @internal
 * @param {UInt64Fast} uint64
 * @param {boolean} littleEndian
 * @returns {number[]}
 */
function uint64ToBytes([high, low], littleEndian = true) {
    const res = [
        (0x000000ff & low),
        (0x0000ff00 & low) >>> 8,
        (0x00ff0000 & low) >>> 16,
        (0xff000000 & low) >>> 24,
        (0x000000ff & high),
        (0x0000ff00 & high) >>> 8,
        (0x00ff0000 & high) >>> 16,
        (0xff000000 & high) >>> 24,
    ];

    if (!littleEndian) {
        res.reverse();
    }

    return res;
}

/**
 * @internal
 * @param {UInt64Fast} a
 * @param {UInt64Fast} b
 * @returns {boolean}
 */
function uint64Eq([ha, la], [hb, lb]) {
    return (ha == hb) && (la == lb);
}

/**
 * @internal
 * @param {UInt64Fast} uint64
 * @returns {UInt64Fast} 
 */
function uint64Not([high, low]) {
   return [imod32(~high), imod32(~low)];
}

/**
 * @internal
 * @param {UInt64Fast} a
 * @param {UInt64Fast} b
 * @returns {UInt64Fast}
 */
function uint64And([ha, la], [hb, lb]) {
    return [imod32(ha & hb), imod32(la & lb)];
}

/**
 * @internal
 * @param {UInt64Fast} a
 * @param {UInt64Fast} b
 * @returns {UInt64Fast}
 */
function uint64Xor([ha, la], [hb, lb]) {
    return [imod32(ha ^ hb), imod32(la ^ lb)];
}

/**
 * @internal
 * @param {UInt64Fast} a 
 * @param {UInt64Fast} b
 * @returns {UInt64Fast}
 */
function uint64Add([ha, la], [hb, lb]) {
    const low = la + lb;

    let high = ha + hb;

    if (low >= 0x100000000) {
        high += 1;
    }

    return [imod32(high), imod32(low)];
}

/**
 * @internal
 * @param {UInt64Fast} uint64
 * @param {number} n 
 * @returns {UInt64Fast}
 */
function uint64Rotr([high, low], n) {
    if (n == 32) {
        return [low, high];
    } else if (n > 32) {
        n -= 32;
        [high, low] = [low, high];
    }

    return [
        imod32((high >>> n) | (low << (32 - n))),
        imod32((low >>> n) | (high << (32 - n)))
    ];
}

/**
 * @internal
 * @param {UInt64Fast} uint64
 * @param {number} n
 * @returns {UInt64Fast}
 */
function uint64Shiftr([high, low], n) {
    if (n >= 32) {
        return [
            0, 
            imod32(high >>> n - 32)
        ];
    } else {
        return [
            imod32(high >>> n), 
            imod32((low >>> n) | (high << (32 - n)))
        ];
    }
}

/**
 * UInt64 number (represented by 2 UInt32 numbers)
 * @internal
 */
class UInt64 {
    high;
    low;

    /**
     * @param {number} high  - uint32 number
     * @param {number} low - uint32 number
     */
    constructor(high, low) {
        this.high = high;
        this.low = low;
    }

    /**
     * @internal
     * @returns {UInt64}
     */
    static zero() {
        return new UInt64(0, 0);
    }

    /**
     * @internal
     * @param {number[]} bytes - 8 uint8 numbers
     * @param {boolean} littleEndian
     * @returns {UInt64}
     */
    static fromBytes(bytes, littleEndian = true) {
        /** @type {number} */
        let low;

        /** @type {number} */
        let high;

        if (littleEndian) {
            low = (bytes[0] << 0) | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24);
            high = (bytes[4] << 0) | (bytes[5] << 8) | (bytes[6] << 16) | (bytes[7] << 24);
        } else {
            high = (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | (bytes[3] << 0);
            low = (bytes[4] << 24) | (bytes[5] << 16) | (bytes[6] << 8) | (bytes[7] << 0);
        }

        return new UInt64(imod32(high), imod32(low));
    }

    /**
     * @internal
     * @param {string} str 
     * @returns {UInt64}
     */
    static fromString(str) {
        const high = parseInt(str.slice(0, 8), 16);
        const low = parseInt(str.slice(8, 16), 16);

        return new UInt64(imod32(high), imod32(low));
    }

    /**
     * Returns [low[0], low[1], low[2], low[3], high[0], high[1], high[2], high[3]] if littleEndian==true
     * @internal
     * @param {boolean} littleEndian
     * @returns {number[]}
     */
    toBytes(littleEndian = true) {
        const res = [
            (0x000000ff & this.low),
            (0x0000ff00 & this.low) >>> 8,
            (0x00ff0000 & this.low) >>> 16,
            (0xff000000 & this.low) >>> 24,
            (0x000000ff & this.high),
            (0x0000ff00 & this.high) >>> 8,
            (0x00ff0000 & this.high) >>> 16,
            (0xff000000 & this.high) >>> 24,
        ];

        if (!littleEndian) {
            res.reverse();
        }

        return res;
    }

    /**
     * @internal
     * @param {UInt64} other 
     * @returns {boolean}
     */
    eq(other) {
        return (this.high == other.high) && (this.low == other.low);
    }

    /**
     * @internal
     * @returns {UInt64} 
     */
    not() {
        return new UInt64(~this.high, ~this.low);
    }

    /**
     * @internal
     * @param {UInt64} other
     * @returns {UInt64}
     */
    and(other) {
        return new UInt64(this.high & other.high, this.low & other.low);
    }

    /**
     * @internal
     * @param {UInt64} other 
     * @returns {UInt64}
     */
    xor(other) {
        return new UInt64(
            imod32(this.high ^ other.high),
            imod32(this.low ^ other.low)
        );
    }

    /**
     * @internal
     * @param {UInt64} other 
     * @returns {UInt64}
     */
    add(other) {
        const low = this.low + other.low;

        let high = this.high + other.high;

        if (low >= 0x100000000) {
            high += 1;
        }

        return new UInt64(imod32(high), imod32(low));
    }

    /**
     * @internal
     * @param {number} n 
     * @returns {UInt64}
     */
    rotr(n) {
        let h = this.high;
        let l = this.low;

        if (n == 32) {
            return new UInt64(l, h);
        } else if (n > 32) {
            n  -= 32;
            [h, l] = [l, h];   
        }

        return new UInt64(
            imod32((h >>> n) | (l << (32 - n))),
            imod32((l >>> n) | (h << (32 - n)))
        );
    }

    /**
     * @internal
     * @param {number} n
     * @returns {UInt64}
     */
    shiftr(n) {
        if (n >= 32) {
            return new UInt64(
                0, 
                this.high >>> n - 32
            );
        } else {
            return new UInt64(
                this.high >>> n, 
                imod32((this.low >>> n) | this.high << (32 - n))
            );
        }
    }
}

/**
 * @internal
 * @param {number[]} bytes 
 * @returns {number[]} - list of numbers between 0 and 32
 */
function encodeBase32Bytes(bytes) {
    const result = [];

    const reader = new BitReader(bytes, false);

    while (!reader.eof()) {
        result.push(reader.readBits(5));
    }

    return result;
}

/**
 * Expand human readable prefix of the bech32 encoding so it can be used in the checkSum.
 * @internal
 * @param {string} hrp
 * @returns {number[]}
 */
function expandBech32HumanReadablePart(hrp) {
    const bytes = [];
    for (let c of hrp) {
        bytes.push(c.charCodeAt(0) >> 5);
    }

    bytes.push(0);

    for (let c of hrp) {
        bytes.push(c.charCodeAt(0) & 31);
    }

    return bytes;
}

/**
 * Generate the bech32 checksum.
 * @internal
 * @param {string} hrp 
 * @param {number[]} data - numbers between 0 and 32
 * @returns {number[]} - 6 numbers between 0 and 32
 */
function calcBech32Checksum(hrp, data) {
    const bytes = expandBech32HumanReadablePart(hrp).concat(data);

    const chk = calcBech32Polymod(bytes.concat([0, 0, 0, 0, 0, 0])) ^ 1;

    const chkSum = [];
    for (let i = 0; i < 6; i++) {
        chkSum.push((chk >> 5 * (5 - i)) & 31);
    }

    return chkSum;
}


/**
 * Used as part of the bech32 checksum.
 * @internal
 * @param {number[]} bytes 
 * @returns {number}
 */
function calcBech32Polymod(bytes) {
    const GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];

    let chk = 1;
    for (let b of bytes) {
        const c = (chk >> 25);
        chk = (chk & 0x1fffffff) << 5 ^ b;

        for (let i = 0; i < 5; i++) {
            if (((c >> i) & 1) != 0) {
                chk ^= GEN[i];
            }
        }
    }

    return chk;
}

/**
 * Don't use this directly, use Crypto.hmacSha2_256 or Crypto.hmacSha2_512 instead
 * @internal
 * @param {(x: number[]) => number[]} algorithm 
 * @param {number} b - blockSize of algorithm
 * @param {number[]} key 
 * @param {number[]} message 
 * @returns {number[]}
 */
function hmac(algorithm, b, key, message) {
    if (key.length > b) {
        key = algorithm(key);
    } else {
        key = key.slice();
    }

    while (key.length < b) {
        key.push(0x00);
    }

    const iPadded = key.map(k => (k ^ 0x36));
    const oPadded = key.map(k => (k ^ 0x5c));

    return algorithm(oPadded.concat(algorithm(iPadded.concat(message))));
}

/**
 * Rfc 4648 base32 alphabet
 * @type {string}
 */
const DEFAULT_BASE32_ALPHABET = "abcdefghijklmnopqrstuvwxyz234567";

/**
 * Bech32 base32 alphabet
 * @type {string}
 */
const BECH32_BASE32_ALPHABET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
/**
 * The Helios `Crypto` namespace contains a collection of cryptography primitives.
 * 
 * These functions have been implemented as part of the Helios library in order to avoid external dependencies
 * (there still isn't a standardized Javascript [Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto) that provides all the needed functionality).
 * @namespace
 */
export const Crypto = {
    /**
     * A simple pseudo-random number generator for use in tests that requires some randomness but need to be deterministic
     * (i.e. each test run gives the same result).
     * @param {number} seed
     * @returns {NumberGenerator} The returned function returns a new random number between 0 and 1 upon each call.
     */
    mulberry32: (seed) => {
        /**
         * @type {NumberGenerator}
         */
        return function () {
            let t = seed += 0x6D2B79F5;
            t = Math.imul(t ^ t >>> 15, t | 1);
            t ^= t + Math.imul(t ^ t >>> 7, t | 61);
            return ((t ^ t >>> 14) >>> 0) / 4294967296;
        }
    },

    /**
     * Alias for `mulberry32`.
     * @param {number} seed
     * @returns {NumberGenerator} The returned function returns a new random number between 0 and 1 upon each call.
     */
    rand: (seed) => {
        return Crypto.mulberry32(seed);
    },

    /**
     * Encodes bytes in using Base32.
     * @example
     * Crypto.encodeBase32(textToBytes("f")) == "my"
     * @example
     * Crypto.encodeBase32(textToBytes("fo")) == "mzxq"
     * @example
     * Crypto.encodeBase32(textToBytes("foo")) == "mzxw6"
     * @example
     * Crypto.encodeBase32(textToBytes("foob")) == "mzxw6yq"
     * @example
     * Crypto.encodeBase32(textToBytes("fooba")) == "mzxw6ytb"
     * @example
     * Crypto.encodeBase32(textToBytes("foobar")) == "mzxw6ytboi"
     * @param {number[]} bytes list of uint8 numbers
     * @param {string} alphabet list of chars, defaults to "abcdefghijklmnopqrstuvwxyz234567"
     * @return {string}
     */
    encodeBase32: (bytes, alphabet = DEFAULT_BASE32_ALPHABET) => {
        return encodeBase32Bytes(bytes).map(c => alphabet[c]).join("");
    },

    /**
     * Decodes a Base32 string into bytes.
     * @example
     * bytesToText(Crypto.decodeBase32("my")) == "f"
     * @example
     * bytesToText(Crypto.decodeBase32("mzxq")) == "fo"
     * @example
     * bytesToText(Crypto.decodeBase32("mzxw6")) == "foo"
     * @example
     * bytesToText(Crypto.decodeBase32("mzxw6yq")) == "foob"
     * @example
     * bytesToText(Crypto.decodeBase32("mzxw6ytb")) == "fooba"
     * @example
     * bytesToText(Crypto.decodeBase32("mzxw6ytboi")) == "foobar"
     * @param {string} encoded 
     * @param {string} alphabet list of chars, defaults to "abcdefghijklmnopqrstuvwxyz234567"
     * @return {number[]}
     */
    decodeBase32: (encoded, alphabet = DEFAULT_BASE32_ALPHABET) => {
        const writer = new BitWriter();

        const n = encoded.length;

        for (let i = 0; i < n; i++) {
            const c = encoded[i];
            const code = alphabet.indexOf(c.toLowerCase());

            if (i == n - 1) {
                // last, make sure we align to byte

                const nCut = n * 5 - 8 * Math.floor(n * 5 / 8);

                const bits = padZeroes(code.toString(2), 5)

                writer.write(bits.slice(0, 5 - nCut));
            } else {
                const bits = padZeroes(code.toString(2), 5);

                writer.write(bits);
            }
        }

        const result = writer.finalize(false);

        return result;
    },

    /**
     * Creates a Bech32 checksummed string (eg. used to represent Cardano addresses).
     * @example
     * Crypto.encodeBech32("foo", textToBytes("foobar")) == "foo1vehk7cnpwgry9h96"
     * @example
     * Crypto.encodeBech32("addr_test", hexToBytes("70a9508f015cfbcffc3d88ac4c1c934b5b82d2bb281d464672f6c49539")) == "addr_test1wz54prcptnaullpa3zkyc8ynfddc954m9qw5v3nj7mzf2wggs2uld"
     * @param {string} hrp  human-readable part (eg. "addr")
     * @param {number[]} data a list of uint8 bytes
     * @returns {string}
     */
    encodeBech32: (hrp, data) => {
        assert(hrp.length > 0, "human-readable-part must have non-zero length");

        data = encodeBase32Bytes(data);

        const chkSum = calcBech32Checksum(hrp, data);

        return hrp + "1" + data.concat(chkSum).map(i => BECH32_BASE32_ALPHABET[i]).join("");
    },

    /**
     * Decomposes a Bech32 checksummed string (eg. a Cardano address), and returns the human readable part and the original bytes
     * Throws an error if checksum is invalid.
     * @example
     * bytesToHex(Crypto.decodeBech32("addr_test1wz54prcptnaullpa3zkyc8ynfddc954m9qw5v3nj7mzf2wggs2uld")[1]) == "70a9508f015cfbcffc3d88ac4c1c934b5b82d2bb281d464672f6c49539"
     * @param {string} addr 
     * @returns {[string, number[]]} First part is the human-readable part, second part is a list containing the underlying bytes.
     */
    decodeBech32: (addr) => {
        assert(Crypto.verifyBech32(addr), "invalid bech32 addr");

        const i = addr.indexOf("1");

        assert(i != -1);

        const hrp = addr.slice(0, i);

        addr = addr.slice(i + 1);

        const data = Crypto.decodeBase32(addr.slice(0, addr.length - 6), BECH32_BASE32_ALPHABET);

        return [hrp, data];
    },

    /**
     * Verifies a Bech32 checksum.
     * @example
     * Crypto.verifyBech32("foo1vehk7cnpwgry9h96") == true
     * @example
     * Crypto.verifyBech32("foo1vehk7cnpwgry9h97") == false
     * @example
     * Crypto.verifyBech32("a12uel5l") == true
     * @example
     * Crypto.verifyBech32("mm1crxm3i") == false
     * @example
     * Crypto.verifyBech32("A1G7SGD8") == false
     * @example
     * Crypto.verifyBech32("abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw") == true
     * @example
     * Crypto.verifyBech32("?1ezyfcl") == true
     * @example
     * Crypto.verifyBech32("addr_test1wz54prcptnaullpa3zkyc8ynfddc954m9qw5v3nj7mzf2wggs2uld") == true
     * @param {string} encoded
     * @returns {boolean}
     */
    verifyBech32: (encoded) => {
        const data = [];

        const i = encoded.indexOf("1");

        if (i == -1 || i == 0) {
            return false;
        }

        const hrp = encoded.slice(0, i);

        encoded = encoded.slice(i + 1);

        for (let c of encoded) {
            const j = BECH32_BASE32_ALPHABET.indexOf(c);
            if (j == -1) {
                return false;
            }

            data.push(j);
        }

        const chkSumA = data.slice(data.length - 6);

        const chkSumB = calcBech32Checksum(hrp, data.slice(0, data.length - 6));

        for (let j = 0; j < 6; j++) {
            if (chkSumA[j] != chkSumB[j]) {
                return false;
            }
        }

        return true;
    },

    /**
     * Calculates sha2-256 (32bytes) hash of a list of bytes.
     * Result is also a list of bytes.
     * @example 
     * bytesToHex(Crypto.sha2_256([0x61, 0x62, 0x63])) == "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
     * @example
     * Crypto.sha2_256(textToBytes("Hello, World!")) == [223, 253, 96, 33, 187, 43, 213, 176, 175, 103, 98, 144, 128, 158, 195, 165, 49, 145, 221, 129, 199, 247, 10, 75, 40, 104, 138, 54, 33, 130, 152, 111]
     * @param {number[]} bytes List of uint8 numbers
     * @returns {number[]} List of uint8 numbers.
     */
    sha2_256: (bytes) => {
        /**
         * Pad a bytearray so its size is a multiple of 64 (512 bits).
         * Internal method.
         * @param {number[]} src - list of uint8 numbers
         * @returns {number[]}
         */
        function pad(src) {
            const nBits = src.length * 8;

            let dst = src.slice();

            dst.push(0x80);

            if ((dst.length + 8) % 64 != 0) {
                let nZeroes = (64 - dst.length % 64) - 8;
                if (nZeroes < 0) {
                    nZeroes += 64;
                }

                for (let i = 0; i < nZeroes; i++) {
                    dst.push(0);
                }
            }

            assert((dst.length + 8) % 64 == 0, "bad padding");

            const lengthPadding = bigIntToBytes(BigInt(nBits));

            assert(lengthPadding.length <= 8, "input data too big");

            while (lengthPadding.length < 8) {
                lengthPadding.unshift(0)
            }

            dst = dst.concat(lengthPadding);

            return dst;
        }

        /**
         * @type {number[]} - 64 uint32 numbers
         */
        const k = [
            0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
            0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
            0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
            0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
            0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
            0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
            0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
            0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
            0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
            0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
            0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
            0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
            0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
            0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
            0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
            0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
        ];

        /**
         * Initial hash (updated during compression phase)
         * @type {number[]} - 8 uint32 number
         */
        const hash = [
            0x6a09e667,
            0xbb67ae85,
            0x3c6ef372,
            0xa54ff53a,
            0x510e527f,
            0x9b05688c,
            0x1f83d9ab,
            0x5be0cd19,
        ];

        /**
         * @param {number} x
         * @returns {number}
         */
        function sigma0(x) {
            return irotr(x, 7) ^ irotr(x, 18) ^ (x >>> 3);
        }

        /**
         * @param {number} x
         * @returns {number}
         */
        function sigma1(x) {
            return irotr(x, 17) ^ irotr(x, 19) ^ (x >>> 10);
        }

        bytes = pad(bytes);

        // break message in successive 64 byte chunks
        for (let chunkStart = 0; chunkStart < bytes.length; chunkStart += 64) {
            const chunk = bytes.slice(chunkStart, chunkStart + 64);

            const w = (new Array(64)).fill(0); // array of 32 bit numbers!

            // copy chunk into first 16 positions of w
            for (let i = 0; i < 16; i++) {
                w[i] = (chunk[i * 4 + 0] << 24) |
                    (chunk[i * 4 + 1] << 16) |
                    (chunk[i * 4 + 2] << 8) |
                    (chunk[i * 4 + 3]);
            }

            // extends the first 16 positions into the remaining 48 positions
            for (let i = 16; i < 64; i++) {
                w[i] = imod32(w[i - 16] + sigma0(w[i - 15]) + w[i - 7] + sigma1(w[i - 2]));
            }

            // intialize working variables to current hash value
            let a = hash[0];
            let b = hash[1];
            let c = hash[2];
            let d = hash[3];
            let e = hash[4];
            let f = hash[5];
            let g = hash[6];
            let h = hash[7];

            // compression function main loop
            for (let i = 0; i < 64; i++) {
                const S1 = irotr(e, 6) ^ irotr(e, 11) ^ irotr(e, 25);
                const ch = (e & f) ^ ((~e) & g);
                const temp1 = imod32(h + S1 + ch + k[i] + w[i]);
                const S0 = irotr(a, 2) ^ irotr(a, 13) ^ irotr(a, 22);
                const maj = (a & b) ^ (a & c) ^ (b & c);
                const temp2 = imod32(S0 + maj);

                h = g;
                g = f;
                f = e;
                e = imod32(d + temp1);
                d = c;
                c = b;
                b = a;
                a = imod32(temp1 + temp2);
            }

            // update the hash
            hash[0] = imod32(hash[0] + a);
            hash[1] = imod32(hash[1] + b);
            hash[2] = imod32(hash[2] + c);
            hash[3] = imod32(hash[3] + d);
            hash[4] = imod32(hash[4] + e);
            hash[5] = imod32(hash[5] + f);
            hash[6] = imod32(hash[6] + g);
            hash[7] = imod32(hash[7] + h);
        }

        // produce the final digest of uint8 numbers
        const result = [];
        for (let i = 0; i < 8; i++) {
            const item = hash[i];

            result.push(imod8(item >> 24));
            result.push(imod8(item >> 16));
            result.push(imod8(item >> 8));
            result.push(imod8(item >> 0));
        }

        return result;
    },

    /**
     * Calculates sha2-512 (64bytes) hash of a list of uint8 numbers.
     * Result is also a list of uint8 number.
     * @example 
     * bytesToHex(Crypto.sha2_512([0x61, 0x62, 0x63])) == "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f"
     * @example 
     * bytesToHex(Crypto.sha2_512([])) == "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"
     * @example
     * bytesToHex(Crypto.sha2_512(textToBytes("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"))) == "204a8fc6dda82f0a0ced7beb8e08a41657c16ef468b228a8279be331a703c33596fd15c13b1b07f9aa1d3bea57789ca031ad85c7a71dd70354ec631238ca3445"
     * @example
     * bytesToHex(Crypto.sha2_512(textToBytes("abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstuu"))) == "23565d109ac0e2aa9fb162385178895058b28489a6bc31cb55491ed83956851ab1d4bbd46440586f5c9c4b69c9c280118cbc55c71495d258cc27cc6bb25ee720"
     * @param {number[]} bytes List of uint8 numbers
     * @returns {number[]} List of uint8 numbers.
     */
    sha2_512: (bytes) => {
        /**
         * Pad a bytearray so its size is a multiple of 128 (1024 bits).
         * Internal method.
         * @param {number[]} src - list of uint8 numbers
         * @returns {number[]}
         */
        function pad(src) {
            const nBits = src.length * 8;

            let dst = src.slice();

            dst.push(0x80);

            if ((dst.length + 16) % 128 != 0) {
                let nZeroes = (128 - dst.length % 128) - 16;
                if (nZeroes < 0) {
                    nZeroes += 128;
                }

                for (let i = 0; i < nZeroes; i++) {
                    dst.push(0);
                }
            }

            assert((dst.length + 16) % 128 == 0, "bad padding");

            // assume nBits fits in 32 bits
            const lengthPadding = bigIntToBytes(BigInt(nBits));

            assert(lengthPadding.length <= 16, "input data too big");

            while (lengthPadding.length < 16) {
                lengthPadding.unshift(0);
            }

            dst = dst.concat(lengthPadding);

            assert(dst.length % 128 == 0, "bad length padding");

            return dst;
        }

        /**
         * @type {number[]} - 80 uint64 numbers as 160 uint32 numbers
         */
        const k = [
            0x428a2f98, 0xd728ae22, 0x71374491, 0x23ef65cd,
            0xb5c0fbcf, 0xec4d3b2f, 0xe9b5dba5, 0x8189dbbc,
            0x3956c25b, 0xf348b538, 0x59f111f1, 0xb605d019,
            0x923f82a4, 0xaf194f9b, 0xab1c5ed5, 0xda6d8118,
            0xd807aa98, 0xa3030242, 0x12835b01, 0x45706fbe,
            0x243185be, 0x4ee4b28c, 0x550c7dc3, 0xd5ffb4e2,
            0x72be5d74, 0xf27b896f, 0x80deb1fe, 0x3b1696b1,
            0x9bdc06a7, 0x25c71235, 0xc19bf174, 0xcf692694,
            0xe49b69c1, 0x9ef14ad2, 0xefbe4786, 0x384f25e3,
            0x0fc19dc6, 0x8b8cd5b5, 0x240ca1cc, 0x77ac9c65,
            0x2de92c6f, 0x592b0275, 0x4a7484aa, 0x6ea6e483,
            0x5cb0a9dc, 0xbd41fbd4, 0x76f988da, 0x831153b5,
            0x983e5152, 0xee66dfab, 0xa831c66d, 0x2db43210,
            0xb00327c8, 0x98fb213f, 0xbf597fc7, 0xbeef0ee4,
            0xc6e00bf3, 0x3da88fc2, 0xd5a79147, 0x930aa725,
            0x06ca6351, 0xe003826f, 0x14292967, 0x0a0e6e70,
            0x27b70a85, 0x46d22ffc, 0x2e1b2138, 0x5c26c926,
            0x4d2c6dfc, 0x5ac42aed, 0x53380d13, 0x9d95b3df,
            0x650a7354, 0x8baf63de, 0x766a0abb, 0x3c77b2a8,
            0x81c2c92e, 0x47edaee6, 0x92722c85, 0x1482353b,
            0xa2bfe8a1, 0x4cf10364, 0xa81a664b, 0xbc423001,
            0xc24b8b70, 0xd0f89791, 0xc76c51a3, 0x0654be30,
            0xd192e819, 0xd6ef5218, 0xd6990624, 0x5565a910,
            0xf40e3585, 0x5771202a, 0x106aa070, 0x32bbd1b8,
            0x19a4c116, 0xb8d2d0c8, 0x1e376c08, 0x5141ab53,
            0x2748774c, 0xdf8eeb99, 0x34b0bcb5, 0xe19b48a8,
            0x391c0cb3, 0xc5c95a63, 0x4ed8aa4a, 0xe3418acb,
            0x5b9cca4f, 0x7763e373, 0x682e6ff3, 0xd6b2b8a3,
            0x748f82ee, 0x5defb2fc, 0x78a5636f, 0x43172f60,
            0x84c87814, 0xa1f0ab72, 0x8cc70208, 0x1a6439ec,
            0x90befffa, 0x23631e28, 0xa4506ceb, 0xde82bde9,
            0xbef9a3f7, 0xb2c67915, 0xc67178f2, 0xe372532b,
            0xca273ece, 0xea26619c, 0xd186b8c7, 0x21c0c207,
            0xeada7dd6, 0xcde0eb1e, 0xf57d4f7f, 0xee6ed178,
            0x06f067aa, 0x72176fba, 0x0a637dc5, 0xa2c898a6,
            0x113f9804, 0xbef90dae, 0x1b710b35, 0x131c471b,
            0x28db77f5, 0x23047d84, 0x32caab7b, 0x40c72493,
            0x3c9ebe0a, 0x15c9bebc, 0x431d67c4, 0x9c100d4c,
            0x4cc5d4be, 0xcb3e42b6, 0x597f299c, 0xfc657e2a,
            0x5fcb6fab, 0x3ad6faec, 0x6c44198c, 0x4a475817,
        ];

        /**
         * Initial hash (updated during compression phase)
         * @type {number[]} - 8 uint64 numbers as 16 uint32 number
         */
        const hash = [
            0x6a09e667, 0xf3bcc908,
            0xbb67ae85, 0x84caa73b,
            0x3c6ef372, 0xfe94f82b,
            0xa54ff53a, 0x5f1d36f1,
            0x510e527f, 0xade682d1,
            0x9b05688c, 0x2b3e6c1f,
            0x1f83d9ab, 0xfb41bd6b,
            0x5be0cd19, 0x137e2179,
        ];

        /**
         * @param {number[]} hash 
         * @param {number} i 
         * @param {number} h 
         * @param {number} l 
         */
        function updateHash(hash, i, h, l) {
            l = hash[i+1] + l;
            hash[i] = (hash[i] + h + Math.floor(l / 4294967296)) >>> 0;
            hash[i+1] = l >>> 0;
        }

        bytes = pad(bytes);

        // break message in successive 64 byte chunks
        for (let chunkStart = 0; chunkStart < bytes.length; chunkStart += 128) {
            const chunk = bytes.slice(chunkStart, chunkStart + 128);

            /**
             * @type {number[]}
             */
            const w = (new Array(160)).fill(0); // array of 32 bit numbers!

            // copy chunk into first 16 hi/lo positions of w (i.e. into first 32 uint32 positions)
            for (let i = 0; i < 32; i += 2) {
                const bs = chunk.slice(i * 4, i * 4 + 8);
                w[i+0] = ((bs[0] << 24) | (bs[1] << 16) | (bs[2] << 8) | (bs[3] << 0)) >>> 0;
                w[i+1] = ((bs[4] << 24) | (bs[5] << 16) | (bs[6] << 8) | (bs[7] << 0)) >>> 0;
            }

            // extends the first 16 uint64 positions into the remaining 80 uint64 positions (so first 32 uint32 into remaining 160 uint32 positions)
            for (let i = 32; i < 160; i += 2) {
                let h = w[i - 30];
                let l = w[i - 29];

                const sigma0h = (((h >>> 1) | (l << 31)) ^ ((h >>> 8) | (l << 24)) ^ ((h >>> 7))) >>> 0;
                const sigma0l = (((l >>> 1) | (h << 31)) ^ ((l >>> 8) | (h << 24)) ^ ((l >>> 7) | (h << 25))) >>> 0;

                h = w[i - 4];
                l = w[i - 3];

                const sigma1h = (((h >>> 19) | (l << 13)) ^ ((l >>> 29) | (h << 3)) ^ ((h >>> 6))) >>> 0;
                const sigma1l = (((l >>> 19) | (h << 13)) ^ ((h >>> 29) | (l << 3)) ^ ((l >>> 6) | (h << 26))) >>> 0;

                h = sigma1h + w[i-14] + sigma0h + w[i-32];
                l = sigma1l + w[i-13] + sigma0l + w[i-31];

                w[i] = (h + Math.floor(l / 4294967296)) >>> 0;
                w[i+1] = l >>> 0;
            }

            // intialize working variables to current hash value
            let ah = hash[0];
            let al = hash[1];
            let bh = hash[2];
            let bl = hash[3];
            let ch = hash[4];
            let cl = hash[5];
            let dh = hash[6];
            let dl = hash[7];
            let eh = hash[8];
            let el = hash[9];
            let fh = hash[10];
            let fl = hash[11];
            let gh = hash[12];
            let gl = hash[13];
            let hh = hash[14];
            let hl = hash[15];

            // compression function main loop
            for (let i = 0; i < 160; i+=2) {
                const S0h = (((ah >>> 28) | (al << 4)) ^ ((al >>> 2) | (ah << 30)) ^ ((al >>> 7) | (ah << 25))) >>> 0;
                const S0l = (((al >>> 28) | (ah << 4)) ^ ((ah >>> 2) | (al << 30)) ^ ((ah >>> 7) | (al << 25))) >>> 0;
                
                const S1h = (((eh >>> 14) | (el << 18)) ^ ((eh >>> 18) | (el << 14)) ^ ((el >>> 9) | (eh << 23))) >>> 0;
                const S1l = (((el >>> 14) | (eh << 18)) ^ ((el >>> 18) | (eh << 14)) ^ ((eh >>> 9) | (el << 23))) >>> 0;

                const majh = ((ah & bh) ^ (ah & ch) ^ (bh & ch)) >>> 0;
                const majl = ((al & bl) ^ (al & cl) ^ (bl & cl)) >>> 0;

                const chh = ((eh & fh) ^ (~eh & gh)) >>> 0;
                const chl = ((el & fl) ^ (~el & gl)) >>> 0;

                let temp1l = hl + S1l + chl + k[i+1] + w[i+1];
                let temp1h = (hh + S1h + chh + k[i] + w[i] + Math.floor(temp1l / 4294967296)) >>> 0;
                temp1l = temp1l >>> 0;

                let temp2l = S0l + majl;
                const temp2h = (S0h + majh + Math.floor(temp2l / 4294967296)) >>> 0;
                temp2l = temp2l >>> 0;

                hh = gh;
                hl = gl;
                gh = fh;
                gl = fl;
                fh = eh;
                fl = el;
                el = dl + temp1l;
                eh = (dh + temp1h + Math.floor(el / 4294967296)) >>> 0;
                el = el >>> 0;
                dh = ch;
                dl = cl;
                ch = bh;
                cl = bl;
                bh = ah;
                bl = al;
                al = temp1l + temp2l;
                ah = (temp1h + temp2h + Math.floor(al / 4294967296)) >>> 0;
                al = al >>> 0;
            }

            updateHash(hash,  0, ah, al);
            updateHash(hash,  2, bh, bl);
            updateHash(hash,  4, ch, cl);
            updateHash(hash,  6, dh, dl);
            updateHash(hash,  8, eh, el);
            updateHash(hash, 10, fh, fl);
            updateHash(hash, 12, gh, gl);
            updateHash(hash, 14, hh, hl);
        }

        // produce the final digest of uint8 numbers
        let result = [];
        for (let i = 0; i < 16; i+=2) {
            const h = hash[i];
            const l = hash[i+1];
            const bs = [
                (0xff000000 & h) >>> 24,
                (0x00ff0000 & h) >>> 16,
                (0x0000ff00 & h) >>> 8,
                (0x000000ff & h),
                (0xff000000 & l) >>> 24,
                (0x00ff0000 & l) >>> 16,
                (0x0000ff00 & l) >>> 8,
                (0x000000ff & l)    
            ];

            result = result.concat(bs);
        }

        return result;
    },

    /**
     * Calculates sha3-256 (32bytes) hash of a list of uint8 numbers.
     * Result is also a list of uint8 number.
     * @example
     * bytesToHex(Crypto.sha3(textToBytes("abc"))) == "3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532"
     * @example
     * bytesToHex(Crypto.sha3((new Array(136)).fill(1))) == "b36dc2167c4d9dda1a58b87046c8d76a6359afe3612c4de8a38857e09117b2db"
     * @example
     * bytesToHex(Crypto.sha3((new Array(135)).fill(2))) == "5bdf5d815d29a9d7161c66520efc17c2edd7898f2b99a029e8d2e4ff153407f4"
     * @example
     * bytesToHex(Crypto.sha3((new Array(134)).fill(3))) == "8e6575663dfb75a88f94a32c5b363c410278b65020734560d968aadd6896a621"
     * @example
     * bytesToHex(Crypto.sha3((new Array(137)).fill(4))) == "f10b39c3e455006aa42120b9751faa0f35c821211c9d086beb28bf3c4134c6c6"
     * @param {number[]} bytes List of uint8 numbers
     * @returns {number[]} List of uint8 numbers.
     */
    sha3: (bytes) => {
        /**
         * Sha3 uses only bit-wise operations, so 64-bit operations can easily be replicated using 2 32-bit operations instead.
         */

        /**
         * @type {number} - state width (1600 bits, )
         */
        const WIDTH = 200;

        /**
         * @type {number} - rate (1088 bits, 136 bytes)
         */
        const RATE = 136;

        /**
         * @type {number} - capacity
         */
        const CAP = WIDTH - RATE;

        /**
         * Apply 1000...1 padding until size is multiple of r.
         * If already multiple of r then add a whole block of padding.
         * @param {number[]} src - list of uint8 numbers
         * @returns {number[]} - list of uint8 numbers
         */
        function pad(src) {
            const dst = src.slice();

            /** @type {number} */
            let nZeroes = RATE - 2 - (dst.length % RATE);
            if (nZeroes < -1) {
                nZeroes += RATE - 2;
            }

            if (nZeroes == -1) {
                dst.push(0x86);
            } else {
                dst.push(0x06);

                for (let i = 0; i < nZeroes; i++) {
                    dst.push(0);
                }

                dst.push(0x80);
            }

            assert(dst.length % RATE == 0);

            return dst;
        }

        /**
         * 24 numbers used in the sha3 permute function
         * @type {number[]}
         */
        const OFFSETS = [6, 12, 18, 24, 3, 9, 10, 16, 22, 1, 7, 13, 19, 20, 4, 5, 11, 17, 23, 2, 8, 14, 15, 21];

        /**
         * 24 numbers used in the sha3 permute function
         * @type {number[]}
         */
        const SHIFTS = [-12, -11, 21, 14, 28, 20, 3, -13, -29, 1, 6, 25, 8, 18, 27, -4, 10, 15, -24, -30, -23, -7, -9, 2];

        /**
         * Round constants used in the sha3 permute function
         * @type {UInt64[]} 
         */
        const RC = [
            new UInt64(0x00000000, 0x00000001),
            new UInt64(0x00000000, 0x00008082),
            new UInt64(0x80000000, 0x0000808a),
            new UInt64(0x80000000, 0x80008000),
            new UInt64(0x00000000, 0x0000808b),
            new UInt64(0x00000000, 0x80000001),
            new UInt64(0x80000000, 0x80008081),
            new UInt64(0x80000000, 0x00008009),
            new UInt64(0x00000000, 0x0000008a),
            new UInt64(0x00000000, 0x00000088),
            new UInt64(0x00000000, 0x80008009),
            new UInt64(0x00000000, 0x8000000a),
            new UInt64(0x00000000, 0x8000808b),
            new UInt64(0x80000000, 0x0000008b),
            new UInt64(0x80000000, 0x00008089),
            new UInt64(0x80000000, 0x00008003),
            new UInt64(0x80000000, 0x00008002),
            new UInt64(0x80000000, 0x00000080),
            new UInt64(0x00000000, 0x0000800a),
            new UInt64(0x80000000, 0x8000000a),
            new UInt64(0x80000000, 0x80008081),
            new UInt64(0x80000000, 0x00008080),
            new UInt64(0x00000000, 0x80000001),
            new UInt64(0x80000000, 0x80008008),
        ];

        /**
         * @param {UInt64[]} s 
         */
        function permute(s) {
            /**
             * @type {UInt64[]}
             */
            const c = new Array(5);

            /**
             * @type {UInt64[]}
             */
            const b = new Array(25);

            for (let round = 0; round < 24; round++) {
                for (let i = 0; i < 5; i++) {
                    c[i] = s[i].xor(s[i + 5]).xor(s[i + 10]).xor(s[i + 15]).xor(s[i + 20]);
                }

                for (let i = 0; i < 5; i++) {
                    const i1 = (i + 1) % 5;
                    const i2 = (i + 4) % 5;

                    const tmp = c[i2].xor(c[i1].rotr(63));

                    for (let j = 0; j < 5; j++) {
                        s[i + 5 * j] = s[i + 5 * j].xor(tmp);
                    }
                }

                b[0] = s[0];

                for (let i = 1; i < 25; i++) {
                    const offset = OFFSETS[i - 1];

                    const left = Math.abs(SHIFTS[i - 1]);
                    const right = 32 - left;

                    if (SHIFTS[i - 1] < 0) {
                        b[i] = s[offset].rotr(right);
                    } else {
                        b[i] = s[offset].rotr(right + 32);
                    }
                }

                for (let i = 0; i < 5; i++) {
                    for (let j = 0; j < 5; j++) {
                        s[i * 5 + j] = b[i * 5 + j].xor(b[i * 5 + (j + 1) % 5].not().and(b[i * 5 + (j + 2) % 5]))
                    }
                }

                s[0] = s[0].xor(RC[round]);
            }
        }

        bytes = pad(bytes);

        // initialize the state
        /**
         * @type {UInt64[]}
         */
        const state = (new Array(WIDTH / 8)).fill(UInt64.zero());

        for (let chunkStart = 0; chunkStart < bytes.length; chunkStart += RATE) {
            // extend the chunk to become length WIDTH
            const chunk = bytes.slice(chunkStart, chunkStart + RATE).concat((new Array(CAP)).fill(0));

            // element-wise xor with 'state'
            for (let i = 0; i < WIDTH; i += 8) {
                state[i / 8] = state[i / 8].xor(UInt64.fromBytes(chunk.slice(i, i + 8)));

                // beware: a uint32 is stored as little endian, but a pair of uint32s that form a uin64 are stored in big endian format!
            }

            // apply block permutations
            permute(state);
        }

        /** @type {number[]} */
        let hash = [];
        for (let i = 0; i < 4; i++) {
            hash = hash.concat(state[i].toBytes());
        }

        return hash;
    },

    /**
     * Calculates blake2b hash of a list of uint8 numbers (variable digest size).
     * Result is also a list of uint8 numbers.
     * @example                                        
     * bytesToHex(Crypto.blake2b([0, 1])) == "01cf79da4945c370c68b265ef70641aaa65eaa8f5953e3900d97724c2c5aa095"
     * @example
     * bytesToHex(Crypto.blake2b(textToBytes("abc"), 64)) == "ba80a53f981c4d0d6a2797b69f12f6e94c212f14685ac4b74b12bb6fdbffa2d17d87c5392aab792dc252d5de4533cc9518d38aa8dbf1925ab92386edd4009923"
     * @param {number[]} bytes 
     * @param {number} digestSize Defaults to 32. Can't be greater than 64.
     * @returns {number[]} List of uint8 numbers.
     */
    blake2b: (bytes, digestSize = BLAKE2B_DIGEST_SIZE) => {
        /**
         * Blake2b is a 64bit algorithm, so we need to be careful when replicating 64-bit operations with 2 32-bit numbers
         * (low-word overflow must spill into high-word, and shifts must go over low/high boundary).
         */

        /**
         * 128 bytes (16*8 byte words)
         * @type {number}
         */
        const WIDTH = 128;

        /**
         * Initialization vector
         */
        const IV = [
            new UInt64(0x6a09e667, 0xf3bcc908),
            new UInt64(0xbb67ae85, 0x84caa73b),
            new UInt64(0x3c6ef372, 0xfe94f82b),
            new UInt64(0xa54ff53a, 0x5f1d36f1),
            new UInt64(0x510e527f, 0xade682d1),
            new UInt64(0x9b05688c, 0x2b3e6c1f),
            new UInt64(0x1f83d9ab, 0xfb41bd6b),
            new UInt64(0x5be0cd19, 0x137e2179),
        ];

        const SIGMA = [
            [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
            [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3],
            [11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4],
            [7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8],
            [9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13],
            [2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9],
            [12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11],
            [13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10],
            [6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5],
            [10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0],
        ];

        /**
         * @param {number[]} src - list of uint8 bytes
         * @returns {number[]} - list of uint8 bytes
         */
        function pad(src) {
            const dst = src.slice();

            const nZeroes = dst.length == 0 ? WIDTH : (WIDTH - dst.length % WIDTH) % WIDTH;

            // just padding with zeroes, the actual message length is used during compression stage of final block in order to uniquely hash messages of different lengths
            for (let i = 0; i < nZeroes; i++) {
                dst.push(0);
            }

            return dst;
        }

        /**
         * @param {UInt64[]} v
         * @param {UInt64[]} chunk
         * @param {number} a - index
         * @param {number} b - index
         * @param {number} c - index
         * @param {number} d - index
         * @param {number} i - index in chunk for low word 1
         * @param {number} j - index in chunk for low word 2
         */
        function mix(v, chunk, a, b, c, d, i, j) {
            const x = chunk[i];
            const y = chunk[j];

            v[a] = v[a].add(v[b]).add(x);
            v[d] = v[d].xor(v[a]).rotr(32);
            v[c] = v[c].add(v[d]);
            v[b] = v[b].xor(v[c]).rotr(24);
            v[a] = v[a].add(v[b]).add(y);
            v[d] = v[d].xor(v[a]).rotr(16);
            v[c] = v[c].add(v[d]);
            v[b] = v[b].xor(v[c]).rotr(63);
        }

        /**
         * @param {UInt64[]} h - state vector
         * @param {UInt64[]} chunk
         * @param {number} t - chunkEnd (expected to fit in uint32)
         * @param {boolean} last
         */
        function compress(h, chunk, t, last) {
            // work vectors
            const v = h.slice().concat(IV.slice());

            v[12] = v[12].xor(new UInt64(0, imod32(t))); // v[12].high unmodified
            // v[13] unmodified

            if (last) {
                v[14] = v[14].xor(new UInt64(0xffffffff, 0xffffffff));
            }

            for (let round = 0; round < 12; round++) {
                const s = SIGMA[round % 10];

                for (let i = 0; i < 4; i++) {
                    mix(v, chunk, i, i + 4, i + 8, i + 12, s[i * 2], s[i * 2 + 1]);
                }

                for (let i = 0; i < 4; i++) {
                    mix(v, chunk, i, (i + 1) % 4 + 4, (i + 2) % 4 + 8, (i + 3) % 4 + 12, s[8 + i * 2], s[8 + i * 2 + 1]);
                }
            }

            for (let i = 0; i < 8; i++) {
                h[i] = h[i].xor(v[i].xor(v[i + 8]));
            }
        }

        const nBytes = bytes.length;

        bytes = pad(bytes);

        // init hash vector
        const h = IV.slice();


        // setup the param block
        const paramBlock = new Uint8Array(64);
        paramBlock[0] = digestSize; // n output  bytes
        paramBlock[1] = 0; // key-length (always zero in our case) 
        paramBlock[2] = 1; // fanout
        paramBlock[3] = 1; // depth

        //mix in the parameter block
        const paramBlockView = new DataView(paramBlock.buffer);
        for (let i = 0; i < 8; i++) {
            h[i] = h[i].xor(new UInt64(
                paramBlockView.getUint32(i * 8 + 4, true),
                paramBlockView.getUint32(i * 8, true),
            ));
        }

        // loop all chunks
        for (let chunkStart = 0; chunkStart < bytes.length; chunkStart += WIDTH) {
            const chunkEnd = chunkStart + WIDTH; // exclusive
            const chunk = bytes.slice(chunkStart, chunkStart + WIDTH);

            const chunk64 = new Array(WIDTH / 8);
            for (let i = 0; i < WIDTH; i += 8) {
                chunk64[i / 8] = UInt64.fromBytes(chunk.slice(i, i + 8));
            }

            if (chunkStart == bytes.length - WIDTH) {
                // last block
                compress(h, chunk64, nBytes, true);
            } else {
                compress(h, chunk64, chunkEnd, false);
            }
        }

        // extract lowest BLAKE2B_DIGEST_SIZE bytes from h

        /** @type {number[]} */
        let hash = [];
        for (let i = 0; i < digestSize / 8; i++) {
            hash = hash.concat(h[i].toBytes());
        }

        return hash.slice(0, digestSize);
    },

    /**
     * Hmac using sha2-256.
     * @example
     * bytesToHex(Crypto.hmacSha2_256(textToBytes("key"), textToBytes("The quick brown fox jumps over the lazy dog"))) == "f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8"
     * @param {number[]} key 
     * @param {number[]} message 
     * @returns {number[]}
     */
    hmacSha2_256: (key, message) => {
        return hmac((x) => Crypto.sha2_256(x), 64, key, message);
    },

    /**
     * Hmac using sha2-512.
     * @example
     * bytesToHex(Crypto.hmacSha2_512(textToBytes("key"), textToBytes("The quick brown fox jumps over the lazy dog"))) == "b42af09057bac1e2d41708e48a902e09b5ff7f12ab428a4fe86653c73dd248fb82f948a549f7b791a5b41915ee4d1ec3935357e4e2317250d0372afa2ebeeb3a"
     * @param {number[]} key 
     * @param {number[]} message 
     * @returns {number[]}
     */
    hmacSha2_512: (key, message) => {
        return hmac((x) => Crypto.sha2_512(x), 128, key, message);
    },

    /**
     * Password-Based Key Derivation Function 2.
     * @example
     * bytesToHex(Crypto.pbkdf2(Crypto.hmacSha2_256, textToBytes("password"), textToBytes("salt"), 1, 20)) == "120fb6cffcf8b32c43e7225256c4f837a86548c9"
     * @example
     * bytesToHex(Crypto.pbkdf2(Crypto.hmacSha2_512, textToBytes("password"), textToBytes("salt"), 2, 20)) == "e1d9c16aa681708a45f5c7c4e215ceb66e011a2e"
     * @param {(key: number[], msg: number[]) => number[]} prf 
     * @param {number[]} password 
     * @param {number[]} salt 
     * @param {number} iters
     * @param {number} dkLength 
     * @returns {number[]}
     */
    pbkdf2: (prf, password, salt, iters, dkLength) => {
        /**
         * @param {number[]} a 
         * @param {number[]} b 
         * @returns {number[]}
         */
        const xor = (a, b) => {
            const c = new Array(a.length);

            for (let i = 0; i < a.length; i++) {
                c[i] = a[i] ^ b[i];
            }

            return c;
        }

        /**
         * @type {number[]}
         */
        let dk = [];

        let i = 1n;
        while (dk.length < dkLength) {
            const bi = bigIntToBytes(i);
            while (bi.length < 4) {
                bi.unshift(0);
            }

            let U = prf(password, salt.slice().concat(bi));
            let T = U;

            for (let j = 1; j < iters; j++) {
                U = prf(password, U);
                T = xor(T, U);
            }

            dk = dk.concat(T);

            i += 1n;
        }

        if (dk.length > dkLength) {
            dk = dk.slice(0, dkLength);
        }

        return dk;
    }
}


/**
 * @template {CurvePoint<T>} T
 * @typedef {{
 *   add(other: T): T
 *   mul(scalar: bigint): T
 *   equals(other: T): boolean
 *   encode(): number[]
 * }} CurvePoint
 */

const ED25519_Q = 57896044618658097711785492504343953926634992332820282019728792003956564819949n; // ipowi(255n) - 19n
const ED25519_Q38 = 7237005577332262213973186563042994240829374041602535252466099000494570602494n; // (Q + 3n)/8n
const ED25519_CURVE_ORDER = 7237005577332262213973186563042994240857116359379907606001950938285454250989n; // ipow2(252n) + 27742317777372353535851937790883648493n;
const ED25519_D = -4513249062541557337682894930092624173785641285191125241628941591882900924598840740n; // -121665n * invert(121666n);
const ED25519_I = 19681161376707505956807079304988542015446066515923890162744021073123829784752n; // expMod(2n, (Q - 1n)/4n, Q);

/**
* @param {bigint} b 
* @param {bigint} e 
* @param {bigint} m 
* @returns {bigint}
*/
function expMod(b, e, m) {
    if (e == 0n) {
        return 1n;
    } else {
        let t = expMod(b, e / 2n, m);
        t = (t * t) % m;

        if ((e % 2n) != 0n) {
            t = posMod(t * b, m)
        }

        return t;
    }
}

/**
* @param {bigint} x 
* @returns {bigint}
*/
function curveMod(x) {
    return posMod(x, ED25519_Q)
}

/**
* @param {bigint} n 
* @returns {bigint}
*/
function curveInvert(n) {
    let a = curveMod(n);
    let b = ED25519_Q;

    let x = 0n;
    let y = 1n;
    let u = 1n;
    let v = 0n;

    while (a !== 0n) {
        const q = b / a;
        const r = b % a;
        const m = x - u * q;
        const n = y - v * q;
        b = a;
        a = r;
        x = u;
        y = v;
        u = m;
        v = n;
    }

    return curveMod(x);
}

/**
* @param {bigint} y 
* @returns {bigint}
*/
function recoverX(y) {
    const yy = y * y;
    const xx = (yy - 1n) * curveInvert(ED25519_D * yy + 1n);
    let x = expMod(xx, ED25519_Q38, ED25519_Q);

    if (((x * x - xx) % ED25519_Q) != 0n) {
        x = (x * ED25519_I) % ED25519_Q;
    }

    if ((x % 2n) != 0n) {
        x = ED25519_Q - x;
    }

    return x;
}

/**
* Curve point 'multiplication'
* @param {bigint} y 
* @returns {number[]}
*/
function encodeCurveInt(y) {
    return bigIntToLe32Bytes(y);
}

/**
* @param {number[]} s 
* @returns {bigint}
*/
function decodeCurveInt(s) {
    return leBytesToBigInt(s);
}

/**
* @param {number[]} bytes 
* @param {number} i - bit index
* @returns {number} - 0 or 1
*/
function getBit(bytes, i) {
    return (bytes[Math.floor(i / 8)] >> i % 8) & 1
}

/**
 * @internal
 * @implements {CurvePoint<AffinePoint>}
 */
class AffinePoint {
    #x;
    #y;

    /**
     * @param {bigint} x 
     * @param {bigint} y 
     */
    constructor(x, y) {
        this.#x = x;
        this.#y = y;
    }

    /**
     * @type {AffinePoint}
     */
    static get BASE() {
        return new AffinePoint(
            15112221349535400772501151409588531511454012693041857206046113283949847762202n, // recoverX(B[1]) % Q
            46316835694926478169428394003475163141307993866256225615783033603165251855960n  // (4n*invert(5n)) % Q
        );
    }

    /**
     * @type {AffinePoint}
     */
    static get ZERO() {
        return new AffinePoint(0n, 1n);
    }

    /**
     * @param {number[]} bytes 
     * @returns {AffinePoint}
     */
    static decode(bytes) {
        assert(bytes.length == 32);

        const tmp = bytes.slice();
        tmp[31] = tmp[31] & 0b01111111;

        const y = decodeCurveInt(tmp);

        let x = recoverX(y);
        if (Number(x & 1n) != getBit(bytes, 255)) {
            x = ED25519_Q - x;
        }

        const point = new AffinePoint(x, y);

        assert(point.isOnCurve(), "point isn't on curve");

        return point;
    }

    /**
     * @type {bigint}
     */
    get x() {
        return this.#x;
    }

    /**
     * @type {bigint}
     */
    get y() {
        return this.#y;
    }

    /** 
     * Curve point 'addition'
     * Note: the invert call in this calculation is very slow (prefer ExtendedPoint for speed)
     * @param {AffinePoint} other 
     * @returns {AffinePoint}
     */
    add(other) {
        const x1 = this.#x;
        const y1 = this.#y;
        const x2 = other.#x;
        const y2 = other.#y;
        const dxxyy = ED25519_D * x1 * x2 * y1 * y2;
        const x3 = (x1 * y2 + x2 * y1) * curveInvert(1n + dxxyy);
        const y3 = (y1 * y2 + x1 * x2) * curveInvert(1n - dxxyy);

        return new AffinePoint(
            curveMod(x3),
            curveMod(y3)
        );
    }

    /**
     * @param {AffinePoint} other 
     * @returns {boolean}
     */
    equals(other) {
        return this.x == other.x && this.y == other.y;
    }

    /**
     * @returns {boolean}
     */
    isOnCurve() {
        const x = this.#x;
        const y = this.#y;
        const xx = x * x;
        const yy = y * y;
        return (-xx + yy - 1n - ED25519_D * xx * yy) % ED25519_Q == 0n;
    }

    /**
     * @param {bigint} s 
     * @returns {AffinePoint}
     */
    mul(s) {
        if (s == 0n) {
            return AffinePoint.ZERO;
        } else {
            let sum = this.mul(s / 2n);

            sum = sum.add(sum);

            if ((s % 2n) != 0n) {
                sum = sum.add(this);
            }

            return sum;
        }
    }

    /**
     * @returns {number[]}
     */
    encode() {
        const bytes = encodeCurveInt(this.#y);

        // last bit is determined by x

        bytes[31] = (bytes[31] & 0b011111111) | (Number(this.#x & 1n) * 0b10000000);

        return bytes;
    }
}

/**
 * Extended point implementation take from @noble/ed25519
 * @internal
 * @implements {CurvePoint<ExtendedPoint>}
 */
class ExtendedPoint {
    #x;
    #y;
    #z;
    #t;

    /**
     * @param {bigint} x 
     * @param {bigint} y 
     * @param {bigint} z 
     * @param {bigint} t 
     */
    constructor(x, y, z, t) {
        this.#x = x;
        this.#y = y;
        this.#z = z;
        this.#t = t;
    }

    /**
     * @type {ExtendedPoint}
     */
    static get BASE() {
        return new ExtendedPoint(
            AffinePoint.BASE.x,
            AffinePoint.BASE.y,
            1n,
            curveMod(AffinePoint.BASE.x * AffinePoint.BASE.y)
        )
    }

    /**
     * @type {ExtendedPoint}
     */
    static get ZERO() {
        return new ExtendedPoint(0n, 1n, 1n, 0n);
    }

    /**
     * @param {number[]} bytes 
     * @returns {ExtendedPoint}
     */
    static decode(bytes) {
        return ExtendedPoint.fromAffine(AffinePoint.decode(bytes));
    }

    /**
     * @param {AffinePoint} affine 
     * @returns {ExtendedPoint}
     */
    static fromAffine(affine) {
        return new ExtendedPoint(affine.x, affine.y, 1n, curveMod(affine.x * affine.y));
    }

    /**
     * @param {ExtendedPoint} other 
     * @returns {ExtendedPoint}
     */
    add(other) {
        const x1 = this.#x;
        const y1 = this.#y;
        const z1 = this.#z;
        const t1 = this.#t;

        const x2 = other.#x;
        const y2 = other.#y;
        const z2 = other.#z;
        const t2 = other.#t;

        const a = curveMod(x1 * x2);
        const b = curveMod(y1 * y2);
        const c = curveMod(ED25519_D * t1 * t2);
        const d = curveMod(z1 * z2);
        const e = curveMod((x1 + y1) * (x2 + y2) - a - b);
        const f = curveMod(d - c);
        const g = curveMod(d + c);
        const h = curveMod(a + b);
        const x3 = curveMod(e * f);
        const y3 = curveMod(g * h);
        const z3 = curveMod(f * g);
        const t3 = curveMod(e * h);

        return new ExtendedPoint(x3, y3, z3, t3);
    }

    /**
     * @returns {number[]}
     */
    encode() {
        return this.toAffine().encode()
    }

    /**
     * @param {ExtendedPoint} other 
     * @returns {boolean}
     */
    equals(other) {
        return (curveMod(this.#x * other.#z) == curveMod(other.#x * this.#z)) && (curveMod(this.#y * other.#z) == curveMod(other.#y * this.#z));
    }

    /**
     * @returns {boolean}
     */
    isBase() {
        return this.equals(ExtendedPoint.BASE);
    }

    /**
     * @returns {boolean}
     */
    isZero() {
        return this.equals(ExtendedPoint.ZERO);
    }

    /**
     * @param {bigint} s 
     * @returns {ExtendedPoint}
     */
    mul(s) {
        if (s == 0n) {
            return ExtendedPoint.ZERO;
        } else {
            let sum = this.mul(s / 2n);

            sum = sum.add(sum);

            if ((s % 2n) != 0n) {
                sum = sum.add(this);
            }

            return sum;
        }
    }

    /**
     * @returns {AffinePoint}
     */
    toAffine() {
        if (this.isZero()) {
            return AffinePoint.ZERO;
        } else {
            const zInverse = curveInvert(this.#z);

            return new AffinePoint(
                curveMod(this.#x * zInverse),
                curveMod(this.#y * zInverse)
            );
        }
    }
}

/**
 * @internal
 * @param {number[]} h 
 * @returns {bigint}
 */
function clamp(h) {
    const bytes = h.slice(0, 32);

    bytes[0] &= 0b11111000;
    bytes[31] &= 0b00111111;
    bytes[31] |= 0b01000000;

    return decodeCurveInt(bytes);
}

/**
 * @internal
 * @param {number[]} m 
 * @returns {bigint}
 */
function nonce(m) {
    const h = Crypto.sha2_512(m);

    return decodeCurveInt(h);
}

/**
 * @internal
 */
const CurvePointImpl = ExtendedPoint;

/**
 * The elliptic curve signature algorithm used by Cardano wallets.
 * 
 * Ported from: [https://ed25519.cr.yp.to/python/ed25519.py](https://ed25519.cr.yp.to/python/ed25519.py).
 * 
 * ExtendedPoint implementation taken from: [https://github.com/paulmillr/noble-ed25519](https://github.com/paulmillr/noble-ed25519).
 * @namespace
 */
export const Ed25519 = {
    /**
     * Similar to `Ed25519.derivePublicKey`, but doesn't hash the input key.
     * @param {number[]} extendedKey
     * @returns {number[]} 32 byte public key.
     */
    deriveBip32PublicKey: (extendedKey) => {
        const a = clamp(extendedKey);
        const A = CurvePointImpl.BASE.mul(a);

        return A.encode();
    },

    /**
     * Derive a public key from a private key.
     * The private key can be any number of bytes (it's hashed internally).
     * The returned public key is 32 bytes long.
     * @param {number[]} privateKey
     * @returns {number[]} 32 byte public key.
     */
    derivePublicKey: (privateKey) => {
        return Ed25519.deriveBip32PublicKey(Crypto.sha2_512(privateKey));
    },

    /**
     * Like `Ed25519.sign`, but doesn't hash the input key.
     * @param {number[]} message 
     * @param {number[]} extendedKey 
     * @returns {number[]} 64 byte signature.
     */
    signBip32: (message, extendedKey) => {
        const a = clamp(extendedKey);

        // for convenience calculate publicKey here:
        const publicKey = CurvePointImpl.BASE.mul(a).encode();

        const r = nonce(extendedKey.slice(32, 64).concat(message));
        const R = CurvePointImpl.BASE.mul(r);
        const Rencoded = R.encode();
        const ih = nonce(Rencoded.concat(publicKey).concat(message));
        const S = posMod(r + ih * a, ED25519_CURVE_ORDER);

        return Rencoded.concat(encodeCurveInt(S));
    },

    /**
     * Creates a 64 byte signature.
     * @param {number[]} message 
     * @param {number[]} privateKey 
     * @returns {number[]} 64 byte signature.
     */
    sign: (message, privateKey) => {
        return Ed25519.signBip32(message, Crypto.sha2_512(privateKey));
    },

    /**
     * Returns `true` if the signature is correct.
     * @param {number[]} signature 
     * @param {number[]} message 
     * @param {number[]} publicKey 
     * @returns {boolean}
     */
    verify: (signature, message, publicKey) => {
        if (signature.length != 64) {
            throw new Error(`unexpected signature length ${signature.length}`);
        }

        if (publicKey.length != 32) {
            throw new Error(`unexpected publickey length ${publicKey.length}`);
        }

        const R = CurvePointImpl.decode(signature.slice(0, 32));
        const A = CurvePointImpl.decode(publicKey);
        const S = decodeCurveInt(signature.slice(32, 64));
        const h = nonce(signature.slice(0, 32).concat(publicKey).concat(message));

        const left = CurvePointImpl.BASE.mul(S);
        const right = R.add(A.mul(h));

        return left.equals(right);
    }
}

/**
 * Standard English Bip39 dictionary consisting of 2048 words allowing wallet root keys to be formed by a phrase of 12, 15, 18, 21 or 24 of these words.
 */
export const BIP39_DICT_EN = [
    "abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", "accident", "account", "accuse", "achieve", "acid", "acoustic", "acquire", "across", "act", "action", "actor", "actress", "actual", "adapt", "add", "addict", "address", "adjust", "admit", "adult", "advance", "advice", "aerobic", "affair", "afford", "afraid", "again", "age", "agent", "agree", "ahead", "aim", "air", "airport", "aisle", "alarm", "album", "alcohol", "alert", "alien", "all", "alley", "allow", "almost", "alone", "alpha", "already", "also", "alter", "always", "amateur", "amazing", "among", "amount", "amused", "analyst", "anchor", "ancient", "anger", "angle", "angry", "animal", "ankle", "announce", "annual", "another", "answer", "antenna", "antique", "anxiety", "any", "apart", "apology", "appear", "apple", "approve", "april", "arch", "arctic", "area", "arena", "argue", "arm", "armed", "armor", "army", "around", "arrange", "arrest", "arrive", "arrow", "art", "artefact", "artist", "artwork", "ask", "aspect", "assault", "asset", "assist", "assume", "asthma", "athlete", "atom", "attack", "attend", "attitude", "attract", "auction", "audit", "august", "aunt", "author", "auto", "autumn", "average", "avocado", "avoid", "awake", "aware", "away", "awesome", "awful", "awkward", "axis",
    "baby", "bachelor", "bacon", "badge", "bag", "balance", "balcony", "ball", "bamboo", "banana", "banner", "bar", "barely", "bargain", "barrel", "base", "basic", "basket", "battle", "beach", "bean", "beauty", "because", "become", "beef", "before", "begin", "behave", "behind", "believe", "below", "belt", "bench", "benefit", "best", "betray", "better", "between", "beyond", "bicycle", "bid", "bike", "bind", "biology", "bird", "birth", "bitter", "black", "blade", "blame", "blanket", "blast", "bleak", "bless", "blind", "blood", "blossom", "blouse", "blue", "blur", "blush", "board", "boat", "body", "boil", "bomb", "bone", "bonus", "book", "boost", "border", "boring", "borrow", "boss", "bottom", "bounce", "box", "boy", "bracket", "brain", "brand", "brass", "brave", "bread", "breeze", "brick", "bridge", "brief", "bright", "bring", "brisk", "broccoli", "broken", "bronze", "broom", "brother", "brown", "brush", "bubble", "buddy", "budget", "buffalo", "build", "bulb", "bulk", "bullet", "bundle", "bunker", "burden", "burger", "burst", "bus", "business", "busy", "butter", "buyer", "buzz",
    "cabbage", "cabin", "cable", "cactus", "cage", "cake", "call", "calm", "camera", "camp", "can", "canal", "cancel", "candy", "cannon", "canoe", "canvas", "canyon", "capable", "capital", "captain", "car", "carbon", "card", "cargo", "carpet", "carry", "cart", "case", "cash", "casino", "castle", "casual", "cat", "catalog", "catch", "category", "cattle", "caught", "cause", "caution", "cave", "ceiling", "celery", "cement", "census", "century", "cereal", "certain", "chair", "chalk", "champion", "change", "chaos", "chapter", "charge", "chase", "chat", "cheap", "check", "cheese", "chef", "cherry", "chest", "chicken", "chief", "child", "chimney", "choice", "choose", "chronic", "chuckle", "chunk", "churn", "cigar", "cinnamon", "circle", "citizen", "city", "civil", "claim", "clap", "clarify", "claw", "clay", "clean", "clerk", "clever", "click", "client", "cliff", "climb", "clinic", "clip", "clock", "clog", "close", "cloth", "cloud", "clown", "club", "clump", "cluster", "clutch", "coach", "coast", "coconut", "code", "coffee", "coil", "coin", "collect", "color", "column", "combine", "come", "comfort", "comic", "common", "company", "concert", "conduct", "confirm", "congress", "connect", "consider", "control", "convince", "cook", "cool", "copper", "copy", "coral", "core", "corn", "correct", "cost", "cotton", "couch", "country", "couple", "course", "cousin", "cover", "coyote", "crack", "cradle", "craft", "cram", "crane", "crash", "crater", "crawl", "crazy", "cream", "credit", "creek", "crew", "cricket", "crime", "crisp", "critic", "crop", "cross", "crouch", "crowd", "crucial", "cruel", "cruise", "crumble", "crunch", "crush", "cry", "crystal", "cube", "culture", "cup", "cupboard", "curious", "current", "curtain", "curve", "cushion", "custom", "cute", "cycle",
    "dad", "damage", "damp", "dance", "danger", "daring", "dash", "daughter", "dawn", "day", "deal", "debate", "debris", "decade", "december", "decide", "decline", "decorate", "decrease", "deer", "defense", "define", "defy", "degree", "delay", "deliver", "demand", "demise", "denial", "dentist", "deny", "depart", "depend", "deposit", "depth", "deputy", "derive", "describe", "desert", "design", "desk", "despair", "destroy", "detail", "detect", "develop", "device", "devote", "diagram", "dial", "diamond", "diary", "dice", "diesel", "diet", "differ", "digital", "dignity", "dilemma", "dinner", "dinosaur", "direct", "dirt", "disagree", "discover", "disease", "dish", "dismiss", "disorder", "display", "distance", "divert", "divide", "divorce", "dizzy", "doctor", "document", "dog", "doll", "dolphin", "domain", "donate", "donkey", "donor", "door", "dose", "double", "dove", "draft", "dragon", "drama", "drastic", "draw", "dream", "dress", "drift", "drill", "drink", "drip", "drive", "drop", "drum", "dry", "duck", "dumb", "dune", "during", "dust", "dutch", "duty", "dwarf", "dynamic",
    "eager", "eagle", "early", "earn", "earth", "easily", "east", "easy", "echo", "ecology", "economy", "edge", "edit", "educate", "effort", "egg", "eight", "either", "elbow", "elder", "electric", "elegant", "element", "elephant", "elevator", "elite", "else", "embark", "embody", "embrace", "emerge", "emotion", "employ", "empower", "empty", "enable", "enact", "end", "endless", "endorse", "enemy", "energy", "enforce", "engage", "engine", "enhance", "enjoy", "enlist", "enough", "enrich", "enroll", "ensure", "enter", "entire", "entry", "envelope", "episode", "equal", "equip", "era", "erase", "erode", "erosion", "error", "erupt", "escape", "essay", "essence", "estate", "eternal", "ethics", "evidence", "evil", "evoke", "evolve", "exact", "example", "excess", "exchange", "excite", "exclude", "excuse", "execute", "exercise", "exhaust", "exhibit", "exile", "exist", "exit", "exotic", "expand", "expect", "expire", "explain", "expose", "express", "extend", "extra", "eye", "eyebrow",
    "fabric", "face", "faculty", "fade", "faint", "faith", "fall", "false", "fame", "family", "famous", "fan", "fancy", "fantasy", "farm", "fashion", "fat", "fatal", "father", "fatigue", "fault", "favorite", "feature", "february", "federal", "fee", "feed", "feel", "female", "fence", "festival", "fetch", "fever", "few", "fiber", "fiction", "field", "figure", "file", "film", "filter", "final", "find", "fine", "finger", "finish", "fire", "firm", "first", "fiscal", "fish", "fit", "fitness", "fix", "flag", "flame", "flash", "flat", "flavor", "flee", "flight", "flip", "float", "flock", "floor", "flower", "fluid", "flush", "fly", "foam", "focus", "fog", "foil", "fold", "follow", "food", "foot", "force", "forest", "forget", "fork", "fortune", "forum", "forward", "fossil", "foster", "found", "fox", "fragile", "frame", "frequent", "fresh", "friend", "fringe", "frog", "front", "frost", "frown", "frozen", "fruit", "fuel", "fun", "funny", "furnace", "fury", "future",
    "gadget", "gain", "galaxy", "gallery", "game", "gap", "garage", "garbage", "garden", "garlic", "garment", "gas", "gasp", "gate", "gather", "gauge", "gaze", "general", "genius", "genre", "gentle", "genuine", "gesture", "ghost", "giant", "gift", "giggle", "ginger", "giraffe", "girl", "give", "glad", "glance", "glare", "glass", "glide", "glimpse", "globe", "gloom", "glory", "glove", "glow", "glue", "goat", "goddess", "gold", "good", "goose", "gorilla", "gospel", "gossip", "govern", "gown", "grab", "grace", "grain", "grant", "grape", "grass", "gravity", "great", "green", "grid", "grief", "grit", "grocery", "group", "grow", "grunt", "guard", "guess", "guide", "guilt", "guitar", "gun", "gym",
    "habit", "hair", "half", "hammer", "hamster", "hand", "happy", "harbor", "hard", "harsh", "harvest", "hat", "have", "hawk", "hazard", "head", "health", "heart", "heavy", "hedgehog", "height", "hello", "helmet", "help", "hen", "hero", "hidden", "high", "hill", "hint", "hip", "hire", "history", "hobby", "hockey", "hold", "hole", "holiday", "hollow", "home", "honey", "hood", "hope", "horn", "horror", "horse", "hospital", "host", "hotel", "hour", "hover", "hub", "huge", "human", "humble", "humor", "hundred", "hungry", "hunt", "hurdle", "hurry", "hurt", "husband", "hybrid",
    "ice", "icon", "idea", "identify", "idle", "ignore", "ill", "illegal", "illness", "image", "imitate", "immense", "immune", "impact", "impose", "improve", "impulse", "inch", "include", "income", "increase", "index", "indicate", "indoor", "industry", "infant", "inflict", "inform", "inhale", "inherit", "initial", "inject", "injury", "inmate", "inner", "innocent", "input", "inquiry", "insane", "insect", "inside", "inspire", "install", "intact", "interest", "into", "invest", "invite", "involve", "iron", "island", "isolate", "issue", "item", "ivory",
    "jacket", "jaguar", "jar", "jazz", "jealous", "jeans", "jelly", "jewel", "job", "join", "joke", "journey", "joy", "judge", "juice", "jump", "jungle", "junior", "junk", "just",
    "kangaroo", "keen", "keep", "ketchup", "key", "kick", "kid", "kidney", "kind", "kingdom", "kiss", "kit", "kitchen", "kite", "kitten", "kiwi", "knee", "knife", "knock", "know",
    "lab", "label", "labor", "ladder", "lady", "lake", "lamp", "language", "laptop", "large", "later", "latin", "laugh", "laundry", "lava", "law", "lawn", "lawsuit", "layer", "lazy", "leader", "leaf", "learn", "leave", "lecture", "left", "leg", "legal", "legend", "leisure", "lemon", "lend", "length", "lens", "leopard", "lesson", "letter", "level", "liar", "liberty", "library", "license", "life", "lift", "light", "like", "limb", "limit", "link", "lion", "liquid", "list", "little", "live", "lizard", "load", "loan", "lobster", "local", "lock", "logic", "lonely", "long", "loop", "lottery", "loud", "lounge", "love", "loyal", "lucky", "luggage", "lumber", "lunar", "lunch", "luxury", "lyrics",
    "machine", "mad", "magic", "magnet", "maid", "mail", "main", "major", "make", "mammal", "man", "manage", "mandate", "mango", "mansion", "manual", "maple", "marble", "march", "margin", "marine", "market", "marriage", "mask", "mass", "master", "match", "material", "math", "matrix", "matter", "maximum", "maze", "meadow", "mean", "measure", "meat", "mechanic", "medal", "media", "melody", "melt", "member", "memory", "mention", "menu", "mercy", "merge", "merit", "merry", "mesh", "message", "metal", "method", "middle", "midnight", "milk", "million", "mimic", "mind", "minimum", "minor", "minute", "miracle", "mirror", "misery", "miss", "mistake", "mix", "mixed", "mixture", "mobile", "model", "modify", "mom", "moment", "monitor", "monkey", "monster", "month", "moon", "moral", "more", "morning", "mosquito", "mother", "motion", "motor", "mountain", "mouse", "move", "movie", "much", "muffin", "mule", "multiply", "muscle", "museum", "mushroom", "music", "must", "mutual", "myself", "mystery", "myth",
    "naive", "name", "napkin", "narrow", "nasty", "nation", "nature", "near", "neck", "need", "negative", "neglect", "neither", "nephew", "nerve", "nest", "net", "network", "neutral", "never", "news", "next", "nice", "night", "noble", "noise", "nominee", "noodle", "normal", "north", "nose", "notable", "note", "nothing", "notice", "novel", "now", "nuclear", "number", "nurse", "nut",
    "oak", "obey", "object", "oblige", "obscure", "observe", "obtain", "obvious", "occur", "ocean", "october", "odor", "off", "offer", "office", "often", "oil", "okay", "old", "olive", "olympic", "omit", "once", "one", "onion", "online", "only", "open", "opera", "opinion", "oppose", "option", "orange", "orbit", "orchard", "order", "ordinary", "organ", "orient", "original", "orphan", "ostrich", "other", "outdoor", "outer", "output", "outside", "oval", "oven", "over", "own", "owner", "oxygen", "oyster", "ozone",
    "pact", "paddle", "page", "pair", "palace", "palm", "panda", "panel", "panic", "panther", "paper", "parade", "parent", "park", "parrot", "party", "pass", "patch", "path", "patient", "patrol", "pattern", "pause", "pave", "payment", "peace", "peanut", "pear", "peasant", "pelican", "pen", "penalty", "pencil", "people", "pepper", "perfect", "permit", "person", "pet", "phone", "photo", "phrase", "physical", "piano", "picnic", "picture", "piece", "pig", "pigeon", "pill", "pilot", "pink", "pioneer", "pipe", "pistol", "pitch", "pizza", "place", "planet", "plastic", "plate", "play", "please", "pledge", "pluck", "plug", "plunge", "poem", "poet", "point", "polar", "pole", "police", "pond", "pony", "pool", "popular", "portion", "position", "possible", "post", "potato", "pottery", "poverty", "powder", "power", "practice", "praise", "predict", "prefer", "prepare", "present", "pretty", "prevent", "price", "pride", "primary", "print", "priority", "prison", "private", "prize", "problem", "process", "produce", "profit", "program", "project", "promote", "proof", "property", "prosper", "protect", "proud", "provide", "public", "pudding", "pull", "pulp", "pulse", "pumpkin", "punch", "pupil", "puppy", "purchase", "purity", "purpose", "purse", "push", "put", "puzzle", "pyramid",
    "quality", "quantum", "quarter", "question", "quick", "quit", "quiz", "quote",
    "rabbit", "raccoon", "race", "rack", "radar", "radio", "rail", "rain", "raise", "rally", "ramp", "ranch", "random", "range", "rapid", "rare", "rate", "rather", "raven", "raw", "razor", "ready", "real", "reason", "rebel", "rebuild", "recall", "receive", "recipe", "record", "recycle", "reduce", "reflect", "reform", "refuse", "region", "regret", "regular", "reject", "relax", "release", "relief", "rely", "remain", "remember", "remind", "remove", "render", "renew", "rent", "reopen", "repair", "repeat", "replace", "report", "require", "rescue", "resemble", "resist", "resource", "response", "result", "retire", "retreat", "return", "reunion", "reveal", "review", "reward", "rhythm", "rib", "ribbon", "rice", "rich", "ride", "ridge", "rifle", "right", "rigid", "ring", "riot", "ripple", "risk", "ritual", "rival", "river", "road", "roast", "robot", "robust", "rocket", "romance", "roof", "rookie", "room", "rose", "rotate", "rough", "round", "route", "royal", "rubber", "rude", "rug", "rule", "run", "runway", "rural",
    "sad", "saddle", "sadness", "safe", "sail", "salad", "salmon", "salon", "salt", "salute", "same", "sample", "sand", "satisfy", "satoshi", "sauce", "sausage", "save", "say", "scale", "scan", "scare", "scatter", "scene", "scheme", "school", "science", "scissors", "scorpion", "scout", "scrap", "screen", "script", "scrub", "sea", "search", "season", "seat", "second", "secret", "section", "security", "seed", "seek", "segment", "select", "sell", "seminar", "senior", "sense", "sentence", "series", "service", "session", "settle", "setup", "seven", "shadow", "shaft", "shallow", "share", "shed", "shell", "sheriff", "shield", "shift", "shine", "ship", "shiver", "shock", "shoe", "shoot", "shop", "short", "shoulder", "shove", "shrimp", "shrug", "shuffle", "shy", "sibling", "sick", "side", "siege", "sight", "sign", "silent", "silk", "silly", "silver", "similar", "simple", "since", "sing", "siren", "sister", "situate", "six", "size", "skate", "sketch", "ski", "skill", "skin", "skirt", "skull", "slab", "slam", "sleep", "slender", "slice", "slide", "slight", "slim", "slogan", "slot", "slow", "slush", "small", "smart", "smile", "smoke", "smooth", "snack", "snake", "snap", "sniff", "snow", "soap", "soccer", "social", "sock", "soda", "soft", "solar", "soldier", "solid", "solution", "solve", "someone", "song", "soon", "sorry", "sort", "soul", "sound", "soup", "source", "south", "space", "spare", "spatial", "spawn", "speak", "special", "speed", "spell", "spend", "sphere", "spice", "spider", "spike", "spin", "spirit", "split", "spoil", "sponsor", "spoon", "sport", "spot", "spray", "spread", "spring", "spy", "square", "squeeze", "squirrel", "stable", "stadium", "staff", "stage", "stairs", "stamp", "stand", "start", "state", "stay", "steak", "steel", "stem", "step", "stereo", "stick", "still", "sting", "stock", "stomach", "stone", "stool", "story", "stove", "strategy", "street", "strike", "strong", "struggle", "student", "stuff", "stumble", "style", "subject", "submit", "subway", "success", "such", "sudden", "suffer", "sugar", "suggest", "suit", "summer", "sun", "sunny", "sunset", "super", "supply", "supreme", "sure", "surface", "surge", "surprise", "surround", "survey", "suspect", "sustain", "swallow", "swamp", "swap", "swarm", "swear", "sweet", "swift", "swim", "swing", "switch", "sword", "symbol", "symptom", "syrup", "system",
    "table", "tackle", "tag", "tail", "talent", "talk", "tank", "tape", "target", "task", "taste", "tattoo", "taxi", "teach", "team", "tell", "ten", "tenant", "tennis", "tent", "term", "test", "text", "thank", "that", "theme", "then", "theory", "there", "they", "thing", "this", "thought", "three", "thrive", "throw", "thumb", "thunder", "ticket", "tide", "tiger", "tilt", "timber", "time", "tiny", "tip", "tired", "tissue", "title", "toast", "tobacco", "today", "toddler", "toe", "together", "toilet", "token", "tomato", "tomorrow", "tone", "tongue", "tonight", "tool", "tooth", "top", "topic", "topple", "torch", "tornado", "tortoise", "toss", "total", "tourist", "toward", "tower", "town", "toy", "track", "trade", "traffic", "tragic", "train", "transfer", "trap", "trash", "travel", "tray", "treat", "tree", "trend", "trial", "tribe", "trick", "trigger", "trim", "trip", "trophy", "trouble", "truck", "true", "truly", "trumpet", "trust", "truth", "try", "tube", "tuition", "tumble", "tuna", "tunnel", "turkey", "turn", "turtle", "twelve", "twenty", "twice", "twin", "twist", "two", "type", "typical",
    "ugly", "umbrella", "unable", "unaware", "uncle", "uncover", "under", "undo", "unfair", "unfold", "unhappy", "uniform", "unique", "unit", "universe", "unknown", "unlock", "until", "unusual", "unveil", "update", "upgrade", "uphold", "upon", "upper", "upset", "urban", "urge", "usage", "use", "used", "useful", "useless", "usual", "utility",
    "vacant", "vacuum", "vague", "valid", "valley", "valve", "van", "vanish", "vapor", "various", "vast", "vault", "vehicle", "velvet", "vendor", "venture", "venue", "verb", "verify", "version", "very", "vessel", "veteran", "viable", "vibrant", "vicious", "victory", "video", "view", "village", "vintage", "violin", "virtual", "virus", "visa", "visit", "visual", "vital", "vivid", "vocal", "voice", "void", "volcano", "volume", "vote", "voyage",
    "wage", "wagon", "wait", "walk", "wall", "walnut", "want", "warfare", "warm", "warrior", "wash", "wasp", "waste", "water", "wave", "way", "wealth", "weapon", "wear", "weasel", "weather", "web", "wedding", "weekend", "weird", "welcome", "west", "wet", "whale", "what", "wheat", "wheel", "when", "where", "whip", "whisper", "wide", "width", "wife", "wild", "will", "win", "window", "wine", "wing", "wink", "winner", "winter", "wire", "wisdom", "wise", "wish", "witness", "wolf", "woman", "wonder", "wood", "wool", "word", "work", "world", "worry", "worth", "wrap", "wreck", "wrestle", "wrist", "write", "wrong",
    "yard", "year", "yellow", "you", "young", "youth",
    "zebra", "zero", "zone", "zoo"
];



//////////////////////////////////
// Section 5: Cbor encoder/decoder
//////////////////////////////////

/**
 * @typedef {(i: number, bytes: number[]) => void} Decoder
 */

/**
 * Base class of any Cbor serializable data class
 * Also 
 */
export class CborData {
	constructor() {
	}

	/**
	 * @returns {number[]}
	 */
	toCbor() {
		throw new Error("not yet implemented");
	}

	/**
	 * @returns {string}
	 */
	toCborHex() {
		return bytesToHex(this.toCbor())
	}
}

/**
 * Helper methods for (de)serializing data to/from Cbor.
 * 
 * **Note**: Each decoding method mutates the input `bytes` by shifting it to the following CBOR element.
 * @namespace
 */
export const Cbor = {
	/**
	 * @param {number} m - major type
	 * @param {bigint} n - size parameter
	 * @returns {number[]} - uint8 bytes
	 */
	encodeHead: (m, n) => {
		if (n <= 23n) {
			return [32*m + Number(n)];
		} else if (n >= 24n && n <= 255n) {
			return [32*m + 24, Number(n)];
		} else if (n >= 256n && n <= 256n*256n - 1n) {
			return [32*m + 25, Number((n/256n)%256n), Number(n%256n)];
		} else if (n >= 256n*256n && n <= 256n*256n*256n*256n - 1n) {
			const e4 = bigIntToBytes(n);

			while (e4.length < 4) {
				e4.unshift(0);
			}
			return [32*m + 26].concat(e4);
		} else if (n >= 256n*256n*256n*256n && n <= 256n*256n*256n*256n*256n*256n*256n*256n - 1n) {
			const e8 = bigIntToBytes(n);

			while(e8.length < 8) {
				e8.unshift(0);
			}
			return [32*m + 27].concat(e8);
		} else {
			throw new Error("n out of range");
		}
	},

	/**
	 * @param {number[]} bytes - mutated to contain the rest
	 * @returns {[number, bigint]} - [majorType, n]
	 */
	decodeHead: (bytes) => {
		if (bytes.length == 0) {
			throw new Error("empty cbor head");
		}

		const first = assertDefined(bytes.shift());

		if (first%32 <= 23) {
			return [idiv(first, 32), BigInt(first%32)];
		} else if (first%32 == 24) {
			return [idiv(first, 32), bytesToBigInt(bytes.splice(0, 1))];
		} else if (first%32 == 25) {
			return [idiv(first, 32), bytesToBigInt(bytes.splice(0, 2))];
		} else if (first%32 == 26) {
			return [idiv(first, 32), bytesToBigInt(bytes.splice(0, 4))];
		} else if (first%32 == 27) {
			return [idiv(first, 32), bytesToBigInt(bytes.splice(0, 8))];
		} else {
			throw new Error("bad header");
		}
	},

	/**
	 * @param {number} m
	 * @returns {number[]}
	 */
	encodeIndefHead: (m) => {
		return [32*m + 31];
	},

	/**
	 * @param {number[]} bytes - cbor bytes
	 * @returns {number} - majorType
	 */
	decodeIndefHead: (bytes) => {
		const first = assertDefined(bytes.shift());

		const m = idiv(first - 31, 32);

		return m;
	},

	/**
	 * @param {number[]} bytes
	 * @returns {boolean}
	 */
	isNull: (bytes) => {
		return bytes[0] == 246;
	},

	/**
	 * Encode `null` into its CBOR representation.
	 * @returns {number[]}
	 */
	encodeNull: () => {
		return [246];
	},

	/**
	 * Checks if next element in `bytes` is a `null`.
	 * Throws an error if it isn't. 
	 * @param {number[]} bytes
	 */
	decodeNull: (bytes) => {
		const b = assertDefined(bytes.shift());

		if (b != 246) {
			throw new Error("not null");
		}
	},

	/**
	 * Encodes a `boolean` into its CBOR representation.
	 * @param {boolean} b
	 * @returns {number[]}
	 */
	encodeBool: (b) => {
		if (b) {
			return [245];
		} else {
			return [244];
		}
	},

	/**
	 * Decodes a CBOR encoded `boolean`.
	 * Throws an error if the next element in bytes isn't a `boolean`.
	 * @param {number[]} bytes
	 * @returns {boolean}
	 */
	decodeBool: (bytes) => {
		const b = assertDefined(bytes.shift());

		if (b == 245) {
			return true;
		} else if (b == 244) {
			return false;
		} else {
			throw new Error("unexpected non-boolean cbor object");
		}
	},

	/**
	 * @param {number[]} bytes 
	 * @returns {boolean}
	 */
	isBytes: (bytes) => {
		return Cbor.isDefBytes(bytes) || Cbor.isIndefBytes(bytes);
	},

	/**
	 * @param {number[]} bytes
	 * @returns {boolean}
	 */
	isDefBytes: (bytes) => {
		if (bytes.length == 0) {
			throw new Error("empty cbor bytes");
		}

		const [m, _] = Cbor.decodeHead(bytes.slice(0, 9));

		return m == 2;
	},

	/**
	 * @param {number[]} bytes
	 * @returns {boolean}
	 */
	isIndefBytes: (bytes) => {
		if (bytes.length == 0) {
			throw new Error("empty cbor bytes");
		}

		return 2*32 + 31 == bytes[0];
	},

	/**
	 * Wraps a list of bytes using CBOR. Optionally splits the bytes into chunks.
	 * @example
	 * bytesToHex(Cbor.encodeBytes(hexToBytes("4d01000033222220051200120011"))) == "4e4d01000033222220051200120011"
	 * @param {number[]} bytes
	 * @param {boolean} splitIntoChunks
	 * @returns {number[]} - cbor bytes
	 */
	encodeBytes: (bytes, splitIntoChunks = false) => {
		bytes = bytes.slice();

		if (bytes.length <= 64 || !splitIntoChunks) {
			const head = Cbor.encodeHead(2, BigInt(bytes.length));
			return head.concat(bytes);
		} else {
			let res = Cbor.encodeIndefHead(2);

			while (bytes.length > 0) {
				const chunk = bytes.splice(0, 64);

				res = res.concat(Cbor.encodeHead(2, BigInt(chunk.length))).concat(chunk);
			}

			res.push(255);

			return res;
		}
	},

	/**
	 * Unwraps a CBOR encoded list of bytes. 
	 * @example
	 * bytesToHex(Cbor.decodeBytes(hexToBytes("4e4d01000033222220051200120011"))) == "4d01000033222220051200120011"
	 * @param {number[]} bytes - cborbytes, mutated to form remaining
	 * @returns {number[]} - byteArray
	 */
	decodeBytes: (bytes) => {
		// check header type
		assert(bytes.length > 0);

		if (Cbor.isIndefBytes(bytes)) {
			// multiple chunks
			void bytes.shift();

			/**
			 * @type {number[]}
			 */
			let res = [];

			while(bytes[0] != 255) {
				const [_, n] = Cbor.decodeHead(bytes);
				if (n > 64n) {
					throw new Error("bytearray chunk too large");
				}

				res = res.concat(bytes.splice(0, Number(n)));
			}

			assert(bytes.shift() == 255);

			return res;
		} else {
			const [_, n] = Cbor.decodeHead(bytes);

			return bytes.splice(0, Number(n));
		}
	},

	/**
	 * @param {number[]} bytes
	 * @returns {boolean}
	 */
	isUtf8: (bytes) => {
		const [m, _] = Cbor.decodeHead(bytes.slice());

		return m == 3;
	},

	/**
	 * Encodes a Utf8 string into Cbor bytes.
	 * Strings can be split into lists with chunks of up to 64 bytes
	 * to play nice with Cardano tx metadata constraints.
	 * @param {string} str
	 * @param {boolean} split
	 * @returns {number[]}
	 */
	encodeUtf8: (str, split = false) => {
		const bytes = textToBytes(str);

		if (split && bytes.length > 64) {
			/** @type {number[][]} */
			const chunks = [];

			let i = 0;
			while (i < bytes.length) {
				// We encode the largest chunk up to 64 bytes
				// that is valid UTF-8
				let maxChunkLength = 64, chunk;
				while (true) {
					try {
						chunk = bytes.slice(i, i + maxChunkLength);
						bytesToText(chunk); // Decode to validate utf-8
						break;
					} catch(_) {
						maxChunkLength--;
					}
				}
				chunks.push(Cbor.encodeHead(3, BigInt(chunk.length)).concat(chunk));
				i += chunk.length;
			}

			return Cbor.encodeDefList(chunks);
		} else {
			return Cbor.encodeHead(3, BigInt(bytes.length)).concat(bytes);
		}
	},

	/**
	 * @internal
	 * @param {number[]} bytes
	 * @returns {string}
	 */
	decodeUtf8Internal: (bytes) => {
		const [m, n] = Cbor.decodeHead(bytes);

		assert(m === 3);

		return bytesToText(bytes.splice(0, Number(n)));
	},

	/**
	 * @param {number[]} bytes
	 * @returns {string}
	 */
	decodeUtf8: (bytes) => {
		assert(bytes.length > 0);

		if (Cbor.isDefList(bytes)) {
			let result = "";

			Cbor.decodeList(bytes, (_, itemBytes) => {
				result += Cbor.decodeUtf8Internal(itemBytes);
			});

			return result;
		} else {
			return Cbor.decodeUtf8Internal(bytes);
		}
	},

	/**
	 * Encodes a bigint integer using CBOR.
	 * @param {bigint} n
	 * @returns {number[]}
	 */
	encodeInteger: (n) => {
		if (n >= 0n && n <= (2n << 63n) - 1n) {
			return Cbor.encodeHead(0, n);
		} else if (n >= (2n << 63n)) {
			return Cbor.encodeHead(6, 2n).concat(Cbor.encodeBytes(bigIntToBytes(n)));
		} else if (n <= -1n && n >= -(2n << 63n)) {
			return Cbor.encodeHead(1, -n - 1n);
		} else {
			return Cbor.encodeHead(6, 3n).concat(Cbor.encodeBytes(bigIntToBytes(-n - 1n)));
		}
	},

	/**
	 * Decodes a CBOR encoded bigint integer.
	 * @param {number[]} bytes
	 * @returns {bigint}
	 */
	decodeInteger: (bytes) => {
		const [m, n] = Cbor.decodeHead(bytes);

		if (m == 0) {
			return n;
		} else if (m == 1) {
			return -n - 1n;
		} else if (m == 6) {
			if (n == 2n) {
				const b = Cbor.decodeBytes(bytes);

				return bytesToBigInt(b);
			} else if (n == 3n) {
				const b = Cbor.decodeBytes(bytes);

				return -bytesToBigInt(b) - 1n;
			} else {
				throw new Error(`unexpected tag n:${n}`);
			}
		} else {
			throw new Error(`unexpected tag m:${m}`);
		}
	},

	/**
	 * @param {number[]} bytes
	 * @returns {boolean}
	 */
	isIndefList: (bytes) => {
		if (bytes.length == 0) {
			throw new Error("empty cbor bytes");
		}

		return 4*32 + 31 == bytes[0];
	},

	/**
	 * @internal
	 * @returns {number[]}
	 */
	encodeIndefListStart: () => {
		return Cbor.encodeIndefHead(4);
	},

	/**
	 * @internal
	 * @param {CborData[] | number[][]} list
	 * @returns {number[]}
	 */
	encodeListInternal: (list) => {
		/**
		 * @type {number[]}
		 */
		let res = [];
		for (let item of list) {
			if (item instanceof CborData) {
				res = res.concat(item.toCbor());
			} else {
				res = res.concat(item);
			}
		}

		return res;
	},

	/**
	 * @internal
	 * @returns {number[]}
	 */
	encodeIndefListEnd: () => {
		return [255];
	},

	/**
	 * This follows the serialization format that the Haskell input-output-hk/plutus UPLC evaluator (i.e. empty lists use `encodeDefList`, non-empty lists use `encodeIndefList`).
	 * See [well-typed/cborg/serialise/src/Codec/Serialise/Class.hs](https://github.com/well-typed/cborg/blob/4bdc818a1f0b35f38bc118a87944630043b58384/serialise/src/Codec/Serialise/Class.hs#L181).
	 * @param {CborData[] | number[][]} list
	 * @returns {number[]}
	 */
	encodeList: (list) => {
		return list.length ? Cbor.encodeIndefList(list) : Cbor.encodeDefList(list);
	},

	/**
	 * Encodes a list of CBOR encodeable items using CBOR indefinite length encoding.
	 * @param {CborData[] | number[][]} list Each item is either already serialized, or a CborData instance with a toCbor() method.
	 * @returns {number[]}
	 */
	encodeIndefList: (list) => {
		return Cbor.encodeIndefListStart().concat(Cbor.encodeListInternal(list)).concat(Cbor.encodeIndefListEnd());
	},

	/**
	 * @param {number[]} bytes
	 * @returns {boolean}
	 */
	isDefList: (bytes) => {
		try {
			const [m, _] = Cbor.decodeHead(bytes.slice(0, 9));
			return m == 4;
		} catch (error) {
			if (error.message.includes("bad header")) return false;
			throw error;
		}
	},

	/**
	 * @param {bigint} n
	 * @returns {number[]}
	 */
	encodeDefListStart: (n) => {
		return Cbor.encodeHead(4, n);
	},

	/**
	 * Encodes a list of CBOR encodeable items using CBOR definite length encoding
	 * (i.e. header bytes of the element represent the length of the list).
	 * @param {CborData[] | number[][]} list Each item is either already serialized, or a CborData instance with a toCbor() method.
	 * @returns {number[]}
	 */
	encodeDefList: (list) => {
		return Cbor.encodeDefListStart(BigInt(list.length)).concat(Cbor.encodeListInternal(list));
	},

	/**
	 * @param {number[]} bytes
	 * @returns {boolean}
	 */
	isList: (bytes) => {
		return Cbor.isIndefList(bytes) || Cbor.isDefList(bytes);
	},

	/**
	 * Decodes a CBOR encoded list.
	 * A decoder function is called with the bytes of every contained item (nothing is returning directly).
	 * @param {number[]} bytes
	 * @param {Decoder} itemDecoder
	 */
	decodeList: (bytes, itemDecoder) => {
		if (Cbor.isIndefList(bytes)) {
			assert(Cbor.decodeIndefHead(bytes) == 4);

			let i = 0;
			while(bytes[0] != 255) {
				itemDecoder(i, bytes);
				i++;
			}

			assert(bytes.shift() == 255);
		} else {
			const [m, n] = Cbor.decodeHead(bytes);

			assert(m == 4);

			for (let i = 0; i < Number(n); i++) {
				itemDecoder(i, bytes);
			}
		}
	},

	/**
	 * @param {number[]} bytes
	 * @returns {boolean}
	 */
	isTuple: (bytes) => {
		return Cbor.isIndefList(bytes) || Cbor.isDefList(bytes);
	},

	/**
	 * @param {number[][]} tuple
	 * @returns {number[]}
	 */
	encodeTuple: (tuple) => {
		return Cbor.encodeDefList(tuple);
	},

	/**
	 * @param {number[]} bytes
	 * @param {Decoder} tupleDecoder
	 * @returns {number} - returns the size of the tuple
	 */
	decodeTuple: (bytes, tupleDecoder) => {
		let count = 0;

		Cbor.decodeList(bytes, (_, itemBytes) => {
			tupleDecoder(count, itemBytes);
			count++;
		});

		return count;
	},

	/**
	 * @param {number[]} bytes
	 * @returns {boolean}
	 */
	isMap: (bytes) => {
		const [m, _] = Cbor.decodeHead(bytes.slice(0, 9));

		return m == 5;
	},

	/**
	 * @internal
	 * @param {[CborData | number[], CborData | number[]][]} pairList
	 * @returns {number[]}
	 */
	encodeMapInternal: (pairList) => {
		/**
		 * @type {number[]}
		 */
		let res = [];

		for (let pair of pairList) {
			const key = pair[0];
			const value = pair[1];

			if (key instanceof CborData) {
				res = res.concat(key.toCbor());
			} else {
				res = res.concat(key);
			}

			if (value instanceof CborData) {
				res = res.concat(value.toCbor());
			} else {
				res = res.concat(value);
			}
		}

		return res;
	},

	/**
	 * Encodes a list of key-value pairs.
	 * @param {[CborData | number[], CborData | number[]][]} pairList  Each key and each value is either a CborData instance with a toCbor method defined, or an already encoded list of CBOR bytes.
	 * @returns {number[]}
	 */
	encodeMap: (pairList) => {
		return Cbor.encodeHead(5, BigInt(pairList.length)).concat(Cbor.encodeMapInternal(pairList));
	},

	/**
	 * Decodes a CBOR encoded map.
	 * Calls a decoder function for each key-value pair (nothing is returned directly).
	 * 
	 * The decoder function is responsible for separating the key from the value,
	 * which are simply stored as consecutive CBOR elements.
	 * @param {number[]} bytes
	 * @param {Decoder} pairDecoder
	 */
	decodeMap: (bytes, pairDecoder) => {
		const [m, n] = Cbor.decodeHead(bytes);

		assert(m == 5);

		for (let i = 0; i < n; i++) {
			pairDecoder(i, bytes);
		}
	},

	/**
	 * @param {number[]} bytes
	 * @returns {boolean}
	 */
	isObject: (bytes) => {
		return Cbor.isMap(bytes);
	},

	/**
	 * Encodes an object with optional fields.
	 * @param {Map<number, CborData | number[]>} object A `Map` with integer keys representing the field indices.
	 * @returns {number[]}
	 */
	encodeObject: (object) => {
		return Cbor.encodeMap(Array.from(object.entries()).map(pair => [
			Cbor.encodeInteger(BigInt(pair[0])),
			pair[1]
		]));
	},

	/**
	 * Decodes a CBOR encoded object. For each field a decoder is called which takes the field index and the field bytes as arguments.
	 * @param {number[]} bytes
	 * @param {Decoder} fieldDecoder
	 * @returns {Set<number>}
	 */
	decodeObject: (bytes, fieldDecoder) => {
		/** @type {Set<number>} */
		const done = new Set();

		Cbor.decodeMap(bytes, (_, pairBytes) => {
			let i = Number(Cbor.decodeInteger(pairBytes));

			fieldDecoder(i, pairBytes);

			done.add(i);
		});

		return done;
	},

	/**
	 * Unrelated to constructor
	 * @param {bigint} tag
	 * @returns {number[]}
	 */
	encodeTag: (tag) => {
		return Cbor.encodeHead(6, tag);
	},

	/**
	 * @param {number[]} bytes
	 * @returns {bigint}
	 */
	decodeTag: (bytes) => {
		const [m, n] = Cbor.decodeHead(bytes);

		assert(m == 6);

		return n;
	},

	/**
	 * @param {number[]} bytes
	 * @returns {boolean}
	 */
	isConstr: (bytes) => {
		if (bytes.length == 0) {
			throw new Error("empty cbor bytes");
		}

		const [m, _] = Cbor.decodeHead(bytes.slice(0, 9));

		return m == 6;
	},

	/**
	 * Encode a constructor tag of a ConstrData type
	 * @param {number} tag
	 * @returns {number[]}
	 */
	encodeConstrTag: (tag) => {
		if (tag >= 0 && tag <= 6) {
			return Cbor.encodeHead(6, 121n + BigInt(tag));
		} else if (tag >= 7 && tag <= 127) {
			return Cbor.encodeHead(6, 1280n + BigInt(tag - 7));
		} else {
			return Cbor.encodeHead(6, 102n).concat(Cbor.encodeHead(4, 2n)).concat(Cbor.encodeInteger(BigInt(tag)));
		}
	},

	/**
	 * @param {number} tag
	 * @param {CborData[] | number[][]} fields
	 * @returns {number[]}
	 */
	encodeConstr: (tag, fields) => {
		return Cbor.encodeConstrTag(tag).concat(Cbor.encodeList(fields));
	},

	/**
	 * @param {number[]} bytes
	 * @returns {number}
	 */
	decodeConstrTag: (bytes) => {
		// constr
		const [m, n] = Cbor.decodeHead(bytes);

		assert(m == 6);

		if (n < 127n) {
			return Number(n - 121n);
		} else if (n == 102n) {
			const [mCheck, nCheck] = Cbor.decodeHead(bytes);
			assert(mCheck == 4 && nCheck == 2n);

			return Number(Cbor.decodeInteger(bytes));
		} else {
			return Number(n - 1280n + 7n);
		}
	},

	/**
	 * Returns the tag
	 * @param {number[]} bytes
	 * @param {Decoder} fieldDecoder
	 * @returns {number}
	 */
	decodeConstr: (bytes, fieldDecoder) => {
		const tag = Cbor.decodeConstrTag(bytes);

		Cbor.decodeList(bytes, fieldDecoder);

		return tag;
	}
}



/////////////////////////////
// Section 6: Uplc data types
/////////////////////////////

/**
 * Min memory used by a UplcData value during validation
 * @internal
 * @type {number}
 */
const UPLC_DATA_NODE_MEM_SIZE = 4;

/**
 * Base class for Plutus-core data classes (not the same as Plutus-core value classes!)
 */
export class UplcData extends CborData {
	constructor() {
		super();
	}

	/**
	 * @param {TransferUplcAst} other 
	 * @returns {any}
	 */
	transfer(other) {
		throw new Error("not yet implemented");
	}

	/**
	 * Estimate of memory usage during validation
	 * @type {number}
	 */
	get memSize() {
		throw new Error("not yet implemented");
	}

	/**
	 * Compares the schema jsons
	 * @param {UplcData} other
	 * @returns {boolean}
	 */
	isSame(other) {
		return this.toSchemaJson() == other.toSchemaJson();
	}

	/**
	 * @internal
	 * @type {number[]}
	 */
	get bytes() {
		throw new Error("not a bytearray");
	}

	/**
	 * @internal
	 * @type {bigint}
	 */
	get int() {
		throw new Error("not an int");
	}

	/**
	 * @internal
	 * @type {number}
	 */
	get index() {
		throw new Error("not a constr");
	}

	/**
	 * @internal
	 * @type {UplcData[]}
	 */
	get fields() {
		throw new Error("not a constr");
	}

	/**
	 * @internal
	 * @type {UplcData[]}
	 */
	get list() {
		throw new Error("not a list");
	}

	/**
	 * @internal
	 * @type {[UplcData, UplcData][]}
	 */
	get map() {
		throw new Error("not a map");
	}

	/**
	 * @returns {string}
	 */
	toString() {
		throw new Error("not yet implemented");
	}

	/**
	 * @internal
	 * @returns {IR}
	 */
	toIR() {
		throw new Error("not yet implemented");
	}

	/**
	 * @returns {string}
	 */
	toSchemaJson() {
		throw new Error("not yet implemented");
	}

	/**
	 * @param {number[] | string} bytes
	 * @returns {UplcData}
	 */
	static fromCbor(bytes) {
		if (typeof bytes == "string") {
			return UplcData.fromCbor(hexToBytes(bytes));
		} else {
			if (Cbor.isList(bytes)) {
				return ListData.fromCbor(bytes);
			} else if (Cbor.isIndefBytes(bytes)) {
				return ByteArrayData.fromCbor(bytes);
			} else {
				if (Cbor.isDefBytes(bytes)) {
					return ByteArrayData.fromCbor(bytes);
				} else if (Cbor.isMap(bytes)) {
					return MapData.fromCbor(bytes);
				} else if (Cbor.isConstr(bytes)) {
					return ConstrData.fromCbor(bytes);
				} else {
					// int, must come last
					return IntData.fromCbor(bytes);
				}
			}
		}
	}
}

/**
 * Represents an unbounded integer (bigint).
 */
export class IntData extends UplcData {
	#value;

	/**
	 * @param {bigint} value
	 */
	constructor(value) {
		super();
		this.#value = value;
	}

	/**
	 * @param {TransferUplcAst} other 
	 * @returns {any}
	 */
	transfer(other) {
		return other.transferIntData(this.#value);
	}

	/**
	 * @type {bigint}
	 */
	get value() {
		return this.#value;
	}

	/**
	 * Alias for `IntData.value`.
	 * @type {bigint}
	 */
	get int() {
		return this.#value;
	}

    /**
     * Calculate the mem size of a integer (without the DATA_NODE overhead)
	 * @internal
     * @param {bigint} value
     * @returns {number}
     */
    static memSizeInternal(value) {
        if (value == 0n) {
			return 1;
		} else {
			const abs = value > 0n ? value : -value;

			return Math.floor(Math.floor(Math.log2(Number(abs)))/64) + 1;
		}
    }

	/**
	 * @type {number}
	 */
	get memSize() {
		return UPLC_DATA_NODE_MEM_SIZE + IntData.memSizeInternal(this.#value);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return this.#value.toString();
	}

	/**
	 * Returns integer literal wrapped with integer data function call.
	 * @internal
	 * @returns {IR}
	 */
	toIR() {
		return new IR(`__core__iData(${this.#value.toString()})`);
	}

	/**
	 * Returns string, not js object, because of unbounded integers
	 * @returns {string}
	 */
	toSchemaJson() {
		return `{"int": ${this.#value.toString()}}`;
	}

	/**
	 * @returns {number[]}
	 */
	toCbor() {
		return Cbor.encodeInteger(this.#value);
	}

	/**
	 * @param {number[]} bytes
	 * @returns {IntData}
	 */
	static fromCbor(bytes) {
		return new IntData(Cbor.decodeInteger(bytes));
	}
}

/**
 * Plutus-core bytearray data class.
 * Wraps a regular list of uint8 numbers (so not Uint8Array)
 */
export class ByteArrayData extends UplcData {
	#bytes;

	/**
	 * @param {number[]} bytes
	 */
	constructor(bytes) {
		super();
		this.#bytes = bytes;
	}

	/**
	 * Applies utf-8 encoding
	 * @param {string} s
	 * @returns {ByteArrayData}
	 */
	static fromString(s) {
		let bytes = textToBytes(s);

		return new ByteArrayData(bytes);
	}

	/**
	 * @param {TransferUplcAst} other 
	 * @returns {any}
	 */
	transfer(other) {
		return other.transferByteArrayData(this.#bytes);
	}

	/**
	 * Returns a copy of the underlying bytes.
	 * @type {number[]}
	 */
	get bytes() {
		return this.#bytes.slice();
	}

    /**
     * Calculates the mem size of a byte array without the DATA_NODE overhead.
     * @param {number[]} bytes
     * @returns {number}
     */
    static memSizeInternal(bytes) {
        const n = bytes.length;

		if (n === 0) {
			return 1; // this is so annoying: haskell reference implementation says it should be 0, but current (20220925) testnet and mainnet settings say it's 1
		} else {
			return Math.floor((n - 1)/8) + 1;
		}
    }

	/**
	 * @type {number}
	 */
	get memSize() {
		return UPLC_DATA_NODE_MEM_SIZE + ByteArrayData.memSizeInternal(this.#bytes);
	}

	/**
	 * @returns {string}
	 */
	toHex() {
		return bytesToHex(this.#bytes);
	}

	/**
	 * @type {string}
	 */
	get hex() {
		return this.toHex();
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `#${this.toHex()}`;
	}

	/**
	 * Returns bytearray literal wrapped with bytearray data function as IR.
	 * @internal
	 * @returns {IR}
	 */
	toIR() {
		return new IR(`__core__bData(#${this.toHex()})`);
	}

	/**
	 * @returns {string}
	 */
	toSchemaJson() {
		return `{"bytes": "${this.toHex()}"}`;
	}

	/**
	 * @returns {number[]}
	 */
	toCbor() {
		return Cbor.encodeBytes(this.#bytes, true);
	}

	/**
	 * @param {number[]} bytes
	 * @returns {ByteArrayData}
	 */
	static fromCbor(bytes) {
		return new ByteArrayData(Cbor.decodeBytes(bytes));
	}

	/**
	 * Bytearray comparison, which can be used for sorting bytearrays
	 * @internal
	 * @example
	 * ByteArrayData.comp(hexToBytes("0101010101010101010101010101010101010101010101010101010101010101"), hexToBytes("0202020202020202020202020202020202020202020202020202020202020202")) == -1
	 * @param {number[]} a
	 * @param {number[]} b
	 * @returns {number} - 0 -> equals, 1 -> gt, -1 -> lt
	 */
	static comp(a, b) {
		/** @return {boolean} */
		function lessThan() {
			for (let i = 0; i < Math.min(a.length, b.length); i++) {
				if (a[i] != b[i]) {
					return a[i] < b[i];
				}
			}

			return a.length < b.length;
		}

		/** @return {number} */
		function lessOrGreater() {
			return lessThan() ? -1 : 1;
		}

		if (a.length != b.length) {
			return lessOrGreater();
		} else {
			for (let i = 0; i < a.length; i++) {
				if (a[i] != b[i]) {
					return lessOrGreater();
				}
			}

			return 0;
		}
	}

	/**
	 * Cbor-specific Bytearray comparison (see https://datatracker.ietf.org/doc/html/rfc7049#section-3.9)
	 * Used by Assets.sort()
	 * @internal
	 * @param {number[]} a
	 * @param {number[]} b
	 * @returns {number} - 0 -> equals, 1 -> gt, -1 -> lt
	 */
	static compLengthFirst(a, b) {
		if (a.length != b.length) {
			return a.length < b.length ? -1 : 1;
		} else {
			for (let i = 0; i < a.length; i++) {
				if (a[i] != b[i]) {
					return a[i] < b[i] ? -1 : 1;
				}
			}

			return 0;
		}
	}
}

/**
 * Represents a list of other `UplcData` instances.
 */
export class ListData extends UplcData {
	#items;

	/**
	 * @param {UplcData[]} items
	 */
	constructor(items) {
		super();
		assert(items.every(f => f instanceof UplcData), "expected exclusively UplcData items");
		this.#items = items;
	}

	/**
	 * @param {TransferUplcAst} other 
	 */
	transfer(other) {
		return other.transferListData(
			this.#items.map(item => item.transfer(other))
		);
	}

	/**
	 * @type {UplcData[]}
	 */
	get list() {
		return this.#items.slice();
	}

	/**
	 * @type {number}
	 */
	get memSize() {
		let sum = UPLC_DATA_NODE_MEM_SIZE;

		for (let item of this.#items) {
			sum += item.memSize;
		}

		return sum;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `[${this.#items.map(item => item.toString()).join(", ")}]`;
	}

	/**
	 * @internal
	 * @returns {IR}
	 */
	toIR() {
		let ir = new IR("__core__mkNilData(())");
		for (let i = this.#items.length - 1; i >= 0; i--) {
			ir = new IR([new IR("__core__mkCons("), this.#items[i].toIR(), new IR(", "), ir, new IR(")")]);
		}

		return new IR([new IR("__core__listData("), ir, new IR(")")]);
	}

	/**
	 * @returns {string}
	 */
	toSchemaJson() {
		return `{"list":[${this.#items.map(item => item.toSchemaJson()).join(", ")}]}`;
	}

	/**
	 * @returns {number[]}
	 */
	toCbor() {
		return Cbor.encodeList(this.#items);
	}

	/**
	 * @param {number[]} bytes
	 * @returns {ListData}
	 */
	static fromCbor(bytes) {
		/**
		 * @type {UplcData[]}
		 */
		let list = [];

		Cbor.decodeList(bytes, (_, itemBytes) => {
			list.push(UplcData.fromCbor(itemBytes));
		});

		return new ListData(list);
	}
}

/**
 * Represents a list of pairs of other `UplcData` instances.
 */
export class MapData extends UplcData {
	#pairs;

	/**
	 * @param {[UplcData, UplcData][]} pairs
	 */
	constructor(pairs) {
		super();
		this.#pairs = pairs;
	}

	/**
	 * @param {TransferUplcAst} other 
	 * @returns {any}
	 */
	transfer(other) {
		return other.transferMapData(
			this.#pairs.map(([a, b]) => {
				return [a.transfer(other), b.transfer(other)]
			})
		);
	}

	/**
	 * @type {[UplcData, UplcData][]}
	 */
	get map() {
		return this.#pairs.slice();
	}

	/**
	 * @type {number}
	 */
	get memSize() {
		let sum = UPLC_DATA_NODE_MEM_SIZE;

		for (let [k, v] of this.#pairs) {
			sum += k.memSize + v.memSize;
		}

		return sum;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `{${this.#pairs.map(([fst, snd]) => `${fst.toString()}: ${snd.toString()}`).join(", ")}}`;
	}

	/**
	 * @internal
	 * @returns {IR}
	 */
	toIR() {
		let ir = new IR("__core__mkNilPairData(())");

		for (let i = this.#pairs.length - 1; i >= 0; i--) {
			const a = this.#pairs[i][0].toIR();
			const b = this.#pairs[i][1].toIR();

			ir = new IR([new IR("__core__mkCons(__core__mkPairData("), a, new IR(", "), b, new IR(", "), new IR(")"), new IR(", "), ir, new IR(")")]);
		}

		return new IR([new IR("__core__mapData("), ir, new IR(")")]);
	}

	/**
	 * @returns {string}
	 */
	toSchemaJson() {
		return `{"map": [${this.#pairs.map(pair => { return "{\"k\": " + pair[0].toSchemaJson() + ", \"v\": " + pair[1].toSchemaJson() + "}" }).join(", ")}]}`;
	}

	/**
	 * @returns {number[]}
	 */
	toCbor() {
		return Cbor.encodeMap(this.#pairs);
	}

	/**
	 * @param {number[]} bytes
	 * @returns {MapData}
	 */
	static fromCbor(bytes) {
		/**
		 * @type {[UplcData, UplcData][]}
		 */
		let pairs = [];

		Cbor.decodeMap(bytes, (_, pairBytes) => {
			pairs.push([UplcData.fromCbor(pairBytes), UplcData.fromCbor(pairBytes)]);
		});

		return new MapData(pairs);
	}
}

/**
 * Represents a tag index and a list of `UplcData` fields.
 */
export class ConstrData extends UplcData {
	#index;
	#fields;

	/**
	 * @param {number} index
	 * @param {UplcData[]} fields
	 */
	constructor(index, fields) {
		super();
		assert(fields.every(f => f instanceof UplcData), "expected exclusively UplcData fields");
		this.#index = index;
		this.#fields = fields;
	}

	/**
	 * @param {TransferUplcAst} other 
	 * @returns {any}
	 */
	transfer(other) {
		return other.transferConstrData(
			this.#index,
			this.#fields.map(f => f.transfer(other))
		);
	}

	/**
	 * @type {number}
	 */
	get index() {
		return this.#index;
	}

	/**
	 * @type {UplcData[]}
	 */
	get fields() {
		return this.#fields.slice();
	}

	/**
	 * @type {number}
	 */
	get memSize() {
		let sum = UPLC_DATA_NODE_MEM_SIZE;

		for (let field of this.#fields) {
			sum += field.memSize;
		}

		return sum;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		let parts = this.#fields.map(field => field.toString());
		return `${this.#index.toString()}{${parts.join(", ")}}`;
	}

	/**
	 * @returns {IR}
	 */
	toIR() {
		let ir = new IR("__core__mkNilData(())");
		for (let i = this.#fields.length - 1; i >= 0; i--) {
			ir = new IR([new IR("__core__mkCons("), this.#fields[i].toIR(), new IR(", "), ir, new IR(")")]);
		}

		return new IR([new IR("__core__constrData("), new IR(this.#index.toString()), new IR(", "), ir, new IR(")")]);
	}

	/**
	 * @returns {string}
	 */
	toSchemaJson() {
		return `{"constructor": ${this.#index.toString()}, "fields": [${this.#fields.map(f => f.toSchemaJson()).join(", ")}]}`;
	}

	/**
	 * @returns {number[]}
	 */
	toCbor() {
		return Cbor.encodeConstr(this.#index, this.#fields);
	}

	/**
	 * @param {number[]} bytes
	 * @returns {ConstrData}
	 */
	static fromCbor(bytes) {
		/**
		 * @type {UplcData[]}
		 */
		let fields = [];

		let tag = Cbor.decodeConstr(bytes, (_, fieldBytes) => {
			fields.push(UplcData.fromCbor(fieldBytes));
		});

		return new ConstrData(tag, fields);
	}
}


/////////////////////////////////
// Section 7: Helios data objects
/////////////////////////////////

/**
 * Base-type of all data-types that exist both on- and off-chain, and map directly to Helios instances.
 * @deprecated
 */
export class HeliosData extends CborData {
	constructor() {
        super();
	}

    /**
     * Name begins with underscore so it can never conflict with structure field names.
     * @internal
     * @returns {UplcData}
     */
	_toUplcData() {
        throw new Error("not yet implemented");
	}

    /**
     * @returns {string}
     */
	toSchemaJson() {
		return this._toUplcData().toSchemaJson();
	}

	/**
	 * Defaults to cbor encoding of uplc data structure.
	 * @returns {number[]}
	 */
	toCbor() {
		return this._toUplcData().toCbor();
	}

	/**
	 * Most HeliosData classes are builtins.
	 * @internal
	 * @returns {boolean}
	 */
	static isBuiltin() {
		return true;
	}
}

/**
 * Deprecated
 * @internal
 * @template {HeliosData} T
 * @typedef {{
 *   new(...args: any[]): T
 *   fromUplcCbor: (bytes: (string | number[])) => T
 *   fromUplcData: (data: UplcData) => T
 *   isBuiltin(): boolean
 * }} HeliosDataClass
 */

/**
 * @deprecated
 * @typedef {number | bigint} HIntProps
 */

/**
 * Helios Int type
 * @deprecated
 */
export class HInt extends HeliosData {
    /**
     * @type {bigint}
     */
    #value;

    /**
     * @internal
     * @param {HIntProps} rawValue
     * @returns {bigint}
     */
    static cleanConstructorArg(rawValue) {
        const value = BigInt(rawValue);

        if (value.toString() != rawValue.toString()) {
            throw new Error("not a valid integer");
        }

        return value;
    }

    /**
     * @param {HIntProps} rawValue
     */
    constructor(rawValue) {
        super();

        this.#value = HInt.cleanConstructorArg(rawValue);
    }

	/**
	 * @returns {string}
	 */
	toString() {
		return this.#value.toString();
	}

	/**
	 * @param {HInt | HIntProps} props 
	 * @returns {HInt}
	 */
	static fromProps(props) {
		return props instanceof HInt ? props : new HInt(props);
	}

    /**
     * @type {bigint}
     */
    get value() {
        return this.#value;
    }

    /**
     * @internal
     * @returns {UplcData}
     */
    _toUplcData() {
        return new IntData(this.#value);
    }

    /**
     * @param {UplcData} data
     * @returns {HInt}
     */
    static fromUplcData(data) {
        return new HInt(data.int);
    }

    /**
     * @param {string | number[]} bytes
     * @returns {HInt}
     */
    static fromUplcCbor(bytes) {
        return HInt.fromUplcData(UplcData.fromCbor(bytes));
    }

	/**
	 * @param {number[]} bytes 
	 * @returns {HInt}
	 */
	static fromCbor(bytes) {
		return new HInt(Cbor.decodeInteger(bytes));
	}

	/**
	 * @returns {number[]}
	 */
	toCbor() {
		return  Cbor.encodeInteger(this.value);
	}

	/**
	 * @returns {string}
	 */
	dump() {
		return this.#value.toString();
	}

	/**
	 * @param {HInt | HIntProps} other 
	 * @returns {boolean}
	 */
	eq(other) {
		return this.#value == HInt.fromProps(other).#value
	}

	/**
	 * @param {HInt | HIntProps} other 
	 * @returns {boolean}
	 */
	neq(other) {
		return this.#value != HInt.fromProps(other).value;
	}

	/**
	 * @param {HInt | HIntProps} other 
	 * @returns {boolean}
	 */
	ge(other) {
		return this.#value >= HInt.fromProps(other).#value;
	}

	/**
	 * @param {HInt | HIntProps} other 
	 * @returns {boolean}
	 */
	gt(other) {
		return this.#value > HInt.fromProps(other).#value;
	}

	/**
	 * @param {HInt | HIntProps} other 
	 * @returns {boolean}
	 */
	le(other) {
		return this.#value <= HInt.fromProps(other).#value;
	}

	/**
	 * @param {HInt | HIntProps} other 
	 * @returns {boolean}
	 */
	lt(other) {
		return this.#value < HInt.fromProps(other).#value;
	}

	/**
	 * @param {HInt| HIntProps} other 
	 * @returns {HInt}
	 */
	add(other) {
		return new HInt(this.#value + HInt.fromProps(other).#value);
	}

	/**
	 * @param {HInt | HIntProps} other 
	 * @returns {HInt}
	 */
	sub(other) {
		return new HInt(this.#value - HInt.fromProps(other).#value);
	}

	/**
	 * @param {HInt| HIntProps} other 
	 * @returns {HInt}
	 */
	mul(other) {
		return new HInt(this.#value * HInt.fromProps(other).#value);
	}
}

/**
 * @internal
 * @typedef {number | bigint | string | Date} TimeProps
 */

/**
 * Milliseconds since 1 jan 1970
 * @internal
 */
export class Time extends HInt {
     /**
     * @internal
     * @param {TimeProps} props
     * @returns {bigint}
     */
	static cleanConstructorArg(props) {
        if (props instanceof Date) {
            return BigInt(props.getTime());
		} else if (typeof props == "string") {
			return BigInt(Date.parse(props));
        } else {
            const value = BigInt(props);

            if (value.toString() != props.toString()) {
                throw new Error("not a valid integer");
            } else {
                return value;
            }
        }
    }

    /**
     * @param {TimeProps} props
     */
    constructor(props) {
        super(Time.cleanConstructorArg(props));
    }

	/**
	 * @param {Time | TimeProps} props 
	 * @returns {Time}
	 */
	static fromProps(props) {
		return props instanceof Time ? props : new Time(props);
	}

    /**
     * @param {UplcData} data
     * @returns {Time}
     */
    static fromUplcData(data) {
        return new Time(data.int);
    }

    /**
     * @param {string | number[]} bytes
     * @returns {Time}
     */
    static fromUplcCbor(bytes) {
        return Time.fromUplcData(UplcData.fromCbor(bytes));
    }
}

/**
 * @internal
 * @typedef {HIntProps} DurationProps
 */

/**
 * Difference between two time values in milliseconds.
 * @internal
 */
export class Duration extends HInt {
	/**
	 * @param {Duration | DurationProps} props 
	 */
	static fromProps(props) {
		return props instanceof Duration ? props : new Duration(props);
	}

    /**
     * @param {UplcData} data
     * @returns {Duration}
     */
    static fromUplcData(data) {
        return new Duration(data.int);
    }

    /**
     * @param {string | number[]} bytes
     * @returns {Duration}
     */
    static fromUplcCbor(bytes) {
        return Duration.fromUplcData(UplcData.fromCbor(bytes));
    }
}

/**
 * @internal
 * @typedef {boolean | string} BoolProps
 */

/**
 * Helios Bool type
 * @internal
 */
export class Bool extends HeliosData {
    /**
     * @type {boolean}
     */
    #value;

    /**
     * @internal
     * @param {BoolProps} props 
     * @returns {boolean}
     */
    static cleanConstructorArg(props) {
        if (typeof props == "string") {
            if (props == "false") {
                return false;
            } else if (props == "true") {
                return true;
            } else {
                throw new Error("not a valid string representation of a Bool");
            }
        } else if (typeof props == "boolean") {
            return props;
        } else {
            throw new Error("can't convert to boolean");
        }
    }

    /**
     * @param {BoolProps} props 
     */
    constructor(props) {
        super();

        this.#value = Bool.cleanConstructorArg(props);
    }

	/**
	 * @param {Bool | BoolProps} props 
	 * @returns {Bool}
	 */
	static fromProps(props) {
		return props instanceof Bool ? props : new Bool(props);
	}

	/**
	 * @type {boolean}
	 */
    get bool() {
        return this.#value;
    }

    /**
     * @internal
     * @returns {UplcData}
     */
    _toUplcData() {
        return new ConstrData(this.#value ? 1 : 0, []);
    }

    /**
     * @param {UplcData} data
     * @returns {Bool}
     */
    static fromUplcData(data) {
        assert(data.fields.length == 0, "bool data can't have fields");

        if (data.index == 0) {
            return new Bool(false);
        } else if (data.index == 1) {
            return new Bool(true);
        } else {
            throw new Error("expected 0 or 1 for ConstrData representing Bool");
        }
    }

    /**
     * @param {string | number[]} bytes
     * @returns {Bool}
     */
    static fromUplcCbor(bytes) {
        return Bool.fromUplcData(UplcData.fromCbor(bytes));
    }
}

/**
 * @internal
 * @typedef {string} HStringProps
 */

/**
 * Helios String type.
 * Can't be named 'String' because that would interfere with the javascript 'String'-type
 * @internal
 */
export class HString extends HeliosData {
    /**
     * @type {string}
     */
    #value;

    /**
     * @param {HStringProps} props 
     */
    constructor(props) {
        super();

        this.#value = props;
    }

	/**
	 * @param {HString | HStringProps} props
	 * @returns {HString}
	 */
	static fromProps(props) {
		return props instanceof HString ? props : new HString(props);
	}

	/**
	 * @type {string}
	 */
    get string() {
        return this.#value;
    }

    /**
     * @internal
     * @returns {UplcData}
     */
    _toUplcData() {
        return new ByteArrayData(textToBytes(this.#value));
    }

    /**
     * @param {UplcData} data
     * @returns {HString}
     */
    static fromUplcData(data) {
        return new HString(bytesToText(data.bytes));
    }

    /**
     * @param {string | number[]} bytes
     * @returns {HString}
     */
    static fromUplcCbor(bytes) {
        return HString.fromUplcData(UplcData.fromCbor(bytes));
    }
}

/**
 * @deprecated
 * @typedef {number[] | string} ByteArrayProps
 */

/**
 * Helios ByteArray type
 * @deprecated
 */
export class ByteArray extends HeliosData {
    /**
     * @type {number[]}
     */
    #bytes;

    /**
     * @internal
     * @param {ByteArrayProps} props 
     */
    static cleanConstructorArg(props) {
        if (Array.isArray(props)) {
            return props;
        } else if (typeof props == "string") {
            if (props.startsWith("#")) {
                props = props.slice(1);
            }

            return hexToBytes(props);
        } else {
            throw new Error("unexpected bytes type");
        }
    }

    /**
     * @param {ByteArrayProps} props 
     */
    constructor(props) {
        super();

        this.#bytes = ByteArray.cleanConstructorArg(props);
    }

	/**
	 * @param {ByteArray | ByteArrayProps} props 
	 * @returns {ByteArray}
	 */
	static fromProps(props) {
		return props instanceof ByteArray ? props : new ByteArray(props);
	}

    /**
     * @type {number[]}
     */
    get bytes() {
        return this.#bytes;
    }

    /**
	 * Hexadecimal representation.
     * @type {string}
     */
    get hex() {
        return bytesToHex(this.#bytes);
    }

    /**
     * @internal
     * @returns {UplcData}
     */
    _toUplcData() {
        return new ByteArrayData(this.#bytes);
    }

	/**
	 * @returns {number[]}
	 */
	toCbor() {
		return Cbor.encodeBytes(this.#bytes);
	}

    /**
     * @param {UplcData} data
     * @returns {ByteArray}
     */
    static fromUplcData(data) {
        return new ByteArray(data.bytes);
    }

    /**
     * @param {string | number[]} bytes
     * @returns {ByteArray}
     */
    static fromUplcCbor(bytes) {
        return ByteArray.fromUplcData(UplcData.fromCbor(bytes));
    }

	/**
	 * @param {number[]} bytes 
	 * @returns {ByteArray}
	 */
	static fromCbor(bytes) {
		return new ByteArray(Cbor.decodeBytes(bytes));
	}

	/**
	 * @param {ByteArray | ByteArrayProps} other 
	 * @returns {boolean}
	 */
	eq(other) {
		return eq(this.#bytes, ByteArray.fromProps(other).#bytes);
	}
}

/**
 * Dynamically constructs a new List class, depending on the item type.
 * @internal
 * @template {HeliosData} T
 * @param {HeliosDataClass<T>} ItemClass
 * @returns {HeliosDataClass<HList_>}
 */
export function HList(ItemClass) {
    assert(!new.target, "List can't be called with new");
    assert(ItemClass.prototype instanceof HeliosData);

    const typeName = `[]${ItemClass.name}`;

    class HList_ extends HeliosData {
        /**
         * @type {T[]}
         */
        #items;

        /**
         * @param {any[]} rawList
         */
        constructor(rawList) {
            super();

            this.#items = rawList.map(item => {
                if (item instanceof ItemClass) {
                    return item;
                } else {
                    return new ItemClass(item);
                }
            });
        }

        /**
         * @internal
         * @type {string}
         */
        get _listTypeName() {
            return typeName;
        }

        /**
         * Overload 'instanceof' operator
         * @internal
         * @param {any} other
         * @returns {boolean}
         */
        static [Symbol.hasInstance](other) {
            return (other._listTypeName === typeName) && (other instanceof HeliosData);
        }

        /**
         * @type {T[]}
         */
        get items() {
            return this.#items;
        }

        /**
         * @internal
         * @returns {UplcData}
         */
        _toUplcData() {
            return new ListData(this.#items.map(item => item._toUplcData()))
        }

        /**
         * @param {UplcData} data
         * @returns {HList_}
         */
        static fromUplcData(data) {
            return new HList_(data.list.map(d => ItemClass.fromUplcData(d)));
        }

        /**
         * @param {string | number[]} bytes
         * @returns {HList_}
         */
        static fromUplcCbor(bytes) {
            return HList_.fromUplcData(UplcData.fromCbor(bytes));
        }
    }

    Object.defineProperty(HList_, "name", {
        value: typeName,
        writable: false
    });

    return HList_;
}

/**
 * @internal
 * @template {HeliosData} TKey
 * @template {HeliosData} TValue
 * @param {HeliosDataClass<TKey>} KeyClass
 * @param {HeliosDataClass<TValue>} ValueClass
 * @returns {HeliosDataClass<HMap_>}
 */
export function HMap(KeyClass, ValueClass) {
    assert(!new.target, "HMap can't be called with new");
    assert(KeyClass.prototype instanceof HeliosData);
    assert(ValueClass.prototype instanceof HeliosData);

    const typeName = `Map[${KeyClass.name}]${ValueClass.name}`;

    class HMap_ extends HeliosData {
        /**
         * @type {[TKey, TValue][]}
         */
        #pairs;

        /**
         * @internal
         * @param {...any} args
         * @returns {[any, any][]}
         */
        static cleanConstructorArgs(...args) {
            /** @type {[any, any][]} */
            let pairs = [];

            if (args.length == 1) {
                const arg = args[0];

                if (arg instanceof Map) {
                    return HMap_.cleanConstructorArgs(Array.from(arg.entries()));
                } else if (!Array.isArray(arg)) {
                    throw new Error("expected array or Map arg");
                } else {
                    const lst = arg;

                    pairs = lst.map(item => {
                        if (!Array.isArray(item)) {
                            throw new Error("expected array item (pair)");
                        } else if (item.length != 2) {
                            throw new Error("expected array item of length 2 (pair)");
                        } else {
                            return [item[0], item[1]];
                        }
                    });
                }
            } else if (args.length == 2) {
                const [keys, values] = args;

                if (!Array.isArray(keys)) {
                    throw new Error("expected keys array arg");
                } else if (!Array.isArray(values)) {
                    throw new Error("expected values array arg");
                } else if (keys.length != values.length) {
                    throw new Error("keys and values list don't have same length");
                } else {
                    pairs = keys.map((key, i) => {
                        const value = values[i];

                        return [key, value];
                    });
                }
            } else {
                throw new Error("unexpected number of args");
            }

            return pairs;
        }

        /**
         * @param  {...any} args
         */
        constructor(...args) {
            const rawPairs = HMap_.cleanConstructorArgs(...args);

            /**
             * @type {[TKey, TValue][]}
             */
            const pairs = rawPairs.map(([rawKey, rawValue]) => {
                const key = function() {
                    if (rawKey instanceof KeyClass) {
                        return rawKey;
                    } else {
                        return new KeyClass(rawKey);
                    }
                }();

                const value = function() {
                    if (rawValue instanceof ValueClass) {
                        return rawValue;
                    } else {
                        return new ValueClass(rawValue);
                    }
                }();

                return [key, value];
            });

            super();

            this.#pairs = pairs;
        }

        /**
         * @internal
         * @type {string}
         */
        get _mapTypeName() {
            return typeName;
        }

        /**
         * Overload 'instanceof' operator
         * @internal
         * @param {any} other
         * @returns {boolean}
         */
        static [Symbol.hasInstance](other) {
            return (other._mapTypeName === typeName) && (other instanceof HeliosData);
        }

        /**
         * @type {[TKey, TValue][]}
         */
        get pairs() {
            return this.#pairs;
        }

        /**
         * @internal
         * @returns {UplcData}
         */
        _toUplcData() {
            return new MapData(this.#pairs.map(([key, value]) => [key._toUplcData(), value._toUplcData()]));
        }

        /**
         * @param {UplcData} data
         * @returns {HMap_}
         */
        static fromUplcData(data) {
            return new HMap_(data.map.map(([kd, vd]) => [KeyClass.fromUplcData(kd), ValueClass.fromUplcData(vd)]));
        }

        /**
         * @param {string | number[]} bytes
         * @returns {HMap_}
         */
        static fromUplcCbor(bytes) {
            return HMap_.fromUplcData(UplcData.fromCbor(bytes));
        }
    }

    Object.defineProperty(HMap_, "name", {
        value: typeName,
        writable: false
    });

    return HMap_;
}

/**
 * @internal
 * @template {HeliosData} T
 * @param {HeliosDataClass<T>} SomeClass
 * @returns {HeliosDataClass<Option_>}
 */
export function Option(SomeClass) {
    assert(!new.target, "Option can't be called with new");
    assert(SomeClass.prototype instanceof HeliosData);

    const typeName = `Option[${SomeClass.name}]`;

    class Option_ extends HeliosData {
        /**
         * @type {?T}
         */
        #value;

        /**
         * @internal
         * @param {?any} rawValue
         * @returns {?T}
         */
        static cleanConstructorArg(rawValue) {
            if (rawValue == null) {
                return null;
            } else if (!(rawValue instanceof SomeClass)) {
                return new SomeClass(rawValue);
            } else {
                return rawValue;
            }
        }

        /**
         * @param {?any} rawValue
         */
        constructor(rawValue = null) {
            super();

            this.#value = Option_.cleanConstructorArg(rawValue);
        }

        /**
         * @internal
         * @type {string}
         */
        get _optionTypeName() {
            return typeName;
        }

        /**
         * Overload 'instanceof' operator
         * @internal
         * @param {any} other
         * @returns {boolean}
         */
        static [Symbol.hasInstance](other) {
            return (other._optionTypeName === typeName) && (other instanceof HeliosData);
        }

        /**
         * @type {?T}
         */
        get some() {
            return this.#value;
        }

        /**
         * @internal
         * @returns {UplcData}
         */
        _toUplcData() {
            return new ConstrData(this.#value === null ? 1 : 0, this.#value === null ? [] : [this.#value._toUplcData()]);
        }

        /**
         * @param {UplcData} data
         * @returns {Option_}
         */
        static fromUplcData(data) {
            if (data.index == 1) {
                assert(data.fields.length == 0);

                return new Option_(null);
            } else if (data.index == 0) {
                assert(data.fields.length == 1);

                return new Option_(SomeClass.fromUplcData(data.fields[0]))
            } else {
                throw new Error("unexpected option constr index");
            }
        }

        /**
         * @param {string | number[]} bytes
         * @returns {Option_}
         */
        static fromUplcCbor(bytes) {
            return Option_.fromUplcData(UplcData.fromCbor(bytes));
        }
    }

    Object.defineProperty(HList, "name", {
        value: typeName,
        writable: false
    });

    return Option_;
}

/**
 * @typedef {number[] | string} HashProps
 */

/**
 * Base class of all hash-types
 */
export class Hash extends HeliosData {
	/** 
	 * @readonly
	 * @type {number[]} 
	 */
	bytes;

	/**
	 * @internal
	 * @param {HashProps} props 
	 * @returns {number[]}
	 */
	static cleanConstructorArg(props) {
		if (typeof props == "string") {
			return hexToBytes(props);
		} else {
			return props;
		}
	}

	/**
	 * @param {HashProps} props 
	 */
	constructor(props) {
		super();
		this.bytes = Hash.cleanConstructorArg(props);
	}

	/**
	 * @param {Hash | HashProps} props 
	 * @returns {Hash}
	 */
	static fromProps(props) {
		return props instanceof Hash ? props : new Hash(props);
	}

	/**
	 * Hexadecimal representation.
	 * @returns {string}
	 */
	get hex() {
		return bytesToHex(this.bytes);
	}

	/**
	 * Hexadecimal representation.
	 * @returns {string}
	 */
	toString() {
		return this.hex;
	}

	/**
	 * @returns {number[]}
	 */
	toCbor() {
		return Cbor.encodeBytes(this.bytes);
	}

    /**
     * @returns {UplcData}
     */
    _toUplcData() {
        return new ByteArrayData(this.bytes);
    }

	/**
	 * Used internally for metadataHash and scriptDataHash
	 * @param {number[]} bytes
	 * @returns {Hash}
	 */
	static fromCbor(bytes) {
		return new Hash(Cbor.decodeBytes(bytes));
	}

	/**
	 * Might be needed for internal use
	 * @param {string} str 
	 * @returns {Hash}
	 */
	static fromHex(str) {
		return new Hash(hexToBytes(str));
	}

	/**
	 * @internal
	 * @returns {string}
	 */
	dump() {
		return bytesToHex(this.bytes);
	}

	/**
	 * @param {Hash} other
	 * @returns {boolean}
	 */
	eq(other) {
		return eq(this.bytes, other.bytes);
	}

	/**
	 * @param {Hash} a
	 * @param {Hash} b
	 * @returns {number}
	 */
	static compare(a, b) {
		return ByteArrayData.comp(a.bytes, b.bytes);
	}
}

/**
 * @typedef {HashProps} DatumHashProps
 */

/**
 * Represents a blake2b-256 hash of datum data.
 */
export class DatumHash extends Hash {
	/**
	 * @param {DatumHashProps} props
	 */
	constructor(props) {
		const bytes = Hash.cleanConstructorArg(props);

		assert(bytes.length == 32);
		super(bytes);
	}

	/**
	 * @param {DatumHash | DatumHashProps} props 
	 */
	static fromProps(props) {
		return props instanceof DatumHash ? props : new DatumHash(props);
	}

	/**
	 * @param {number[]} bytes 
	 * @returns {DatumHash}
	 */
	static fromCbor(bytes) {
		return new DatumHash(Cbor.decodeBytes(bytes));
	}

	/**
	 * @param {UplcData} data
	 * @returns {DatumHash}
	 */
	 static fromUplcData(data) {
		return new DatumHash(data.bytes);
	}

	/**
	 * @param {string | number[]} bytes
	 * @returns {DatumHash}
	 */
	static fromUplcCbor(bytes) {
		return DatumHash.fromUplcData(UplcData.fromCbor(bytes));
	}

	/**
	 * @param {string} str
	 * @returns {DatumHash}
	 */
	static fromHex(str) {
		return new DatumHash(hexToBytes(str));
	}
}

/**
 * @typedef {number[] | string} PubKeyProps
 */

export class PubKey extends HeliosData {
	#bytes;

	/**
	 * @param {PubKeyProps} props 
	 */
	constructor(props) {
		super();
		const bytes = (typeof props == "string") ? hexToBytes(props) : props;

		assert(bytes.length == 32, `expected 32 for PubKey, got ${bytes.length}`);
		this.#bytes = bytes;
	}

	/**
	 * @param {PubKey | PubKeyProps} props 
	 * @returns {PubKey}
	 */
	static fromProps(props) {
		return props instanceof PubKey ? props : new PubKey(props);
	}

	/**
	 * @returns {PubKey}
	 */
	static dummy() {
		return new PubKey((new Array(32)).fill(0));
	}

	/**
	 * @type {number[]}
	 */
	get bytes() {
		return this.#bytes;
	}

	/**
	 * Hexadecimal representation.
	 * @type {string}
	 */
	get hex() {
		return bytesToHex(this.#bytes);
	}

	/**
	 * Can also be used as a Stake key hash
	 * @type {PubKeyHash}
	 */
	get pubKeyHash() {
		return new PubKeyHash(this.hash());
	}

	/**
	 * @param {UplcData} data
	 * @returns {PubKey}
	 */
	static fromUplcData(data) {
		return new PubKey(data.bytes)
	}

	/**
	 * @param {string | number[]} bytes
	 * @returns {PubKey}
	 */
	static fromUplcCbor(bytes) {
		return PubKey.fromUplcData(UplcData.fromCbor(bytes));
	}

	/**
	 * @param {number[]} bytes
	 * @returns {PubKey}
	 */
	static fromCbor(bytes) {
		return new PubKey(Cbor.decodeBytes(bytes));
	}

	/**
	 * @returns {boolean}
	 */
	isDummy() {
		return this.#bytes.every(b => b == 0);
	}

	/**
	 * @returns {number[]}
	 */
	toCbor() {
		return Cbor.encodeBytes(this.#bytes);
	}

	/**
     * @returns {UplcData}
     */
    _toUplcData() {
        return new ByteArrayData(this.#bytes);
    }

	/**
	 * @returns {number[]}
	 */
	hash() {
		return Crypto.blake2b(this.#bytes, 28);
	}

	/**
	 * @returns {string}
	 */
	dump() {
		return this.hex;
	}
}

/**
 * Represents a blake2b-224 hash of a PubKey
 * 
 * **Note**: A `PubKeyHash` can also be used as the second part of a payment `Address`, or to construct a `StakeAddress`.
 * @typedef {HashProps} PubKeyHashProps
 */
export class PubKeyHash extends Hash {

	/**
	 * @param {PubKeyHashProps} props 
	 */
	constructor(props) {
		const bytes = Hash.cleanConstructorArg(props);

		assert(bytes.length == 28, `expected 28 bytes for PubKeyHash, got ${bytes.length}`);
		super(bytes);
	}

	/**
	 * @returns {PubKeyHash}
	 */
	static dummy() {
		const bytes = new Array(28).fill(0);

		return new PubKeyHash(bytes);
	}

	/**
	 * @param {PubKeyHash | PubKeyHashProps} props 
	 * @returns {PubKeyHash}
	 */
	static fromProps(props) {
		return props instanceof PubKeyHash ? props : new PubKeyHash(props);
	}

	/**
	 * @param {number[]} bytes 
	 * @returns {PubKeyHash}
	 */
	static fromCbor(bytes) {
		return new PubKeyHash(Cbor.decodeBytes(bytes));
	}

	/**
	 * @param {UplcData} data
	 * @returns {PubKeyHash}
	 */
	static fromUplcData(data) {
		return new PubKeyHash(data.bytes);
	}

	/**
	 * @param {string | number[]} bytes
	 * @returns {PubKeyHash}
	 */
	static fromUplcCbor(bytes) {
		return PubKeyHash.fromUplcData(UplcData.fromCbor(bytes));
	}

	/**
	 * @param {string} str
	 * @returns {PubKeyHash}
	 */
	static fromHex(str) {
		return new PubKeyHash(hexToBytes(str));
	}
}

/**
 * @internal
 * @typedef {HashProps} ScriptHashProps
 */

/**
 * Base class of MintingPolicyHash, ValidatorHash and StakingValidatorHash
 */
export class ScriptHash extends Hash {
	/**
	 * @param {ScriptHashProps} rawValue
	 */
	constructor(rawValue) {
		const bytes = Hash.cleanConstructorArg(rawValue);
		assert(bytes.length == 28 || bytes.length == 0, `expected 0 or 28 bytes for ScriptHash, got ${bytes.length}`);
		super(bytes);
	}

	/**
	 * @param {ScriptHash | ScriptHashProps} props 
	 * @returns {ScriptHash}
	 */
	static fromProps(props) {
		return props instanceof ScriptHash ? props : new ScriptHash(props);
	}
}

/**
 * @typedef {HashProps} MintingPolicyHashProps
 */

/**
 * Represents a blake2b-224 hash of a minting policy script
 * 
 * **Note**: to calculate this hash the script is first encoded as a CBOR byte-array and then prepended by a script version byte.
 */
export class MintingPolicyHash extends ScriptHash {
	/**
	 * @param {MintingPolicyHashProps} rawValue
	 */
	constructor(rawValue) {
		const bytes = Hash.cleanConstructorArg(rawValue);
		assert(bytes.length == 28 || bytes.length == 0, `expected 0 or 28 bytes for MintingPolicyHash, got ${bytes.length}`);
		super(bytes);
	}

	/**
	 * @param {MintingPolicyHash | MintingPolicyHashProps} props 
	 * @returns {MintingPolicyHash}
	 */
	static fromProps(props) {
		return props instanceof MintingPolicyHash ? props : new MintingPolicyHash(props);
	}

	/**
	 * @param {number[]} bytes 
	 * @returns {MintingPolicyHash}
	 */
	static fromCbor(bytes) {
		return new MintingPolicyHash(Cbor.decodeBytes(bytes));
	}

	/**
	 * @param {UplcData} data
	 * @returns {MintingPolicyHash}
	 */
	static fromUplcData(data) {
		return new MintingPolicyHash(data.bytes);
	}

	/**
	 * @param {string | number[]} bytes
	 * @returns {MintingPolicyHash}
	 */
	static fromUplcCbor(bytes) {
		return MintingPolicyHash.fromUplcData(UplcData.fromCbor(bytes));
	}

	/**
	 * @param {string} str
	 * @returns {MintingPolicyHash}
	 */
	static fromHex(str) {
		return new MintingPolicyHash(hexToBytes(str));
	}

	/**
	 * Encodes as bech32 string using 'asset' as human readable part
	 * @returns {string}
	 */
	toBech32() {
		return Crypto.encodeBech32("asset", Crypto.blake2b(this.bytes, 20));
	}
}

/**
 * @typedef {HashProps} StakingValidatorHashProps
 */

/**
 * Represents a blake2b-224 hash of a staking script.
 * 
 * **Note**: before hashing, the staking script is first encoded as a CBOR byte-array and then prepended by a script version byte.
 */
export class StakingValidatorHash extends ScriptHash {
	/**
	 * @param {StakingValidatorHashProps} rawValue
	 */
	constructor(rawValue) {
		const bytes = Hash.cleanConstructorArg(rawValue);
		assert(bytes.length == 28, `expected 28 bytes for StakingValidatorHash, got ${bytes.length}`);
		super(bytes);
	}

	/**
	 * @param {StakingValidatorHash | StakingValidatorHashProps} props 
	 * @returns {StakingValidatorHash}
	 */
	static fromProps(props) {
		return props instanceof StakingValidatorHash ? props : new StakingValidatorHash(props);
	}

	/**
	 * @param {number[]} bytes 
	 * @returns {StakingValidatorHash}
	 */
	static fromCbor(bytes) {
		return new StakingValidatorHash(Cbor.decodeBytes(bytes));
	}

	/**
	 * @param {UplcData} data
	 * @returns {StakingValidatorHash}
	 */
	static fromUplcData(data) {
		return new StakingValidatorHash(data.bytes);
	}

	/**
	 * @param {string | number[]} bytes
	 * @returns {StakingValidatorHash}
	 */
	static fromUplcCbor(bytes) {
		return StakingValidatorHash.fromUplcData(UplcData.fromCbor(bytes));
	}

	/**
	 * @param {string} str
	 * @returns {StakingValidatorHash}
	 */
	static fromHex(str) {
		return new StakingValidatorHash(hexToBytes(str));
	}
}

/**
 * @typedef {HashProps} ValidatorHashProps
 */

/**
 * Represents a blake2b-224 hash of a spending validator script (first encoded as a CBOR byte-array and prepended by a script version byte).
 */
export class ValidatorHash extends ScriptHash {
	/**
	 * @param {ValidatorHashProps} rawValue
	 */
	constructor(rawValue) {
		const bytes = Hash.cleanConstructorArg(rawValue);
		assert(bytes.length == 28, `expected 28 bytes for ValidatorHash, got ${bytes.length}`);
		super(bytes);
	}

	/**
	 * @param {ValidatorHash | ValidatorHashProps} props 
	 * @returns {ValidatorHash}
	 */
	static fromProps(props) {
		return props instanceof ValidatorHash ? props : new ValidatorHash(props);
	}

	/**
	 * @param {number[]} bytes 
	 * @returns {ValidatorHash}
	 */
	static fromCbor(bytes) {
		return new ValidatorHash(Cbor.decodeBytes(bytes));
	}

	/**
	 * @param {UplcData} data
	 * @returns {ValidatorHash}
	 */
	static fromUplcData(data) {
		return new ValidatorHash(data.bytes);
	}

	/**
	 * @param {string | number[]} bytes
	 * @returns {ValidatorHash}
	 */
	static fromUplcCbor(bytes) {
		return ValidatorHash.fromUplcData(UplcData.fromCbor(bytes));
	}

	/**
	 * @param {string} str
	 * @returns {ValidatorHash}
	 */
	static fromHex(str) {
		return new ValidatorHash(hexToBytes(str));
	}
}

/**
 * @typedef {HashProps} TxIdProps
 */

/**
 * Represents the hash of a transaction.
 * 
 * This is also used to identify an UTxO (along with the index of the UTxO in the list of UTxOs created by the transaction).
 */
export class TxId extends Hash {
	/**
	 * @param {TxIdProps} props 
	 */
	constructor(props) {
        const bytes = Hash.cleanConstructorArg(props);

		assert(bytes.length == 32, `expected 32 bytes for TxId, got ${bytes.length}`);
		super(bytes);
	}

	/**
	 * @param {TxId | TxIdProps} props 
	 * @returns {TxId}
	 */
	static fromProps(props) {
		return props instanceof TxId ? props : new TxId(props);
	}

    /**
     * @returns {UplcData}
     */
    _toUplcData() {
        return new ConstrData(0, [new ByteArrayData(this.bytes)]);
    }

	/**
	 * @param {number[]} bytes
	 * @returns {TxId}
	 */
	static fromCbor(bytes) {
		return new TxId(Cbor.decodeBytes(bytes));
	}

    /**
     * @param {UplcData} data
     * @returns {TxId}
     */
    static fromUplcData(data) {
        assert(data.index == 0);
        assert(data.fields.length == 1);

        return new TxId(data.fields[0].bytes);
    }

    /**
     * @param {string | number[]} bytes
     * @returns {TxId}
     */
    static fromUplcCbor(bytes) {
        return TxId.fromUplcData(UplcData.fromCbor(bytes));
    }

	/**
	 * @param {string} str 
	 * @returns {TxId}
	 */
	static fromHex(str) {
		return new TxId(hexToBytes(str));
	}

	/**
	 * Filled with 255 so that the internal show() function has max execution budget cost
	 * @param {number} fill
	 * @returns {TxId}
	 */
	static dummy(fill = 255) {
		return new TxId((new Array(32)).fill(fill));
	}
}

/**
 * @typedef {string | [
 * 	 TxId | TxIdProps, 
 *   HInt | HIntProps
 * ] | {
 *   txId: TxId | TxIdProps
 *   utxoId: HInt | HIntProps
 * }} TxOutputIdProps
 */

/**
 * Id of a Utxo
 */
export class TxOutputId extends HeliosData {
    /** @type {TxId} */
    #txId;

    /** @type {HInt} */
    #utxoIdx;

    /**
     * @param  {TxOutputIdProps} props
     * @returns {[TxId | TxIdProps, HInt | HIntProps]}
     */
    static cleanConstructorArgs(props) {
        if (typeof props == "string") {
			const parts = props.trim().split("#");

			assert(parts.length == 2);
			const utxoIdx = parseInt(parts[1]);

			assert(utxoIdx.toString() == parts[1]);

			return [parts[0], utxoIdx];
        } else if (Array.isArray(props) && props.length == 2) {
            return [props[0], props[1]];
        } else if (typeof props == "object") {
			return [assertDefined(props.txId), assertDefined(props.utxoId)];
		} else {
            throw new Error("unexpected number of args");
        }
    }


	/**
	 * @overload
	 * @param {TxId} txId
	 * @param {bigint | number} utxoId
	 */

    /**
	 * @overload
     * @param {TxOutputIdProps} props
     */

	/**
	 * @param {([TxOutputIdProps] | [TxId, bigint | number])} args
	 */
    constructor(...args) {
        const [rawTxId, rawUtxoIdx] = args.length == 1 ? TxOutputId.cleanConstructorArgs(args[0]) : [args[0], args[1]];

        super();

        this.#txId = TxId.fromProps(rawTxId);
        this.#utxoIdx = HInt.fromProps(rawUtxoIdx);
    }

	/**
	 * @returns {TxOutputId}
	 */
	static dummy() {
		return new TxOutputId(TxId.dummy(), 0);
	}

	/**
	 * @param {TxOutputId | TxOutputIdProps} props 
	 * @returns {TxOutputId}
	 */
	static fromProps(props) {
		return props instanceof TxOutputId ? props : new TxOutputId(props);
	}

	/**
	 * @type {TxId}
	 */
    get txId() {
        return this.#txId;
    }

	/**
	 * @type {number}
	 */
    get utxoIdx() {
        return Number(this.#utxoIdx.value);
    }

	/**
	 * @param {TxOutputId} other
	 * @returns {boolean}
	 */
	eq(other) {
		return this.#txId.eq(other.#txId) && this.#utxoIdx.value == other.#utxoIdx.value;
	}

    /**
     * @returns {ConstrData}
     */
    _toUplcData() {
        return new ConstrData(0, [this.#txId._toUplcData(), this.#utxoIdx._toUplcData()])
    }

    /**
     * @param {UplcData} data
     * @returns {TxOutputId}
     */
    static fromUplcData(data) {
        assert(data.index == 0, `TxOutputId.fromUplcData: expected constructor index 0, got ${data.index}`);
        assert(data.fields.length == 2, "TxOutputId.fromUplcData: expected 2 fields");

        return new TxOutputId(TxId.fromUplcData(data.fields[0]), HInt.fromUplcData(data.fields[1]).value);
    }

    /**
     * @param {string | number[]} bytes
     * @returns {TxOutputId}
     */
    static fromUplcCbor(bytes) {
        return TxOutputId.fromUplcData(UplcData.fromCbor(bytes));
    }

	/**
	 * @param {string | number[]} rawBytes 
	 * @returns {TxOutputId}
	 */
	static fromCbor(rawBytes) {
		const bytes = Array.isArray(rawBytes) ? rawBytes : hexToBytes(rawBytes);

		/** @type {null | TxId} */
		let txId = null;

		/** @type {null | bigint} */
		let utxoIdx = null;

		Cbor.decodeTuple(bytes, (i, fieldBytes) => {
			switch(i) {
				case 0:
					txId = TxId.fromCbor(fieldBytes);
					break;
				case 1:
					utxoIdx = Cbor.decodeInteger(fieldBytes);
					break;
				default:
					throw new Error("unrecognized field");
			}
		});

		if (txId === null || utxoIdx === null) {
			throw new Error("unexpected");
		} else {
			return new TxOutputId(txId, utxoIdx);
		}
	}

	/**
	 * @returns {number[]}
	 */
	toCbor() {
		return Cbor.encodeTuple([
			this.#txId.toCbor(),
			Cbor.encodeInteger(this.#utxoIdx.value)
		]);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `${this.#txId.hex}#${this.#utxoIdx.value.toString()}`;
	}

	/**
	 * 
	 * @param {TxOutputId} a 
	 * @param {TxOutputId} b 
	 * @returns {number}
	 */
	static comp(a, b) {
		let res = ByteArrayData.comp(a.#txId.bytes, b.#txId.bytes);

		if (res == 0) {
			return Number(a.#utxoIdx.value - b.#utxoIdx.value);
		} else {
			return res;
		}
	}
}

/**
 * An array of bytes, a Bech32 encoded address, or the hexadecimal representation of the underlying bytes.
 * @typedef {number[] | string} AddressProps
 */

/**
 * Wrapper for Cardano address bytes. An `Address` consists of three parts internally:
 *   * Header (1 byte, see [CIP 19](https://cips.cardano.org/cips/cip19/))
 *   * Witness hash (28 bytes that represent the `PubKeyHash` or `ValidatorHash`)
 *   * Optional staking credential (0 or 28 bytes)
 */
export class Address extends HeliosData {
	/** @type {number[]} */
	#bytes;

    /**
	 * @internal
	 * @param {AddressProps} props
	 * @returns {number[]}
	 */
    static cleanConstructorArg(props) {
        if (typeof props == "string") {
            if (props.startsWith("addr")) {
                return Address.fromBech32(props).bytes;
            } else {
                if (props.startsWith("#")) {
                    props = props.slice(1);
                }

                return hexToBytes(props);
            }
        } else {
            return props;
        }
    }

	/**
	 * @param {number[] | string} bytesOrBech32String
	 */
	constructor(bytesOrBech32String) {
		super();
		this.#bytes = Address.cleanConstructorArg(bytesOrBech32String);

        assert(this.#bytes.length == 29 || this.#bytes.length == 57, `expected 29 or 57 bytes for Address, got ${this.#bytes.length}`);
	}

	/**
	 * @param {Address | AddressProps} props 
	 * @returns {Address}
	 */
	static fromProps(props) {
		return props instanceof Address ? props : new Address(props);
	}

	/**
	 * Returns a dummy address (based on a PubKeyHash with all null bytes)
	 * @returns {Address}
	 */
	static dummy() {
		return Address.fromPubKeyHash(PubKeyHash.dummy())
	}

	/**
	 * @type {number[]}
	 */
	get bytes() {
		return this.#bytes.slice();
	}

	/**
	 * Converts an `Address` into its CBOR representation.
	 * @returns {number[]}
	 */
	toCbor() {
		return Cbor.encodeBytes(this.#bytes);
	}

	/**
	 * Deserializes bytes into an `Address`.
	 * @param {number[]} bytes
	 * @returns {Address}
	 */
	static fromCbor(bytes) {
		return new Address(Cbor.decodeBytes(bytes));
	}

	/**
	 * Converts a Bech32 string into an `Address`.
	 * @param {string} str
	 * @returns {Address}
	 */
	static fromBech32(str) {
		// ignore the prefix (encoded in the bytes anyway)
		let [prefix, bytes] = Crypto.decodeBech32(str);

		let result = new Address(bytes);

		assert(prefix == (Address.isForTestnet(result) ? "addr_test" : "addr"), "invalid Address prefix");

		return result;
	}

	/**
	 * Constructs an `Address` using a hexadecimal string representation of the address bytes.
	 * Doesn't check validity.
	 * @param {string} hex
	 * @returns {Address}
	 */
	static fromHex(hex) {
		return new Address(hexToBytes(hex));
	}

	/**
	 * Converts a `Address` into its hexadecimal representation.
	 * @returns {string}
	 */
	toHex() {
		return bytesToHex(this.#bytes);
	}

	/**
	 * Converts a `Address` into its hexadecimal representation.
	 * @returns {string}
	 */
	get hex() {
		return this.toHex();
	}

	 /**
	 * Constructs an Address using either a `PubKeyHash` (i.e. simple payment address)
	 * or `ValidatorHash` (i.e. script address),
	 * without a staking hash.
     * @param {PubKeyHash | ValidatorHash} hash
     * @param {boolean} isTestnet Defaults to `config.IS_TESTNET`
     * @returns {Address}
     */
	static fromHash(hash, isTestnet = config.IS_TESTNET) {
		return Address.fromHashes(hash, null, isTestnet);
	}

    /**
	 * Constructs an Address using either a `PubKeyHash` (i.e. simple payment address)
	 * or `ValidatorHash` (i.e. script address),
	 * in combination with an optional staking hash (`PubKeyHash` or `StakingValidatorHash`).
     * @param {PubKeyHash | ValidatorHash} hash
     * @param {null | (PubKeyHash | StakingValidatorHash)} stakingHash
     * @param {boolean} isTestnet Defaults to `config.IS_TESTNET`
     * @returns {Address}
     */
    static fromHashes(hash, stakingHash = null, isTestnet = config.IS_TESTNET) {
        if (hash instanceof PubKeyHash) {
            return Address.fromPubKeyHash(hash, stakingHash, isTestnet);
        } else if (hash instanceof ValidatorHash) {
            return Address.fromValidatorHash(hash, stakingHash, isTestnet);
        } else {
            throw new Error("unexpected");
        }
    }

	/**
	 * Simple payment address with an optional staking hash (`PubKeyHash` or `StakingValidatorHash`).
	 * @internal
	 * @param {PubKeyHash} hash
	 * @param {null | (PubKeyHash | StakingValidatorHash)} stakingHash
     * @param {boolean} isTestnet Defaults to `config.IS_TESTNET`
	 * @returns {Address}
	 */
	static fromPubKeyHash(hash, stakingHash = null, isTestnet = config.IS_TESTNET) {
		if (stakingHash !== null) {
			if (stakingHash instanceof PubKeyHash) {
				return new Address(
					[isTestnet ? 0x00 : 0x01].concat(hash.bytes).concat(stakingHash.bytes)
				);
			} else {
				assert(stakingHash instanceof StakingValidatorHash);
				return new Address(
					[isTestnet ? 0x20 : 0x21].concat(hash.bytes).concat(stakingHash.bytes)
				);
			}
		} else {
			return new Address([isTestnet ? 0x60 : 0x61].concat(hash.bytes));
		}
	}

	/**
	 * Simple script address with an optional staking hash (`PubKeyHash` or `StakingValidatorHash`).
	 * @internal
	 * @param {ValidatorHash} hash
	 * @param {null | (PubKeyHash | StakingValidatorHash)} stakingHash
     * @param {boolean} isTestnet Defaults to `config.IS_TESTNET`
	 * @returns {Address}
	 */
	static fromValidatorHash(hash, stakingHash = null, isTestnet = config.IS_TESTNET) {
		if (stakingHash !== null) {
			if (stakingHash instanceof PubKeyHash) {
				return new Address(
					[isTestnet ? 0x10 : 0x11].concat(hash.bytes).concat(stakingHash.bytes)
				);
			} else {
				assert(stakingHash instanceof StakingValidatorHash);
				return new Address(
					[isTestnet ? 0x30 : 0x31].concat(hash.bytes).concat(stakingHash.bytes)
				);
			}
		} else {
			return new Address([isTestnet ? 0x70 : 0x71].concat(hash.bytes));
		}
	}

	/**
	 * Converts an `Address` into its Bech32 representation.
	 * @returns {string}
	 */
	toBech32() {
		return Crypto.encodeBech32(
			Address.isForTestnet(this) ? "addr_test" : "addr",
			this.#bytes
		);
	}

	/**
	 * @param {Address} other 
	 * @returns {boolean}
	 */
	eq(other) {
		return ByteArrayData.comp(this.#bytes, other.bytes) == 0;
	}

	/**
	 * @returns {Object}
	 */
	dump() {
		return {
			hex: bytesToHex(this.#bytes),
			bech32: this.toBech32(),
		};
	}

	/**
	 * Returns `true` if the given `Address` is a testnet address.
	 * @param {Address} address
	 * @returns {boolean}
	 */
	static isForTestnet(address) {
		let type = address.bytes[0] & 0b00001111;

		return type == 0;
	}

	/**
     * @internal
	 * @returns {ConstrData}
	 */
	toCredentialData() {
		let vh = this.validatorHash;

		if (vh !== null) {
			return new ConstrData(1, [new ByteArrayData(vh.bytes)]);
		} else {
			let pkh = this.pubKeyHash;

			if (pkh === null) {
				throw new Error("unexpected");
			} else {
				return new ConstrData(0, [new ByteArrayData(pkh.bytes)]);
			}
		}
	}

	/**
	 * @internal
	 * @returns {ConstrData}
	 */
	toStakingData() {
        const type = this.#bytes[0] >> 4;
		const sh = this.stakingHash;

		if (sh == null) {
			return new ConstrData(1, []); // none
		} else {
            if (type == 4 || type == 5) {
                throw new Error("not yet implemented");
            } else if (type == 3 || type == 2) {
                // some
                return new ConstrData(0, [
                    // staking credential -> 0, 1 -> pointer
                    new ConstrData(0, [
                        // StakingValidator credential
                        new ConstrData(1, [new ByteArrayData(sh.bytes)]),
                    ]),
                ]);
            } else if (type == 0 || type == 1) {
                // some
                return new ConstrData(0, [
                    // staking credential -> 0, 1 -> pointer
                    new ConstrData(0, [
                        // PubKeyHash credential
                        new ConstrData(0, [new ByteArrayData(sh.bytes)]),
                    ]),
                ]);
            } else {
                throw new Error("unexpected");
            }
		}
	}

	/**
	 * @returns {UplcData}
	 */
	_toUplcData() {
		return new ConstrData(0, [this.toCredentialData(), this.toStakingData()]);
	}

    /**
     * @param {UplcData} data
     * @param {boolean} isTestnet
     * @returns {Address}
     */
    static fromUplcData(data, isTestnet = config.IS_TESTNET) {
        assert(data.index == 0);
        assert(data.fields.length == 2);

        const credData = data.fields[0];
        const stakData = data.fields[1];

        assert(credData.fields.length == 1);

        /**
         * @type {null | (PubKeyHash | StakingValidatorHash)}
         */
        let sh;

		// for some weird reason Option::None has index 1
        if (stakData.index == 1) {
            sh = null;
        } else if (stakData.index == 0) {
            assert(stakData.fields.length == 1);

            const inner = stakData.fields[0];
            assert(inner.fields.length == 1);

            if (inner.index == 0) {
                const innerInner = inner.fields[0];
                assert(innerInner.fields.length == 1);

                if (innerInner.index == 0) {
                    sh = new PubKeyHash(innerInner.fields[0].bytes);
                } else if (innerInner.index == 1) {
                    sh = new StakingValidatorHash(innerInner.fields[0].bytes);
                } else {
                    throw new Error("unexpected");
                }
            } else if (inner.index == 1) {
                throw new Error("staking pointer not yet handled");
            } else {
                throw new Error("unexpected");
            }
        } else {
            throw new Error("unexpected");
        }

        if (credData.index == 0) {
            return Address.fromPubKeyHash(new PubKeyHash(credData.fields[0].bytes), sh, isTestnet);
        } else if (credData.index == 1) {
            return Address.fromValidatorHash(new ValidatorHash(credData.fields[0].bytes), sh, isTestnet);
        } else {
            throw new Error("unexpected");
        }
    }

    /**
	 * @internal
     * @param {string | number[]} bytes
     * @param {boolean} isTestnet
     * @returns {Address}
     */
    static fromUplcCbor(bytes, isTestnet = config.IS_TESTNET) {
        return Address.fromUplcData(UplcData.fromCbor(bytes), isTestnet);
    }

	/**
	 * Returns the underlying `PubKeyHash` of a simple payment address, or `null` for a script address.
	 * @type {null | PubKeyHash}
	 */
	get pubKeyHash() {
		let type = this.#bytes[0] >> 4;

		if (type % 2 == 0) {
			return new PubKeyHash(this.#bytes.slice(1, 29));
		} else {
			return null;
		}
	}

	/**
	 * Returns the underlying `ValidatorHash` of a script address, or `null` for a regular payment address.
	 * @type {null | ValidatorHash}
	 */
	get validatorHash() {
		let type = this.#bytes[0] >> 4;

		if (type % 2 == 1) {
			return new ValidatorHash(this.#bytes.slice(1, 29));
		} else {
			return null;
		}
	}

	/**
	 * Returns the underlying `PubKeyHash` or `StakingValidatorHash`, or `null` for non-staked addresses.
	 * @type {null | PubKeyHash | StakingValidatorHash}
	 */
	get stakingHash() {
		let type = this.#bytes[0] >> 4;

        let bytes = this.#bytes.slice(29);


        if (type == 0 || type == 1) {
            assert(bytes.length == 28);
            return new PubKeyHash(bytes);
        } else if (type == 2 || type == 3) {
            assert(bytes.length == 28);
            return new StakingValidatorHash(bytes);
        } else if (type == 4 || type == 5) {
            throw new Error("staking pointer not yet supported");
        } else {
			return null;
		}
	}

	/**
	 * Used to sort txbody withdrawals.
	 * @internal
	 * @param {Address} a
	 * @param {Address} b
	 * @return {number}
	 */
	static compStakingHashes(a, b) {
		return Hash.compare(assertDefined(a.stakingHash), assertDefined(b.stakingHash));
	}
}

/**
 * @typedef {string | [
 *   MintingPolicyHash | MintingPolicyHashProps,
 *   ByteArray | ByteArrayProps
 * ] | {
 *   mph: MintingPolicyHash | MintingPolicyHashProps,
 *   tokenName: ByteArray | ByteArrayProps
 * }} AssetClassProps
 */

/**
 * Represents a `MintingPolicyHash` combined with a token name.
 */
export class AssetClass extends HeliosData {
	/**
	 * @type {MintingPolicyHash}
	 */
	#mph;

	/**
	 * @type {ByteArray}
	 */
	#tokenName;

	/**
	 * @internal
	 * @param {AssetClassProps} props
	 * @returns {[MintingPolicyHash | MintingPolicyHashProps, ByteArray | ByteArrayProps]}
	 */
	static cleanConstructorArgs(props) {
		if (typeof props == "string") {
			const fields = props.split(".")

			assert(fields.length == 2, "expected '.' in hex encoded AssetClass");

			return [fields[0], hexToBytes(fields[1])];
		} else if (Array.isArray(props) && props.length == 2) {
			return [props[0], props[1]];
		} else if (typeof props == "object") {
			return [assertDefined(props.mph), assertDefined(props.tokenName)];
		} else {
			throw new Error("unexpected number of AssetClass args");
		}
	}

	/**
	 * Intelligently converts arguments. 
	 * 
	 * The format for single argument string is "<hex-encoded-mph>.<hex-encoded-token-name>".
	 * @param {AssetClassProps} props
	 */
	constructor(props) {
		super();
		const [rawMph, rawTokenName] = AssetClass.cleanConstructorArgs(props);

		this.#mph = MintingPolicyHash.fromProps(rawMph);
		this.#tokenName = ByteArray.fromProps(rawTokenName);
	}

	/**
	 * @param {AssetClass | AssetClassProps} props 
	 * @returns {AssetClass}
	 */
	static fromProps(props) {
		return props instanceof AssetClass ? props : new AssetClass(props);
	}

	/**
	 * @type {MintingPolicyHash}
	 */
	get mintingPolicyHash() {
		return this.#mph;
	}

	/**
	 * @type {ByteArray}
	 */
	get tokenName() {
		return this.#tokenName;
	}

	/**
	 * Used when generating script contexts for running programs
	 * @returns {ConstrData}
	 */
	_toUplcData() {
		return new ConstrData(0, [
			this.#mph._toUplcData(),
			this.#tokenName._toUplcData()
		])
	}

	/**
	 *
	 * @param {UplcData} data
	 * @returns {AssetClass}
	 */
	static fromUplcData(data) {
		assert(data.index == 0);
		assert(data.fields.length == 2);

		const mph = MintingPolicyHash.fromUplcData(data.fields[0]);
		const tokenName = ByteArray.fromUplcData(data.fields[1]);

		return new AssetClass([mph, tokenName]);
	}

	/**
	 * Cip14 fingerprint
	 * This involves a hash, so you can't use a fingerprint to calculate the underlying policy/tokenName.
	 * @returns {string}
	 */
	toFingerprint() {
		return Crypto.encodeBech32("asset", Crypto.blake2b(this.#mph.bytes.concat(this.#tokenName.bytes), 20));
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `${this.#mph.hex}.${bytesToHex(this.#tokenName.bytes)}`;
	}

	/**
	 * Converts an `AssetClass` instance into its CBOR representation.
	 * @returns {number[]}
	 */
	toCbor() {
		return Cbor.encodeConstr(0, [
			this.#mph.toCbor(),
			this.#tokenName.toCbor()
		]);
	}

	/**
	 * Deserializes bytes into an `AssetClass`.
	 * @param {number[]} bytes
	 * @returns {AssetClass}
	 */
	static fromCbor(bytes) {
		/**
		 * @type {MintingPolicyHash | null}
		 */
		let mph = null;

		/**
		 * @type {ByteArray | null}
		 */
		let tokenName = null;

		const tag = Cbor.decodeConstr(bytes, (i, fieldBytes) => {
			switch (i) {
				case 0:
					mph = MintingPolicyHash.fromCbor(fieldBytes);
					break;
				case 1:
					tokenName = ByteArray.fromCbor(fieldBytes);
					break;
				default:
					throw new Error("unexpected field");
			}
		});

		assert(tag == 0);

		if (mph == null || tokenName == null) {
			throw new Error("insufficient fields");
		} else {
			return new AssetClass([mph, tokenName]);
		}
	}

    /**
     * @param {string | number[]} bytes
     * @returns {AssetClass}
     */
    static fromUplcCbor(bytes) {
        return AssetClass.fromUplcData(UplcData.fromCbor(bytes));
    }

	/**
	 * @type {AssetClass}
	 */
	static get ADA() {
		return new AssetClass(["", ""]);
	}
}

/**
 * @typedef {[
 *   AssetClass | AssetClassProps,
 *   HInt | HIntProps
 * ][] | [
 *   MintingPolicyHash | MintingPolicyHashProps,
 *   [
 *     ByteArray | ByteArrayProps,
 *     HInt | HIntProps
 *   ][]
 * ][]} AssetsProps
 */

/**
 * Represents a list of non-Ada tokens. 
 */
export class Assets extends CborData {
	/** 
	 * @private
	 * @type {[MintingPolicyHash, [ByteArray, HInt][]][]} 
	 */
	assets;

	/**
	 * **Note**: the assets are normalized by removing entries with 0 tokens, and merging all entries with the same MintingPolicyHash and token name.
	 * @param {AssetsProps} props Either a list of `AssetClass`/quantity pairs, or a list of `MintingPolicyHash`/`tokens` pairs (where each `tokens` entry is a bytearray/quantity pair).
	 */
	constructor(props = []) {
		super();

		this.assets = props.map((outerPair) => {
			if (Array.isArray(outerPair[1])) {
				const mph = MintingPolicyHash.fromProps(outerPair[0]);

				/**
				 * @type {[MintingPolicyHash, [ByteArray, HInt][]]}
				 */
				const mapped = [
					mph,
					outerPair[1].map((innerPair) => [ByteArray.fromProps(innerPair[0]), HInt.fromProps(innerPair[1])])
				];

				return mapped;
			} else {
				const assetClass = AssetClass.fromProps(outerPair[0]);
				const qty = HInt.fromProps(outerPair[1]);

				/**
				 * @type {[MintingPolicyHash, [ByteArray, HInt][]]}
				 */
				const mapped = [
					assetClass.mintingPolicyHash,
					[[assetClass.tokenName, qty]]
				];

				return mapped;
			}
		});

		this.normalize();
	}

	/**
	 * @param {Assets | AssetsProps} props 
	 * @returns {Assets}
	 */
	static fromProps(props) {
		return props instanceof Assets ? props : new Assets(props);
	}

	/**
	 * Returns a list of all the minting policies.
	 * @type {MintingPolicyHash[]}
	 */
	get mintingPolicies() {
		return this.assets.map(([mph, _]) => mph);
	}

	/**
	 * @type {number}
	 */
	get nTokenTypes() {
		let count = 0;

		this.assets.forEach(([mph, tokens]) => {
			tokens.forEach(([tokenName, _]) => {
				count += 1
			})
		})

		return count;
	}

	/**
	 * Returns empty if mph not found
	 * @param {MintingPolicyHash} mph
	 * @returns {[ByteArray, HInt][]}
	 */
	getTokens(mph) {
		const i = this.assets.findIndex(entry => entry[0].eq(mph));

		if (i != -1) {
			return this.assets[i][1];
		} else {
			return [];
		}
	}

	/**
	 * @returns {boolean}
	 */
	isZero() {
		return this.assets.length == 0;
	}

	/**
	 * @param {MintingPolicyHash | MintingPolicyHashProps} mph
	 * @param {ByteArray | ByteArrayProps} tokenName 
	 * @returns {boolean}
	 */
	has(mph, tokenName) {
		const mph_ = MintingPolicyHash.fromProps(mph);
		const tokenName_ = ByteArray.fromProps(tokenName);

		const inner = this.assets.find(asset => mph_.eq(asset[0]));

		if (inner !== undefined) {
			return inner[1].findIndex(pair => pair[0].eq(tokenName_)) != -1;
		} else {
			return false;
		}
	}

	/**
	 * @param {MintingPolicyHash | MintingPolicyHashProps} mph
	 * @param {ByteArray | ByteArrayProps} tokenName 
	 * @returns {bigint}
	 */
	get(mph, tokenName) {
		const mph_ = MintingPolicyHash.fromProps(mph);
		const tokenName_ = ByteArray.fromProps(tokenName);

		const inner = this.assets.find(asset => mph_.eq(asset[0]));

		if (inner !== undefined) {
			const token = inner[1].find(pair => pair[0].eq(tokenName_));

			if (token !== undefined) {
				return token[1].value;
			} else {
				return 0n;
			}
		} else {
			return 0n;
		}
	}

	/**
	 * Mutates 'this'
	 */
	removeZeroes() {
		for (let asset of this.assets) {
			asset[1] = asset[1].filter(token => !token[1].eq(0n));
		}

		this.assets = this.assets.filter(asset => asset[1].length != 0);
	}

	/**
	 * Removes zeros and merges duplicates.
	 * In-place algorithm.
	 * Keeps the same order as much as possible.
	 */
	normalize() {
		/**
		 * @type {Map<string, Map<string, bigint>>}
		 */
		const assets = new Map();

		for (let [mph, tokens] of this.assets) {
			let outerPrev = assets.get(mph.hex);

			if (!outerPrev) {
				outerPrev = new Map();
			} 

			for (let [tokenName, qty] of tokens) {
				let innerPrev = outerPrev.get(tokenName.hex);

				if (!innerPrev) {
					innerPrev = 0n;
				}

				innerPrev += qty.value;

				outerPrev.set(tokenName.hex, innerPrev);
			}

			assets.set(mph.hex, outerPrev);
		}

		const entries = Array.from(assets.entries());

		this.assets = entries.map(([rawMph, rawTokens]) => {
			const tokens = Array.from(rawTokens.entries());

			return [MintingPolicyHash.fromProps(rawMph), tokens.map(([rawTokenName, rawQty]) => {
				return [ByteArray.fromProps(rawTokenName), HInt.fromProps(rawQty)];
			})];
		});
	}

	/**
	 * Mutates 'this'.
	 * @param {MintingPolicyHash | MintingPolicyHashProps} mph
	 * @param {ByteArray | ByteArrayProps} tokenName 
	 * @param {HInt | HIntProps} qty
	 */
	addComponent(mph, tokenName, qty) {
		const mph_ = MintingPolicyHash.fromProps(mph);
		const tokenName_ = ByteArray.fromProps(tokenName);
		const qty_ = HInt.fromProps(qty);

		if (qty_.eq(0n)) {
			return;
		}

		const inner = this.assets.find(asset => mph_.eq(asset[0]));

		if (inner === undefined) {
			this.assets.push([mph_, [[tokenName_, qty_]]]);
		} else {
			const token = inner[1].find(pair => pair[0].eq(tokenName_));

			if (token === undefined) {
				inner[1].push([tokenName_, qty_]);
			} else {
				token[1] = token[1].add(qty_);
			}
		}

		this.removeZeroes();
	}

	/**
	 * @internal
	 * @param {Assets} other
	 * @param {(a: bigint, b: bigint) => bigint} op
	 * @returns {Assets}
	 */
	applyBinOp(other, op) {
		let res = new Assets();

		for (let [mph, tokens] of this.assets) {
			for (let [tokenName, quantity] of tokens) {
				res.addComponent(mph, tokenName, new HInt(op(quantity.value, 0n)));
			}
		}

		for (let [mph, tokens] of other.assets) {
			for (let [tokenName, quantity] of tokens) {
				res.addComponent(mph, tokenName, new HInt(op(0n, quantity.value)));
			}
		}

		return res;
	}

	/**
	 * @param {Assets} other
	 * @returns {Assets}
	 */
	add(other) {
		return this.applyBinOp(other, (a, b) => a + b);
	}

	/**
	 * @param {Assets} other
	 * @returns {Assets}
	 */
	sub(other) {
		return this.applyBinOp(other, (a, b) => a - b);
	}

	/**
	 * @param {HInt | HIntProps} scalar 
	 * @returns {Assets}
	 */
	mul(scalar) {
		const s = HInt.fromProps(scalar);

		return new Assets(this.assets.map(([mph, tokens]) => {
			/**
			 * @type {[MintingPolicyHash, [ByteArray, HInt][]]}
			 */
			const mapped = [mph, tokens.map(([token, qty]) => [token, qty.mul(s)])]

			return mapped;
		}))
	}

	/**
	 * Mutates 'this'.
	 * Throws error if mph is already contained in 'this'.
	 * @param {MintingPolicyHash | MintingPolicyHashProps} mph
	 * @param {[ByteArray | ByteArrayProps, HInt | HIntProps][]} tokens
	 */
	addTokens(mph, tokens) {
		const mph_ = MintingPolicyHash.fromProps(mph);

		for (let asset of this.assets) {
			if (asset[0].eq(mph_)) {
				throw new Error(`MultiAsset already contains ${mph_.hex}`);
			}
		}

		/**
		 * @type {[ByteArray, HInt][]}
		 */
		const tokens_ = tokens.map(([tokenName, qty]) => [ByteArray.fromProps(tokenName), HInt.fromProps(qty)]);

		this.assets.push([mph_, tokens_]);

		// sort immediately
		this.sort();
	}

	/**
	 * @param {MintingPolicyHash | MintingPolicyHashProps} mph
	 * @returns {ByteArray[]}
	 */
	getTokenNames(mph) {
		const mph_ = MintingPolicyHash.fromProps(mph);

		for (let [otherMph, tokens] of this.assets) {
			if (otherMph.eq(mph_)) {
				return tokens.map(([tokenName, _]) => tokenName);
			}
		}

		return [];
	}

	/**
	 * @param {Assets} other
	 * @returns {boolean}
	 */
	eq(other) {
		for (let asset of this.assets) {
			for (let token of asset[1]) {
				if (token[1].neq(other.get(asset[0], token[0]))) {
					return false;
				}
			}
		}

		for (let asset of other.assets) {
			for (let token of asset[1]) {
				if (token[1].neq(this.get(asset[0], token[0]))) {
					return false;
				}
			}
		}

		return true;
	}

	/**
	 * Strict gt, if other contains assets this one doesn't contain => return false
	 * @param {Assets} other
	 * @returns {boolean}
	 */
	gt(other) {
		if (this.isZero()) {
			return false;
		}

		for (let asset of this.assets) {
			for (let token of asset[1]) {
				if (token[1].le(other.get(asset[0], token[0]))) {
					return false;
				}
			}
		}

		for (let asset of other.assets) {
			for (let token of asset[1]) {
				if (!this.has(asset[0], token[0])) {
					return false;
				}
			}
		}

		return true;
	}

	/**
	 * @param {Assets} other
	 * @returns {boolean}
	 */
	ge(other) {
		if (this.isZero()) {
			return other.isZero();
		}

		for (let asset of this.assets) {
			for (let token of asset[1]) {
				if (token[1].lt(other.get(asset[0], token[0]))) {
					return false;
				}
			}
		}

		for (let asset of other.assets) {
			for (let token of asset[1]) {
				if (!this.has(asset[0], token[0])) {
					return false;
				}
			}
		}

		return true;
	}

	/**
	 * @returns {boolean}
	 */
	allPositive() {
		for (let asset of this.assets) {
			for (let pair of asset[1]) {
				if (pair[1].lt(0n)) {
					return false;
				} else if (pair[1].eq(0n)) {
					throw new Error("unexpected");
				}
			}
		}

		return true;
	}

	/**
	 * Throws an error if any contained quantity <= 0n
	 */
	assertAllPositive() {
		assert(this.allPositive(), "non-positive token amounts detected");
	}

	/**
	 * @returns {number[]}
	 */
	toCbor() {
		return Cbor.encodeMap(
			this.assets.map(
				outerPair => {
					return [outerPair[0].toCbor(), Cbor.encodeMap(outerPair[1].map(
						innerPair => [innerPair[0].toCbor(), innerPair[1].toCbor()]
					))];
				}
			)
		);
	}

	/**
	 * @param {number[]} bytes
	 * @returns {Assets}
	 */
	static fromCbor(bytes) {
		let ms = new Assets();

		Cbor.decodeMap(bytes, (_, pairBytes) => {
			let mph = MintingPolicyHash.fromCbor(pairBytes);

			/**
			 * @type {[ByteArray, HInt][]}
			 */
			let innerMap = [];

			Cbor.decodeMap(pairBytes, (_, innerPairBytes) => {
				innerMap.push([
					ByteArray.fromCbor(innerPairBytes),
					HInt.fromCbor(innerPairBytes)
				]);
			});

			ms.assets.push([mph, innerMap]);
		});

		return ms;
	}

	/**
	 * @returns {Object}
	 */
	dump() {
		let obj = {};

		for (let [mph, tokens] of this.assets) {
			let innerObj = {};

			for (let [tokenName, quantity] of tokens) {
				innerObj[tokenName.hex] = quantity.toString();
			}

			obj[mph.hex] = innerObj;
		}

		return obj;
	}

	/**
	 * Used when generating script contexts for running programs
	 * @returns {MapData}
	 */
	_toUplcData() {
		/** @type {[UplcData, UplcData][]} */
		const pairs = [];

		for (let asset of this.assets) {
			/** @type {[UplcData, UplcData][]} */
			const innerPairs = [];

			for (let token of asset[1]) {
				innerPairs.push([
					token[0]._toUplcData(),
					token[1]._toUplcData()
				]);
			}

			pairs.push([
				new ByteArrayData(asset[0].bytes),
				new MapData(innerPairs),
			])
		}

		return new MapData(pairs);
	}

	/**
	 * Makes sure minting policies are in correct order, and for each minting policy make sure the tokens are in the correct order
	 * Mutates 'this'
	 */
	sort() {
		this.assets.sort((a, b) => {
			return Hash.compare(a[0], b[0]);
		});

		this.assets.forEach(([_, tokens]) => {
			tokens.sort((a, b) => {
				return ByteArrayData.compLengthFirst(a[0].bytes, b[0].bytes);
			});
		});
	}

	assertSorted() {
		this.assets.forEach((b, i) => {
			if (i > 0) {
				const a = this.assets[i-1];

				assert(Hash.compare(a[0], b[0]) == -1, `assets not sorted (${a[0].hex} vs ${b[0].hex})`);

				b[1].forEach((bb, j) => {
					if (j > 0) {
						const aa = b[1][j-1];

						assert(ByteArrayData.compLengthFirst(aa[0].bytes, bb[0].bytes) < 0, "tokens not sorted");
					}
				})
			}
		})
	}
}

/**
 * @typedef {HInt | HIntProps | [
 *   HInt | HIntProps,
 *   Assets | AssetsProps
 * ] | {
 *   lovelace: HInt| HIntProps,
 *   assets?:   Assets | AssetsProps
 * }} ValueProps
 */

/**
 * Represents a collection of tokens.
 */
export class Value extends HeliosData {
	/** @type {HInt} */
	#lovelace;

	/** @type {Assets} */
	#assets;

	/**
	 * @param {ValueProps} props 
	 * @param {null | Assets | AssetsProps} maybeAssets 
	 * @returns {[HInt | HIntProps, Assets | AssetsProps]}
	 */
	static cleanConstructorArgs(props, maybeAssets) {
		if (Array.isArray(props)) {
			assert(props.length == 2, "expected two entries for AssetsProps");

			if (maybeAssets) {
				throw new Error("can't combine assets arg with ValueProps that also contains assets");
			}

			return [props[0], props[1]];
		} else if (props instanceof HInt) {
			return [props, maybeAssets ? maybeAssets : new Assets()];
		} else if (typeof props == "object") {
			if (maybeAssets) {
				throw new Error("can't combine assets arg with ValueProps that also contains assets");
			}

			return [props.lovelace, props.assets ?? new Assets()];
		} else {
			return [props, maybeAssets ? maybeAssets : new Assets()];
		}
	}

	/**
	 * @param {ValueProps} props 
	 * @param {null | Assets | AssetsProps} assets 
	 */
	constructor(props = 0n, assets = null) {
		super();

		const [rawLovelace, rawAssets] = Value.cleanConstructorArgs(props, assets);

		this.#lovelace = HInt.fromProps(rawLovelace);
		this.#assets = Assets.fromProps(rawAssets);
	}

	/**
	 * @param {ValueProps | Value} props 
	 * @returns {Value}
	 */
	static fromProps(props) {
		if (props instanceof Value) {
			return props;
		} else {
			return new Value(props);
		}
	}

	/**
	 * @param {MintingPolicyHash | MintingPolicyHashProps} mph 
	 * @param {ByteArray | ByteArrayProps} tokenName 
	 * @param {HInt | HIntProps} qty 
	 * @returns {Value}
	 */
	static asset(mph, tokenName, qty) {
		const mph_ = MintingPolicyHash.fromProps(mph);
		const tokenName_ = ByteArray.fromProps(tokenName);
		const qty_ = HInt.fromProps(qty);

		return new Value({
			lovelace: 0n, 
			assets: new Assets([
				[mph_, [
					[tokenName_, qty_]
				]]
			])
		});
	}

	/**
	 * Gets the `Assets` contained in the `Value`.
	 * @type {Assets}
	 */
	get assets() {
		return this.#assets;
	}

	/**
	 * Gets the lovelace quantity contained in the `Value`.
	 * @type {bigint}
	 */
	get lovelace() {
		return this.#lovelace.value;
	}

	/**
	 * Mutates the quantity of lovelace in a `Value`.
	 * @param {HInt | HIntProps} lovelace
	 */
	setLovelace(lovelace) {
		this.#lovelace = HInt.fromProps(lovelace);
	}

	/**
	 * @returns {number[]}
	 */
	toCbor() {
		if (this.#assets.isZero()) {
			return this.#lovelace.toCbor()
		} else {
			return Cbor.encodeTuple([
				this.#lovelace.toCbor(),
				this.#assets.toCbor()
			]);
		}
	}

	/**
	 * @param {number[]} bytes
	 * @returns {Value}
	 */
	static fromCbor(bytes) {
		let mv = new Value();

		if (Cbor.isTuple(bytes)) {
			Cbor.decodeTuple(bytes, (i, fieldBytes) => {
				switch(i) {
					case 0:
						mv.#lovelace = HInt.fromCbor(fieldBytes);
						break;
					case 1:
						mv.#assets = Assets.fromCbor(fieldBytes);
						break;
					default:
						throw new Error("unrecognized field");
				}
			});
		} else {
			mv.#lovelace = HInt.fromCbor(bytes);
		}

		return mv;
	}

	/**
	 * @param {Value[]} values
	 * @returns {Value}
	 */
	static sum(values) {
		let s = new Value(0n);

		values.forEach(v => {
			s = s.add(v);
		});

		return s;
	}

	/**
	 * Adds two `Value` instances together. Returns a new `Value` instance.
	 * @param {Value} other
	 * @returns {Value}
	 */
	add(other) {
		return new Value({
			lovelace: this.#lovelace.add(other.lovelace), 
			assets: this.#assets.add(other.assets)
		});
	}

	/**
	 * Substracts one `Value` instance from another. Returns a new `Value` instance.
	 * @param {Value} other
	 * @returns {Value}
	 */
	sub(other) {
		return new Value({
			lovelace: this.#lovelace.sub(other.lovelace), 
			assets: this.#assets.sub(other.assets)
		});
	}

	/**
	 * Multiplies a `Value` by a whole number.
	 * @param {HInt | HIntProps} scalar 
	 * @returns {Value}
	 */
	mul(scalar) {
		return new Value({
			lovelace: this.#lovelace.mul(scalar), 
			assets: this.#assets.mul(scalar)
		})
	}

	/**
	 * Checks if two `Value` instances are equal (`Assets` need to be in the same order).
	 * @param {Value} other
	 * @returns {boolean}
	 */
	eq(other) {
		return this.#lovelace.eq(other.lovelace) && (this.#assets.eq(other.assets));
	}

	/**
	 * Checks if a `Value` instance is strictly greater than another `Value` instance. Returns false if any asset is missing.
	 * @param {Value} other
	 * @returns {boolean}
	 */
	gt(other) {
		return this.#lovelace.gt(other.lovelace) && (this.#assets.gt(other.assets));
	}

	/**
	 * Checks if a `Value` instance is strictly greater or equal to another `Value` instance. Returns false if any asset is missing.
	 * @param {Value} other
	 * @returns {boolean}
	 */
	ge(other) {
		return this.#lovelace.ge(other.lovelace) && (this.#assets.ge(other.assets));
	}

	/**
	 * Throws an error if any of the `Value` entries is negative.
	 * 
	 * Used when building transactions because transactions can't contain negative values.
	 * @returns {Value} - returns this
	 */
	assertAllPositive() {
		assert(this.#lovelace.ge(0n));

		this.#assets.assertAllPositive();

		return this;
	}

	/**
	 * @returns {Object}
	 */
	dump() {
		return {
			lovelace: this.#lovelace.dump(),
			assets: this.#assets.dump()
		};
	}

	/**
	 * Used when building script context
	 * @param {boolean} isInScriptContext
	 * @returns {MapData}
	 */
	_toUplcData(isInScriptContext = false) {
		let map = this.#assets._toUplcData();

		if (this.#lovelace.neq(0n) || isInScriptContext) {
			const inner = map.map; 

			inner.unshift([
				new ByteArrayData([]),
				new MapData([
					[new ByteArrayData([]), this.#lovelace._toUplcData()]
				]),
			]);

			// 'inner' is copy, so mutating won't change the original
			map = new MapData(inner);
		}

		return map;
	}

	/**
	 * Converts a `UplcData` instance into a `Value`. Throws an error if it isn't in the right format.
	 * @param {UplcData} data
	 * @returns {Value}
	 */
	static fromUplcData(data) {
		let sum = new Value();

		let outerMap = data.map;

		for (let [mphData, tokensData] of outerMap) {
			let mphBytes = mphData.bytes;

			let innerMap = tokensData.map;

			if (mphBytes.length == 0) {
				//lovelace
				assert(innerMap.length == 1 && innerMap[0][0].bytes.length == 0, `bad ada token map`); 
				sum = sum.add(new Value({lovelace: innerMap[0][1].int}));
			} else {
				// other assets
				let mph = new MintingPolicyHash(mphBytes);

				for (let [tokenNameData, quantityData] of innerMap) {
					let tokenName = tokenNameData.bytes;
					let quantity = quantityData.int;

					sum = sum.add(Value.asset(mph, tokenName, quantity));
				}
			}
		}

		return sum;
	}

	/**
	 * @param {string | number[]} bytes
	 * @returns {Value}
	 */
	static fromUplcCbor(bytes) {
		return Value.fromUplcData(UplcData.fromCbor(bytes));
	}
}



//////////////////////////////
// Section 8: Uplc cost-models
//////////////////////////////

/**
 * @typedef {Object} Cost
 * @property {bigint} mem
 * @property {bigint} cpu
 */

/**
 * @typedef {() => bigint} LiveSlotGetter
 */

/**
 * Wrapper for the raw JSON containing all the current network parameters.
 * 
 * NetworkParams is needed to be able to calculate script budgets and perform transaction building checks.
 * 
 * The raw JSON can be downloaded from the following CDN locations:
 * 
 *  - Preview: [https://d1t0d7c2nekuk0.cloudfront.net/preview.json](https://d1t0d7c2nekuk0.cloudfront.net/preview.json)
 *  - Preprod: [https://d1t0d7c2nekuk0.cloudfront.net/preprod.json](https://d1t0d7c2nekuk0.cloudfront.net/preprod.json)
 *  - Mainnet: [https://d1t0d7c2nekuk0.cloudfront.net/mainnet.json](https://d1t0d7c2nekuk0.cloudfront.net/mainnet.json)
 * 
 * These JSONs are updated every 15 minutes.
 */
export class NetworkParams {
	#raw;

	/**
	 * Should only be set by the network emulator
	 * @type {null | LiveSlotGetter}
	 */
	#liveSlotGetter;

	/**
	 * @param {Object} raw 
	 * @param {null | LiveSlotGetter} liveSlotGetter
	 */
	constructor(raw, liveSlotGetter = null) {
		if(typeof raw !== 'object'){
		    throw new Error("raw param must be of type object");
        }
		
		this.#raw = raw;
		this.#liveSlotGetter = liveSlotGetter;
	}

	/**
	 * @type {Object}
	 */
	get raw() {
		return this.#raw;
	}
	
	/**
	 * @type {null | bigint}
	 */
	get liveSlot() {
		if (this.#liveSlotGetter) {
			return this.#liveSlotGetter()
		} else {
			return null;
		}
	}

    /**
     * @internal
     * @type {Object}
     */
	get costModel() {
		return assertDefined(this.#raw?.latestParams?.costModels?.PlutusScriptV2, "'obj.latestParams.costModels.PlutusScriptV2' undefined");
	}

	/**
     * @internal
	 * @param {string} key 
	 * @returns {number}
	 */
	getCostModelParameter(key) {
		return assertNumber(this.costModel[key], `'obj.${key}' undefined`);
	}

	/**
     * @internal
	 * @param {string} name 
	 * @returns {Cost}
	 */
	getTermCost(name) {
		let memKey = `cek${name}Cost-exBudgetMemory`;
		let cpuKey = `cek${name}Cost-exBudgetCPU`;

		return {
			mem: BigInt(assertNumber(this.costModel[memKey], `'obj.${memKey}' undefined`)),
			cpu: BigInt(assertNumber(this.costModel[cpuKey], `'obj.${cpuKey}' undefined`)),
		};
	}

	/**
     * @internal
	 * @type {Cost}
	 */
	get plutusCoreStartupCost() {
		return this.getTermCost("Startup");
	}

	/**
     * @internal
	 * @type {Cost}
	 */
	get plutusCoreVariableCost() {
		return this.getTermCost("Var");
	}

	/**
     * @internal
	 * @type {Cost}
	 */
	get plutusCoreLambdaCost() {
		return this.getTermCost("Lam");
	}

	/**
     * @internal
	 * @type {Cost}
	 */
	get plutusCoreDelayCost() {
		return this.getTermCost("Delay");
	}

	/**
     * @internal
	 * @type {Cost}
	 */
	get plutusCoreCallCost() {
		return this.getTermCost("Apply");
	}

	/**
     * @internal
	 * @type {Cost}
	 */
	get plutusCoreConstCost() {
		return this.getTermCost("Const");
	}

	/**
     * @internal
	 * @type {Cost}
	 */
	get plutusCoreForceCost() {
		return this.getTermCost("Force");
	}

	/**
     * @internal
	 * @type {Cost}
	 */
	get plutusCoreBuiltinCost() {
		return this.getTermCost("Builtin");
	}

	/**
     * @internal
	 * @type {[number, number]} - a + b*size
	 */
	get txFeeParams() {
		return [
			assertNumber(this.#raw?.latestParams?.txFeeFixed),
			assertNumber(this.#raw?.latestParams?.txFeePerByte),
		];
	}

	/**
     * @internal
	 * @type {[number, number]} - [memFee, cpuFee]
	 */
	get exFeeParams() {
		return [
			assertNumber(this.#raw?.latestParams?.executionUnitPrices?.priceMemory),
			assertNumber(this.#raw?.latestParams?.executionUnitPrices?.priceSteps),
		];
	}
	
	/**
     * @internal
	 * @type {number[]}
	 */
	get sortedCostParams() {
		let baseObj = this.#raw?.latestParams?.costModels?.PlutusScriptV2;
		let keys = Object.keys(baseObj);

		keys.sort();

		return keys.map(key => assertNumber(baseObj[key]));
	}

	/**
     * @internal
	 * @type {number}
	 */
	get lovelacePerUTXOByte() {
		return assertNumber(this.#raw?.latestParams?.utxoCostPerByte);
	}

	/**
     * @internal
	 * @type {number}
	 */
	get minCollateralPct() {
		return assertNumber(this.#raw?.latestParams?.collateralPercentage);
	}

	/**
     * @internal
	 * @type {number}
	 */
	get maxCollateralInputs() {
		return assertNumber(this.#raw?.latestParams?.maxCollateralInputs);
	}

	/**
     * @internal
	 * @type {[number, number]} - [mem, cpu]
	 */
	get maxTxExecutionBudget() {
		return [
			assertNumber(this.#raw?.latestParams?.maxTxExecutionUnits?.memory),
			assertNumber(this.#raw?.latestParams?.maxTxExecutionUnits?.steps),
		];
	}

	/**
     * @internal
	 * @type {number}
	 */
	get maxTxSize() {
		return assertNumber(this.#raw?.latestParams?.maxTxSize);
	}

	/**
	 * @type {bigint}
	 */
	get stakeAddressDeposit() {
		return BigInt(assertNumber(this.#raw?.latestParams?.stakeAddressDeposit));
	}

	/**
	 * Tx balancing picks additional inputs by starting from maxTxFee. 
	 * This is done because the order of the inputs can have a huge impact on the tx fee, so the order must be known before balancing.
	 * If there aren't enough inputs to cover the maxTxFee and the min deposits of newly created UTxOs, the balancing will fail.
	 * @type {bigint}
	 */
	get maxTxFee() {
		const [a, b] = this.txFeeParams;
		const [feePerMem, feePerCpu] = this.exFeeParams;
		const [maxMem, maxCpu] = this.maxTxExecutionBudget;

		return BigInt(a) + BigInt(Math.ceil(b*this.maxTxSize)) + BigInt(Math.ceil(feePerMem*maxMem)) + BigInt(Math.ceil(feePerCpu*maxCpu));
	}

	/**
	 * Calculates the time (in milliseconds in 01/01/1970) associated with a given slot number.
	 * @param {bigint} slot
	 * @returns {bigint}
	 */
	slotToTime(slot) {
		let secondsPerSlot = assertNumber(this.#raw?.shelleyGenesis?.slotLength);

		let lastSlot = BigInt(assertNumber(this.#raw?.latestTip?.slot));
		let lastTime = BigInt(assertNumber(this.#raw?.latestTip?.time));

		let slotDiff = slot - lastSlot;

		return lastTime + slotDiff*BigInt(secondsPerSlot*1000);
	}

	/**
	 * Calculates the slot number associated with a given time. Time is specified as milliseconds since 01/01/1970.
	 * @param {bigint} time Milliseconds since 1970
	 * @returns {bigint}
	 */
	timeToSlot(time) {
		let secondsPerSlot = assertNumber(this.#raw?.shelleyGenesis?.slotLength);

		let lastSlot = BigInt(assertNumber(this.#raw?.latestTip?.slot));
		let lastTime = BigInt(assertNumber(this.#raw?.latestTip?.time));

		let timeDiff = time - lastTime;

		return lastSlot + BigInt(Math.round(Number(timeDiff)/(1000*secondsPerSlot)));
	}
}

/**
 * Each builtin has an associated CostModel.
 * The CostModel calculates the execution cost of a builtin, depending on the byte-size of the inputs.
 * @internal
 */
export class CostModel {
	constructor() {
	}

	/**
	 * @param {NetworkParams} params
	 * @param {string} baseName
	 * @returns {CostModel}
	 */
	static fromParams(params, baseName) {
		throw new Error("not yet implemented");
	}

	/**
	 * @param {number[]} args 
	 * @returns {bigint}
	 */
	calc(args) {
		throw new Error("not yet implemented");
	}

	/**
	 * @returns {string}
	 */
	dump() {
		throw new Error("not yet implemented");
	}
}

/**
 * A simple constant cost, independent of arg size.
 * @internal
 */
export class ConstCost extends CostModel {
	#constant;

	/**
	 * @param {bigint} constant
	 */
	constructor(constant) {
		super();
		this.#constant = constant;
	}

	/**
	 * @param {NetworkParams} params 
	 * @param {string} baseName - eg. addInteger-cpu-arguments
	 * @returns {ConstCost}
	 */
	static fromParams(params, baseName) {
		let a = params.getCostModelParameter(`${baseName}`);

		return new ConstCost(BigInt(a));
	}

	/**
	 * @param {number[]} args
	 * @returns {bigint}
	 */
	calc(args) {
		return this.#constant;
	}

	/**
	 * @returns {string}
	 */
	dump() {
		return `const: ${this.#constant.toString()}`;
	}
}

/**
 * cost = a + b*size(arg)
 * @internal
 */
export class LinearCost extends CostModel {
	#a;
	#b;

	/**
	 * a + b*SizeFn(x, y)
	 * @param {bigint} a - intercept
	 * @param {bigint} b - slope
	 */
	constructor(a, b) {
		super();
		this.#a = a;
		this.#b = b;
	}

	/**
	 * @param {NetworkParams} params 
	 * @param {string} baseName - eg. addInteger-cpu-arguments
	 * @returns {[bigint, bigint]}
	 */
	static getParams(params, baseName) {
		let a = params.getCostModelParameter(`${baseName}-intercept`);
		let b = params.getCostModelParameter(`${baseName}-slope`);

		return [BigInt(a), BigInt(b)];
	}

	/**
	 * @param  {number} size
	 * @returns {bigint}
	 */
	calcInternal(size) {
		return this.#a + this.#b*BigInt(size);
	}

	/**
	 * @returns {string}
	 */
	dump() {
		return `intercept: ${this.#a.toString()}, slope: ${this.#b.toString()}`;
	}
}

/**
 * cost = a + b*size(args[i])
 * @internal
 */
export class ArgSizeCost extends LinearCost {
	#i;

	/**
	 * @param {bigint} a - intercept
	 * @param {bigint} b - slope
	 * @param {number} i - index of the arg
	 */
	constructor(a, b, i) {
		super(a, b);
		this.#i = i;
	}

	/**
	 * @param {number[]} args
	 * @returns {bigint}
	 */
	calc(args) {
		assert(this.#i < args.length && this.#i >= 0);

		return this.calcInternal(args[this.#i]);
	}
}

/**
 * cost = a + b*size(arg0)
 * @internal
 */
export class Arg0SizeCost extends ArgSizeCost {
	/**
	 * @param {bigint} a 
	 * @param {bigint} b 
	 */
	constructor(a, b) {
		super(a, b, 0);
	}

	/**
	 * @param {NetworkParams} params 
	 * @param {string} baseName - eg. addInteger-cpu-arguments
	 * @returns {Arg0SizeCost}
	 */
	static fromParams(params, baseName) {
		let [a, b] = LinearCost.getParams(params, baseName);

		return new Arg0SizeCost(a, b);
	}
}

/**
 * cost = a + b*size(arg1)
 * @internal
 */
export class Arg1SizeCost extends ArgSizeCost {
	/**
	 * @param {bigint} a 
	 * @param {bigint} b 
	 */
	constructor(a, b) {
		super(a, b, 1);
	}

	/**
	 * @param {NetworkParams} params 
	 * @param {string} baseName - eg. addInteger-cpu-arguments
	 * @returns {Arg1SizeCost}
	 */
	static fromParams(params, baseName) {
		let [a, b] = LinearCost.getParams(params, baseName);

		return new Arg1SizeCost(a, b);
	}
}

/**
 * cost = a + b*size(arg2)
 * @internal
 */
export class Arg2SizeCost extends ArgSizeCost {
	/**
	 * @param {bigint} a 
	 * @param {bigint} b 
	 */
	constructor(a, b) {
		super(a, b, 2);
	}

	/**
	 * @param {NetworkParams} params 
	 * @param {string} baseName - eg. addInteger-cpu-arguments
	 * @returns {Arg2SizeCost}
	 */
	static fromParams(params, baseName) {
		let [a, b] = LinearCost.getParams(params, baseName);

		return new Arg2SizeCost(a, b);
	}
}

/**
 * cost = a + b*min(args)
 * @internal
 */
export class MinArgSizeCost extends LinearCost {
	/**
	 * @param {bigint} a - intercept
	 * @param {bigint} b - slope
	 */
	constructor(a, b) {
		super(a, b);
	}
	/**
	 * @param {NetworkParams} params 
	 * @param {string} baseName - eg. addInteger-cpu-arguments
	 * @returns {MaxArgSizeCost}
	 */
	static fromParams(params, baseName) {
		let [a, b] = LinearCost.getParams(params, baseName);

		return new MinArgSizeCost(a, b);
	}

	/**
	 * @param  {number[]} args
	 * @returns {bigint}
	 */
	calc(args) {
		return this.calcInternal(Math.min(...args));
	}
}

/**
 * cost = a + b*max(args)
 * @internal
 */
export class MaxArgSizeCost extends LinearCost {
	/**
	 * @param {bigint} a - intercept
	 * @param {bigint} b - slope
	 */
	constructor(a, b) {
		super(a, b);
	}

	/**
	 * @param {NetworkParams} params 
	 * @param {string} baseName - eg. addInteger-cpu-arguments
	 * @returns {MaxArgSizeCost}
	 */
	static fromParams(params, baseName) {
		let [a, b] = LinearCost.getParams(params, baseName);

		return new MaxArgSizeCost(a, b);
	}

	/**
	 * @param  {number[]} args
	 * @returns {bigint}
	 */
	calc(args) {
		return this.calcInternal(Math.max(...args));
	}
}

/**
 * cost = a + b*sum(sizes(args))
 * @internal
 */
export class SumArgSizesCost extends LinearCost {
	/**
	 * @param {bigint} a - intercept
	 * @param {bigint} b - slope
	 */
	constructor(a, b) {
		super(a, b);
	}

	/**
	 * @param {NetworkParams} params 
	 * @param {string} baseName - eg. addInteger-cpu-arguments
	 * @returns {MaxArgSizeCost}
	 */
	static fromParams(params, baseName) {
		let [a, b] = LinearCost.getParams(params, baseName);

		return new SumArgSizesCost(a, b);
	}

	/**
	 * @param  {number[]} args
	 * @returns {bigint}
	 */
	calc(args) {
		let sum = 0;

		for (let arg of args) {
			sum += arg;
		}

		return this.calcInternal(sum);
	}
}

/**
 * cost = a + b*max(size(arg0)-size(arg1), min)
 * (only for Uplc functions with two arguments) 
 * @internal
 */
export class ArgSizeDiffCost extends LinearCost {
	#min;

	/**
	 * @param {bigint} a - intercept
	 * @param {bigint} b - slope
	 * @param {number} min
	 */
	constructor(a, b, min) {
		super(a, b);
		this.#min = min
	}
	/**
	 * @param {NetworkParams} params 
	 * @param {string} baseName - eg. addInteger-cpu-arguments
	 * @returns {ArgSizeDiffCost}
	 */
	static fromParams(params, baseName) {
		let [a, b] = LinearCost.getParams(params, baseName);
		let min = params.getCostModelParameter(`${baseName}-minimum`);

		return new ArgSizeDiffCost(a, b, min);
	}

	/**
	 * @param {number[]} args
	 * @returns {bigint}
	 */
	calc(args) {
		assert(args.length == 2);
		let [x, y] = args;

		return this.calcInternal(Math.max(x - y, this.#min));
	}

	/**
	 * @returns {string}
	 */
	dump() {
		return super.dump() + `, minimum: ${this.#min.toString()}`;
	}
}

/**
 * cost = (size(arg0) < size(arg1)) ? constant : a + b*size(arg0)*size(arg1)
 * (only for Uplc functions with two arguments)
 * @internal
 */
export class ArgSizeProdCost extends LinearCost {
	#constant;

	/**
	 * @param {bigint} a - intercept
	 * @param {bigint} b - slope
	 * @param {bigint} constant
	 */
	constructor(a, b, constant) {
		super(a, b);
		this.#constant = constant;
	}

	/**
	 * @param {NetworkParams} params 
	 * @param {string} baseName - eg. addInteger-cpu-arguments
	 * @returns {MaxArgSizeCost}
	 */
	static fromParams(params, baseName) {
		const [a, b] = LinearCost.getParams(params, `${baseName}-model-arguments`);
		const constant = params.getCostModelParameter(`${baseName}-constant`);

		return new ArgSizeProdCost(a, b, BigInt(constant));
	}

	/**
	 * @param {number[]} args
	 * @returns {bigint}
	 */
	calc(args) {
		assert(args.length == 2);
		
		const [x, y] = args;

		if (x < y) {
			return this.#constant;
		} else {
			return this.calcInternal(x*y);
		}
	}

	/**
	 * @returns {string}
	 */
	dump() {
		return super.dump() + `, constant: ${this.#constant.toString()}`;
	}
}

/**
 * cost = (size(arg0) != size(arg1)) ? constant : a + b*size(arg0)
 * (only for Uplc functions with two arguments)
 * @internal
 */
export class ArgSizeDiagCost extends LinearCost {
	#constant;

	/**
	 * @param {bigint} a
	 * @param {bigint} b
	 * @param {bigint} constant
	 */
	constructor(a, b, constant) {
		super(a, b);
		this.#constant = constant;
	}
	/**
	 * @param {NetworkParams} params 
	 * @param {string} baseName - eg. addInteger-cpu-arguments
	 * @returns {ArgSizeDiagCost}
	 */
	static fromParams(params, baseName) {
		const [a, b] = LinearCost.getParams(params, baseName);
		const constant = params.getCostModelParameter(`${baseName}-constant`);

		return new ArgSizeDiagCost(a, b, BigInt(constant));
	}

	/**
	 * @param {number[]} args 
	 * @returns {bigint}
	 */
	calc(args) {
		assert(args.length == 2);

		if (args[0] == args[1]) {
			return this.calcInternal(args[0]);
		} else {
			return this.#constant;
		}
	}

	/**
	 * @returns {string}
	 */
	dump() {
		return super.dump() + `, constant: ${this.#constant.toString()}`;
	}
}

/**
 * @internal
 * @typedef CostModelClass
 * @property {(params: NetworkParams, baseName: string) => CostModel} fromParams
 */


/////////////////////////////////////
// Section 9: Uplc built-in functions
/////////////////////////////////////

/**
 * @internal
 */
export const BUILTIN_PREFIX = "__core__";

/**
 * Calls to builtins that are known not to throw errors (eg. tailList inside last branch of chooseList)
 * @internal
 */
export const SAFE_BUILTIN_SUFFIX = "__safe"; 

/**
 * Special off-chain builtins like network.get()
 * @internal
 */
export const MACRO_BUILTIN_PREFIX = "__core__macro";

/**
 * Cost-model configuration of UplcBuiltin.
 * Also specifies the number of times a builtin must be 'forced' before being callable.
 * @internal
 */
 export class UplcBuiltinConfig {
	#name;
	#forceCount;
	#nArgs;
	#allowAny;
	#memCostModelClass;
	#cpuCostModelClass;

	/**
	 * @param {string} name 
	 * @param {number} forceCount - number of type parameters of a Plutus-core builtin function (0, 1 or 2)
	 * @param {number} nArgs
	 * @param {boolean} allowAny
	 * @param {CostModelClass} memCostModelClass 
	 * @param {CostModelClass} cpuCostModelClass 
	 */
	constructor(name, forceCount, nArgs, allowAny, memCostModelClass, cpuCostModelClass) {
		this.#name = name;
		this.#forceCount = forceCount;
		this.#nArgs = nArgs;
		this.#allowAny = allowAny;
		this.#memCostModelClass = memCostModelClass;
		this.#cpuCostModelClass = cpuCostModelClass;
	}

	get name() {
		return this.#name;
	}

	get forceCount() {
		return this.#forceCount;
	}

	get nArgs() {
		return this.#nArgs;
	}

	get allowAny() {
		return this.#allowAny;
	}

	/**
	 * @param {NetworkParams} params
	 * @returns {[CostModel, CostModel]}
	 */
	instantiateCostModels(params) {
		if (this.#memCostModelClass !== null && this.#cpuCostModelClass !== null) {
			let memCostModel = this.#memCostModelClass.fromParams(params, `${this.#name}-memory-arguments`);
			let cpuCostModel = this.#cpuCostModelClass.fromParams(params, `${this.#name}-cpu-arguments`);

			return [memCostModel, cpuCostModel];
		} else {
			throw new Error(`cost model not yet implemented for builtin ${this.#name}`);
		}
	}

	/**
	 * @param {NetworkParams} params
	 * @param {number[]} argSizes
	 * @returns {Cost}
	 */
	calcCost(params, argSizes) {
		// Note: instantiating everytime might be slow. Should this be cached (eg. in the params object?)?
		const [memCostModel, cpuCostModel] = this.instantiateCostModels(params);

		const memCost = memCostModel.calc(argSizes);
		const cpuCost = cpuCostModel.calc(argSizes);

		return {mem: memCost, cpu: cpuCost};
	}

	/**
	 * @param {NetworkParams} params
	 */
	dumpCostModel(params) {
		const [memCostModel, cpuCostModel] = this.instantiateCostModels(params);

		console.log(`${this.name}-memory-arguments={${memCostModel.dump()},\n${this.name}-cpu-arguments={${cpuCostModel.dump()}}`);
	}
}

/** 
 * A list of all PlutusScript builins, with associated costmodels (actual costmodel parameters are loaded from NetworkParams during runtime)
 * @internal
 * @type {UplcBuiltinConfig[]} 
 */
export const UPLC_BUILTINS = (
	/**
	 * @returns {UplcBuiltinConfig[]}
	 */
	function () {
		/**
		 * Constructs a builtinInfo object
		 * @param {string} name 
		 * @param {number} forceCount 
		 * @param {number} nArgs
		 * @param {boolean} allowAny
		 * @param {CostModelClass} memCostModel
		 * @param {CostModelClass} cpuCostModel
		 * @returns {UplcBuiltinConfig}
		 */
		function builtinConfig(name, forceCount, nArgs, allowAny, memCostModel, cpuCostModel) {
			// builtins might need be wrapped in `force` a number of times if they are not fully typed
			return new UplcBuiltinConfig(name, forceCount, nArgs, allowAny, memCostModel, cpuCostModel);
		}

		return [
			builtinConfig("addInteger",               0, 2, false, MaxArgSizeCost, MaxArgSizeCost), // 0
			builtinConfig("subtractInteger",          0, 2, false, MaxArgSizeCost, MaxArgSizeCost),
			builtinConfig("multiplyInteger",          0, 2, false, SumArgSizesCost, SumArgSizesCost),
			builtinConfig("divideInteger",            0, 2, false, ArgSizeDiffCost, ArgSizeProdCost),
			builtinConfig("quotientInteger",          0, 2, false, ArgSizeDiffCost, ArgSizeProdCost), 
			builtinConfig("remainderInteger",         0, 2, false, ArgSizeDiffCost, ArgSizeProdCost),
			builtinConfig("modInteger",               0, 2, false, ArgSizeDiffCost, ArgSizeProdCost),
			builtinConfig("equalsInteger",            0, 2, false, ConstCost, MinArgSizeCost),
			builtinConfig("lessThanInteger",          0, 2, false, ConstCost, MinArgSizeCost),
			builtinConfig("lessThanEqualsInteger",    0, 2, false, ConstCost, MinArgSizeCost),
			builtinConfig("appendByteString",         0, 2, false, SumArgSizesCost, SumArgSizesCost), // 10
			builtinConfig("consByteString",           0, 2, false, SumArgSizesCost, Arg1SizeCost),
			builtinConfig("sliceByteString",          0, 3, false, Arg2SizeCost, Arg2SizeCost),
			builtinConfig("lengthOfByteString",       0, 1, false, ConstCost, ConstCost),
			builtinConfig("indexByteString",          0, 2, false, ConstCost, ConstCost),
			builtinConfig("equalsByteString",         0, 2, false, ConstCost, ArgSizeDiagCost),
			builtinConfig("lessThanByteString",       0, 2, false, ConstCost, MinArgSizeCost),
			builtinConfig("lessThanEqualsByteString", 0, 2, false, ConstCost, MinArgSizeCost),
			builtinConfig("sha2_256",                 0, 1, false, ConstCost, Arg0SizeCost),
			builtinConfig("sha3_256",                 0, 1, false, ConstCost, Arg0SizeCost),
			builtinConfig("blake2b_256",              0, 1, false, ConstCost, Arg0SizeCost), // 20
			builtinConfig("verifyEd25519Signature",   0, 3, false, ConstCost, Arg2SizeCost),
			builtinConfig("appendString",             0, 2, false, SumArgSizesCost, SumArgSizesCost),
			builtinConfig("equalsString",             0, 2, false, ConstCost, ArgSizeDiagCost),
			builtinConfig("encodeUtf8",               0, 1, false, Arg0SizeCost, Arg0SizeCost),
			builtinConfig("decodeUtf8",               0, 1, false, Arg0SizeCost, Arg0SizeCost),
			builtinConfig("ifThenElse",               1, 3, true , ConstCost, ConstCost),
			builtinConfig("chooseUnit",               1, 2, false, ConstCost, ConstCost),
			builtinConfig("trace",                    1, 2, true , ConstCost, ConstCost),
			builtinConfig("fstPair",                  2, 1, false, ConstCost, ConstCost),
			builtinConfig("sndPair",                  2, 1, false, ConstCost, ConstCost), // 30
			builtinConfig("chooseList",               2, 3, true , ConstCost, ConstCost),
			builtinConfig("mkCons",                   1, 2, false, ConstCost, ConstCost),
			builtinConfig("headList",                 1, 1, false, ConstCost, ConstCost),
			builtinConfig("tailList",                 1, 1, false, ConstCost, ConstCost),
			builtinConfig("nullList",                 1, 1, false, ConstCost, ConstCost),
			builtinConfig("chooseData",               1, 6, true , ConstCost, ConstCost),
			builtinConfig("constrData",               0, 2, false, ConstCost, ConstCost),
			builtinConfig("mapData",                  0, 1, false, ConstCost, ConstCost),
			builtinConfig("listData",                 0, 1, false, ConstCost, ConstCost),
			builtinConfig("iData",                    0, 1, false, ConstCost, ConstCost), // 40
			builtinConfig("bData",                    0, 1, false, ConstCost, ConstCost),
			builtinConfig("unConstrData",             0, 1, false, ConstCost, ConstCost),
			builtinConfig("unMapData",                0, 1, false, ConstCost, ConstCost),
			builtinConfig("unListData",               0, 1, false, ConstCost, ConstCost),
			builtinConfig("unIData",                  0, 1, false, ConstCost, ConstCost),
			builtinConfig("unBData",                  0, 1, false, ConstCost, ConstCost),
			builtinConfig("equalsData",               0, 2, false, ConstCost, MinArgSizeCost),
			builtinConfig("mkPairData",               0, 2, false, ConstCost, ConstCost),
			builtinConfig("mkNilData",                0, 1, false, ConstCost, ConstCost),
			builtinConfig("mkNilPairData",            0, 1, false, ConstCost, ConstCost), // 50
			builtinConfig("serialiseData",            0, 1, false, Arg0SizeCost, Arg0SizeCost),
			builtinConfig("verifyEcdsaSecp256k1Signature",   0, 3, false, ConstCost, ConstCost), // these parameters are from aiken, but the cardano-cli parameter file differ?
			builtinConfig("verifySchnorrSecp256k1Signature", 0, 3, false, ConstCost, Arg1SizeCost), // these parameters are from, but the cardano-cli parameter file differs?
		];
	}
)();

/**
 * @internal
 */
export const UPLC_MACROS_OFFSET = UPLC_BUILTINS.length;

/**
 * Index to helios-specific macro mapping
 * @internal
 */ 
export const UPLC_MACROS = [
	"compile",
	"finalize",
	"get_utxo",
	"now",
	"pick",
	"utxos_at"
];

/**
 * Use this function to check cost-model parameters
 * @internal
 * @param {NetworkParams} networkParams
 */
export function dumpCostModels(networkParams) {
	for (let builtin of UPLC_BUILTINS) {
		builtin.dumpCostModel(networkParams);
	}
}

/**
 * Returns index of a named builtin
 * Throws an error if builtin doesn't exist
 * @internal
 * @param {string} name 
 * @returns 
 */
export function findUplcBuiltin(name) {
	let i = UPLC_BUILTINS.findIndex(info => { return BUILTIN_PREFIX + info.name == name });
	assert(i != -1, `${name} is not a real builtin`);
	return i;
}

/**
 * Checks if a named builtin exists
 * @internal
 * @param {string} name 
 * @param {boolean} strict - if true then throws an error if builtin doesn't exist
 * @returns {boolean}
 */
export function isUplcBuiltin(name, strict = false) {
	if (name.startsWith(BUILTIN_PREFIX)) {
		if (strict) {
			void this.findBuiltin(name); // assert that builtin exists
		}
		return true;
	} else {
		return false;
	}
}


///////////////////////
// Section 10: Uplc AST
///////////////////////

/**
 * A Helios/Uplc Program can have different purposes
 * @typedef {"testing" | "minting" | "spending" | "staking" | "endpoint" | "module" | "unknown"} ScriptPurpose
 */

/**
 * UplcValue is passed around by Plutus-core expressions.
 * @interface
 * @typedef {object} UplcValue
 * @property {(other: TransferUplcAst) => any} transfer
 * @property {bigint} int
 * @property {number[]} bytes
 * @property {string} string
 * @property {boolean} bool
 * @property {() => boolean} isPair
 * @property {UplcValue} first
 * @property {UplcValue} second
 * @property {() => boolean} isList
 * @property {UplcType} itemType
 * @property {UplcValue[]} list
 * @property {number} length only relevant for lists and maps
 * @property {() => boolean} isData
 * @property {UplcData} data
 * @property {() => string} toString
 * @property {(newSite: Site) => UplcValue} copy return a copy of the UplcValue at a different Site
 * @property {Site} site
 * @property {number} memSize size in words (8 bytes, 64 bits) occupied in target node
 * @property {number} flatSize size taken up in serialized UPLC program (number of bits)
 * @property {() => boolean} isAny
 * @property {(bitWriter: BitWriter) => void} toFlatValue
 * @property {(bitWriter: BitWriter) => void} toFlatValueInternal like toFlatValue(), but without the typebits
 * @property {() => string} typeBits
 * @property {() => UplcUnit} assertUnit
 */

/** 
 * Base cass for UplcValue implementations.
 */
export class UplcValueImpl {
	#site;

	/**
	 * @param {Site} site 
	 */
	constructor(site) {
		assert(site != undefined && (site instanceof Site));
		this.#site = site;
	}

    /**
     * @type {Site}
     */
	get site() {
		return this.#site;
	}

	/**
	 * @type {number}
	 */
	get length() {
		throw new Error("not a list nor a map");
	}

	/**
	 * @returns {boolean}
	 */
	isAny() {
		return false;
	}

	/**
	 * @type {bigint}
	 */
	get int() {
		throw this.site.typeError(`expected a Plutus-core int, got '${this.toString()}'`);
	}

	/**
	 * @type {number[]}
	 */
	get bytes() {
		throw this.site.typeError(`expected a Plutus-core bytearray, got '${this.toString()}'`);
	}

	/**
	 * @type {string}
	 */
	get string() {
		throw this.site.typeError(`expected a Plutus-core string, got '${this.toString()}'`);
	}
	
	/**
	 * @type {boolean}
	 */
	get bool() {
		throw this.site.typeError(`expected a Plutus-core bool, got '${this.toString()}'`);
	}

	/**
	 * Distinguishes a pair from a mapItem
	 * @returns {boolean}
	 */
	isPair() {
		return false;
	}

	/**
	 * @type {UplcValue}
	 */
	get first() {
		throw this.site.typeError(`expected a Plutus-core pair, got '${this.toString()}'`);
	}

	/**
	 * @type {UplcValue}
	 */
	get second() {
		throw this.site.typeError(`expected a Plutus-core pair, got '${this.toString()}'`);
	}

	/**
	 * Distinguishes a list from a map
	 * @returns {boolean}
	 */
	isList() {
		return false;
	}

	/**
	 * @type {UplcType}
	 */
	get itemType() {
		throw this.site.typeError("not a list");
	}

	/**
	 * @type {UplcValue[]}
	 */
	get list() {
		throw this.site.typeError(`expected a Plutus-core list, got '${this.toString()}'`);
	}

    /**
     * @returns {boolean}
     */
	isData() {
		return false;
	}

	/**
	 * @type {UplcData}
	 */
	get data() {
		throw this.site.typeError(`expected Plutus-core data, got '${this.toString()}'`);
	}

	/**
	 * @returns {UplcUnit}
	 */
	assertUnit() {
		throw this.site.typeError(`expected Plutus-core unit, got '${this.toString}'`);
	}

	/**
	 * @returns {string}
	 */
	typeBits() {
		throw new Error("not yet implemented");
	}

	/**
	 * Encodes value without type header
	 * @param {BitWriter} bitWriter
	 */
	toFlatValueInternal(bitWriter) {
		throw new Error("not yet implemented");
	}

	/**
	 * Encodes value with plutus flat encoding.
	 * Member function not named 'toFlat' as not to confuse with 'toFlat' member of terms.
	 * @param {BitWriter} bitWriter
	 */
	toFlatValue(bitWriter) {
		bitWriter.write('1' + this.typeBits() + '0');
		
		this.toFlatValueInternal(bitWriter);
	}
}

/**
 * Represents the typeBits of a UPLC primitive.
 */
export class UplcType {
	#typeBits;

	/**
	 * @param {string} typeBits 
	 */
	constructor(typeBits) {
		this.#typeBits = typeBits;
	}

	/**
	 * @param {TransferUplcAst} other 
	 * @returns {any}
	 */
	transfer(other) {
		return other.transferUplcType(
			this.#typeBits
		);
	}

	/**
	 * @returns {string}
	 */
	typeBits() {
		return this.#typeBits;
	}

	/**
	 * @param {UplcValue} value 
	 * @returns {boolean}
	 */
	isSameType(value) {
		return this.#typeBits == value.typeBits();
	}

	/**
	 * @returns {boolean}
	 */
	isData() {
		return this.#typeBits == UplcType.newDataType().#typeBits;
	}

	/**
	 * @returns {boolean}
	 */
	isDataPair() {
		return this.#typeBits == UplcType.newDataPairType().#typeBits;
	}

	/**
	 * @returns {UplcType}
	 */
	static newDataType() {
		return new UplcType("1000");
	}

	/**
	 * @returns {UplcType}
	 */
	static newDataPairType() {
		return new UplcType(["0111", "0111", "0110", "1000", "1000"].join("1"));
	}

	/**
	 * @param {number[]} lst
	 * @returns {UplcType}
	 */
	static fromNumbers(lst) {
		return new UplcType(lst.map(x => byteToBitString(x, 4, false)).join("1"));
	}
}

/**
 * @typedef {[null | string, UplcValue][]} UplcRawStack
 */

/**
 * @typedef {{
 *	 onPrint: (msg: string) => Promise<void>
 *   onStartCall: (site: Site, rawStack: UplcRawStack) => Promise<boolean>
 *   onEndCall: (site: Site, rawStack: UplcRawStack) => Promise<void>
 *   onIncrCost: (name: string, isTerm: boolean, cost: Cost) => void
 * }} UplcRTECallbacks
 */

/**
 * @internal
 * @typedef {UplcRTECallbacks & {
 *   macros?: {[name: string]: (rte: UplcRte, args: UplcValue[]) => Promise<UplcValue>}
 * }} UplcRTECallbacksInternal
 */

/**
 * Configures the Uplc evaluator to print messages to `console`.
 * @type {UplcRTECallbacks}
 */
export const DEFAULT_UPLC_RTE_CALLBACKS = {
	onPrint: async (/** @type {string} */ msg) => {console.log(msg)},
	onStartCall: async (/** @type {Site} */ site, /** @type {UplcRawStack} */ rawStack) => {return false},
	onEndCall: async (/** @type {Site} */ site, /** @type {UplcRawStack} */ rawStack) => {return},
	onIncrCost: (/** @type {string} */ name, /** @type {boolean} */ isTerm, /** @type {Cost} */ cost) => {return},
}

/**
 * Plutus-core Runtime Environment is used for controlling the programming evaluation (eg. by a debugger)
 * @internal
 */
export class UplcRte {
	#callbacks;

	#networkParams;

	/**
	 * this.onNotifyCalls is set to 'false' when the debugger is in step over-mode.
	 * @type {boolean}
	 */
	#notifyCalls;

	/**
	 * this.onNotifyCalls is set back to true if the endCall is called with the same rawStack as the marker.
	 * @type {null | UplcRawStack}
	 */
	#marker;

	/**
	 * @type {string[]}
	 */
	#messages;

	/**
	 * @type {string | RuntimeError}
	 */
	#error;

	// cache the costs for quicker lookup
	#startupCost;
	#variableCost;
	#lambdaCost;
	#delayCost;
	#callCost;
	#constCost;
	#forceCost;
	#builtinCost;


	
	/**
	 * @param {UplcRTECallbacksInternal} callbacks 
	 * @param {null | NetworkParams} networkParams
	 */
	constructor(callbacks = DEFAULT_UPLC_RTE_CALLBACKS, networkParams = null) {
		assertDefined(callbacks);
		this.#callbacks = callbacks;
		this.#networkParams = networkParams;
		this.#notifyCalls = true;
		this.#marker = null;
		this.#messages = [];
		this.#error = "";

		this.#startupCost = networkParams?.plutusCoreStartupCost ?? {mem: 0n, cpu: 0n};
		this.#variableCost = networkParams?.plutusCoreVariableCost ?? {mem: 0n, cpu: 0n};
		this.#lambdaCost = networkParams?.plutusCoreLambdaCost ?? {mem: 0n, cpu: 0n};
		this.#delayCost = networkParams?.plutusCoreDelayCost ?? {mem: 0n, cpu: 0n};
		this.#callCost = networkParams?.plutusCoreCallCost ?? {mem: 0n, cpu: 0n};
		this.#constCost = networkParams?.plutusCoreConstCost ?? {mem: 0n, cpu: 0n};
		this.#forceCost = networkParams?.plutusCoreForceCost ?? {mem: 0n, cpu: 0n};
		this.#builtinCost = networkParams?.plutusCoreBuiltinCost ?? {mem: 0n, cpu: 0n};
	}

	/**
	 * @type {string[]}
	 */
	get messages() {
		return this.#messages;
	}

	/**
	 * @returns {string}
	 */
	popLastMessage() {
		return this.#messages.pop() ?? "";
	}

	/**
	 * @returns {boolean}
	 */
	hasError() {
		return this.#error != "";
	}

	/**
	 * @returns {string | RuntimeError}
	 */
	getError() {
		return this.#error;
	}

	/**
	 * @param {string | RuntimeError} err 
	 * @returns {UplcValue}
	 */
	error(err) {
		this.#error = err;
		return new UplcAny(Site.dummy());
	}

	throwError() {
		if (this.#error instanceof RuntimeError) {
			throw this.#error;
		} else if (this.#error != "") {
			throw new RuntimeError(this.#error);
		}
	}

	/**
	 * @param {string} name - for breakdown
	 * @param {boolean} isTerm
	 * @param {Cost} cost 
	 */
	incrCost(name, isTerm, cost) {
		if (cost.mem <= 0n || cost.cpu <= 0n) {
			throw new Error("cost not increasing");
		}

		if (this.#callbacks.onIncrCost !== undefined) {
			this.#callbacks.onIncrCost(name, isTerm, cost);
		}
	}

	incrStartupCost() {
		if (this.#networkParams !== null) {
			this.incrCost("startup", true, this.#startupCost);
		}
	}

	incrVariableCost() {
		if (this.#networkParams !== null) {
			this.incrCost("variable", true, this.#variableCost);
		}
	}

	incrLambdaCost() {
		if (this.#networkParams !== null) {
			this.incrCost("lambda", true, this.#lambdaCost);
		}
	}

	incrDelayCost() {
		if (this.#networkParams !== null) {
			this.incrCost("delay", true, this.#delayCost);
		}
	}

	incrCallCost() {
		if (this.#networkParams !== null) {
			this.incrCost("call", true, this.#callCost);
		}
	}

	incrConstCost() {
		if (this.#networkParams !== null) {
			this.incrCost("const", true, this.#constCost);
		}
	}

	incrForceCost() {
		if (this.#networkParams !== null) {
			this.incrCost("force", true, this.#forceCost);
		}
	}

	incrBuiltinCost() {
		if (this.#networkParams !== null) {
			this.incrCost("builtin", true, this.#builtinCost);
		}
	}

	/**
	 * @param {UplcBuiltin} fn
	 * @param {UplcValue[]} args
	 */
	calcAndIncrCost(fn, ...args) {
		if (this.#networkParams !== null) {
			let cost = fn.calcCost(this.#networkParams, ...args);

			this.incrCost(fn.name, false, cost);
		}
	}

	/**
	 * @param {string} name 
	 * @param {UplcValue[]} args 
	 * @returns {Promise<UplcValue>}
	 */
	callMacro(name, args) {
		const macros = this.#callbacks.macros;

		if (!macros) {
			// use RuntimeError so that IR evalConstants method can fail safely when optimizing
			throw new RuntimeError("macros not avaiable");
		}

		const macro = macros[name];

		if (!macro) {
			throw new Error(`macro ${name} not found`);
		}

		return macro(this, args);
	}

	/**
	 * Gets variable using Debruijn index. Throws error here because UplcRTE is the stack root and doesn't contain any values.
	 * @param {number} i 
	 * @returns {UplcValue}
	 */
	get(i) {
		throw new Error("variable index out of range");
	}

	/**
	 * Creates a child stack.
	 * @param {UplcValue} value 
	 * @param {?string} valueName 
	 * @returns {UplcStack}
	 */
	push(value, valueName = null) {
		return new UplcStack(this, value, valueName);
	}

	/**
	 * Calls the print callback (or does nothing if print callback isn't defined)
	 * @param {string | string[]} rawMsg 
	 * @returns {Promise<void>}
	 */
	async print(rawMsg) {
		const lines = Array.isArray(rawMsg) ? rawMsg : [rawMsg];

		if (this.#callbacks.onPrint != undefined) {
			for (let l of lines) {
				this.#callbacks.onPrint(l);
			}
		}

		this.#messages = this.#messages.concat(lines);
	}

	/**
	 * Calls the onStartCall callback.
	 * @param {Site} site 
	 * @param {UplcRawStack} rawStack 
	 * @returns {Promise<void>}
	 */
	async startCall(site, rawStack) {
		if (this.#notifyCalls && this.#callbacks.onStartCall != undefined) {
			let stopNotifying = await this.#callbacks.onStartCall(site, rawStack);
			if (stopNotifying) {
				this.#notifyCalls = false;
				this.#marker = rawStack;
			}
		}
	}

	/**
	 * Calls the onEndCall callback if '#notifyCalls == true'.
	 * '#notifyCalls' is set to true if 'rawStack == #marker'.
	 * @param {Site} site 
	 * @param {UplcRawStack} rawStack 
	 * @param {UplcValue} result 
	 * @returns {Promise<void>}
	 */
	async endCall(site, rawStack, result) {
		if (!this.#notifyCalls && this.#marker == rawStack) {
			this.#notifyCalls = true;
			this.#marker = null;
		}

		if (this.#notifyCalls && this.#callbacks.onEndCall != undefined) {
			rawStack = rawStack.slice();
			rawStack.push(["__result", result]);
			await this.#callbacks.onEndCall(site, rawStack);
		}
	}

	/**
	 * @returns {UplcRawStack}
	 */
	toList() {
		return [];
	}
}

/**
 * UplcStack contains a value that can be retrieved using a Debruijn index.
 */
class UplcStack {
	#parent;
	#value;
	#valueName;

	/**
	 * @param {null | UplcStack | UplcRte} parent
	 * @param {null | UplcValue} value
	 * @param {null | string} valueName
	 */
	constructor(parent, value = null, valueName = null) {
		this.#parent = parent;
		this.#value = value;
		this.#valueName = valueName;
	}

	incrStartupCost() {
		if (this.#parent !== null) {
			this.#parent.incrStartupCost()
		}
	}

	incrVariableCost() {
		if (this.#parent !== null) {
			this.#parent.incrVariableCost()
		}
	}

	incrLambdaCost() {
		if (this.#parent !== null) {
			this.#parent.incrLambdaCost()
		}
	}
	
	incrDelayCost() {
		if (this.#parent !== null) {
			this.#parent.incrDelayCost();
		}
	}

	incrCallCost() {
		if (this.#parent !== null) {
			this.#parent.incrCallCost();
		}
	}

	incrConstCost() {
		if (this.#parent !== null) {
			this.#parent.incrConstCost();
		}
	}

	incrForceCost() {
		if (this.#parent !== null) {
			this.#parent.incrForceCost()
		}
	}

	incrBuiltinCost() {
		if (this.#parent !== null) {
			this.#parent.incrBuiltinCost()
		}
	}

	/**
	 * @param {UplcBuiltin} fn
	 * @param {UplcValue[]} args
	 */
	calcAndIncrCost(fn, ...args) {
		if (this.#parent !== null) {
			this.#parent.calcAndIncrCost(fn, ...args);
		}
	}

	/**
	 * Gets a value using the Debruijn index. If 'i == 1' then the current value is returned.
	 * Otherwise 'i' is decrement and passed to the parent stack.
	 * @param {number} i 
	 * @returns {UplcValue}
	 */
	get(i) {
		i -= 1;

		if (i == 0) {
			if (this.#value === null) {
				throw new Error("Plutus-core stack value not set");
			} else {
				return this.#value;
			}
		} else {
			assert(i > 0);
			if (this.#parent === null) {
				throw new Error("variable index out of range");
			} else {
				return this.#parent.get(i);
			}
		}
	}
	
	/**
	 * 
	 * @param {string} name 
	 * @param {UplcValue[]} args 
	 * @returns {Promise<UplcValue>}
	 */
	async callMacro(name, args) {
		if (this.#parent) {
			return await this.#parent.callMacro(name, args);
		} else {
			throw new Error("parent not set, can't call macro")
		}
	}

	/**
	 * Instantiates a child stack.
	 * @param {UplcValue} value 
	 * @param {?string} valueName 
	 * @returns {UplcStack}
	 */
	push(value, valueName = null) {
		return new UplcStack(this, value, valueName);
	}

	/**
	 * Calls the onPrint callback in the RTE (root of stack).
	 * @param {string} msg 
	 * @returns {Promise<void>}
	 */
	async print(msg) {
		if (this.#parent !== null) {
			await this.#parent.print(msg);
		}
	}

	/**
	 * Calls the onStartCall callback in the RTE (root of stack).
	 * @param {Site} site 
	 * @param {UplcRawStack} rawStack 
	 * @returns {Promise<void>}
	 */
	async startCall(site, rawStack) {
		if (this.#parent !== null) {
			await this.#parent.startCall(site, rawStack);
		}
	}

	/** 
	 * Calls the onEndCall callback in the RTE (root of stack).
	 * @param {Site} site
	 * @param {UplcRawStack} rawStack
	 * @param {UplcValue} result
	 * @returns {Promise<void>}
	 */
	async endCall(site, rawStack, result) {
		if (this.#parent !== null) {
			await this.#parent.endCall(site, rawStack, result);
		}
	}

	/** 
	 * @returns {UplcRawStack}
	 */
	toList() {
		let lst = this.#parent !== null ? this.#parent.toList() : [];
		if (this.#value !== null) {
			lst.push([this.#valueName, this.#value]);
		}
		return lst;
	}
}

/**
 * Allows doing a dummy eval of a UplcProgram in order to determine some non-changing properties (eg. the address fetched via the network in an EndpointProgram)
 * @internal
 * @implements {UplcValue}
 */
export class UplcAny extends UplcValueImpl {
	/**
	 * @param {Site} site 
	 */
	constructor(site) {
		super(site);
	}

	/**
	 * Should never be part of the uplc ast
	 * @param {TransferUplcAst} other 
	 * @returns {any}
	 */
	transfer(other) {
		throw new Error("not expected to be part of Uplc ast");
	}

	/**
	 * @type {number}
	 */
	get memSize() {
		return 1;
	}

	/**
	 * @type {number}
	 */
	get flatSize() {
		throw new Error("UplcAny shouldn't be part of Ast");
	}

	/**
	 * @param {Site} newSite 
	 * @returns {UplcValue}
	 */
	copy(newSite) {
		return new UplcAny(
			newSite
		);
	}

	/**
	 * @returns {boolean}
	 */
	isAny() {
		return true;
	}

	/**
	 * @type {UplcValue}
	 */
	get first() {
		return this;
	}

	/**
	 * @type {UplcValue}
	 */
	get second() {
		return this;
	}

	/**
     * @internal
	 * @returns {UplcUnit}
	 */
	assertUnit() {
		return new UplcUnit(this.site);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return "Any";
	}
}

/**
 * Primitive equivalent of `IntData`.
 * @implements {UplcValue}
 */
export class UplcInt extends UplcValueImpl {
	/**
	 * @readonly
	 * @type {bigint}
	 */
	value;

	/**
	 * @readonly
	 * @type {boolean}
	 */
	signed;

	/**
	 * @param {Site} site
	 * @param {bigint} value - supposed to be arbitrary precision
	 * @param {boolean} signed - unsigned is only for internal use
	 */
	constructor(site, value, signed = true) {
		super(site);
		assert(typeof value == 'bigint', "not a bigint");
		this.value = value;
		this.signed = signed;
	}

	/**
	 * Constructs a UplcInt without requiring a Site
	 * @param {bigint | number} value
	 * @returns {UplcInt} 
	 */
	static new(value) {
		if (typeof value == 'number') {
			assert(value % 1.0 == 0.0, "must be whole number");
			return new UplcInt(Site.dummy(), BigInt(value));
		} else {
			return new UplcInt(Site.dummy(), value);
		}
	}

	

	/**
	 * @param {TransferUplcAst} other 
	 * @returns {any}
	 */
	transfer(other) {
		return other.transferUplcInt(
			this.site.transfer(other),
			this.value,
			this.signed
		);
	}

	/**
	 * Creates a UplcInt wrapped in a UplcConst, so it can be used a term
	 * @param {Site} site 
	 * @param {bigint} value 
	 * @returns 
	 */
	static newSignedTerm(site, value) {
		return new UplcConst(new UplcInt(site, value, true));
	}

	/**
	 * @type {number}
	 */
	get memSize() {
        return IntData.memSizeInternal(this.value);
	}

	/**
	 * 4 for type, 7 for simple int, (7 + 1)*ceil(n/7) for large int
	 * @type {number}
	 */
	get flatSize() {
		const n = this.toUnsigned().value.toString(2).length;
		return 4 + ((n <= 7) ? 7 : Math.ceil(n / 7)*8);
	}

	/**
	 * @param {Site} newSite 
	 * @returns {UplcInt}
	 */
	copy(newSite) {
		return new UplcInt(newSite, this.value, this.signed);
	}

	/**
	 * @type {bigint}
	 */
	get int() {
		return this.value;
	}

	/**
	 * Parses a single byte in the Plutus-core byte-list representation of an int
	 * @param {number} b 
	 * @returns {number}
	 */
	static parseRawByte(b) {
		return b & 0b01111111;
	}

	/**
	 * Returns true if 'b' is the last byte in the Plutus-core byte-list representation of an int.
	 * @param {number} b 
	 * @returns {boolean}
	 */
	static rawByteIsLast(b) {
		return (b & 0b10000000) == 0;
	}

	/**
	 * Combines a list of Plutus-core bytes into a bigint (leading bit of each byte is ignored).
     * Differs from bytesToBigInt in utils.js because only 7 bits are used from each byte.
	 * @param {number[]} bytes
	 * @returns {bigint}
	 */
	static bytesToBigInt(bytes) {
		let value = BigInt(0);

		let n = bytes.length;

		for (let i = 0; i < n; i++) {
			let b = bytes[i];

			// 7 (not 8), because leading bit isn't used here
			value = value + BigInt(b) * ipow2(BigInt(i) * 7n);
		}

		return value;
	}

	/**
	 * Applies zigzag encoding
	 * @example
	 * (new UplcInt(Site.dummy(), -1n, true)).toUnsigned().int == 1n
	 * @example
	 * (new UplcInt(Site.dummy(), -1n, true)).toUnsigned().toSigned().int == -1n
	 * @example
	 * (new UplcInt(Site.dummy(), -2n, true)).toUnsigned().toSigned().int == -2n
	 * @example
	 * (new UplcInt(Site.dummy(), -3n, true)).toUnsigned().toSigned().int == -3n
	 * @example
	 * (new UplcInt(Site.dummy(), -4n, true)).toUnsigned().toSigned().int == -4n
	 * @returns {UplcInt}
	 */
	toUnsigned() {
		if (this.signed) {
			if (this.value < 0n) {
				return new UplcInt(this.site, -this.value*2n - 1n, false);
			} else {
				return new UplcInt(this.site, this.value * 2n, false);
			}
		} else {
			return this;
		}
	}

	/** 
	 * Unapplies zigzag encoding 
	 * @example
	 * (new UplcInt(Site.dummy(), 1n, false)).toSigned().int == -1n
	 * @returns {UplcInt}
	*/
	toSigned() {
		if (this.signed) {
			return this;
		} else {
			if (this.value % 2n == 0n) {
				return new UplcInt(this.site, this.value / 2n, true);
			} else {
				return new UplcInt(this.site, -(this.value + 1n) / 2n, true);
			}
		}
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return this.value.toString();
	}

	/**
	 * @internal
	 * @param {BitWriter} bitWriter
	 */
	toFlatInternal(bitWriter) {
		let zigzag = this.toUnsigned();
		let bitString = padZeroes(zigzag.value.toString(2), 7);

		// split every 7th
		let parts = [];
		for (let i = 0; i < bitString.length; i += 7) {
			parts.push(bitString.slice(i, i + 7));
		}

		// reverse the parts
		parts.reverse();

		for (let i = 0; i < parts.length; i++) {
			if (i == parts.length - 1) {
				// last
				bitWriter.write('0' + parts[i]);
			} else {
				bitWriter.write('1' + parts[i]);
			}
		}
	}

	/**
	 * Encodes unsigned integer with plutus flat encoding.
	 * Throws error if signed.
	 * Used by encoding plutus core program version and debruijn indices.
	 * @internal
	 * @param {BitWriter} bitWriter 
	 */
	toFlatUnsigned(bitWriter) {
		assert(!this.signed);

		this.toFlatInternal(bitWriter);
	}

	/**
	 * @returns {string}
	 */
	typeBits() {
		return "0000";
	}

	/**
	 * @internal
	 * @param {BitWriter} bitWriter 
	 */
	toFlatValueInternal(bitWriter) {
		assert(this.signed);

		this.toFlatInternal(bitWriter);
	}
}

/**
 * Primitive equivalent of `ByteArrayData`.
 * @implements {UplcValue}
 */
export class UplcByteArray extends UplcValueImpl {
	#bytes;

	/**
	 * @param {Site} site
	 * @param {number[]} bytes
	 */
	constructor(site, bytes) {
		super(site);
		this.#bytes = bytes;
	}

	/**
	 * Construct a UplcByteArray without requiring a Site
	 * @internal
	 * @param {number[]} bytes 
	 * @returns {UplcByteArray}
	 */
	static new(bytes) {
		return new UplcByteArray(Site.dummy(), bytes);
	}

	/**
	 * Creates new UplcByteArray wrapped in UplcConst so it can be used as a term.
	 * @internal
	 * @param {Site} site 
	 * @param {number[]} bytes 
	 * @returns {UplcConst}
	 */
	static newTerm(site, bytes) {
		return new UplcConst(new UplcByteArray(site, bytes));
	}

	/**
	 * @param {TransferUplcAst} other 
	 * @returns {any}
	 */
	transfer(other) {
		return other.transferUplcByteArray(
			this.site.transfer(other),
			this.#bytes
		)
	}

	/**
	 * @type {number}
	 */
	get memSize() {
        return ByteArrayData.memSizeInternal(this.#bytes);
	}

	/**
	 * 4 for header, 8 bits per byte, 8 bits per chunk of 256 bytes, 8 bits final padding
	 * @type {number}
	 */
	get flatSize() {
		const n = this.#bytes.length;

		return 4 + n*8 + Math.ceil(n/256)*8 + 8;
	}

	/**
	 * @param {Site} newSite 
	 * @returns {UplcByteArray}
	 */
	copy(newSite) {
		return new UplcByteArray(newSite, this.#bytes);
	}

	/**
	 * @type {number[]}
	 */
	get bytes() {
		return this.#bytes.slice();
	}

	/**
	 * Returns hex representation of byte array
	 * @returns {string}
	 */
	toString() {
		return `#${bytesToHex(this.#bytes)}`;
	}

	/**
	 * @internal
	 * @returns {string}
	 */
	typeBits() {
		return "0001";
	}

	/**
	 * @internal
	 * @param {BitWriter} bitWriter
	 */
	toFlatValueInternal(bitWriter) {
		UplcByteArray.writeBytes(bitWriter, this.#bytes);
	}

	/**
	 * Write a list of bytes to the bitWriter using flat encoding.
	 * Used by UplcString, UplcByteArray and UplcDataValue
	 * Equivalent to E_B* function in Plutus-core docs
	 * @internal
	 * @param {BitWriter} bitWriter 
	 * @param {number[]} bytes 
	 * @param {boolean} pad
	 */
	static writeBytes(bitWriter, bytes, pad = true) {
		if (pad) {
			bitWriter.padToByteBoundary(true);
		}

		// the rest of this function is equivalent to E_C* function in Plutus-core docs
		let n = bytes.length;
		let pos = 0;

		// write chunks of 255
		while (pos < n) {
			// each iteration is equivalent to E_C function in Plutus-core docs

			let nChunk = Math.min(n - pos, 255);

			// equivalent to E_8 function in Plutus-core docs
			bitWriter.write(padZeroes(nChunk.toString(2), 8));

			for (let i = pos; i < pos + nChunk; i++) {
				let b = bytes[i];

				// equivalent to E_8 function in Plutus-core docs
				bitWriter.write(padZeroes(b.toString(2), 8));
			}

			pos += nChunk;
		}

		if (pad) {
			bitWriter.write('00000000');
		}
	}
}

/**
 * Primitive string value.
 * @implements {UplcValue}
 */
export class UplcString extends UplcValueImpl {
	#value;

	/**
	 * @param {Site} site 
	 * @param {string} value 
	 */
	constructor(site, value) {
		super(site);
		this.#value = value;
	}

	/**
	 * Constructs a UplcStrin without requiring a Site
	 * @param {string} value 
	 * @returns {UplcString}
	 */
	static new(value) {
		return new UplcString(Site.dummy(), value);
	}

	/**
	 * Creates a new UplcString wrapped with UplcConst so it can be used as a term.
	 * @param {Site} site 
	 * @param {string} value 
	 * @returns {UplcConst}
	 */
	static newTerm(site, value) {
		return new UplcConst(new UplcString(site, value));
	}

	/**
	 * @param {TransferUplcAst} other 
	 * @returns {any}
	 */
	transfer(other) {
		return other.transferUplcString(
			this.site.transfer(other),
			this.#value
		);
	}

	/**
	 * @type {number}
	 */
	get memSize() {
		return this.#value.length;
	}

	/**
	 * @type {number}
	 */
	get flatSize() {
		const bytes = Array.from((new TextEncoder()).encode(this.#value));
		return (new UplcByteArray(Site.dummy(), bytes)).flatSize
	}

	/**
	 * @param {Site} newSite 
	 * @returns {UplcString}
	 */
	copy(newSite) {
		return new UplcString(newSite, this.#value);
	}

	/**
	 * @type {string}
	 */
	get string() {
		return this.#value;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `"${this.#value}"`;
	}

	/**
	 * @returns {string}
	 */
	typeBits() {
		return "0010";
	}

	/**
	 * @param {BitWriter} bitWriter
	 */
	toFlatValueInternal(bitWriter) {
		let bytes = Array.from((new TextEncoder()).encode(this.#value));

		UplcByteArray.writeBytes(bitWriter, bytes);
	}
}

/**
 * Primitive unit value.
 * @implements {UplcValue}
 */
export class UplcUnit extends UplcValueImpl {
	/**
	 * @param {Site} site 
	 */
	constructor(site) {
		super(site);
	}

	/**
	 * Constructs a UplcUnit without requiring a Site
	 * @returns {UplcUnit}
	 */
	static new () {
		return new UplcUnit(Site.dummy());
	}

	/**
	 * Creates a new UplcUnit wrapped with UplcConst so it can be used as a term
	 * @param {Site} site 
	 * @returns {UplcConst}
	 */
	static newTerm(site) {
		return new UplcConst(new UplcUnit(site));
	}

	/**
	 * @param {TransferUplcAst} other 
	 * @returns {any}
	 */
	transfer(other) {
		return other.transferUplcUnit(
			this.site.transfer(other)
		);
	}

	/**
	 * @type {number}
	 */
	get memSize() {
		return 1;
	}

	/**
	 * @type {number}
	 */
	get flatSize() {
		return 4;
	}

	/**
	 * @param {Site} newSite 
	 * @returns {UplcUnit}
	 */
	copy(newSite) {
		return new UplcUnit(newSite);
	}

	toString() {
		return "()";
	}

	/**
	 * @returns {string}
	 */
	typeBits() {
		return "0011";
	}

	/**
	 * @param {BitWriter} bitWriter
	 */
	toFlatValueInternal(bitWriter) {
	}

	/**
	 * @returns {UplcUnit}
	 */
	assertUnit() {
		return this;
	}
}

/**
 * JS/TS equivalent of the Helios language `Bool` type.
 * @implements {UplcValue}
 */
export class UplcBool extends UplcValueImpl {
	#value;

	/**
	 * @param {Site} site 
	 * @param {boolean} value 
	 */
	constructor(site, value) {
		super(site);
		this.#value = value;
	}

	/**
	 * Constructs a UplcBool without requiring a Site
	 * @param {boolean} value 
	 * @returns {UplcBool}
	 */
	static new(value) {
		return new UplcBool(Site.dummy(), value);
	}

	/**
	 * Creates a new UplcBool wrapped with UplcConst so it can be used as a term.
	 * @param {Site} site 
	 * @param {boolean} value 
	 * @returns {UplcConst}
	 */
	static newTerm(site, value) {
		return new UplcConst(new UplcBool(site, value));
	}

	/**
	 * @param {TransferUplcAst} other 
	 * @returns {any}
	 */
	transfer(other) {
		return other.transferUplcBool(
			this.site.transfer(other),
			this.#value
		);
	}

	/**
	 * @type {number}
	 */
	get memSize() {
		return 1;
	}

	/**
	 * 4 for type, 1 for value
	 * @type {number}
	 */
	get flatSize() {
		return 5;
	}

	/**
	 * @param {Site} newSite 
	 * @returns {UplcBool}
	 */
	copy(newSite) {
		return new UplcBool(newSite, this.#value);
	}

	/**
	 * @type {boolean}
	 */
	get bool() {
		return this.#value;
	}

	/**
	 * @type {UplcData}
	 */
	get data() {
		return new ConstrData(this.#value ? 1 : 0, []);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return this.#value ? "true" : "false";
	}

	/**
	 * @returns {string}
	 */
	typeBits() {
		return '0100';
	}

	/**
	 * @param {BitWriter} bitWriter
	 */
	toFlatValueInternal(bitWriter) {
		if (this.#value) {
			bitWriter.write('1');
		} else {
			bitWriter.write('0');
		}
	}
}

/**
 * Primitive pair value.
 * @implements {UplcValue}
 */
export class UplcPair extends UplcValueImpl {
	#first;
	#second;

	/**
	 * @param {Site} site
	 * @param {UplcValue} first
	 * @param {UplcValue} second
	 */
	constructor(site, first, second) {
		super(site);
		this.#first = first;
		this.#second = second;
	}

	/**
	 * Constructs a UplcPair without requiring a Site
	 * @param {UplcValue} first 
	 * @param {UplcValue} second 
	 * @returns {UplcPair}
	 */
	static new(first, second) {
		return new UplcPair(Site.dummy(), first, second);
	}

	/**
	 * Creates a new UplcBool wrapped with UplcConst so it can be used as a term.
	 * @param {Site} site 
	 * @param {UplcValue} first
	 * @param {UplcValue} second
	 * @returns {UplcConst}
	 */
	static newTerm(site, first, second) {
		return new UplcConst(new UplcPair(site, first, second));
	}

	/**
	 * @param {TransferUplcAst} other 
	 * @returns {any}
	 */
	transfer(other) {
		return other.transferUplcPair(
			this.site.transfer(other),
			this.#first.transfer(other),
			this.#second.transfer(other)
		);
	}

	/**
	 * @type {number}
	 */
	get memSize() {
		return this.#first.memSize + this.#second.memSize;
	}

	/**
	 * 16 additional type bits on top of #first and #second bits
	 */
	get flatSize() {
		return 16 + this.#first.flatSize + this.#second.flatSize;
	}

	/**
	 * @param {Site} newSite 
	 * @returns {UplcValue}
	 */
	copy(newSite) {
		return new UplcPair(newSite, this.#first, this.#second);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `(${this.#first.toString()}, ${this.#second.toString()})`;
	}

	/**
	 * @returns {boolean}
	 */
	isPair() {
		return true;
	}

	/**
	 * @type {UplcValue}
	 */
	get first() {
		return this.#first;
	}

	/**
	 * @type {UplcValue}
	 */
	get second() {
		return this.#second;
	}

	/**
	 * @type {UplcData}
	 */
	get key() {
		return this.#first.data;
	}

	/**
	 * @type {UplcData}
	 */
	get value() {
		return this.#second.data;
	}

	/**
	 * @returns {string}
	 */
	typeBits() {
		// 7 (7 (6) (fst)) (snd)
		return ["0111", "0111", "0110", this.#first.typeBits(), this.#second.typeBits()].join("1");
	}

	/**
	 * @param {BitWriter} bitWriter
	 */
	toFlatValueInternal(bitWriter) {
		this.#first.toFlatValueInternal(bitWriter);
		this.#second.toFlatValueInternal(bitWriter);
	}
}

/** 
 * Plutus-core list value class.
 * Only used during evaluation.
 * @implements {UplcList}
*/
export class UplcList extends UplcValueImpl {
	#itemType;
	#items;

	/**
	 * @param {Site} site 
	 * @param {UplcType} itemType 
	 * @param {UplcValue[]} items 
	 */
	constructor(site, itemType, items) {
		super(site);
		this.#itemType = itemType;
		this.#items = items;
	}

	/**
	 * Constructs a UplcList without requiring a Site
	 * @param {UplcType} type 
	 * @param {UplcValue[]} items 
	 */
	static new(type, items) {
		return new UplcList(Site.dummy(), type, items);
	}

	/**
	 * @param {TransferUplcAst} other 
	 * @returns {any}
	 */
	transfer(other) {
		return other.transferUplcList(
			this.site.transfer(other),
			this.#itemType.transfer(other),
			this.#items.map(item => item.transfer(other))
		);
	}

	/**
	 * @type {number}
	 */
	get length() {
		return this.#items.length;
	}

	/**
	 * @type {number}
	 */
	get memSize() {
		let sum = 0;

		for (let item of this.#items) {
			sum += item.copy(this.site).memSize;
		}

		return sum;
	}

	/**
	 * 10 + nItemType type bits, value bits of each item (must be corrected by itemType)
	 * @type {number}
	 */
	get flatSize() {
		const nItemType = this.#itemType.typeBits.length;

		return 10 + nItemType + this.#items.reduce((prev, item) => item.flatSize - nItemType + prev, 0);
	}

	/**
	 * @type {UplcType}
	 */
	get itemType() {
		return this.#itemType;
	}

	/**
	 * @param {Site} newSite
	 * @returns {UplcList}
	 */
	copy(newSite) {
		return new UplcList(newSite, this.#itemType, this.#items.slice());
	}

	/**
	 * @returns {boolean}
	 */
	isList() {
		return true;
	}

	/**
	 * @type {UplcValue[]}
	 */
	get list() {
		return this.#items.slice();
	}

	/**
	 * @returns {boolean}
	 */
	isDataList() {
		return this.#itemType.isData();
	}
	
	/**
	 * @returns {boolean}
	 */
	isDataMap() {
		return this.#itemType.isDataPair();
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `[${this.#items.map(item => item.toString()).join(", ")}]`;
	}

	/**
	 * @returns {string}
	 */
	typeBits() {
		// 7 (5) (type bits of content)
		return ["0111", "0101", this.#itemType.typeBits()].join("1");
	}

	/**
	 * @param {BitWriter} bitWriter 
	 */
	toFlatValueInternal(bitWriter) {
		for (let item of this.#items) {
			bitWriter.write('1');

			item.copy(this.site).toFlatValueInternal(bitWriter);
		}

		bitWriter.write('0');
	}
}

/**
 * `UplcValue` that wraps a `UplcData` instance.
 * @implements {UplcValue}
 */
export class UplcDataValue extends UplcValueImpl {
	#data;

	/**
	 * @param {Site} site 
	 * @param {UplcData} data 
	 */
	constructor(site, data) {
		super(site);
		this.#data = assertDefined(data);
		assert(data instanceof UplcData);
	}

	/**
	 * @param {TransferUplcAst} other 
	 * @returns {any}
	 */
	transfer(other) {
		return other.transferUplcDataValue(
			this.site.transfer(other),
			this.#data.transfer(other)
		);
	}

	/**
	 * @type {number}
	 */
	get memSize() {
		return this.#data.memSize;
	}

	/**
	 * Same number of header bits as UplcByteArray
	 * @type {number}
	 */
	get flatSize() {
		const bytes = this.#data.toCbor();

		return (new UplcByteArray(Site.dummy(), bytes)).flatSize;
	}

	/**
	 * @param {Site} newSite 
	 * @returns {UplcDataValue}
	 */
	copy(newSite) {
		return new UplcDataValue(newSite, this.#data);
	}

	/**
	 * @returns {boolean}
	 */
	isData() {
		return true;
	}

	/**
	 * @type {UplcData}
	 */
	get data() {
		return this.#data;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `data(${this.#data.toString()})`;
	}

	/**
	 * @returns {string}
	 */
	typeBits() {
		return UplcType.newDataType().typeBits();
	}

	/**
	 * @param {BitWriter} bitWriter
	 */
	toFlatValueInternal(bitWriter) {
		UplcByteArray.writeBytes(bitWriter, this.#data.toCbor());
	}

	/**
	 * @param {UplcDataValue | UplcData} data 
	 * @returns {UplcData}
	 */
	static unwrap(data) {
		if (data instanceof UplcDataValue) {
			return data.data;
		} else {
			return data;
		}
	}
}

/**
 * Base class of Plutus-core terms
 */
export class UplcTerm {
	#site;
	#type;

	/**
	 * @param {Site} site
	 * @param {number} type
	 */
	constructor(site, type) {
		assert(site != undefined && site instanceof Site);
		this.#site = site;
		this.#type = type;
	}

	/**
	 * @type {number}
	 */
	get type() {
		return this.#type;
	}

	/**
	 * @type {Site}
	 */
	get site() {
		return this.#site;
	}

	/**
	 * @param {TransferUplcAst} other 
	 * @returns {any}
	 */
	transfer(other) {
		throw new Error("not yet implemented");
	}

	/**
	 * Generic term toString method
	 * @returns {string}
	 */
	toString() {
		return `(Term ${this.#type.toString()})`;
	}
	
	/**
	 * Writes bits of flat encoded Plutus-core terms to bitWriter. Doesn't return anything.
	 * @internal
	 * @param {BitWriter} bitWriter 
	 * @param {null | Map<string, number>} codeMapFileIndices
	 */
	toFlat(bitWriter, codeMapFileIndices = null) {
		throw new Error("not yet implemented");
	}
}

/**
 * Plutus-core variable ref term (index is a Debruijn index)
 */
export class UplcVariable extends UplcTerm {
	/**
	 * @readonly
	 * @type {UplcInt}
	 */
	index;

	/**
	 * @param {Site} site 
	 * @param {UplcInt} index 
	 */
	constructor(site, index) {
		super(site, 0);
		this.index = index;
	}

	/**
	 * @param {TransferUplcAst} other 
	 */
	transfer(other) {
		return other.transferUplcVariable(
			this.site.transfer(other),
			this.index.transfer(other)
		);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `x${this.index.toString()}`;
	}

	/**
	 * @internal
	 * @param {BitWriter} bitWriter 
	 * @param {null | Map<string, number>} codeMapFileIndices
	 */
	toFlat(bitWriter, codeMapFileIndices = null) {
		bitWriter.write('0000');
		this.index.toFlatUnsigned(bitWriter);
	}

	/**
	 * @internal
	 * @param {UplcRte} rte
	 * @param {UplcFrame[]} stack
	 * @param {ComputingState} state
	 * @returns {CekState}
	 */
	computeCek(rte, stack, state) {
		rte.incrVariableCost();
        const i = Number(this.index.value);

		const v = state.env.values[state.env.values.length - i];

        return {reducing: v};
	}
}

/**
 * Plutus-core delay term.
 */
export class UplcDelay extends UplcTerm {
	/**
	 * @readonly
	 * @type {UplcTerm}
	 */
	expr;

	/**
	 * @param {Site} site 
	 * @param {UplcTerm} expr 
	 */
	constructor(site, expr) {
		super(site, 1);
		this.expr = expr;
	}

	/**
	 * @param {TransferUplcAst} other 
	 * @returns {any}
	 */
	transfer(other) {
		return other.transferUplcDelay(
			this.site.transfer(other),
			this.expr.transfer(other)
		);
	}

	/**
	 * @returns {string} 
	 */
	toString() {
		return `(delay ${this.expr.toString()})`;
	}

	/**
	 * @internal
	 * @param {BitWriter} bitWriter 
	 * @param {null | Map<string, number>} codeMapFileIndices
	 */
	toFlat(bitWriter, codeMapFileIndices = null) {
		bitWriter.write('0001');
		this.expr.toFlat(bitWriter, codeMapFileIndices);
	}

	/**
	 * @internal
	 * @param {UplcRte} rte 
	 * @param {UplcFrame[]} stack
	 * @param {ComputingState} state 
	 * @returns {CekState}
	 */
	computeCek(rte, stack, state) {
		rte.incrDelayCost();
		return {reducing: new UplcDelayWithEnv(this, state.env)};
	}
}

/**
 * Plutus-core lambda term
 */
export class UplcLambda extends UplcTerm {
	/**
	 * @readonly
	 * @type {UplcTerm}
	 */
	expr;

	#argName;

	/**
	 * @param {Site} site
	 * @param {UplcTerm} expr
	 * @param {null | string} argName
	 */
	constructor(site, expr, argName = null) {
		super(site, 2);
		this.expr = expr;
		this.#argName = argName;
	}

	/**
	 * @param {TransferUplcAst} other 
	 * @returns {any}
	 */
	transfer(other) {
		return other.transferUplcLambda(
			this.site.transfer(other),
			this.expr.transfer(other),
			this.#argName
		);
	}

	/**
	 * Returns string with unicode lambda symbol
	 * @returns {string}
	 */
	toString() {
		return `(\u039b${this.#argName !== null ? " " + this.#argName + " ->" : ""} ${this.expr.toString()})`;
	}

	/**
	 * @internal
	 * @param {BitWriter} bitWriter 
	 * @param {null | Map<string, number>} codeMapFileIndices
	 */
	toFlat(bitWriter, codeMapFileIndices = null) {
		bitWriter.write('0010');
		this.expr.toFlat(bitWriter, codeMapFileIndices);
	}

	/**
	 * @internal
	 * @param {UplcRte} rte 
	 * @param {UplcFrame[]} stack
	 * @param {ComputingState} state 
	 * @returns {CekState}
	 */
	computeCek(rte, stack, state) {
		rte.incrLambdaCost();
		return {reducing: new UplcLambdaWithEnv(this, state.env)};
	}
}

/**
 * Plutus-core function application term (i.e. function call)
 */
export class UplcCall extends UplcTerm {
	/**
	 * @readonly
	 * @type {UplcTerm}
	 */
	fn;

	/**
	 * @readonly
	 * @type {UplcTerm}
	 */
	arg;

	/**
	 * @param {Site} site
	 * @param {UplcTerm} fn
	 * @param {UplcTerm} arg
	 */
	constructor(site, fn, arg) {
		super(site, 3);
		this.fn = fn;
		this.arg = arg;
	}

	/**
	 * @internal
	 * @type {Site}
	 */
	get callSite() {
		return this.site;
	}

	/**
	 * @param {TransferUplcAst} other
	 * @returns {any}
	 */
	transfer(other) {
		return other.transferUplcCall(
			this.site.transfer(other),
			this.fn.transfer(other),
			this.arg.transfer(other)
		);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `[${this.fn.toString()} ${this.arg.toString()}]`;
	}

	/**
	 * @internal
	 * @param {BitWriter} bitWriter 
	 * @param {null | Map<string, number>} codeMapFileIndices
	 */
	toFlat(bitWriter, codeMapFileIndices = null) {
		if (codeMapFileIndices && this.site.codeMapSite) {
			bitWriter.write('1011');
			
			const site = this.site.codeMapSite;

			(new UplcInt(site, BigInt(assertDefined(codeMapFileIndices.get(site.src.name))), false)).toFlatUnsigned(bitWriter);
			(new UplcInt(site, BigInt(site.startPos), false)).toFlatUnsigned(bitWriter);
		} else {
			bitWriter.write('0011');
		}

		this.fn.toFlat(bitWriter, codeMapFileIndices);
		this.arg.toFlat(bitWriter, codeMapFileIndices);
	}

	/**
	 * @internal
	 * @param {UplcRte} rte 
	 * @param {UplcFrame[]} stack 
	 * @param {ComputingState} state 
	 * @returns {CekState}
	 */
	computeCek(rte, stack, state) {
		rte.incrCallCost();

        stack.push(new PreCallFrame(this, state.env));
        return {computing: this.fn, env: state.env};
	}
}

/**
 * Plutus-core const term (i.e. a literal in conventional sense)
 */
export class UplcConst extends UplcTerm {
	/**
	 * @readonly
	 * @type {UplcValue}
	 */
	value;

	/**
	 * @param {UplcValue} value 
	 */
	constructor(value) {
		super(value.site, 4);

		this.value = value;

		if (value instanceof UplcInt) {
			assert(value.signed);
		}
	}

	get flatSize() {
		return 4 + this.value.flatSize;
	}

	/**
	 * @param {TransferUplcAst} other 
	 * @returns {any}
	 */
	transfer(other) {
		return other.transferUplcConst(
			this.value.transfer(other)
		);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return this.value.toString();
	}

	/**
	 * @internal
	 * @param {BitWriter} bitWriter 
	 * @param {null | Map<string, number>} codeMapFileIndices
	 */
	toFlat(bitWriter, codeMapFileIndices = null) {
		bitWriter.write('0100');
		this.value.toFlatValue(bitWriter);
	}

	/**
	 * @internal
	 * @param {UplcRte} rte 
	 * @param {UplcFrame[]} stack
	 * @param {ComputingState} state 
	 * @returns {CekState}
	 */
	computeCek(rte, stack, state) {
		rte.incrConstCost();
        return {reducing: this};
	}

	/**
	 * @internal
	 * @param {UplcRte} rte 
	 * @param {UplcFrame[]} stack 
	 * @param {PreCallFrame} frame 
	 * @returns {CekState}
	 */
	reducePreCallFrame(rte, stack, frame) {
		if (this.value.isAny()) {
			return {reducing: this};
		} else {
			throw new Error("UplcCall term expects UplcLambdaWithEnv or UplcBuiltin for first arg");
		}
	}

	/**
	 * @internal
	 * @param {UplcRte} rte 
	 * @param {UplcFrame[]} stack 
	 * @param {ForceFrame} frame 
	 * @returns {CekState}
	 */
	reduceForceFrame(rte, stack, frame) {
		if (this.value.isAny()) {
			return {reducing: this};
		} else {
			throw new Error(`unexpected ${this.toString()} term in force`);
		}
	}
}

/**
 * Plutus-core force term
 */
export class UplcForce extends UplcTerm {
	/**
	 * @readonly
	 */
	expr;

	/**
	 * @param {Site} site
	 * @param {UplcTerm} expr
	 */
	constructor(site, expr) {
		super(site, 5);
		this.expr = expr;
	}

	/**
	 * @param {TransferUplcAst} other 
	 * @returns {any}
	 */
	transfer(other) {
		return other.transferUplcForce(
			this.site.transfer(other),
			this.expr.transfer(other)
		);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `(force ${this.expr.toString()})`;
	}

	/**
	 * @internal
	 * @param {BitWriter} bitWriter 
	 * @param {null | Map<string, number>} codeMapFileIndices
	 */
	toFlat(bitWriter, codeMapFileIndices = null) {
		if (codeMapFileIndices && this.site.codeMapSite) {
			bitWriter.write('1101');
			
			const site = this.site.codeMapSite;
			(new UplcInt(site, BigInt(assertDefined(codeMapFileIndices.get(site.src.name))), false)).toFlatUnsigned(bitWriter);
			(new UplcInt(site, BigInt(site.startPos), false)).toFlatUnsigned(bitWriter);
		} else {
			bitWriter.write('0101');
		}
		
		this.expr.toFlat(bitWriter, codeMapFileIndices);
	}

	/**
	 * @internal
	 * @param {UplcRte} rte 
	 * @param {UplcFrame[]} stack 
	 * @param {ComputingState} state 
	 * @returns {CekState}
	 */
	computeCek(rte, stack, state) {
		rte.incrForceCost();
		stack.push(new ForceFrame(this, state.env));
       	return {computing: this.expr, env: state.env};
	}
}

/**
 * Plutus-core error term
 */
export class UplcError extends UplcTerm {
	/** 'msg' is only used for debuggin and doesn't actually appear in the final program */
	#msg;

	/**
	 * @param {Site} site 
	 * @param {string} msg 
	 */
	constructor(site, msg = "") {
		super(site, 6);
		this.#msg = msg;
	}

	/**
	 * @param {TransferUplcAst} other 
	 * @returns {any}
	 */
	transfer(other) {
		return other.transferUplcError(
			this.site.transfer(other),
			this.#msg
		);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return "(error)";
	}

	/**
	 * @internal
	 * @param {BitWriter} bitWriter 
	 * @param {null | Map<string, number>} codeMapFileIndices
	 */
	toFlat(bitWriter, codeMapFileIndices = null) {
		bitWriter.write('0110');
	}

	/**
	 * @internal
	 * @param {UplcRte} rte 
	 * @param {UplcFrame[]} stack 
	 * @param {ComputingState} state 
	 * @returns {CekState}
	 */
	computeCek(rte, stack, state) {
		return {error: "", env: state.env};
	}
}

/**
 * Plutus-core builtin function ref term
 */
export class UplcBuiltin extends UplcTerm {
	/** 
	 * Unknown builtins stay integers
	 * @type {string | number}
	 */
	#name;

	/**
	 * @type {number}
	 */
	#forceCount;

	/**
	 * @type {number}
	 */
	#nArgs;

	/**
	 * @param {Site} site 
	 * @param {string | number} name 
	 */
	constructor(site, name) {
		super(site, 7);
		this.#name = assertDefined(name);
		this.#forceCount = (typeof this.#name === "string" && !this.#name.startsWith("macro__")) ? UPLC_BUILTINS[findUplcBuiltin(BUILTIN_PREFIX + this.#name)].forceCount : 0;
		
		if (this.isMacro()) {
			this.#nArgs = -1;
		} else if (typeof this.#name == "string") {
			const i =  UPLC_BUILTINS.findIndex(info => info.name == this.#name);

			assert(i != -1);

			this.#nArgs = UPLC_BUILTINS[i].nArgs;
		} else {
			throw new Error("unknown number of arguments");
		}
	}

	/**
	 * @param {TransferUplcAst} other 
	 * @returns {any}
	 */
	transfer(other) {
		return other.transferUplcBuiltin(
			this.site.transfer(other),
			this.#name
		);
	}

	/**
	 * @type {string}
	 */
	get name() {
		return this.#name.toString();
	}

	/**
	 * @type {number} 
	 */
	get nArgs() {
		return this.#nArgs;
	}

	/**
	 * @internal
	 * @returns {boolean}
	 */
	allowAny() {
		if (typeof this.#name == "string") {
			if (this.#name.startsWith("macro__")) {
				return true;
			} else {
				let i = UPLC_BUILTINS.findIndex(info => info.name == this.#name);

				assert(i != -1);

				return UPLC_BUILTINS[i].allowAny;
			}
		} else {
			return true;
		}
	}

	/**
	 * @internal
	 * @returns {boolean}
	 */
	isMacro() {
		if (typeof this.#name == "string") {
			return this.#name.startsWith("macro__");
		} else {
			return false;
		}
	}

	/**
	 * @returns {string}
	 */
	toString() {
		if (typeof this.#name == "string") {
			return `(builtin ${this.#name})`;
		} else {
			return `(builtin unknown${this.#name.toString()})`;
		}
	}

	/**
	 * @internal
	 * @param {BitWriter} bitWriter 
	 * @param {null | Map<string, number>} codeMapFileIndices
	 */
	toFlat(bitWriter, codeMapFileIndices = null) {
		bitWriter.write('0111');

		/** 
		 * @type {number} 
		 */
		let i;

		if (typeof this.#name == "string") {
			if (this.#name.startsWith("macro__")) {
				const macroName = this.#name.slice(("macro__").length);

				i = UPLC_MACROS.findIndex(entry => entry == macroName);

				assert(i != -1, `macro '${macroName}' not found`);

				i += UPLC_MACROS_OFFSET;
			} else {
				i = UPLC_BUILTINS.findIndex(info => info.name == this.#name);

				assert(i != -1);
			}
		} else {
			i = this.#name;
		}

		let bitString = padZeroes(i.toString(2), 7);

		bitWriter.write(bitString);
	}

	/**
	 * @internal
	 * @param {NetworkParams} params
	 * @param  {...UplcValue} args
	 * @returns {Cost}
	 */
	calcCost(params, ...args) {
		let i = UPLC_BUILTINS.findIndex(info => info.name == this.#name);

		let argSizes = args.map(a => a.memSize);

		if (!argSizes.every(size => !Number.isNaN(size) && size >= 0)) {
			throw new Error("invalid arg size");
		}

		return UPLC_BUILTINS[i].calcCost(params, argSizes);
	}

	/**
	 * Used by IREvaluator
	 * @internal
	 * @param {Word} name
	 * @param {UplcValue[]} args
	 * @returns {UplcValue}
	 */
	static evalStatic(name, args) {
		let builtin = new UplcBuiltin(name.site, name.value);

		let rte = new UplcRte();

		let res = builtin.evalBuiltin(rte, name.site, args, true);

		rte.throwError()

		if (res instanceof Promise) {
			throw new Error("can't call trace through evalStatic");
		} else {
			return res;
		}
	}

	/**
	 * @internal
	 * @param {UplcRte} rte
	 * @param {UplcFrame[]} stack
	 * @param {ComputingState} state
	 * @returns {CekState}
	 */
	computeCek(rte, stack, state) {
		rte.incrBuiltinCost();
        return {reducing: new AppliedUplcBuiltin(this)};
	}

	/**
	 * @internal
	 * @param {UplcRte} rte 
	 * @param {Site} site
	 * @param {UplcValue[]} args
	 * @returns {Promise<UplcValue>}
	 */
	evalMacro(rte, site, args) {
		assert(this.isMacro());
		if (typeof this.#name == "string") {
			// don't include the last Unit
			return rte.callMacro(this.#name.slice(("macro__").length), args);
		} else {
			throw new Error("unexpected");
		}
	}

	/**
	 * @internal
	 * @param {UplcRte} rte 
	 * @param {Site} site
	 * @param {UplcValue[]} args
	 * @param {boolean} syncTrace if true => don't call rte.print method (used by IREvaluator)
	 * @returns {UplcValue | Promise<UplcValue>} // trace returns a Promise (async print), all the other builtins return a synchronous value
	 */
	evalBuiltin(rte, site, args, syncTrace = false) {
		if (!this.allowAny() && args.some(a => a.isAny())) {
			return new UplcAny(site);
		} 

		rte.calcAndIncrCost(this, ...args);

		/**
		 * @type {{[name: string]: (...args: UplcValue[]) => (UplcValue | Promise<UplcValue>)}}
		 */
		const callbacks = {
			addInteger: (a, b) => {
				return new UplcInt(site, a.int + b.int);
			},
			subtractInteger: (a, b) => {
				return new UplcInt(site, a.int - b.int);
			},
			divideInteger: (a, b) => {
				if (b.int === 0n) {
					return rte.error("division by zero");
				} else {
					return new UplcInt(site, a.int / b.int);
				}
			},
			multiplyInteger: (a, b) => {
				return new UplcInt(site, a.int * b.int);
			},
			quotientInteger: (a, b) => {
				if (b.int === 0n) {
					return rte.error("division by zero");
				} else {
					return new UplcInt(site, a.int/b.int + (b.int < 0n ? 1n : 0n));
				}
			},
			modInteger: (a, b) => {
				if (b.int === 0n) {
					return rte.error("division by zero");
				} else {
					return new UplcInt(site, a.int % b.int);
				}
			},
			remainderInteger: (a, b) => {
				if (b.int == 0n) {
					return rte.error("division by zero");
				} else {
					return new UplcInt(site, a.int - (a.int/b.int + (b.int < 0n ? 1n : 0n))*b.int);
				}
			},
			equalsInteger: (a, b) => {
				return new UplcBool(site, a.int == b.int);
			},
			lessThanInteger: (a, b) => {
				return new UplcBool(site, a.int < b.int);
			},
			lessThanEqualsInteger: (a, b) => {
				return new UplcBool(site, a.int <= b.int);
			},
			appendByteString: (a, b) => {
				return new UplcByteArray(site, a.bytes.concat(b.bytes));	
			},
			consByteString: (a, b) => {
				let bytes = b.bytes;

				const byte = Number(a.int)

				if (byte < 0 || byte >= 256) {
					return rte.error("byte out of range");
				}

				bytes.unshift(byte);
				
				return new UplcByteArray(site, bytes);
			},
			sliceByteString: (a, b, c) => {
				const bytes = c.bytes;
				let start = Math.max(Number(a.int), 0);
				let end = Math.min(Number(a.int) + Number(b.int) - 1, bytes.length - 1);
				
				if (end < start) {
					return new UplcByteArray(site, []);
				} else {
					return new UplcByteArray(site, bytes.slice(start, end + 1));
				}
			},
			lengthOfByteString: (a) => {
				return new UplcInt(site, BigInt(a.bytes.length));
			},
			indexByteString: (a, b) => {
				let bytes = a.bytes;
				let i = b.int;
				if (i < 0 || i >= bytes.length) {
					throw new Error("index out of range");
				}

				return new UplcInt(site, BigInt(bytes[Number(i)]));
			},
			equalsByteString: (a, b) => {
				return new UplcBool(site, ByteArrayData.comp(a.bytes, b.bytes) == 0);
			},
			lessThanByteString: (a, b) => {
				return new UplcBool(site, ByteArrayData.comp(a.bytes, b.bytes) == -1);
			},
			lessThanEqualsByteString: (a, b) => {
				return new UplcBool(site, ByteArrayData.comp(a.bytes, b.bytes) <= 0);
			},
			appendString: (a, b) => {
				return new UplcString(site, a.string + b.string);
			},
			equalsString: (a, b) => {
				return new UplcBool(site, a.string == b.string);
			},
			encodeUtf8: (a) => {
				return new UplcByteArray(site, textToBytes(a.string));
			},
			decodeUtf8: (a) => {
				try {
					return new UplcString(site, bytesToText(a.bytes));
				} catch(_) {
					return rte.error("invalid utf-8");
				}
			},
			sha2_256: (a) => {
				return new UplcByteArray(site, Crypto.sha2_256(a.bytes))
			},
			sha3_256: (a) => {
				return new UplcByteArray(site, Crypto.sha3(a.bytes))
			},
			blake2b_256: (a) => {
				return new UplcByteArray(site, Crypto.blake2b(a.bytes)); 
			},
			verifyEd25519Signature: (key, msg, signature) => {
				rte.calcAndIncrCost(this, key, msg, signature);

				let keyBytes = key.bytes;
				if (keyBytes.length != 32) {
					return rte.error(`expected key of length 32 for verifyEd25519Signature, got key of length ${keyBytes.length}`);
				}

				let msgBytes = msg.bytes;
				
				let signatureBytes = signature.bytes;
				if (signatureBytes.length != 64) {
					return rte.error(`expected signature of length 64 for verifyEd25519Signature, got signature of length ${signatureBytes.length}`);
				}

				let ok = Ed25519.verify(signatureBytes, msgBytes, keyBytes);

				return new UplcBool(site, ok);
			},
			ifThenElse: (a, b, c) => {
				if (a.isAny()) {
					return new UplcAny(site);
				} else {
					return a.bool ? b : c;
				}
			},
			chooseUnit: (a, b) => {
				a.assertUnit();

				return b;
			},
			trace: (a, b) => {
				if (a.isAny() || syncTrace) {
					return b;
				} else {
					return rte.print(a.string.split("\n").map(l => `INFO (${site.toString()}) ${l}`)).then(() => {
						return b;
					});
				}
			},
			fstPair: (a) => {
				if (a.isPair()) {
					return a.first;
				} else {
					throw site.typeError(`expected pair or data-pair for first arg, got '${a.toString()}'`);
				}
			},
			sndPair: (a) => {
				if (a.isPair()) {
					return a.second;
				} else {
					throw site.typeError(`expected pair or data-pair for first arg, got '${a.toString()}'`);
				}
			},
			chooseList: (a, b, c) => {
				if (a.isAny()) {
					return new UplcAny(site);
				} else if (a.isList()) {
					if (a.length == 0) {
						return b;
					} else {
						return c;
					}
				} else {
					throw site.typeError(`expected list or map first arg, got '${a.toString()}'`);
				}
			},
			mkCons: (a, b) => {
				if (b.isList()) {
					if (!b.itemType.isSameType(a)) {
						throw site.typeError(`wrong type for 2nd arg of mkCons, expected ${a.toString()}, got ${b.toString()}`);
					}

					let lst = b.list;
					lst.unshift(a);

					return new UplcList(site, b.itemType, lst);
				} else {
					throw site.typeError(`expected list or map for second arg, got '${b.toString()}'`);
				}
			},
			headList: (a) => {
				if (a.isList()) {
					const lst = a.list;
					if (lst.length == 0) {
						return rte.error("empty list");
					}

					return lst[0];
				} else {
					throw site.typeError(`__core__headList expects list or map, got '${a.toString()}'`);
				}
			},
			tailList: (a) => {
				if (a.isList()) {
					let lst = a.list;
					if (lst.length == 0) {
						return rte.error("empty list");
					}

					return new UplcList(site, a.itemType, lst.slice(1));
				} else {
					throw site.typeError(`__core__tailList expects list or map, got '${a.toString()}'`);
				}
			},
			nullList: (a) => {
				if (a.isList()) {
					return new UplcBool(site, a.list.length == 0);
				} else {
					throw site.typeError(`__core__nullList expects list or map, got '${a.toString()}'`);
				}
			},
			chooseData: (a, b, c, d, e, f) => {
				if (a.isAny()) {
					return new UplcAny(site);
				} else {
					const data = a.data;

					if (data instanceof ConstrData) {
						return b;
					} else if (data instanceof MapData) {
						return c;
					} else if (data instanceof ListData) {
						return d;
					} else if (data instanceof IntData) {
						return e;
					} else if (data instanceof ByteArrayData) {
						return f;
					} else {
						throw new Error("unexpected");
					}
				}
			},
			constrData: (a, b) => {
				const i = a.int;
				assert(i >= 0);

				const lst = b.list;

				return new UplcDataValue(site, new ConstrData(Number(i), lst.map(item => item.data)));
			},
			mapData: (a) => {
				return new UplcDataValue(site, new MapData(a.list.map(pair => {
					return [pair.first.data, pair.second.data];
				})));
			},
			listData: (a) => {
				return new UplcDataValue(site, new ListData(a.list.map(item => item.data)));
			},
			iData: (a) => {
				return new UplcDataValue(site, new IntData(a.int));
			},
			bData: (a) => {
				return new UplcDataValue(site, new ByteArrayData(a.bytes));
			},
			unConstrData: (a) => {
				if (!a.isData()) {
					throw site.typeError(`expected data for arg of unConstrData, got ${a.toString()}`);
				}

				let data = a.data;
				if (!(data instanceof ConstrData)) {
					return rte.error(`unexpected unConstrData argument '${data.toString()}'`);
				} else {
					return new UplcPair(site, new UplcInt(site, BigInt(data.index)), new UplcList(site, UplcType.newDataType(), data.fields.map(f => new UplcDataValue(site, f))));
				}
			},
			unMapData: (a) => {
				if (!a.isData()) {
					throw site.typeError(`expected data for arg of unMapData, got ${a.toString()}`);
				}

				let data = a.data;
				if (!(data instanceof MapData)) {
					return rte.error(`unexpected unMapData argument '${data.toString()}'`);
				} else {
					return new UplcList(site, UplcType.newDataPairType(), data.map.map(([fst, snd]) => new UplcPair(site, new UplcDataValue(site, fst), new UplcDataValue(site, snd))));
				}
			},
			unListData: (a) => {
				if (!a.isData()) {
					throw site.typeError(`expected data for arg of unListData, got ${a.toString()}`);
				}

				let data = a.data;
				if (!(data instanceof ListData)) {
					return rte.error(`unexpected unListData argument '${data.toString()}'`);
				} else {
					return new UplcList(site, UplcType.newDataType(), data.list.map(item => new UplcDataValue(site, item)));
				}
			},
			unIData: (a) => {
				if (!a.isData()) {
					throw site.typeError(`expected data for arg of unIData, got ${a.toString()}`);
				}

				let data = a.data;
				if (!(data instanceof IntData)) {
					return rte.error(`unexpected unIData argument '${data.toString()}'`);
				} else {
					return new UplcInt(site, data.value);
				}
			},
			unBData: (a) => {
				if (!a.isData()) {
					throw site.typeError(`expected data for arg of unBData, got ${a.toString()}`);
				}

				let data = a.data;
				if (!(data instanceof ByteArrayData)) {
					return rte.error(`unexpected unBData argument '${data.toString()}'`);
				} else {
					return new UplcByteArray(site, data.bytes);
				}
			},
			equalsData: (a, b) => {
				if (!a.isData()) {
					throw site.typeError(`expected data for 1st arg of equalsData, got ${a.toString()}`);
				}

				if (!b.isData()) {
					throw site.typeError(`expected data for 2nd arg of equalsData, got ${b.toString()}`);
				}

				return new UplcBool(site, a.data.isSame(b.data));
			},
			mkPairData: (a, b) => {
				return new UplcPair(site, new UplcDataValue(site, a.data), new UplcDataValue(site, b.data));
			},
			mkNilData: (a) => {
				a.assertUnit();
				return new UplcList(site, UplcType.newDataType(), []);
			},
			mkNilPairData: (a) => {
				a.assertUnit();

				return new UplcList(site, UplcType.newDataPairType(), []);
			},
			serialiseData: (a) => {
				return new UplcByteArray(site, a.data.toCbor());
			},
			verifyEcdsaSecp256k1Signature: (a, b, c) => {
				throw new Error("no immediate need, so don't bother yet");
			},
			verifySchnorrSecp256k1Signature: (a, b, c) => {
				throw new Error("no immediate need, so don't bother yet");
			}
		};
			
		return assertDefined(callbacks[this.#name], `UplcBuiltin ${this.#name} not yet implemented`)(...args);
	}

	/**
	 * @internal
	 * @type {number}
	 */
	get forceCount() {
		return this.#forceCount;
	}
}

/**
 * @internal
 */
export class UplcFrame {
	/**
	 * @param {UplcRte} rte 
	 * @param {UplcFrame[]} stack 
	 * @param {ReducingState} state 
	 * @returns {Promise<CekState>}
	 */
	async reduceCek(rte, stack, state) {
		throw new Error("not yet implemented");
	}

	/**
	 * @type {Site}
	 */
	get site() {
		throw new Error("not yet implemented");
	}
}

/**
 * @internal
 */
export class ForceFrame extends UplcFrame {
    /**
     * @readonly
     * @type {UplcForce}
     */
    term;

	/**
	 * @readonly
	 * @type {CekEnv}
	 */
	env;

    /**
     * @param {UplcForce} term
	 * @param {CekEnv} env
     */
    constructor(term, env) {
        super();
        this.term = term;
		this.env = env;
    }

	get site() {
		return this.term.site;
	}

	/**
	 * @param {UplcRte} rte 
	 * @param {UplcFrame[]} stack 
	 * @param {ReducingState} state 
	 * @returns {Promise<CekState>}
	 */
	async reduceCek(rte, stack, state) {
		const term = state.reducing;

		return await term.reduceForceFrame(rte, stack, this);
	}
}

/**
 * @internal
 */
export class PreCallFrame extends UplcFrame {
    /**
     * @readonly
     * @type {UplcCall}
     */
    term;

    /**
     * @readonly
     * @type {CekEnv}
     */
    env;

    /**
     * @param {UplcCall} term
     * @param {CekEnv} env
     */
    constructor(term, env) {
        super();
        this.term = term;
        this.env = env;
    }

	/**
	 * @type {Site}
	 */
	get site() {
		return this.term.site;
	}

	/**
	 * 
	 * @param {UplcRte} rte 
	 * @param {UplcFrame[]} stack 
	 * @param {ReducingState} state 
	 * @returns {Promise<CekState>}
	 */
	async reduceCek(rte, stack, state) {
		const term = state.reducing;

		return term.reducePreCallFrame(rte, stack, this);
	}
}

/**
 * @internal
 */
export class CallFrame extends UplcFrame {
    /**
     * @readonly
     * @type {UplcCall}
     */
    term;

    /**
     * @readonly
     * @type {UplcLambdaWithEnv | AppliedUplcBuiltin}
     */
    fn;

	/**
	 * @readonly
	 * @type {CekEnv}
	 */
	env;

    /**
     * @param {UplcCall} term
     * @param {UplcLambdaWithEnv | AppliedUplcBuiltin} fn
	 * @param {CekEnv} env
     */
    constructor(term, fn, env) {
        super();
        this.term = term;
        this.fn = fn;
		this.env = env;
    }

	/**
	 * @type {Site}
	 */
	get site() {
		return this.term.site;
	}

	/**
	 * @param {UplcRte} rte 
	 * @param {UplcFrame[]} stack 
	 * @param {ReducingState} state 
	 * @returns {Promise<CekState>}
	 */
	async reduceCek(rte, stack, state) {
		return this.fn.reduceCallFrame(rte, stack, state, this);
	}
}

/**
 * @internal
 * @template {UplcTerm} T
 */
class UplcTermWithEnv {
    /**
    * @readonly
    * @type {T}
    */
    term;

    /**
     * @readonly
     * @type {CekEnv}
     */
    env;

    /**
     * @param {T} term 
     * @param {CekEnv} env 
     */
    constructor(term, env) {
        this.term = term;
        this.env = env;
    }

	/**
	 * @type {Site}
	 */
	get site() {
		return this.term.site;
	}

    toString() {
        return `(WithEnv ${this.term.toString()})`;
    }
}

/**
 * @internal
 * @extends {UplcTermWithEnv<UplcLambda>}
 */
class UplcLambdaWithEnv extends UplcTermWithEnv {
	/**
	 * @param {UplcRte} rte 
	 * @param {UplcFrame[]} stack 
	 * @param {PreCallFrame} frame 
	 * @returns {Promise<CekState>}
	 */
	async reducePreCallFrame(rte, stack, frame) {
		stack.push(new CallFrame(frame.term, this, frame.env));
		return {computing: frame.term.arg, env: frame.env};
	}

	/**
	 * @param {UplcRte} rte 
	 * @param {UplcFrame[]} stack 
	 * @param {ForceFrame} frame 
	 * @returns {Promise<CekState>}
	 */
	async reduceForceFrame(rte, stack, frame) {
		throw new Error("expected force after delay");
	}

	/**
	 * @param {UplcRte} rte 
	 * @param {UplcFrame[]} stack 
	 * @param {ReducingState} state 
	 * @param {CallFrame} frame 
	 * @returns {Promise<CekState>}
	 */
	async reduceCallFrame(rte, stack, state, frame) {
		return {
			computing: this.term.expr, 
			env: {
				values: this.env.values.concat([state.reducing]),
				callSites: frame.env.callSites.concat([frame.term.callSite])
			}
		};
	}
}

/**
 * @internal
 * @extends {UplcTermWithEnv<UplcDelay>}
 */
class UplcDelayWithEnv extends UplcTermWithEnv {
	/**
	 * @param {UplcRte} rte 
	 * @param {UplcFrame[]} stack 
	 * @param {PreCallFrame} frame 
	 * @returns {CekState}
	 */
	reducePreCallFrame(rte, stack, frame) {
		throw new Error("UplcCall term expects UplcLambdaWithEnv or UplcBuiltin for first arg");
	}

	/**
	 * @param {UplcRte} rte 
	 * @param {UplcFrame[]} stack 
	 * @param {ForceFrame} frame 
	 * @returns {CekState}
	 */
	reduceForceFrame(rte, stack, frame) {
		return {
			computing: this.term.expr, 
			env: {
				values: this.env.values,
				callSites: frame.env.callSites.concat([frame.term.site])
			}
		};
	}
}

/**
 * @internal
 * @implements {UplcValue}
 */
class UplcAnonValue extends UplcValueImpl {
    /**
     * @readonly
     * @type {AppliedUplcBuiltin | UplcLambdaWithEnv | UplcDelayWithEnv}
     */
    term;

    /**
     * @param {Site} site
     * @param {AppliedUplcBuiltin | UplcLambdaWithEnv | UplcDelayWithEnv} term 
     */
    constructor(site, term) {
        super(site);
        this.term = term;
    }

	/**
	 * @param {TransferUplcAst} other 
	 * @returns {any}
	 */
	transfer(other) {
		throw new Error("shouldn't be part of AST");
	}

	/**
	 * @param {Site} newSite 
	 * @returns {UplcValue}
	 */
	copy(newSite) {
		throw new Error("shouldn't be part of AST");
	}

    /**
     * @type {number}
     */
    get memSize() {
        return 1;
    }

	/**
	 * @type {number}
	 */
	get flatSize() {
		throw new Error("shouldn't be part of AST");
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return this.term.toString();
	}
}

/**
 * @internal
 */
class AppliedUplcBuiltin {
    /**
     * @readonly
     * @type {UplcBuiltin}
     */
    term;

    /**
     * @readonly
     * @type {number}
     */
    forceCount;

    /**
     * @readonly
     * @type {CekValue[]}
     */
    args;

    /**
     * @param {UplcBuiltin} term
     * @param {number} forceCount
     * @param {CekValue[]} args
     */
    constructor(term, forceCount = 0, args = []) {
        this.term = term;
        this.forceCount = forceCount;
        this.args = args;
    }

	/**
	 * @type {Site}
	 */
	get site() {
		return this.term.site;
	}

    /**
     * @returns {string}
     */
    toString() {
        return `(AppliedBuiltin ${this.forceCount} [${this.args.map(a => a.toString()).join(", ")}] ${this.term.toString()})`;
    }

    /**
     * @returns {AppliedUplcBuiltin}
     */
    force() {
        assert(this.forceCount < this.term.forceCount);

        return new AppliedUplcBuiltin(this.term, this.forceCount + 1, this.args);
    }

    /**
     * @param {CekValue} arg 
     * @returns {AppliedUplcBuiltin}
     */
    apply(arg) {
        assert(this.term.nArgs == -1 || this.args.length < this.term.nArgs);

        return new AppliedUplcBuiltin(this.term, this.forceCount, this.args.concat([arg]));
    }

    /**
     * @returns {boolean}
     */
    isFullForced() {
        return this.forceCount == this.term.forceCount;
    }

    /**
     * @returns {boolean}
     */
    isFullyApplied() {
        if (this.term.isMacro()) {
            if (this.args.length == 0) {
                return false;
            } else {
                const last = this.args[this.args.length - 1];

                if (last instanceof UplcConst && last.value instanceof UplcUnit) {
                    return true;
                } else {
                    return false;
                }
            }
        } else {
            return this.args.length == this.term.nArgs;
        }
    }

    /**
	 * @internal
     * @param {UplcRte} rte
     * @param {Site[]} sites
     * @returns {Promise<CekValue>}
     */
    async evalCek(rte, sites) {
		let site = sites[sites.length - 1];

        const args = this.args.map(a => {
            if (a instanceof UplcConst) {
                return a.value;
            } else {
                return new UplcAnonValue(Site.dummy(), a);
            }
        });

		/**
		 * @type {UplcValue}
		 */
		let res;

		if (this.term.isMacro()) {
			// don't include the last unit
			assertDefined(args.pop()).assertUnit();
			res = await this.term.evalMacro(rte, site, args);
		} else {
			if (!this.term.allowAny() && args.some(a => a.isAny())) {
				res = new UplcAny(site);
			} else if (this.term.name == "trace") {
				if (!site.codeMapSite) {
					for (let i = sites.length - 1; i >= 0; i--) {
						const s = sites[i];
						if (s.codeMapSite) {
							site = s.codeMapSite;
							break;
						}
					}
				} else {
					site = site.codeMapSite;
				}
				
				res = await this.term.evalBuiltin(rte, site, args);
			} else {
				res = await this.term.evalBuiltin(rte, site, args);
			}
		}

		if (res instanceof UplcAnonValue) {
			return res.term;
		} else {
			return new UplcConst(res);
		}
    }

	/**
	 * @param {UplcRte} rte 
	 * @param {UplcFrame[]} stack 
	 * @param {PreCallFrame} frame 
	 * @returns {Promise<CekState>}
	 */
	async reducePreCallFrame(rte, stack, frame) {
		stack.push(new CallFrame(frame.term, this, frame.env));
		return {computing: frame.term.arg, env: frame.env};
	}

	/**
	 * @param {UplcRte} rte 
	 * @param {UplcFrame[]} stack 
	 * @param {ForceFrame} frame 
	 * @returns {Promise<CekState>}
	 */
	async reduceForceFrame(rte, stack, frame) {
		if (!this.isFullForced()) {
			return {reducing: this.force()};
		} else {
			throw new Error(`can't apply force to ${this.term.name}`);
		}
	}

	/**
	 * @param {UplcRte} rte 
	 * @param {UplcFrame[]} stack 
	 * @param {ReducingState} state 
	 * @param {CallFrame} frame 
	 * @returns {Promise<CekState>}
	 */
	async reduceCallFrame(rte, stack, state, frame) {
		if (!this.isFullForced()) {
			throw new Error("must be fully forced before call");
		} else if (!this.isFullyApplied()) {
			const fn = this.apply(state.reducing);

			if (fn.isFullyApplied()) {
				const callSites = frame.env.callSites.concat([frame.term.callSite]);

				const res = await fn.evalCek(rte, callSites);

				if (rte.hasError()) {
					return {
						error: rte.getError(), 
						env: {
							values: frame.env.values,
							callSites: callSites
						}
					};
				} else {
					return {reducing: res};
				}
			} else {
				return {reducing: fn};
			}
		} else {
			throw new Error("already fully applied");
		}
	}
}

/**
 * @internal
 * @typedef {UplcConst | AppliedUplcBuiltin | UplcLambdaWithEnv | UplcDelayWithEnv} CekValue
 */

/**
 * @internal
 * @typedef {{
 * 	 values: CekValue[],
 *   callSites: Site[]
 * }} CekEnv
 */

/**
 * @internal
 * @typedef {{computing: UplcTerm, env: CekEnv}} ComputingState
 */

/**
 * @internal
 * @typedef {{reducing: CekValue}} ReducingState
 */

/**
 * @internal
 * @typedef {{error: string | RuntimeError, env: CekEnv}} ErrorState
 */

/**
 * @internal
 * @typedef {ComputingState | ReducingState | ErrorState} CekState
 */

/**
 * @internal
 * @param {UplcRte} rte
 * @param {UplcTerm} start
 * @param {null | UplcValue[]} args
 * @returns {Promise<UplcValue>}
 */
export async function evalCek(rte, start, args = null) {
    if (args !== null) {
        if (args.length == 0) {
            start = new UplcForce(start.site, start);
        } else {
            for (let arg of args) {
                start = new UplcCall(start.site, start, new UplcConst(arg));
            }
        }
    }

    /**
     * @type {UplcFrame[]}
     */
    const stack = [];

    /**
     * @type {CekState}
     */
    let state = {computing: start, env: {values: [], callSites: []}};

    // add the startup costs
    rte.incrStartupCost();

	while (true) {
		if ("computing" in state) {
			const term = state.computing;

			state = term.computeCek(rte, stack, state);
		} else if ("reducing" in state) {
			const f = stack.pop();

			const term = state.reducing;

			if (!f) {
				if (term instanceof UplcConst) {
					return term.value;
				} else {
					throw new Error("final UplcTerm in CEK isn't a UplcConst but a " + term.toString());
				}
			} else {
				state = await f.reduceCek(rte, stack, state);
			}
		} else if ("error" in state) {
			/**
			 * @type {string[]}
			 */
			let lines = rte.messages.slice();

			let err = state.error;

			const summary = "thrown during UPLC evaluation";

			if (typeof err == "string") {
				let msg = err;

				if (msg == "" && lines.length > 0) {
					msg = assertDefined(lines.pop()).split(")").slice(1).join(")").trim();
				}

				/**
				 * @type {boolean}
				 */
				let codeMapSiteFound = false;

				for (let i = state.env.callSites.length - 1; i >= 0; i--) {
					const s = state.env.callSites[i];

					if (s.codeMapSite) {
						if (!codeMapSiteFound) {
							lines.push(`ERROR (${s.codeMapSite.toString()}) ${msg}`);
							codeMapSiteFound = true;
						} else {
							lines.push(`TRACE (${s.codeMapSite.toString()})`);
						}
					}
				}

				if (!codeMapSiteFound) {
					lines.push(`ERROR ${msg}`);
				}

				
			} else {
				lines = lines.concat(err.message.split("\n").filter(l => l != summary));
			}

			lines.unshift(summary);

			throw new RuntimeError(lines.join("\n"));
		} else {
			throw new Error("unhandled CEK state");
		}
	}
}


///////////////////////////
// Section 11: Uplc program
///////////////////////////


/**
 * This library uses version "1.0.0" of Plutus-core
 * @internal
 */
const UPLC_VERSION_COMPONENTS = [1n, 0n, 0n];

 /**
  * i.e. "1.0.0"
  * @internal
  * @type {string}
  */
const UPLC_VERSION = UPLC_VERSION_COMPONENTS.map(c => c.toString()).join(".");

/**
 * This library uses V2 of the Plutus Ledger API, and is no longer compatible with V1
 * @internal
 */
const PLUTUS_SCRIPT_VERSION = "PlutusScriptV2";

/**
 * @internal
 * @type {Object.<string, number>}
 */
const UPLC_TAG_WIDTHS = {
	term:      4,
	type:      3,
	constType: 4,
	builtin:   7,
	constant:  4,
	kind:      1
};

/**
 * TODO: purpose as enum type
 * @typedef {{
 *   purpose: null | ScriptPurpose 
 *   callsTxTimeRange: boolean
 *   name?: string
 * }} ProgramProperties
 */

/**
 * The constructor returns 'any' because it is an instance of TransferableUplcProgram, and the instance methods don't need to be defined here
 * @template TInstance
 * @typedef {{
 *   transferUplcProgram: (expr: any, properties: ProgramProperties, version: any[]) => TInstance,
 *   transferUplcAst: TransferUplcAst
 * }} TransferableUplcProgram
 */

/**
 * @typedef {Cost & {
 * 	 count: number
 * }} CostCount
 */
/**
 * @typedef {{
 *   mem: bigint, 
 *   cpu: bigint,
 *   size?: number,
 *   builtins?: {[name: string]: CostCount},
 *   terms?: {[name: string]: CostCount},
 *   result?: RuntimeError | UplcValue,
 *   messages?: string[]
 * }} Profile
 *
 *
 * mem:  in 8 byte words (i.e. 1 mem unit is 64 bits)
 * cpu:  in reference cpu microseconds
 * size: in bytes
 * builtins: breakdown per builtin
 * terms: breakdown per termtype
 * result: result of evaluation
 * messages: printed messages (can be helpful when debugging)
 */

/**
 * Result of `program.compile()`. Contains the Untyped Plutus-Core AST, along with a code-mapping to the original source.
 */
 export class UplcProgram {
	#version;
	#expr;
	#properties;

	/**
	 * @param {UplcTerm} expr 
	 * @param {ProgramProperties} properties
	 * @param {UplcInt[]} version
	 */
	constructor(expr, properties = {purpose: null, callsTxTimeRange: false}, version = UPLC_VERSION_COMPONENTS.map(v => new UplcInt(expr.site, v, false))) {
		this.#version    = version;
		this.#expr       = expr;
		this.#properties = properties;
	}

	/**
	 * @type {UplcTerm}
	 */
	get expr() {
		return this.#expr;
	}

	/**
	 * @type {Site}
	 */
	get site() {
		return new Site(this.#expr.site.src, 0);
	}

	/**
	 * Returns the IR source
	 * @type {string}
	 */
	get src() {
		return this.site.src.raw;
	}

	/**
	 * @type {ProgramProperties}
	 */
	get properties() {
		return this.#properties;
	}

	/**
	 * Transfers a `UplcProgram` from an old version of Helios to a new version of Helios, keeping the script hash the same.
	 * 
	 * The main benefit for calling this method instead of serializing/deserializing is that the code mapping is maintained.
	 * @template TInstance
	 * @param {TransferableUplcProgram<TInstance>} other
	 * @returns {TInstance}
	 */
	transfer(other) {
		return other.transferUplcProgram(
			this.#expr.transfer(other.transferUplcAst),
			this.#properties,
			this.#version.map(i => i.transfer(other.transferUplcAst))
		);
	}

	/**
	 * Returns version of Plutus-core (!== Plutus script version!)
	 * @type {string}
	 */
	get versionString() {
		return this.#version.map(v => v.toString()).join(".");
	}

	/**
	 * @returns {string}
	 */
	plutusScriptVersion() {
		// Note: only supports PlutusScriptV2 for now
		return PLUTUS_SCRIPT_VERSION;
	}

	/**
	 * Returns 1 for PlutusScriptV1, 2 for PlutusScriptV2
	 * @returns {number}
	 */
	versionTag() {
		let v = this.plutusScriptVersion();

		switch (v) {
			case "PlutusScriptV1":
				return 1;
			case "PlutusScriptV2":
				return 2;
			default:
				throw new Error(`unhandled script version '${v}'`);
		}
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `(program ${this.versionString} ${this.#expr.toString()})`;
	}

	/**
	 * Flat encodes the entire Plutus-core program.
	 * Note that final padding isn't added now but is handled by bitWriter upon finalization.
	 * @internal
	 * @param {BitWriter} bitWriter
	 */
	toFlat(bitWriter) {
		this.toFlatWithMapping(bitWriter, null);
	}

	/**
	 * @internal
	 * @param {BitWriter} bitWriter
	 * @param {null | Map<string, number>} codeMapFileIndices
	 */
	toFlatWithMapping(bitWriter, codeMapFileIndices) {
		for (let v of this.#version) {
			v.toFlatUnsigned(bitWriter);
		}

		this.#expr.toFlat(bitWriter, codeMapFileIndices);
	}
	
	/**
	 * Evaluates the term contained in UplcProgram (assuming it is a lambda term)
	 * @internal
	 * @param {null | UplcValue[]} args
	 * @param {UplcRTECallbacksInternal} callbacks
	 * @param {null | NetworkParams} networkParams
	 * @returns {Promise<UplcValue>}
	 */
	async runInternal(args, callbacks = DEFAULT_UPLC_RTE_CALLBACKS, networkParams = null) {
		assertDefined(callbacks);

		let rte = new UplcRte(callbacks, networkParams);
		
		return await evalCek(rte, this.#expr, args);
	}

	/**
	 * Wrap the top-level term with consecutive UplcCall (not exported) terms.
	 * 
	 * Returns a new UplcProgram instance, leaving the original untouched.
	 * @param {UplcValue[]} args
	 * @returns {UplcProgram} - a new UplcProgram instance
	 */
	apply(args) {
		let expr = this.expr;

		for (let arg of args) {
			expr = new UplcCall(arg.site, expr, new UplcConst(arg));
		}

		return new UplcProgram(expr, this.#properties, this.#version);
	}

	/**
	 * @param {null | UplcValue[]} args - if null the top-level term is returned as a value
	 * @param {UplcRTECallbacks} callbacks 
	 * @param {null | NetworkParams} networkParams
	 * @returns {Promise<UplcValue | RuntimeError>}
	 */
	async run(args, callbacks = DEFAULT_UPLC_RTE_CALLBACKS, networkParams = null) {
		try {
			return await this.runInternal(args, callbacks, networkParams);
		} catch (e) {
			if (!(e instanceof RuntimeError)) {
				throw e;
			} else {
				return e;
			}
		}
	}

	/**
	 * Run a `UplcProgram`. The printed messages are part of the return value.
	 * @param {null | UplcValue[]} args
	 * @returns {Promise<[(UplcValue | RuntimeError), string[]]>}
	 */
	async runWithPrint(args) {
		/**
		 * @type {string[]}
		 */
		const messages = [];

		const callbacks = Object.assign({}, DEFAULT_UPLC_RTE_CALLBACKS);

		callbacks.onPrint = async function(msg) {
			messages.push(msg);
		};

		const res = await this.run(args, callbacks);

		return [res, messages];
	}

	/**
	 * Runs and profiles a `UplcProgram`. Needs the `NetworkParams` in order to calculate the execution budget.
	 * @param {UplcValue[]} args
	 * @param {NetworkParams} networkParams
	 * @returns {Promise<Profile>} The returned profile contains a breakdown of the execution cost per Uplc term type and per Uplc builtin function type.
	 */
	async profile(args, networkParams) {
		let callbacks = Object.assign({}, DEFAULT_UPLC_RTE_CALLBACKS);

		let memCost = 0n;
		let cpuCost = 0n;

		/**
		 * @type {{[name: string]: CostCount}}
		 */
		const builtins = {};

		/**
		 * @type {{[name: string]: CostCount}}
		 */
		const terms = {};
		
		/**
		 * @type {(name: string, isTerm: boolean, cost: Cost) => void}
		 */
		callbacks.onIncrCost = (name, isTerm, cost) => {
			memCost += cost.mem;
			cpuCost += cost.cpu;

			if (name !== undefined) {
				if (isTerm) {
					const prev = terms[name];
					if (prev !== undefined) {
						terms[name] = {
							mem: prev.mem + cost.mem,
							cpu: prev.cpu + cost.cpu,
							count: prev.count + 1
						};
					} else {
						terms[name] = {...cost, count: 1};
					}
				} else {
					const prev = builtins[name];

					if (prev !== undefined) {
						builtins[name] = {
							mem: prev.mem + cost.mem,
							cpu: prev.cpu + cost.cpu,
							count: prev.count + 1
						};
					} else {
						builtins[name] = {...cost, count: 1};
					}
				}
			}
		};
		
		/** @type {string[]} */
		let messages = [];

		/**
		 * @type {(msg: string) => Promise<void>}
		 */
		callbacks.onPrint = async function(msg) {
			messages.push(msg);
		};

		let result = await this.run(args, callbacks, networkParams);

		return {
			mem: memCost,
			cpu: cpuCost,
			size: this.calcSize(),
			builtins: builtins,
			terms: terms,
			result: result,
			messages: messages
		};
	}

	/**
	 * Returns flat bytes of serialized script
	 * @returns {number[]}
	 */
	serializeBytes() {
		let bitWriter = new BitWriter();

		this.toFlat(bitWriter);

		return bitWriter.finalize();
	}

	/**
	 * Calculates the on chain size of the program (number of bytes).
	 * @returns {number}
	 */
	calcSize() {
		return this.serializeBytes().length;
	}

	/**
	 * Returns the Cbor encoding of a script (flat bytes wrapped twice in Cbor bytearray).
	 * @returns {number[]}
	 */
	toCbor() {
		return Cbor.encodeBytes(Cbor.encodeBytes(this.serializeBytes()));
	}

	/**
	 * @internal
	 * @param {Map<string, number>} codeMapFileIndices 
	 */
	toCborWithMapping(codeMapFileIndices) {
		let bitWriter = new BitWriter();

		this.toFlatWithMapping(bitWriter, codeMapFileIndices);

		return Cbor.encodeBytes(Cbor.encodeBytes(bitWriter.finalize()));
	}

	/**
	 * Returns the JSON representation of the serialized program (needed by cardano-cli).
	 * @returns {string}
	 */
	serialize() {
		const cborHex = bytesToHex(this.toCbor());

		return `{"type": "${this.plutusScriptVersion()}", "description": "", "cborHex": "${cborHex}"}`;
	}

	/**
	 * @returns {number[]} - 28 byte hash
	 */
	hash() {
		let innerBytes = Cbor.encodeBytes(this.serializeBytes());

		innerBytes.unshift(this.versionTag());

		// used for both script addresses and minting policy hashes
		return Crypto.blake2b(innerBytes, 28);
	}

	/**
	 * Returns the `ValidatorHash` of the script. Throws an error if this isn't a spending validator script.
	 * @type {ValidatorHash}
	 */
	get validatorHash() {
		const purpose = this.#properties.purpose;

		assert(purpose === null || purpose === "spending");

		return new ValidatorHash(this.hash());
	}

	/**
	 * Returns the `MintingPolicyHash` of the script. Throws an error if this isn't a minting policy.
	 * @type {MintingPolicyHash}
	 */
	get mintingPolicyHash() {
		const purpose = this.#properties.purpose;

		assert(purpose === null || purpose === "minting");

		return new MintingPolicyHash(this.hash());
	}

	/**
	 * Returns the `StakingValidatorHash` of the script. Throws an error if this isn't a staking validator script.
	 * @type {StakingValidatorHash}
	 */
	get stakingValidatorHash() {
		const purpose = this.#properties.purpose;

		assert(purpose === null || purpose === "staking");

		return new StakingValidatorHash(this.hash());
	}


	/**
	 * @internal
	 * @param {number[] | string} bytes 
	 * @param {ProgramProperties} properties
	 * @param {Source[]} files
	 * @returns {UplcProgram}
	 */
	static fromCborWithMapping(bytes, files, properties = {purpose: null, callsTxTimeRange: false}) {
		if (typeof bytes == "string") {
			return UplcProgram.fromCborWithMapping(hexToBytes(bytes), files, properties)
		} else {
			if (Cbor.isBytes(bytes)) {
				bytes = Cbor.decodeBytes(bytes);
			}

			if (Cbor.isBytes(bytes)) {
				bytes = Cbor.decodeBytes(bytes);
			}

			return UplcProgram.fromFlatWithMapping(bytes, files, properties);
		}
	}

	/**
	 * @param {number[] | string} bytes 
	 * @param {ProgramProperties} properties
	 * @returns {UplcProgram}
	 */
   	static fromCbor(bytes, properties = {purpose: null, callsTxTimeRange: false}) {
		return UplcProgram.fromCborWithMapping(bytes, [], properties);
	}

	/**
	 * @param {number[]} bytes 
	 * @param {ProgramProperties} properties
	 * @returns {UplcProgram}
	 */
	static fromFlat(bytes, properties = {purpose: null, callsTxTimeRange: false}) {
		return UplcProgram.fromFlatWithMapping(bytes, [], properties);
	}

	/**
	 * @internal
	 * @param {number[]} bytes 
	 * @param {ProgramProperties} properties
	 * @param {Source[]} files
	 * @returns {UplcProgram}
	 */
	static fromFlatWithMapping(bytes, files, properties = {purpose: null, callsTxTimeRange: false}) {
		const reader = new UplcDeserializer(bytes, files);

		const version = [
			reader.readInteger(),
			reader.readInteger(),
			reader.readInteger(),
		];

		const versionKey = version.map(v => v.toString()).join(".");

		const expr = reader.readTerm();

		reader.finalize();

		// check version here, so any other errors in the deserialization are thrown first (which means the input is garbage)
		if (versionKey != UPLC_VERSION) {
			console.error(`Warning: Plutus-core script doesn't match version of Helios (expected ${UPLC_VERSION}, got ${versionKey})`);
		}

		return new UplcProgram(expr, properties, version);
	}

	/**
	 * Intended for transfer only
	 * @param {any} expr 
	 * @param {ProgramProperties} properties 
	 * @param {any[]} version 
	 * @returns {UplcProgram}
	 */
	static transferUplcProgram(expr, properties, version) {
		if (!(expr instanceof UplcTerm)) {
			throw new Error("program expr not transferred correctly");
		} else if (!version.every(v => v instanceof UplcInt)) {
			throw new Error("program version ints not transferred correctly");
		} else {
			return new UplcProgram(expr, properties, version);
		}
	}

	/**
	 * @type {TransferUplcAst}
	 */
	static get transferUplcAst() {
		return {
			transferByteArrayData: (bytes) => new ByteArrayData(bytes),
			transferConstrData:    (index, fields) => new ConstrData(index, fields),
			transferIntData:       (value) => new IntData(value),
			transferListData:      (items) => new ListData(items),
			transferMapData:       (pairs) => new MapData(pairs),
			transferSite:          (src, startPos, endPos, codeMapSite = null) => new Site(src, startPos, endPos, codeMapSite),
			transferSource:        (raw, file) => new Source(raw, file?.toString() ?? "<>"), // in older versions of Helios the file arg had type (null | number)
			transferUplcBool:      (site, value) => new UplcBool(site, value),
			transferUplcBuiltin:   (site, name) => new UplcBuiltin(site, name),
			transferUplcByteArray: (site, bytes) => new UplcByteArray(site, bytes),
			transferUplcCall:      (site, a, b) => new UplcCall(site, a, b),
			transferUplcConst:     (value) => new UplcConst(value),
			transferUplcDataValue: (site, data) => new UplcDataValue(site, data),
			transferUplcDelay:     (site, expr) => new UplcDelay(site, expr),
			transferUplcError:     (site, msg) => new UplcError(site, msg),
			transferUplcForce:     (site, expr) => new UplcForce(site, expr),
			transferUplcInt:       (site, value, signed) => new UplcInt(site, value, signed),
			transferUplcLambda:    (site, rhs, name = null) => new UplcLambda(site, rhs, name),
			transferUplcList:      (site, itemType, items) => new UplcList(site, itemType, items),
			transferUplcPair:      (site, first, second) => new UplcPair(site, first, second),
			transferUplcString:    (site, value) => new UplcString(site, value),
			transferUplcType:      (typeBits) => new UplcType(typeBits),
			transferUplcUnit:      (site) => new UplcUnit(site),
			transferUplcVariable:  (site, index) => new UplcVariable(site, index)
		};
	}
}

/**
 * Plutus-core deserializer creates a Plutus-core form an array of bytes
 */
 class UplcDeserializer extends BitReader {
	/**
	 * @readonly
	 * @type {Source[]}
	 */
	#files;

	/**
	 * @param {number[]} bytes 
	 * @param {Source[]} files - for serialized codeMapping
	 */
	constructor(bytes, files = []) {
		super(bytes);

		this.#files = files;
	}

	/**
	 * @param {string} category 
	 * @returns {number}
	 */
	tagWidth(category) {
		assert(category in UPLC_TAG_WIDTHS, `unknown tag category ${category.toString()}`);

		return UPLC_TAG_WIDTHS[category];
	}

	/**
	 * Returns the name of a known builtin
	 * Returns the integer id if id is out of range (thus if the builtin is unknown)
	 * @param {number} id
	 * @returns {string | number}
	 */
	builtinName(id) {
		let all = UPLC_BUILTINS;

		if (id >= 0 && id < all.length) {
			return all[id].name;
		} else if (id >= UPLC_MACROS_OFFSET && id < UPLC_MACROS_OFFSET + UPLC_MACROS.length) {
			return `macro__${assertDefined(UPLC_MACROS[id - UPLC_MACROS_OFFSET])}`;
		} else {
			console.error(`Warning: builtin id ${id.toString()} out of range`);

			return id;
		}
	}

	/**
	 * Reads a Plutus-core list with a specified size per element
	 * Calls itself recursively until the end of the list is reached
	 * @param {number} elemSize 
	 * @returns {number[]}
	 */
	readLinkedList(elemSize) {
		// Cons and Nil constructors come from Lisp/Haskell
		//  cons 'a' creates a linked list node,
		//  nil      creates an empty linked list
		let nilOrCons = this.readBits(1);

		if (nilOrCons == 0) {
			return [];
		} else {
			return [this.readBits(elemSize)].concat(this.readLinkedList(elemSize));
		}
	}

	/**
	 * Reads a single UplcTerm
	 * @returns {UplcTerm}
	 */
	readTerm() {
		let tag = this.readBits(this.tagWidth("term"));

		switch (tag) {
			case 0:
				return this.readVariable();
			case 1:
				return this.readDelay();
			case 2:
				return this.readLambda();
			case 3:
				return this.readCall(); // aka function application
			case 4:
				return this.readConstant();
			case 5:
				return this.readForce();
			case 6:
				return new UplcError(Site.dummy());
			case 7:
				return this.readBuiltin();
			case 11:
				return this.readCallWithSite();
			case 13:
				return this.readForceWithSite();
			default:
				throw new Error("term tag " + tag.toString() + " unhandled");
		}
	}

	/**
	 * Reads a single unbounded integer
	 * @param {boolean} signed 
	 * @returns {UplcInt}
	 */
	readInteger(signed = false) {
		let bytes = [];

		let b = this.readByte();
		bytes.push(b);

		while (!UplcInt.rawByteIsLast(b)) {
			b = this.readByte();
			bytes.push(b);
		}

		// strip the leading bit
		let res = new UplcInt(Site.dummy(), UplcInt.bytesToBigInt(bytes.map(b => UplcInt.parseRawByte(b))), false); // raw int is unsigned

		if (signed) {
			res = res.toSigned(); // unzigzag is performed here
		}

		return res;
	}

	/**
	 * Reads bytearray or string characters
	 * @returns {number[]}
	 */
	readBytes() {
		this.moveToByteBoundary(true);

		let bytes = [];

		let nChunk = this.readByte();

		while (nChunk > 0) {
			for (let i = 0; i < nChunk; i++) {
				bytes.push(this.readByte());
			}

			nChunk = this.readByte();
		}

		return bytes;
	}

	/**
	 * Reads a literal bytearray
	 * @returns {UplcByteArray}
	 */
	readByteArray() {
		let bytes = this.readBytes();

		return new UplcByteArray(Site.dummy(), bytes);
	}

	/**
	 * Reads a literal string
	 * @returns {UplcString}
	 */
	readString() {
		let bytes = this.readBytes();

		let s = bytesToText(bytes);

		return new UplcString(Site.dummy(), s);
	}

	/**
	 * @param {() => UplcValue} typedReader 
	 * @returns {UplcValue[]}
	 */
	readList(typedReader) {
		/** @type {UplcValue[]} */
		let items = [];

		while (this.readBits(1) == 1) {
			items.push(typedReader());
		}

		return items;
	}

	/**
	 * Reads a data object
	 * @returns {UplcData}
	 */
	readData() {
		let bytes = this.readBytes();

		return UplcData.fromCbor(bytes);
	}

	/**
	 * Reads a variable term
	 * @returns {UplcVariable}
	 */
	readVariable() {
		let index = this.readInteger()

		return new UplcVariable(Site.dummy(), index);
	}

	/**
	 * Reads a lambda expression term
	 * @returns {UplcLambda}
	 */
	readLambda() {
		let rhs = this.readTerm();

		return new UplcLambda(Site.dummy(), rhs);
	}

	/**
	 * Reads a function application term
	 * @returns {UplcCall}
	 */
	readCall() {
		let a = this.readTerm();
		let b = this.readTerm();

		return new UplcCall(Site.dummy(), a, b);
	}

	/**
	 * Reads a function application term with a callSite (needed for bundler)
	 * @internal
	 * @returns {UplcCall}
	 */
	readCallWithSite() {
		let [fileIndex, pos] = [
			Number(this.readInteger().value),
			Number(this.readInteger().value)
		];

		const src = assertDefined(this.#files[fileIndex], "serialized UplcProgram contains codeMapping symbols, requires list of original sources");

		let site = new Site(src, pos);
		// also add self as codeMapSite
		site = new Site(src, pos, undefined, site);

		let a = this.readTerm();
		let b = this.readTerm();

		return new UplcCall(site, a, b);
	}

	/**
	 * Reads a single constant
	 * @returns {UplcConst}
	 */
	readConstant() {
		let typeList = this.readLinkedList(this.tagWidth("constType"));

		let res = new UplcConst(this.readTypedValue(typeList));

		return res;
	}

	/**
	 * Reads a single constant
	 * @param {number[]} typeList 
	 * @returns {UplcValue}
	 */
	readTypedValue(typeList) {
		const typedReader = this.constructTypedReader(typeList);

		assertEq(typeList.length, 0, "Did not consume all type parameters");

		return typedReader();
	}

	/**
	 * Constructs a reader for a single construct recursively
	 * @param {number[]} typeList 
	 * NOTE: the implicit assumption is that this functions modifies the typeList
	 * by removing all elements that it "consumed" to define a type
	 * @returns {() => UplcValue}
	 */
	constructTypedReader(typeList){
		const type = assertDefined(typeList.shift());

		switch (type) {
			case 0: // signed Integer
				return () => this.readInteger(true);
			case 1: // bytearray
				return () => this.readByteArray();
			case 2: // utf8-string
				return () => this.readString();
			case 3:
				return () => new UplcUnit(Site.dummy()); // no reading needed
			case 4: // Bool
				return () => new UplcBool(Site.dummy(), this.readBits(1) == 1);
			case 5:
			case 6:
				throw new Error("unexpected type tag without type application");
			case 7:
				let containerType = assertDefined(typeList.shift());
				if (containerType == 5) {
					// typeList is consumed by the construct call, so make sure to read it before!
					const listType = UplcType.fromNumbers(typeList);
					const typeReader = this.constructTypedReader(typeList);

					return () => new UplcList(Site.dummy(), listType, this.readList(typeReader));
				} else {
					assertEq(containerType, 7, "Unexpected type tag");
					containerType = assertDefined(typeList.shift());
					if (containerType == 6) {
						// typeList is consumed by the construct call, so make sure to read it in correct order!
						const leftReader = this.constructTypedReader(typeList);
						const rightReader = this.constructTypedReader(typeList);
						return () => new UplcPair(Site.dummy(), leftReader(), rightReader())
					}
				}
			case 8:
				return () => new UplcDataValue(Site.dummy(), this.readData());
			default:
				throw new Error(`unhandled constant type ${type.toString()}`);
		}
	}

	/**
	 * Reads a delay term
	 * @returns {UplcDelay}
	 */
	readDelay() {
		let expr = this.readTerm();

		return new UplcDelay(Site.dummy(), expr);
	}

	/**
	 * Reads a force term
	 * @returns {UplcForce}
	 */
	readForce() {
		let expr = this.readTerm();

		return new UplcForce(Site.dummy(), expr);
	}

	/**
	 * @internal
	 * @returns {UplcForce}
	 */
	readForceWithSite() {
		let [fileIndex, pos] = [
			Number(this.readInteger().value),
			Number(this.readInteger().value)
		];
		
		const src = assertDefined(this.#files[fileIndex], "serialized UplcProgram contains codeMapping symbols, requires list of original sources");

		let site = new Site(src, pos);
		// also add self as codeMapSite
		site = new Site(src, pos, undefined, site);

		let expr = this.readTerm();

		return new UplcForce(site, expr);
	}

	/**
	 * Reads a builtin function ref term
	 * @returns {UplcBuiltin}
	 */
	readBuiltin() {
		let id = this.readBits(this.tagWidth("builtin"));

		let name = this.builtinName(id);

		return new UplcBuiltin(Site.dummy(), name);
	}

	/**
	 * Move to the next byteboundary
	 * (and check that we are at the end)
	 */
	finalize() {
		this.moveToByteBoundary(true);

		assert(this.eof(), "not at end");
	}
}

/**
 * Deserializes a flat encoded `UplcProgram`.
 * @param {number[]} bytes 
 * @param {ProgramProperties} properties
 * @returns {UplcProgram}
 */
export function deserializeUplcBytes(bytes, properties = {purpose: null, callsTxTimeRange: false}) {
	return UplcProgram.fromFlat(bytes, properties);
}

/**
 * Parses a plutus core program. Returns a `UplcProgram` instance.
 * @param {string | {cborHex: string}} json a raw JSON string or a parsed JSON object
 * @returns {UplcProgram}
 */
export function deserializeUplc(json) {
	const obj = typeof json == "string" ? JSON.parse(json) : json;

	if (!("cborHex" in obj)) {
		throw UserError.syntaxError(new Source(typeof json == "string" ? json : JSON.stringify(json, undefined, 4), "<json>"), 0, 1, "cborHex field not in json")
	}

	let cborHex = obj.cborHex;
	if (typeof cborHex !== "string") {
		const raw = typeof json == "string" ? json : JSON.stringify(json, undefined, 4);
		const src = new Source(raw, "<json>");
		const re = /cborHex/;
		const cborHexMatch = raw.match(re);
		if (cborHexMatch === null) {
			throw UserError.syntaxError(src, 0, 1, "'cborHex' key not found");
		} else {
			const pos = raw.search(re)
			throw UserError.syntaxError(src, pos, pos+1, "cborHex not a string");
		}
	}

	return UplcProgram.fromCbor(hexToBytes(cborHex));
}



///////////////////////////
// Section 12: Tokenization
///////////////////////////

/**
 * @internal
 */
export class Tokenizer {
	#src;
	#pos;

	/**
	 * Tokens are accumulated in '#ts'
	 * @type {Token[]} 
	 */
	#ts;
	#codeMap;
	#codeMapPos;

	#irMode;

	/**
	 * @param {Source} src 
	 * @param {?CodeMap} codeMap 
	 * @param {boolean} irMode - if true '@' is treated as a regular character
	 */
	constructor(src, codeMap = null, irMode = false) {
		assert(src instanceof Source);

		this.#src = src;
		this.#pos = 0;
		this.#ts = []; // reset to empty to list at start of tokenize()
		this.#codeMap = codeMap; // can be a list of pairs [pos, site in another source]
		this.#codeMapPos = 0; // not used if codeMap === null
		this.#irMode = irMode;
	}

	incrPos() {
		this.#pos += 1;
	}

	decrPos() {
		this.#pos -= 1;
		assert(this.#pos >= 0);
	}

	get currentSite() {
		return new Site(this.#src, this.#pos);
	}

	/**
	 * @param {Token} t 
	 */
	pushToken(t) {
		this.#ts.push(t);

		if (this.#codeMap !== null && this.#codeMapPos < this.#codeMap.length) {
			let pair = (this.#codeMap[this.#codeMapPos]);

			if (pair[0] == t.site.startPos) {
				t.site.setCodeMapSite(pair[1]);
				this.#codeMapPos += 1;
			}
		}
	}

	/**
	 * Reads a single char from the source and advances #pos by one
	 * @returns {string}
	 */
	readChar() {
		assert(this.#pos >= 0);

		let c;
		if (this.#pos < this.#src.length) {
			c = this.#src.getChar(this.#pos);
		} else {
			c = '\0';
		}

		this.incrPos();

		return c;
	}

	/**
	 * @returns {string}
	 */
	peekChar() {
		assert(this.#pos >= 0);

		if (this.#pos < this.#src.length) {
			return this.#src.getChar(this.#pos);
		} else {
			return '\0';
		}
	}

	/**
	 * Decreases #pos by one
	 */
	unreadChar() {
		this.decrPos();
	}

	/**
	 * Start reading precisely one token
	 * @param {Site} site 
	 * @param {string} c 
	 */
	readToken(site, c) {
		if (c == 'b') {
			this.readMaybeUtf8ByteArray(site);
		} else if (c == '_' || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (this.#irMode && (c == '@' || c == '[' || c == ']'))) {
			this.readWord(site, c);
		} else if (c == '/') {
			this.readMaybeComment(site);
		} else if (c == '0') {
			this.readSpecialInteger(site);
		} else if (c >= '1' && c <= '9') {
			this.readDecimal(site, c);
		} else if (c == '#') {
			this.readByteArray(site);
		} else if (c == '"') {
			this.readString(site);
		} else if (c == '?' || c == '!' || c == '%' || c == '&' || (c >= '(' && c <= '.') || (c >= ':' && c <= '>') || c == '[' || c == ']' || (c >= '{' && c <= '}')) {
			this.readSymbol(site, c);
		} else if (!(c == ' ' || c == '\n' || c == '\t' || c == '\r')) {
			site.syntaxError(`invalid source character '${c}' (utf-8 not yet supported outside string literals)`);
		}
	}

	/**
	 * Tokenize the complete source.
	 * Nests groups before returning a list of tokens
	 * @returns {Token[] | null}
	 */
	tokenize() {
		// reset #ts
		this.#ts = [];

		let site = this.currentSite;
		let c = this.readChar();

		while (c != '\0') {
			this.readToken(site, c);

			site = this.currentSite;
			c = this.readChar();
		}

		return this.nestGroups(this.#ts);
	}

	/** 
	 * Returns a generator
	 * Use gen.next().value to access to the next Token
	 * Doesn't perform any grouping
	 * Used for quickly parsing the ScriptPurpose header of a script
	 * @returns {Generator<Token>}
	 */
	*streamTokens() {
		this.#ts = [];

		let site = this.currentSite;
		let c = this.readChar();

		while (c != '\0') {
			this.readToken(site, c);

			let t = this.#ts.shift();
			while (t != undefined) {
				yield t;
				t = this.#ts.shift();
			}

			site = this.currentSite;
			c = this.readChar();
		}

		assert(this.#ts.length == 0);
	}

	/**
	 * Reads one word token.
	 * Immediately turns "true" or "false" into a BoolLiteral instead of keeping it as Word
	 * @param {Site} site
	 * @param {string} c0 - first character 
	 */
	readWord(site, c0) {
		let chars = [];

		let c = c0;
		while (c != '\0') {
			if (c == '_' || (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (this.#irMode && (c == '@' || c == '[' || c == ']'))) {
				chars.push(c);
				c = this.readChar();
			} else {
				this.unreadChar();
				break;
			}
		}

		let value = chars.join('');

		if (value == "true" || value == "false") {
			this.pushToken(
				new BoolLiteral(
					new Site(site.src, site.startPos, this.currentSite.startPos),
					value == "true"
				)
			);
		} else {
			this.pushToken(
				new Word(
					new Site(site.src, site.startPos, this.currentSite.startPos),
					value
				)
			);
		}
	}

	/**
	 * Reads and discards a comment if current '/' char is followed by '/' or '*'.
	 * Otherwise pushes Symbol('/') onto #ts
	 * @param {Site} site 
	 */
	// comments are discarded
	readMaybeComment(site) {
		let c = this.readChar();

		if (c == '\0') {
			this.pushToken(new SymbolToken(site, '/'));
		} else if (c == '/') {
			this.readSingleLineComment();
		} else if (c == '*') {
			this.readMultiLineComment(site);
		} else {
			this.pushToken(new SymbolToken(site, '/'));
			this.unreadChar();
		}
	}

	/**
	 * Reads and discards a single line comment (from '//' to end-of-line)
	 */
	readSingleLineComment() {
		let c = this.readChar();

		while (c != '\n' && c != '\0') {
			c = this.readChar();
		}
	}

	/**
	 * Reads and discards a multi-line comment (from '/' '*' to '*' '/')
	 * @param {Site} site 
	 */
	readMultiLineComment(site) {
		let prev = '';
		let c = this.readChar();

		while (true) {
			prev = c;
			c = this.readChar();

			if (c == '/' && prev == '*') {
				break;
			} else if (c == '\0') {
				const errorSite = new Site(site.src, site.startPos, this.currentSite.startPos);
				errorSite.syntaxError("unterminated multiline comment");
				return;
			}
		}
	}

	/**
	 * REads a literal integer
	 * @param {Site} site 
	 */
	readSpecialInteger(site) {
		let c = this.readChar();

		if (c == '\0') {
			this.pushToken(new IntLiteral(site, 0n));
		} else if (c == 'b') {
			this.readBinaryInteger(site);
		} else if (c == 'o') {
			this.readOctalInteger(site);
		} else if (c == 'x') {
			this.readHexInteger(site);
		} else if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) {
			site.syntaxError(`bad literal integer type 0${c}`);
		} else if (c >= '0' && c <= '9') {
			site.syntaxError("unexpected leading 0");
		} else if (c == '.') {
			this.readFixedPoint(site, ['0']);
		} else {
			this.pushToken(new IntLiteral(site, 0n));
			this.unreadChar();
		}
	}

	/**
	 * @param {Site} site 
	 */
	readBinaryInteger(site) {
		this.readRadixInteger(site, "0b", c => (c == '0' || c == '1'));
	}

	/**
	 * @param {Site} site 
	 */
	readOctalInteger(site) {
		this.readRadixInteger(site, "0o", c => (c >= '0' && c <= '7'));
	}

	/**
	 * @param {Site} site 
	 */
	readHexInteger(site) {
		this.readRadixInteger(site, "0x",
			c => ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')));
	}

	/**
	 * @param {Site} site 
	 * @param {string[]} chars 
	 * @param {boolean} reverse
	 * @returns {string[]}
	 */
	static assertCorrectDecimalUnderscores(site, chars, reverse = false) {
		if (chars.some(c => c == '_')) {
			for (let i = 0; i < chars.length; i++) {
				const c = reverse ? chars[chars.length - 1 - i] : chars[i];

				if (i == chars.length - 1) {
					if (c == '_') {
						site.syntaxError("redundant decimal underscore");
					}
				}

				if ((i+1)%4 == 0) {
					if (c != '_') {
						site.syntaxError("bad decimal underscore");
					}
				} else {
					if (c == '_') {
						site.syntaxError("bad decimal underscore");
					}
				}
			}

			return chars.filter(c => c != '_');
		} else {
			return chars;
		}
	}

	/**
	 * @param {Site} site 
	 * @param {string} c0 - first character
	 */
	readDecimal(site, c0) {
		/**
		 * @type {string[]}
		 */
		let chars = [];

		let c = c0;
		while (c != '\0') {
			if ((c >= '0' && c <= '9') || c == '_') {
				chars.push(c);
			} else {
				if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) {
					const errorSite = new Site(site.src, site.startPos, this.currentSite.startPos);

					errorSite.syntaxError("invalid syntax for decimal integer literal");
				} else if (c == '.') {
					const cf = this.peekChar();

					if (cf >= '0' && cf <= '9') {
						this.readFixedPoint(site, chars);

						return;
					}
				}

				this.unreadChar();
				break;
			}

			c = this.readChar();
		}

		const intSite = new Site(site.src, site.startPos, this.currentSite.startPos);

		chars = Tokenizer.assertCorrectDecimalUnderscores(intSite, chars, true);

		this.pushToken(
			new IntLiteral(
				intSite,
				BigInt(chars.filter(c => c != '_').join(''))
			)
		);
	}

	/**
	 * @param {Site} site 
	 * @param {string} prefix 
	 * @param {(c: string) => boolean} valid - checks if character is valid as part of the radix
	 */
	readRadixInteger(site, prefix, valid) {
		let c = this.readChar();

		let chars = [];

		if (!(valid(c))) {
			const errorSite = new Site(site.src, site.startPos, this.currentSite.startPos);

			errorSite.syntaxError(`expected at least one char for ${prefix} integer literal`);

			this.unreadChar();
			return;
		}

		while (c != '\0') {
			if (valid(c)) {
				chars.push(c);
			} else {
				if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) {
					const errorSite = new Site(site.src, site.startPos, this.currentSite.startPos);

					errorSite.syntaxError(`invalid syntax for ${prefix} integer literal`);
				}

				this.unreadChar();
				break;
			}

			c = this.readChar();
		}

		this.pushToken(
			new IntLiteral(
				new Site(site.src, site.startPos, this.currentSite.startPos),
				BigInt(prefix + chars.join(''))
			)
		);
	}

	/**
	 * @param {Site} site 
	 * @param {string[]} leading 
	 */
	readFixedPoint(site, leading) {
		/**
		 * @type {string[]}
		 */
		let trailing = [];

		let c = this.readChar();

		while (c != '\0') {
			if ((c >= '0' && c <= '9') || c == '_') {
				trailing.push(c);

			} else {
				this.unreadChar();
				break;
			}

			c = this.readChar();
		}

		const tokenSite = new Site(site.src, site.startPos, this.currentSite.startPos);

		leading = Tokenizer.assertCorrectDecimalUnderscores(tokenSite, leading, true);

		trailing = Tokenizer.assertCorrectDecimalUnderscores(tokenSite, trailing, false);

		if (trailing.length > REAL_PRECISION) {
			tokenSite.syntaxError(`literal real decimal places overflow (max ${REAL_PRECISION} supported, but ${trailing.length} specified)`);

			trailing.splice(REAL_PRECISION);
		} 
		
		while (trailing.length < REAL_PRECISION) {
			trailing.push('0');
		}

		this.pushToken(
			new RealLiteral(
				tokenSite,
				BigInt(leading.concat(trailing).join(''))
			)
		);
	}

	/**
	 * Reads literal hexadecimal representation of ByteArray
	 * @param {Site} site 
	 */
	readByteArray(site) {
		let c = this.readChar();

		let chars = [];

		// case doesn't matter
		while ((c >= 'a' && c <= 'f') || (c >= '0' && c <= '9')) {
			chars.push(c);
			c = this.readChar();
		}

		// empty byteArray is allowed (eg. for Ada mintingPolicyHash)

		// last char is the one that made the while loop break, so should be unread
		this.unreadChar();

		let bytes = hexToBytes(chars.join(''));

		this.pushToken(
			new ByteArrayLiteral(
				new Site(site.src, site.startPos, this.currentSite.startPos),
				bytes
			)
		);
	}

	/**
	 * Reads literal Utf8 string and immediately encodes it as a ByteArray
	 * @param {Site} site 
	 */
	readMaybeUtf8ByteArray(site) {
		let c = this.readChar();

		if (c == '"') {
			const s = this.readStringInternal(site)
		
			this.pushToken(
				new ByteArrayLiteral(
					new Site(site.src, site.startPos, this.currentSite.startPos),
					textToBytes(s)
				)
			);
		} else {
			this.unreadChar();

			this.readWord(site, 'b');
		}
	}

	/**
	 * Doesn't push a token, instead returning the string itself
	 * @internal
	 * @param {Site} site 
	 * @returns {string}
	 */
	readStringInternal(site) {
		let c = this.readChar();

		let chars = [];

		let escaping = false;
		/** @type {?Site} */
		let escapeSite = null; // for escape syntax errors

		while (!(!escaping && c == '"')) {
			if (c == '\0') {
				site.syntaxError("unmatched '\"'");
				break;
			}

			if (escaping) {
				if (c == 'n') {
					chars.push('\n');
				} else if (c == 't') {
					chars.push('\t');
				} else if (c == '\\') {
					chars.push('\\');
				} else if (c == '"') {
					chars.push(c);
				} else if (escapeSite !== null) {
					const errorSite = new Site(escapeSite.src, escapeSite.startPos, this.currentSite.startPos);

					errorSite.syntaxError(`invalid escape sequence ${c}`);
				} else {
					throw new Error("escape site should be non-null");
				}

				escaping = false;
				escapeSite = null;
			} else {
				if (c == '\\') {
					escapeSite = this.currentSite;
					escaping = true;
				} else {
					chars.push(c);
				}
			}

			c = this.readChar();
		}

		return chars.join('');
	}

	/**
	 * Reads literal string delimited by double quotes.
	 * Allows for three escape character: '\\', '\n' and '\t'
	 * @param {Site} site 
	 */
	readString(site) {
		const s = this.readStringInternal(site)
		
		this.pushToken(
			new StringLiteral(
				new Site(site.src, site.startPos, this.currentSite.startPos),
				s
			)
		);
	}

	/**
	 * Reads single or double character symbols
	 * @param {Site} site 
	 * @param {string} c0 - first character
	 */
	readSymbol(site, c0) {
		let chars = [c0];

		/** @type {(second: string) => boolean} */
		let parseSecondChar = (second) => {
			let d = this.readChar();

			if (d == second) {
				chars.push(d);
				return true;
			} else {
				this.unreadChar();
				return false;
			}
		}

		if (c0 == '|') {
			parseSecondChar('|');
		} else if (c0 == '&') {
			parseSecondChar('&');
		} else if (c0 == '=') {
			parseSecondChar('=') || parseSecondChar('>');
		} else if (c0 == '!' || c0 == '<' || c0 == '>') { // could be !=, ==, <= or >=
			parseSecondChar('=');
		} else if (c0 == ':') {
			parseSecondChar(':');
		} else if (c0 == '-') {
			parseSecondChar('>');
		}

		this.pushToken(
			new SymbolToken(
				new Site(site.src, site.startPos, site.endPos),
				chars.join('')
			)
		);
	}

	/**
	 * Separates tokens in fields (separted by commas)
	 * @param {Token[]} ts 
	 * @returns {Group | null}
	 */
	static buildGroup(ts) {
		const open = assertDefined(ts.shift()).assertSymbol();

		if (!open) {
			return null;
		}

		const stack = [open]; // stack of symbols
		let curField = [];
		let fields = [];

		/** @type {null | SymbolToken} */
		let firstComma = null;

		/** @type {null | SymbolToken} */
		let lastComma = null;

		/** @type {null | Site} */
		let endSite = null;

		while (stack.length > 0 && ts.length > 0) {
			const t = assertToken(ts.shift(), open.site);

			if (!t) {
				return null;
			}

			const prev = stack.pop();

			endSite = t.site;

			if (t != undefined && prev != undefined) {
				if (!t.isSymbol(Group.matchSymbol(prev))) {
					stack.push(prev);

					if (Group.isCloseSymbol(t)) {
						t.site.syntaxError(`unmatched '${assertDefined(t.assertSymbol()).value}'`);
						return null;
					} else if (Group.isOpenSymbol(t)) {
						stack.push(assertDefined(t.assertSymbol()));
						curField.push(t);
					} else if (t.isSymbol(",") && stack.length == 1) {
						if (firstComma === null) {
							firstComma = t.assertSymbol();
						}

						lastComma = t.assertSymbol();
						if (curField.length == 0) {
							t.site.syntaxError("empty field");
							return null;
						} else {
							fields.push(curField);
							curField = [];
						}
					} else {
						curField.push(t);
					}
				} else if (stack.length > 0) {
					curField.push(t);
				}
			} else {
				throw new Error("unexpected");
			}
		}

		let last = stack.pop();
		if (last != undefined) {
			last.syntaxError(`EOF while matching '${last.value}'`);
			return null;
		}

		if (curField.length > 0) {
			// add removing field
			fields.push(curField);
		} else if (lastComma !== null) {
			lastComma.syntaxError(`trailing comma`);
			return null;
		}

		let site = open.site;

		if (endSite) {
			site = site.merge(endSite);
		}

		return new Group(site, open.value, fields, firstComma);
	}

	/**
	 * Match group open with group close symbols in order to form groups.
	 * This is recursively applied to nested groups.
	 * @param {Token[]} ts 
	 * @returns {Token[] | null}
	 */
	nestGroups(ts) {
		/**
		 * @type {Token[][]}
		 */
		const stack = [];

		/**
		 * @type {Token[]}
		 */
		let current = [];

		for (let t of ts) {
			if (Group.isOpenSymbol(t)) {
				stack.push(current);

				current = [t];
			} else if (Group.isCloseSymbol(t)) {
				const prev = current[0]?.assertSymbol();

				if (!prev) {
					return null;
				}

				if (!t.isSymbol(Group.matchSymbol(prev))) {
					prev.syntaxError(`unmatched '${prev.value}'`);
					t.syntaxError(`unmatched '${assertDefined(t.assertSymbol()).value}'`);
					return null;
				}

				current.push(t);

				const group = Tokenizer.buildGroup(current);
				if (!group) {
					return null;
				}

				current = assertDefined(stack.pop());

				current.push(group);
			} else {
				current.push(t);
			}
		}

		if (stack.length > 0) {
			const t = stack[stack.length - 1][0];

			if (!t.isSymbol()) {
				if (current.length > 0) {
					const open = current[0];
					
					if (open && open.isSymbol()) {
						open.syntaxError(`unmatched '${open.assertSymbol()?.value}`);
					} else {
						console.log(current)
						throw new Error("unhandled")
					}
				}
			} else {
				t.syntaxError(`unmatched '${t.assertSymbol()?.value}'`);
			}
		}
		
		return current;
	}
}

/**
 * Tokenizes a string (wrapped in Source)
 * Also used by VSCode plugin
 * @internal
 * @param {Source} src 
 * @returns {Token[] | null}
 */
export function tokenize(src) {
	let tokenizer = new Tokenizer(src);

	return tokenizer.tokenize();
}

/**
 * Tokenizes an IR string with a codemap to the original source
 * @internal
 * @param {string} rawSrc 
 * @param {CodeMap} codeMap 
 * @returns {Token[]}
 */
export function tokenizeIR(rawSrc, codeMap = []) {
	let src = new Source(rawSrc, "<ir>");

	// the Tokenizer for Helios can simply be reused for the IR
	let tokenizer = new Tokenizer(src, codeMap, true);

	const ts = tokenizer.tokenize();

	if (src.errors.length > 0) {
		console.log(src.pretty());
		throw src.errors[0];
	} else if (ts === null) {
		throw new Error("should've been thrown above");
	}

	return ts;
}



////////////////////////////////
// Section 13: Eval common types
////////////////////////////////
/**
 * @template {HeliosData} T
 */

/**
 * @internal
 * @typedef {{
 *   type:  string
 * } | {
 *   type:     "List"
 *   itemType: TypeSchema
 * } | {
 *   type:      "Map"
 *   keyType:   TypeSchema
 *   valueType: TypeSchema
 * } | {
 *   type:     "Option"
 *   someType: TypeSchema
 * } | {
 *   type:       "Struct"
 *   fieldTypes: NamedTypeSchema[]
 * } | {
 *   type:         "Enum"
 *   variantTypes: {name: string, fieldTypes: NamedTypeSchema[]}[]
 * }} TypeSchema
 */

/**
 * @internal
 * @typedef {{
 * 	 name: string
 * } & TypeSchema} NamedTypeSchema
 */

/**
 * @internal
 * @typedef {{
 *   name: string
 *   typeClass: TypeClass
 * }} ParameterI
 */

/**
 * @internal
 * @typedef {Map<ParameterI, Type>} InferenceMap
 */

/**
 * Used by the bundle cli command to generate a typescript annotations and (de)serialization code
 * inputTypes form a type union
 * @internal
 * @typedef {{
 *   inputType:    string
 *   outputType:   string
 *   internalType: TypeSchema
 * }} TypeDetails
 */

/**
 * @internal
 * @typedef {{[name: string]: (obj: any) => Promise<UplcData>}} JsToUplcHelpers
 */

/**
 * @internal
 * @typedef {{[name: string]: (data: UplcData) => Promise<any>}} UplcToJsHelpers
 */

/**
 * @internal
 * @typedef {(obj: any, helpers: JsToUplcHelpers) => Promise<UplcData>} JsToUplcConverter
 */

/**
 * @internal
 * @typedef {(data: UplcData, helpers: UplcToJsHelpers) => Promise<any>} UplcToJsConverter
 */

/**
 * @internal
 * @typedef {Named & Type & {
 *   asDataType:   DataType
 *   fieldNames:   string[]
 *   offChainType: (null | HeliosDataClass<HeliosData>)
 *   typeDetails?: TypeDetails
 *   jsToUplc:     JsToUplcConverter
 *   uplcToJs:     UplcToJsConverter
 *   ready:        boolean
 * }} DataType
 */

/**
 * @internal
 * @typedef {DataType & {
 *   asEnumMemberType: EnumMemberType
 *   constrIndex:      number
 *   parentType:       DataType
 * }} EnumMemberType
 */

/**
 * EvalEntities assert themselves
 * @internal
 * @typedef {{
 *   asDataType:       (null | DataType)
 *   asEnumMemberType: (null | EnumMemberType)
 *   asFunc:           (null | Func)
 *   asInstance:       (null | Instance)
 *   asNamed:          (null | Named)
 *   asNamespace:      (null | Namespace)
 *   asParametric:     (null | Parametric)
 * 	 asType:           (null | Type)
 *   asTyped:          (null | Typed)
 *   asTypeClass:      (null | TypeClass)
 *   toString():       string
 * }} EvalEntity
 */

/**
 * @internal
 * @typedef {Typed & {
 *   asFunc: Func
 * 	 funcType: FuncType
 *   call(site: Site, args: Typed[], namedArgs?: {[name: string]: Typed}): (null | Typed)
 * }} Func
 */

/**
 * @internal
 * @typedef {Typed & {
 *   asInstance:      Instance
 *   fieldNames:      string[]
 *   instanceMembers: InstanceMembers
 * }} Instance
 */

/**
 * @internal
 * @typedef {EvalEntity & {
 *   asNamed: Named
 *   name:    string
 *   path:    string
 * }} Named
 */

/**
 * @internal
 * @typedef {EvalEntity & {
 *   asNamespace: Namespace
 *   namespaceMembers: NamespaceMembers
 * }} Namespace
 */

/**
 * @internal
 * @typedef {EvalEntity & {
 *   asParametric: Parametric
 *   offChainType: (null | ((...any) => HeliosDataClass<HeliosData>))
 *   typeClasses: TypeClass[]
 *   apply(types: Type[], site?: Site): EvalEntity
 *   inferCall(site: Site, args: Typed[], namedArgs?: {[name: string]: Typed}, paramTypes?: Type[]): Func
 * 	 infer(site: Site, map: InferenceMap): Parametric
 * }} Parametric
 */

/**
 * @internal
 * @typedef {EvalEntity & {
 *   asType:               Type
 *   instanceMembers:      InstanceMembers
 *   typeMembers:          TypeMembers
 *   isBaseOf(type: Type): boolean
 *   infer(site: Site, map: InferenceMap, type: null | Type): Type
 *   toTyped():            Typed
 *   isParametric():       boolean
 * }} Type
 */

/**
 * @internal
 * @typedef {EvalEntity & {
 *   asTyped: Typed
 *   type: Type
 * }} Typed
 */

/**
 * @internal
 * @typedef {EvalEntity & {
 *   asTypeClass:                        TypeClass
 *   genInstanceMembers(impl: Type):     TypeClassMembers
 *   genTypeMembers(impl: Type):         TypeClassMembers
 *   isImplementedBy(type: Type):        boolean
 *   toType(name: string, path: string, parameter?: null | ParameterI): Type
 * }} TypeClass
 */

/**
 * @internal
 * @typedef {{[name: string]: (Parametric | Type)}} InstanceMembers
 */

/**
 * @internal
 * @typedef {{[name: string]: EvalEntity}} NamespaceMembers
 */

/**
 * @internal
 * @typedef {{[name: string]: (Parametric | Type | Typed)}} TypeMembers
 */

/**
 * @internal
 * @typedef {{[name: string]: Type}} TypeClassMembers
 */

/**
 * @internal
 * @param {Parametric} parametric
 * @param {Type[]} types
 * @returns {DataType}
 */
export function applyTypes(parametric, ...types) {
    return assertDefined(parametric.apply(types).asDataType);
}

/**
 * @internal
 */
export class Common {
	constructor() {
	}

	/**
	 * @returns {boolean}
	 */
	isParametric() {
		return false;
	}

    /**
     * @param {Typed} i 
     * @param {Type} t 
     * @returns {boolean}
     */
    static instanceOf(i, t) {
        return t.isBaseOf(i.type);
    }

	/**
	 * Compares two types. Throws an error if neither is a Type.
	 * @example
	 * Common.typesEq(IntType, IntType) == true
	 * @param {Type} a 
	 * @param {Type} b 
	 * @returns {boolean}
	 */
	static typesEq(a, b) {
		return a.isBaseOf(b) && b.isBaseOf(a);
	}

	/**
	 * @param {Type} type 
	 */
	static isEnum(type) {
		return Object.values(type.typeMembers).some(v => v.asEnumMemberType);
	}

	/**
	 * @param {Type} type 
	 */
	static countEnumMembers(type) {
		return Object.values(type.typeMembers).reduce((prev, v) => v.asEnumMemberType ? prev + 1 : prev, 0);
	}
  
    /**
     * @param {TypeClass} tc 
     * @returns {string[]}
     */
    static typeClassMembers(tc) {
        const dummy = tc.toType("", "");

        const typeMemberNames = Object.keys(tc.genTypeMembers(dummy)).sort();
        const instanceMemberNames = Object.keys(tc.genInstanceMembers(dummy)).sort();

        return typeMemberNames.concat(instanceMemberNames);
    }

    /**
     * @param {Type} type 
     * @param {TypeClass} tc 
     * @returns {boolean}
     */
    static typeImplements(type, tc) {
		if (type instanceof AllType || type.asDataType?.ready === false) {
			return true;
		}

        const typeMembers = tc.genTypeMembers(type);

        for (let k in typeMembers) {
            const check = type.typeMembers[k]?.asType;

            if ((check && !typeMembers[k].asType?.isBaseOf(check)) || !check) {
                return false;
            } 
        }

        const instanceMembers = tc.genInstanceMembers(type);

        for (let k in instanceMembers) {
            const check = type.instanceMembers[k]?.asType;

            if ((check && !instanceMembers[k].asType?.isBaseOf(check)) || !check) {
                return false;
            }
        }

        return true;
    }

    /**
     * @type {null | DataType}
     */
    get asDataType() {
        return null;
    }

    /**
     * @type {null | EnumMemberType}
     */
    get asEnumMemberType() {
        return null;
    }

    /**
     * @type {null | Func}
     */
    get asFunc() {
        return null;
    }

    /**
	 * @type {null | Instance}
	 */
	get asInstance() {
		return null;
	}

    /**
     * @type {null | Named}
     */
    get asNamed() {
        return null;
    }

    /**
	 * @type {null | Namespace}
	 */
	get asNamespace() {
		return null;
	}

    /**
     * @type {null | Parametric}
     */
    get asParametric() {
        return null;
    }

	/**
	 * @type {null | Type}
	 */
	get asType() {
		return null;
	}

    /**
     * @type {null | Typed}
     */
    get asTyped() {
        return this.asInstance ?? this.asFunc;
    }

	/**
	 * @type {null | TypeClass}
	 */
	get asTypeClass() {
		return null;
	}

	/**
	 * @type {boolean}
	 */
	get ready() {
		return true;
	}

	/**
	 * @param {any} obj
	 * @param {JsToUplcHelpers} helpers
	 * @returns {Promise<UplcData>}
	 */
	jsToUplc(obj, helpers) {
		throw new Error("not yet implemented");
	}

	/**
	 * @param {UplcData} data
	 * @param {UplcToJsHelpers} helpers
	 * @returns {Promise<any>}
	 */
	uplcToJs(data, helpers) {
		throw new Error("not yet implemented");
	}

    /**
     * @returns {string}
     */
    toString() {
        throw new Error("not yet implemented");
    }
}

/**
 * Used to represent all possible types whenever a TypeExpr throws an error (so type evaluation can continue in order to collect all type errors at once)
 * @internal
 * @implements {DataType}
 */
export class AllType extends Common {
	constructor() {
		super();
	}

	/**
	 * @type {DataType}
	 */
	get asDataType() {
		return this;
	}

	/**
	 * @type {Named}
	 */
	get asNamed() {
		return this;
	}

	/**
     * @type {Type}
     */
	get asType() {
        return this;
    }

	/**
	 * @type {HeliosDataClass<HeliosData> | null}
	 */
	get offChainType() {
		return null;
	}

	/**
	 * @type {string[]}
	 */
	get fieldNames() {
		return [];
	}

	/**
	 * @type {InstanceMembers}
	 */
	get instanceMembers() {
		return {};
	}

	/**
	 * @type {string}
	 */
	get name() {
		return "";
	}

	/**
	 * @type {string}
	 */
	get path() {
		return "";
	}

	/**
	 * @type {TypeMembers}
	 */
	get typeMembers() {
		return {}
	}

	/**
     * @param {Site} site 
     * @param {InferenceMap} map 
     * @param {null | Type} type 
     * @returns {Type}
     */
	infer(site, map, type) {
        return this;
    }

	/**
	 * @param {Type} other 
	 * @returns {boolean}
	 */
	isBaseOf(other) {
		return true;
	}

	/**
	 * @returns {Typed}
	 */
	toTyped() {
		throw new Error("can't be turned into a type");
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return "All";
	}
}

/**
 * @internal
 * @implements {DataType}
 */
export class AnyType extends Common {
	constructor() {
		super();
	}

	get fieldNames() {
		return []
	}

	get offChainType() {
		return null;
	}

	/**
	 * @type {string}
	 */
	get name() {
		return "Any";
	}

	/**
	 * @type {string}
	 */
	get path() {
		return "";
	}

	/**
     * @type {Type}
     */
	get asType() {
        return this;
    }

	/**
	 * @type {Named}
	 */
	get asNamed() {
		return this;
	}

	/**
	 * @type {DataType}
	 */
	get asDataType() {
		return this;
	}

	/**
	 * @type {InstanceMembers}
	 */
	get instanceMembers() {
		return {};
	}
	
	/**
	 * @type {TypeMembers}
	 */
	get typeMembers() {
		return {}
	}

	/**
     * @param {Site} site 
     * @param {InferenceMap} map 
     * @param {null | Type} type 
     * @returns {Type}
     */
	infer(site, map, type) {
        return this;
    }

	/**
	 * @param {Type} other 
	 * @returns {boolean}
	 */
	isBaseOf(other) {
		return true;
	}

	/**
	 * @returns {Typed}
	 */
	toTyped() {
		throw new Error("can't be turned into a type");
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return "Any";
	}
}

/**
 * Type of special case of no-return value where execution can't continue.
 * @internal
 * @implements {Type}
 */
export class ErrorType extends Common {
	constructor() {
        super();
	}

    /**
     * @type {InstanceMembers}
     */
    get instanceMembers() {
        return {};
    }

    /**
     * @type {TypeMembers}
     */
    get typeMembers() {
        return {};
    }

    /**
     * @type {Type}
     */
    get asType() {
        return this;
    }

    /**
     * @param {Site} site 
     * @param {InferenceMap} map 
     * @param {null | Type} type 
     * @returns {Type}
     */
    infer(site, map, type) {
        return this;
    }

	/**
	 * @param {Type} type 
	 * @returns {boolean}
	 */
	isBaseOf(type) {
		return type instanceof ErrorType;
	}

    /**
     * @returns {string}
     */
    toString() {
        return "()";
    }

	/**
	 * @returns {Typed}
	 */
	toTyped() {
		return new ErrorEntity();
	}
}

/**
 * @internal
 */
export class ArgType {
	#name;
	#type;
	#optional;

	/**
	 * 
	 * @param {null | Word} name 
	 * @param {Type} type 
	 * @param {boolean} optional 
	 */
	constructor(name, type, optional = false) {
		this.#name = name;
		this.#type = assertDefined(type);
		this.#optional = optional;
	}

	/**
	 * @type {string}
	 */
	get name() {
		if (this.#name === null) {
			return "";
		} else {
			return this.#name.toString();
		}
	}

	/**
	 * @type {Type}
	 */
	get type() {
		return this.#type;
	}

    /**
	 * @internal
	 * @param {Site} site 
	 * @param {InferenceMap} map 
	 * @param {null | Type} type 
	 * @returns {ArgType}
	 */
	infer(site, map, type) {
		return new ArgType(
			this.#name,
			this.#type.infer(site, map, type),
			this.#optional
		);
	}

    /**
	 * @param {ArgType} other 
	 * @returns {boolean}
	 */
	isBaseOf(other) {
		// if this arg has a default value, the other arg must also have a default value
		if (this.#optional && !other.#optional) {
			return false;
		}

		// if this is named, the other must be named as well
		if (this.#name != null) {
			return this.#name.toString() == (other.#name?.toString() ?? "");
		}

		if (!other.#type.isBaseOf(this.#type)) { // note the reversal of the check
			return false;
		}

		return true;
	}

	/**
	 * @returns {boolean}
	 */
	isNamed() {
		return this.#name !== null;
	}

	/**
	 * @returns {boolean}
	 */
	isOptional() {
		return this.#optional;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return [
			this.#name != null ? `${this.#name.toString()}: ` : "",
			this.#optional ? "?" : "",
			this.#type.toString()
		].join("");
	}
}

/**
 * Function type with arg types and a return type
 * @internal
 * @implements {Type}
 */
export class FuncType extends Common {
	/**
	 * @type {ArgType[]}
	 */
	#argTypes;

	/**
	 * @type {Type}
	 */
	#retType;

	/**
	 * @param {Type[] | ArgType[]} argTypes 
	 * @param {Type} retType 
	 */
	constructor(argTypes, retType) {
        super();

		this.#argTypes = argTypes.map(at => (at instanceof ArgType) ? at : new ArgType(null, at));

		this.#retType = retType;
	}

    /**
	 * @type {Type[]}
	 */
	get argTypes() {
		return this.#argTypes.slice().map(at => at.type);
	}

	/**
	 * @type {InstanceMembers}
	 */
	get instanceMembers() {
		return {};
	}

    /**
	 * @type {number}
	 */
	get nArgs() {
		return this.#argTypes.length;
	}

    /**
	 * @type {number}
	 */
	get nNonOptArgs() {
		return this.#argTypes.filter(at => !at.isOptional()).length;
	}

    /**
	 * @type {number}
	 */
	get nOptArgs() {
		return this.#argTypes.filter(at => at.isOptional()).length;
	}

    /**
	 * @type {Type}
	 */
	get retType() {
		return this.#retType;
	}

    /**
	 * @type {TypeMembers}
	 */
	get typeMembers() {
		return {};
	}

	/**
	 * @type {Type}
	 */
	get asType() {
		return this;
    }

	/**
	 * Expand tuples in posArgs, if that matches argTypes better
	 * @param {Typed[]} posArgs 
	 * @returns {Typed[]}
	 */
	expandTuplesInPosArgs(posArgs) {
		posArgs = posArgs.slice();
		let arg = posArgs.shift();

		/**
		 * @type {Typed[]}
		 */
		let result = [];
		
		let i = 0;
		
		while (arg) {
			if (i < this.#argTypes.length && Common.instanceOf(arg, this.#argTypes[i].type)) {
				result.push(arg);
				i++;
			} else {
				const tupleItemTypes = getTupleItemTypes(arg.type);

				if (tupleItemTypes && tupleItemTypes.every((tit, j) => (i+j < this.#argTypes.length) && Common.instanceOf(tit.toTyped(), this.#argTypes[i+j].type))) {
					result = result.concat(tupleItemTypes.map(tit => tit.toTyped()));
					i += tupleItemTypes.length;
				} else {
					// mismatched type, but don't throw error here because better error will be thrown later
					result.push(arg);
					i++;
				}
			}

			arg = posArgs.shift();
		}

		return result;
	}
	
	/**
	 * Checks if arg types are valid.
	 * Throws errors if not valid. Returns the return type if valid. 
	 * @param {Site} site 
	 * @param {Typed[]} posArgs
	 * @param {{[name: string]: Typed}} namedArgs
	 * @returns {null | Type}
	 */
	checkCall(site, posArgs, namedArgs = {}) {
		posArgs = this.expandTuplesInPosArgs(posArgs); 

		if (posArgs.length < this.nNonOptArgs) {
			// check if each nonOptArg is covered by the named args
			for (let i = 0; i < this.nNonOptArgs; i++) {
				if (!this.#argTypes[i].isNamed()) {
					site.typeError(`expected at least ${this.#argTypes.filter(at => !at.isNamed()).length} positional arg(s), got ${posArgs.length} positional arg(s)`);
					return null;
				} else if (!(this.#argTypes[i].name in namedArgs)) {
					site.typeError(`expected at least ${this.nNonOptArgs} arg(s), missing '${this.#argTypes[i].name}'`);
					return null;
				}
			}

		} else if (posArgs.length > this.#argTypes.length) {
			site.typeError(`expected at most ${this.#argTypes.length} arg(s), got ${posArgs.length} arg(s)`);
			return null;
		}

		for (let i = 0; i < posArgs.length; i++) {
			if (!Common.instanceOf(posArgs[i], this.#argTypes[i].type)) {
				site.typeError(`expected '${this.#argTypes[i].type.toString()}' for arg ${i + 1}, got '${posArgs[i].type.toString()}'`);
				return null;
			}
		}

		for (let key in namedArgs) {
			const i = this.#argTypes.findIndex(at => at.name == key);

			if (i == -1) {
				site.typeError(`arg named ${key} not found in function type ${this.toString()}`);
				continue;
			}

			if (i < posArgs.length) {
				site.typeError(`named arg '${key}' already covered by positional arg ${i+1}`);
				continue;
			}

			const thisArg = this.#argTypes[i];

			if (!Common.instanceOf(namedArgs[key], thisArg.type)) {
				site.typeError(`expected '${thisArg.type.toString()}' for arg '${key}', got '${namedArgs[key].toString()}`);
				continue;
			}
		}

		return this.#retType;
	}

    /**
	 * @internal
	 * @param {Site} site
	 * @param {InferenceMap} map 
	 * @param {null | Type} type 
	 * @returns {Type}
	 */
	infer(site, map, type) {
		if (!type) {
			return new FuncType(
				this.#argTypes.map(at => at.infer(site, map, null)),
				this.#retType.infer(site, map, null)
			);
		} else if (type instanceof FuncType) {
			if (type.argTypes.length == this.#argTypes.length) {
				return new FuncType(
					this.#argTypes.map((at, i) => at.infer(site, map, type.argTypes[i])),
					this.#retType.infer(site, map, type.retType)
				);
			}
		}

		throw site.typeError(`unable to infer type of ${this.toString()}`);
	}

    /**
	 * @internal
	 * @param {Site} site 
	 * @param {InferenceMap} map 
	 * @param {Type[]} argTypes 
	 * @returns {FuncType}
	 */
	inferArgs(site, map, argTypes) {
		if (argTypes.length == this.argTypes.length) {
			return new FuncType(
				this.#argTypes.map((at, i) => at.infer(site, map, argTypes[i])),
				this.#retType.infer(site, map, null)
			)
		}

		throw site.typeError(`expected ${this.argTypes.length} arg(s), got ${argTypes.length}`);
	}

    /** 
	 * Checks if any of 'this' argTypes or retType is same as Type.
	 * Only if this checks return true is the association allowed.
	 * @param {Site} site
	 * @param {Type} type
	 * @returns {boolean}
	 */
	isAssociated(site, type) {
		for (let arg of this.#argTypes) {
			if (Common.typesEq(arg.type, type)) {
				return true;
			}
		}

		if (Common.typesEq(type, this.#retType)) {
			return true;
		}

		return false;
	}

    /**
	 * Checks if 'this' is a base type of another FuncType.
	 * The number of args needs to be the same.
	 * Each argType of the FuncType we are checking against needs to be the same or less specific (i.e. isBaseOf(this.#argTypes[i]))
	 * The retType of 'this' needs to be the same or more specific
	 * @param {Type} other 
	 * @returns {boolean}
	 */
	isBaseOf(other) {
		if (other instanceof FuncType) {
			if (this.nNonOptArgs != other.nNonOptArgs) {
				return false;
			} else {
				for (let i = 0; i < this.nNonOptArgs; i++) {
					if (!this.#argTypes[i].isBaseOf(other.#argTypes[i])) {
						return false;
					}
				}

				if (!this.#retType.isBaseOf(other.#retType)) {
					return false;
				}

				return true;
			}

		} else {
			return false;
		}
	}

    /**
	 * Checks if the type of the first arg is the same as 'type'
	 * Also returns false if there are no args.
	 * For a method to be a valid instance member its first argument must also be named 'self', but that is checked elsewhere
	 * @param {Site} site 
	 * @param {Type} type 
	 * @returns {boolean}
	 */
	isMaybeMethod(site, type) {
		if (this.#argTypes.length > 0) {
			return Common.typesEq(this.#argTypes[0].type, type);
		} else {
			return false;
		}
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `(${this.#argTypes.map(a => a.toString()).join(", ")}) -> ${this.#retType.toString()}`;
	}
	
	/**
	 * Throws an error if name isn't found
	 * @param {Site} site 
	 * @param {string} name 
	 * @returns {number}
	 */
	getNamedIndex(site, name) {
		const i = this.#argTypes.findIndex(at => at.name == name);

		if (i == -1) {
			throw site.typeError(`arg name ${name} not found`);
		} else {
			return i;
		}
	}

	/**
	 * @returns {Typed}
	 */
	toTyped() {
		return new FuncEntity(this);
	}
}

/**
 * @internal
 * @template {HeliosData} T
 * @typedef {{
 *   name: string,
 *   path?: string,
 *   offChainType?: HeliosDataClass<T> | null,
 *   genOffChainType?: (() => HeliosDataClass<T>) | null
 *   fieldNames?: string[],
 *   genInstanceMembers: (self: Type) => InstanceMembers,
 *   genTypeMembers: (self: Type) => TypeMembers
 *   genTypeDetails?: (self: Type) => TypeDetails,
 *   jsToUplc?: JsToUplcConverter
 *   uplcToJs?: UplcToJsConverter
 * }} GenericTypeProps
 */

/**
 * Created by statements
 * @internal
 * @template {HeliosData} T
 * @implements {DataType}
 */
export class GenericType extends Common {
    #name;

    /**
     * @type {string}
     */
    #path;

	#genOffChainType;
    #offChainType;
    #fieldNames;

	/**
	 * defer until needed
	 * @type {(self: Type) => InstanceMembers}
	 */
	#genInstanceMembers; 

	/**
	 * defer until needed
	 * @type {(self: Type) => TypeMembers}
	 */
	#genTypeMembers;

	/**
	 * @type {null | InstanceMembers}
	 */
	#instanceMembers;

	/**
	 * @type {null | TypeMembers}
	 */
	#typeMembers;

	/**
	 * @type {null | ((self: Type) => TypeDetails)}
	 */
	#genTypeDetails;

	#genDepth;

	/**
	 * @type {null | JsToUplcConverter}
	 */
	#jsToUplc;

	/**
	 * @type {null | UplcToJsConverter}
	 */
	#uplcToJs;

    /**
     * @param {GenericTypeProps<T>} props
     */
    constructor({
		name, 
		path, 
		offChainType, 
		genOffChainType, 
		fieldNames, 
		genInstanceMembers, 
		genTypeMembers, 
		genTypeDetails,
		jsToUplc,
		uplcToJs
	}) {
        super();

        this.#name = name;
        this.#path = path ?? `__helios__${name.toLowerCase()}`;
		this.#genOffChainType = genOffChainType ?? null;
        this.#offChainType = offChainType ?? null;
        this.#fieldNames = fieldNames ?? [];

		this.#genInstanceMembers = genInstanceMembers;
		this.#genTypeMembers = genTypeMembers;
		this.#instanceMembers = null;
		this.#typeMembers = null;
		this.#genTypeDetails = genTypeDetails ?? null;
		this.#genDepth = 0;
		this.#jsToUplc = jsToUplc ?? null;
		this.#uplcToJs = uplcToJs ?? null;
    }

    /**
     * @type {DataType}
     */
    get asDataType() {
        return this;
    }

    /**
     * @type {Named}
     */
    get asNamed() {
        return this;
    }

    /**
     * @type {Type}
     */
    get asType() {
        return this;
    }

    /**
     * @type {string[]}
     */
    get fieldNames() {
        return this.#fieldNames;
    }

    /**
     * @type {InstanceMembers}
     */
    get instanceMembers() {
		if (!this.#instanceMembers) {
			this.#instanceMembers = this.#genInstanceMembers(this);
		}

		return this.#instanceMembers;
    }

    /**
     * @type {string}
     */
    get name() {
        return this.#name;
    }

    /**
     * @type {null | HeliosDataClass<T>}
     */
    get offChainType() {
		if (this.#offChainType) {
			return this.#offChainType;
		} else if (this.#genOffChainType) {
			return this.#genOffChainType();
		} else {
			return null;
		}
    }

	/**
	 * @type {TypeDetails}
	 */
	get typeDetails() {
		if (this.#genTypeDetails) {
			return this.#genTypeDetails(this);
		} else {
			throw new Error(`typeDetails not available for ${this.toString()}`)
		}
	}

    /**
     * @type {string}
     */
    get path() {
        return this.#path;
    }

	/**
	 * @type {boolean}
	 */
	get ready() {
		return this.#genDepth < 2;
	}

    /**
     * @type {TypeMembers}
     */
    get typeMembers() {
		if (!this.#typeMembers) {
			this.#genDepth += 1;
			this.#typeMembers = this.#genTypeMembers(this);
			this.#genDepth -= 1;
		}

		return this.#typeMembers;
    }

    /**
     * @param {Site} site 
     * @param {InferenceMap} map
     */
    applyInternal(site, map) {
		return {
			name: this.#name,
			path: this.#path,
			fieldNames: this.#fieldNames,
			genInstanceMembers: (self) => {
				/**
				 * @type {InstanceMembers}
				 */
				const instanceMembers = {};

				const oldInstanceMembers = this.#genInstanceMembers(self);

				for (let k in oldInstanceMembers) {
					const v = oldInstanceMembers[k];

					if (v.asParametric) {
						instanceMembers[k] = v.asParametric.infer(site, map);
					} else if (v.asType) {
						instanceMembers[k] = v.asType.infer(site, map, null);
					} else {
						throw new Error("unhandled");
					}
				}

				return instanceMembers;
			},
			genTypeMembers: (self) => {
				/**
				 * @type {TypeMembers}
				 */
				const typeMembers = {};

				const oldTypeMembers = this.#genTypeMembers(self);

				for (let k in oldTypeMembers) {
					const v = oldTypeMembers[k];

					if (v.asParametric) {
						typeMembers[k] = v.asParametric.infer(site, map);
					} else if (v.asTyped) {
						typeMembers[k] = v.asTyped.type.infer(site, map, null).toTyped();
					} else if (v.asType) {
						typeMembers[k] = v.asType.infer(site, map, null);
					} else {
						throw new Error("unhandled");
					}
				}

				return typeMembers;
			}
		}
    }

	/**
     * @param {Site} site 
     * @param {InferenceMap} map 
     * @param {null | Type} type 
     * @returns {Type}
     */
	infer(site, map, type) {
		return this;
	}

	/**
	 * @param {string} name 
	 * @param {string} path 
	 * @returns {GenericType}
	 */
	changeNameAndPath(name, path) {
		return new GenericType({
			name: name,
			path: path,
			fieldNames: this.#fieldNames,
			genInstanceMembers: this.#genInstanceMembers,
			genTypeMembers: this.#genTypeMembers
		});
	}

    /**
     * @param {Type} other 
     * @returns {boolean}
     */
    isBaseOf(other) {
		if (other.asEnumMemberType) {
			return this.isBaseOf(other.asEnumMemberType.parentType);
		} else if (other.asNamed) { 
			return other.asNamed.path == this.#path;
		} else {
			return false;
		}
    }

	/**
	 * @param {any} obj 
	 * @param {JsToUplcHelpers} helpers
	 * @returns {Promise<UplcData>}
	 */
	jsToUplc(obj, helpers) {
		if (this.#jsToUplc) {
			return this.#jsToUplc(obj, helpers);
		} else {
			throw new Error(`'${this.name}' doesn't support converting from JS to Uplc`);
		}
	}

	/**
	 * @param {UplcData} data
	 * @param {UplcToJsHelpers} helpers
	 * @returns {Promise<any>}
	 */
	uplcToJs(data, helpers) {
		if (this.#uplcToJs) {
			return this.#uplcToJs(data, helpers);
		} else {
			throw new Error(`'${this.name}' doesn't support converting from Uplc to JS`);
		}
	}

    /**
     * @returns {string}
     */
    toString() {
        return this.name;
    }

	/**
	 * @returns {Typed}
	 */
	toTyped() {
		return new DataEntity(this);
	}
}


/**
 * @internal
 * @template {HeliosData} T
 * @typedef {{
 *   name: string,
 *   path?: string,
 *   constrIndex: number,
 *   parentType: DataType,
 *   offChainType?: HeliosDataClass<T>,
 *   genOffChainType?: () => HeliosDataClass<T>,
 *   fieldNames?: string[],
 *   genInstanceMembers: (self: Type) => InstanceMembers,
 *   genTypeMembers?: (self: Type) => TypeMembers
 *   genTypeDetails?: (self: Type) => TypeDetails
 *   jsToUplc?: JsToUplcConverter
 *   uplcToJs?: UplcToJsConverter
 * }} GenericEnumMemberTypeProps
 */

/**
 * Created by statements
 * @internal
 * @template {HeliosData} T
 * @implements {EnumMemberType}
 * @extends {GenericType<T>}
 */
export class GenericEnumMemberType extends GenericType {
    #constrIndex;
    #parentType;

    /**
     * @param {GenericEnumMemberTypeProps<T>} props
     */
    constructor({
		name, 
		path, 
		constrIndex, 
		parentType, 
		offChainType, 
		genOffChainType, 
		fieldNames, 
		genInstanceMembers, 
		genTypeMembers,
		genTypeDetails,
		jsToUplc,
		uplcToJs
	}) {
        super({
            name, 
            path: path ?? `${parentType.path}__${name.toLowerCase()}`, 
			genOffChainType,
            offChainType, 
            fieldNames, 
            genInstanceMembers, 
            genTypeMembers: genTypeMembers ?? ((self) => ({})),
			genTypeDetails,
			jsToUplc,
			uplcToJs
        });

        this.#constrIndex = constrIndex;
        this.#parentType = parentType;
    }
    
    /**
     * @type {number}
     */
    get constrIndex() {
        return this.#constrIndex;
    }

    /**
     * @type {DataType}
     */
    get parentType() {
        return this.#parentType;
    }

    /**
     * @type {EnumMemberType}
     */
    get asEnumMemberType() {
        return this;
    }

	/**
     * @param {Site} site 
     * @param {InferenceMap} map 
     * @param {null | Type} type 
     * @returns {Type}
     */
	infer(site, map, type) {
		return this;
	}

	/**
	 * @param {Type} other 
	 * @returns {boolean}
	 */
	isBaseOf(other) {
		if (other instanceof GenericEnumMemberType) {
			return other.path == this.path;
		} else {
			return false;
		}
	}

	/**
	 * @returns {string}
	 */
    toString() {
        return `${this.#parentType.toString()}::${this.name}`;
    }
}


/**
 * Type of return-value of functions that don't return anything (eg. assert, print, error)
 * @internal
 * @implements {Type}
 */
export class VoidType extends Common {
	constructor() {
		super();
	}

	/**
     * @type {InstanceMembers}
     */
    get instanceMembers() {
        return {};
    }

    /**
     * @type {TypeMembers}
     */
    get typeMembers() {
        return {};
    }

    /**
     * @type {Type}
     */
    get asType() {
        return this;
    }

    /**
     * 
     * @param {Site} site 
     * @param {InferenceMap} map 
     * @param {null | Type} type 
     * @returns {Type}
     */
    infer(site, map, type) {
        return this;
    }

	/**
	 * @param {Type} type 
	 * @returns {boolean}
	 */
	isBaseOf(type) {
		return type instanceof VoidType;
	}

    /**
     * @returns {string}
     */
    toString() {
		return "()";
	}
	
	/**
	 * @returns {Typed}
	 */
	toTyped() {
		return new VoidEntity();
	}
}


/**
 * A regular non-Func Instance. DataValues can always be compared, serialized, used in containers.
 * @internal
 * @implements {Instance}
 */
export class DataEntity extends Common {
	#type;

	/**
	 * @param {DataType} type 
	 */
	constructor(type) {
        super();
		assert(!(type instanceof FuncType));
		this.#type = type;
	}

    /**
     * @type {string[]}
     */
    get fieldNames() {
        return this.#type.fieldNames;
    }

    /**
     * @type {InstanceMembers}
     */
    get instanceMembers() {
        return this.#type.instanceMembers;
    }

	/**
	 * @type {Type}
	 */
	get type() {
		return this.#type;
	}

    /**
     * @type {Instance}
     */
    get asInstance() {
        return this;
    }

	/**
	 * @type {Typed}
	 */
	get asTyped() {
		return this;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return this.#type.toString();
	}
}

/**
 * Returned by an error()
 * Special case of no-return-value that indicates that execution can't proceed.
 * @internal
 */
export class ErrorEntity extends Common {
	constructor() {
		super();
	}

	/**
	 * @type {string[]}
	 */
	get fieldNames() {
		return [];
	}

    /**
     * @type {InstanceMembers}
     */
    get instanceMembers() {
        return {};
    }

    /**
     * @type {Type}
     */
	get type() {
		return new ErrorType();
	}

    /**
     * @type {Instance}
     */
    get asInstance() {
        return this;
    }

	/**
	 * @type {Typed}
	 */
	get asTyped() {
		return this;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return "()";
	}
}

/**
 * @internal
 * @implements {Named}
 */
export class NamedEntity {
	#name;
	#path;
	#entity;

	/**
	 * @param {string} name 
	 * @param {string} path 
	 * @param {EvalEntity} entity
	 */
	constructor(name, path, entity) {
		this.#name = name;
		this.#path = path;
		this.#entity = entity;
	}

	/**
	 * @type {null | DataType}
	 */
	get asDataType() {
		return this.#entity.asDataType;
	}

	/**
	 * @type {null | EnumMemberType}
	 */
	get asEnumMemberType() {
		return this.#entity.asEnumMemberType;
	}

	/**
	 * @type {null | Func}
	 */
	get asFunc() {
		return this.#entity.asFunc;
	}

	/**
	 * @type {null | Instance}
	 */
	get asInstance() {
		return this.#entity.asInstance;
	}

	/**
	 * @type {Named}
	 */
	get asNamed() {
		return this;
	}

	/**
	 * @type {null | Namespace}
	 */
	get asNamespace() {
		return this.#entity.asNamespace;
	}

	/**
	 * @type {null | Parametric}
	 */
	get asParametric() {
		return this.#entity.asParametric;
	}

	/**
	 * @type {null | Type}
	 */
	get asType() {
		return this.#entity.asType;
	}

	/**
	 * @type {null | Typed}
	 */
	get asTyped() {
		return this.#entity.asTyped;
	}

	/**
	 * @type {null | TypeClass}
	 */
	get asTypeClass() {
		return this.#entity.asTypeClass;
	}

	/**
	 * @type {string}
	 */
	get name() {
		return this.#name;
	}

	/**
	 * @type {string}
	 */
	get path() {
		return this.#path;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return this.#entity.toString();
	}
}

/**
 * A callable Instance.
 * @internal
 * @implements {Func}
 */
export class FuncEntity extends Common {
	/**
	 * @type {FuncType}
	 */
	#type;

	/**
	 * @param {FuncType} type
	 */
	constructor(type) {
        super();

		assert(type instanceof FuncType);

		this.#type = type;
	}

	/**
	 * @type {Type}
	 */
	get type() {
		return this.#type;
	}

    /**
	 * Returns the underlying FuncType directly.
	 * @type {FuncType}
	 */
	get funcType() {
		return this.#type;
	}

	/**
	 * @type {Func}
	 */
	get asFunc() {
		return this;
	}

	/**
	 * @type {Typed}
	 */
	get asTyped() {
		return this;
	}

    /**
	 * @param {Site} site 
	 * @param {Typed[]} args 
	 * @param {{[name: string]: Typed}} namedArgs
	 * @returns {null | Typed}
	 */
	call(site, args, namedArgs = {}) {
		const type = this.#type.checkCall(site, args, namedArgs);

		if (type === null) {
			return null;
		} else {
			return type.toTyped();
		}
	}

	/**
	 * Returns a string representing the type.
	 * @returns {string}
	 */
	toString() {
		return this.#type.toString();
	}
}

/**
 * @internal
 * @implements {Typed}
 */
export class TypedEntity extends Common {
	/**
	 * @type {Type}
	 */
	#type;

	/**
	 * @param {Type} type 
	 */
	constructor(type) {
		super();

		this.#type = type;
	}

	/**
	 * @returns {Typed}
	 */
	get asTyped() {
		return this
	}

	/**
	 * @type {Type}
	 */
	get type() {
		return this.#type;
	}
}

/**
 * Returned by functions that don't return anything (eg. assert, error, print)
 * @internal
 * @implements {Instance}
 */
export class VoidEntity extends Common {
	constructor() {
		super();
	}

	/**
	 * @type {string[]}
	 */
	get fieldNames() {
		return [];
	}

    /**
     * @type {InstanceMembers}
     */
    get instanceMembers() {
        return {};
    }

    /**
     * @type {Type}
     */
	get type() {
		return new VoidType();
	}

    /**
     * @type {Instance}
     */
    get asInstance() {
        return this;
    }

	/**
	 * @type {Typed}
	 */
	get asTyped() {
		return this;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return "()";
	}
}

/**
 * @internal
 * @implements {Namespace}
 */
export class ModuleNamespace extends Common {
	#members;

	/**
	 * @param {NamespaceMembers} members
	 */
	constructor(members) {
		super();
		this.#members = members;
	}

	/**
	 * @type {NamespaceMembers}
	 */
	get namespaceMembers() {
		return this.#members;
	}

	/**
	 * @type {Namespace}
	 */
	get asNamespace() {
		return this;
	}
}


///////////////////////////////////
// Section 14: Eval primitive types
///////////////////////////////////
/**
 * @internal
 * @param {Type} type
 * @returns {InstanceMembers}
 */
export function genCommonInstanceMembers(type) {
    return {
        serialize: new FuncType([], ByteArrayType),
        show: new FuncType([], StringType)
    }
}

/**
 * @internal
 * @param {Type} type
 * @returns {TypeMembers}
 */
export function genCommonTypeMembers(type) {
    return {
        __eq:      new FuncType([type, type], BoolType),
        __neq:     new FuncType([type, type], BoolType),
        from_data: new FuncType([RawDataType], type),
        __to_data: new FuncType([type], RawDataType),
        is_valid_data: new FuncType([RawDataType], BoolType)
    }
}

/**
 * @internal
 * @param {Type} type
 * @param {Type} parentType
 * @returns {TypeMembers}
 */
export function genCommonEnumTypeMembers(type, parentType) {
    return {
        __eq:      new FuncType([type, parentType], BoolType),
        __neq:     new FuncType([type, parentType], BoolType),
        from_data: new FuncType([RawDataType], type),
        __to_data: new FuncType([type], RawDataType),
        is_valid_data: new FuncType([RawDataType], BoolType)
    }
}

/**
 * Builtin bool type
 * @internal
 * @type {DataType}
 */
export const BoolType = new GenericType({
    name: "Bool",
    offChainType: Bool,
    genTypeDetails: (self) => ({
        inputType: "boolean",
        outputType: "boolean",
        internalType: {
            type: "Bool"
        }
    }),
    jsToUplc: async (obj, helpers) => {
        return new ConstrData(obj ? 1 : 0, []);
    },
    uplcToJs: async (data, helpers) => {
        return data.index != 0;
    },
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        to_int:    new FuncType([], IntType),
        trace:     new FuncType([StringType], self),
        trace_if_false: new FuncType([StringType], self),
        trace_if_true: new FuncType([StringType], self)
    }),
    genTypeMembers: (self) => ({
        ...genCommonTypeMembers(self),
        __and:     new FuncType([self, self], self),
        __neq:     new FuncType([self, self], self),
        __not:     new FuncType([self], self),
        __or:      new FuncType([self, self], self),
        and:       new FuncType([new FuncType([], self), new FuncType([], self)], self),
        or:        new FuncType([new FuncType([], self), new FuncType([], self)], self)
    })
});

/**
 * Builtin bytearray type
 * @internal
 * @type {DataType}
 */
export const ByteArrayType = new GenericType({
    name: "ByteArray",
    offChainType: ByteArray,
    genTypeDetails: (self) => ({
        inputType: "number[] | string",
        outputType: "number[]",
        internalType: {
            type: "ByteArray"
        }
    }),
    jsToUplc: async (obj, helpers) => {
        const bytes = Array.isArray(obj) ? obj : hexToBytes(obj);

        return new ByteArrayData(bytes);
    },
    uplcToJs: async (data, helpers) => {
        return data.bytes;
    },
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        blake2b: new FuncType([], self),
        decode_utf8: new FuncType([], StringType),
        ends_with: new FuncType([self], BoolType),
        length: IntType,
        prepend: new FuncType([IntType], self),
        sha2: new FuncType([], self),
        sha3: new FuncType([], self),
        slice: new FuncType([IntType, IntType], self),
        starts_with: new FuncType([self], BoolType),
    }),
    genTypeMembers: (self) => ({
        ...genCommonTypeMembers(self),
        __add: new FuncType([self, self], self),
        __geq: new FuncType([self, self], BoolType),
        __gt: new FuncType([self, self], BoolType),
        __leq: new FuncType([self, self], BoolType),
        __lt: new FuncType([self, self], BoolType),
        parse: new FuncType([StringType], self)
    })
});

/**
 * @internal
 * @type {DataType}
 */
export const IntType = new GenericType({
    name: "Int",
    offChainType: HInt,
    genTypeDetails: (self) => ({
        inputType: "number | bigint",
        outputType: "bigint",
        internalType: {
            type: "Int"
        }
    }),
    jsToUplc: async (obj, helpers) => {
        return new IntData(BigInt(obj));
    },
    uplcToJs: async (data, helpers) => {
        return data.int;
    },
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        abs: new FuncType([], self),
        bound: new FuncType([self, self], self),
        bound_max: new FuncType([self], self),
        bound_min: new FuncType([self], self),
        decode_zigzag: new FuncType([], self),
        encode_zigzag: new FuncType([], self),
        to_base58: new FuncType([], StringType),
        to_big_endian: new FuncType([], ByteArrayType),
        to_bool: new FuncType([], BoolType),
        to_hex: new FuncType([], StringType),
        to_little_endian: new FuncType([], ByteArrayType),
        to_real: new FuncType([], RealType)
    }),
    genTypeMembers: (self) => ({
        ...genCommonTypeMembers(self),
        __add: new FuncType([self, self], self),
        __add1: new FuncType([self, RealType], RealType),
        __div: new FuncType([self, self], self),
        __div1: new FuncType([self, RealType], RealType),
        __geq: new FuncType([self, self], BoolType),
        __gt: new FuncType([self, self], BoolType),
        __leq: new FuncType([self, self], BoolType),
        __lt: new FuncType([self, self], BoolType),
        __mod: new FuncType([self, self], self),
        __mul: new FuncType([self, self], self),
        __mul1: new FuncType([self, RealType], RealType),
        __neg: new FuncType([self], self),
        __pos: new FuncType([self], self),
        __sub: new FuncType([self, self], self),
        __sub1: new FuncType([self, RealType], RealType),
        from_base58: new FuncType([StringType], self),
        from_big_endian: new FuncType([ByteArrayType], self),
        from_little_endian: new FuncType([ByteArrayType], self),
        max: new FuncType([self, self], self),
        min: new FuncType([self, self], self),
        parse: new FuncType([StringType], self),
        sqrt: new FuncType([self], self)
    })
});

/**
 * Type of external data that must be cast/type-checked before using
 * Not named 'Data' in Js because it's too generic
 * @internal
 * @type {DataType}
 */
export const RawDataType = new GenericType({
    name: "Data",
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        tag: IntType
    }),
    genTypeMembers: (self) => ({
        ...genCommonTypeMembers(self)
    })
});

/**
 * Builtin Real fixed point number type
 * @internal
 * @type {DataType}
 */
export const RealType = new GenericType({
    name: "Real",
    genTypeDetails: (self) => ({
        inputType: "number",
        outputType: "number",
        internalType: {
            type: "Real"
        }
    }),
    jsToUplc: async (obj, helpers) => {
        return new IntData(BigInt(Math.round(obj*1000000)))
    },
    uplcToJs: async (data, helpers) => {
        return Number(data.int)/1000000
    },
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        abs: new FuncType([], self),
        ceil: new FuncType([], IntType),
        floor: new FuncType([], IntType),
        round: new FuncType([], IntType),
        trunc: new FuncType([], IntType)
    }),
    genTypeMembers: (self) => ({
        ...genCommonTypeMembers(self),
        __add: new FuncType([self, self], self),
        __add1: new FuncType([self, IntType], self),
        __div: new FuncType([self, self], self),
        __div1: new FuncType([self, IntType], self),
        __eq1: new FuncType([self, IntType], BoolType),
        __geq: new FuncType([self, self], BoolType),
        __geq1: new FuncType([self, IntType], BoolType),
        __gt: new FuncType([self, self], BoolType),
        __gt1: new FuncType([self, IntType], BoolType),
        __leq: new FuncType([self, self], BoolType),
        __leq1: new FuncType([self, IntType], BoolType),
        __lt: new FuncType([self, self], BoolType),
        __lt1: new FuncType([self, IntType], BoolType),
        __mul: new FuncType([self, self], self),
        __mul1: new FuncType([self, IntType], self),
        __neg: new FuncType([self], self),
        __neq1: new FuncType([self, IntType], BoolType),
        __pos: new FuncType([self], self),
        __sub: new FuncType([self, self], self),
        __sub1: new FuncType([self, IntType], self),
        sqrt: new FuncType([self], self)
    })
});

/**
 * Builtin string type
 * @internal
 * @type {DataType}
 */
export const StringType = new GenericType({
    name: "String",
    offChainType: HString,
    genTypeDetails: (self) => ({
        inputType: "string",
        outputType: "string",
        internalType: {
            type: "String"
        }
    }),
    jsToUplc: async (obj, helpers) => {
        return new ByteArrayData(textToBytes(obj));
    },
    uplcToJs: async (data, helpers) => {
        return bytesToText(data.bytes);
    },
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        encode_utf8: new FuncType([], ByteArrayType),
        ends_with: new FuncType([self], BoolType),
        starts_with: new FuncType([self], BoolType)
    }),
    genTypeMembers: (self) => ({
        ...genCommonTypeMembers(self),
        __add: new FuncType([self, self], self),
        is_valid_utf8: new FuncType([ByteArrayType], BoolType)
    })
});


///////////////////////////////////////
// Section 15: Eval builtin typeclasses
///////////////////////////////////////

/**
 * Created by statements
 * @internal
 * @template {HeliosData} T
 * @implements {DataType}
 */
export class GenericParametricType extends GenericType {
	/**
	 * 
	 * @param {GenericTypeProps<T>} props 
	 */
	constructor(props) {
		super(props);
	}

	/**
     * @param {Site} site 
     * @param {InferenceMap} map
     * @param {null | Type} type 
     * @returns {Type}
     */
	infer(site, map, type) {
		if (type !== null) {
			return this;
		} else {
			let isMaybeParametric = false;
			map.forEach((v) => {
				if (v.isParametric()) {
					isMaybeParametric = true;
				}
			});

			const props = this.applyInternal(site, map);

			return isMaybeParametric ? new GenericParametricType(props) : new GenericType(props);
		}
	}
}


/**
 * Created by statements
 * @internal
 * @template {HeliosData} T
 * @implements {EnumMemberType}
 * @extends {GenericEnumMemberType<T>}
 */
export class GenericParametricEnumMemberType extends GenericEnumMemberType {
	/**
	 * 
	 * @param {GenericEnumMemberTypeProps<T>} props 
	 */
	constructor(props) {
		super(props);
	}

	/**
     * @param {Site} site 
     * @param {InferenceMap} map
     * @param {null | Type} type 
     * @returns {Type}
     */
	infer(site, map, type) {
		if (type !== null) {
			return this;
		} else {
			let isMaybeParametric = false;
			map.forEach((v) => {
				if (v.isParametric()) {
					isMaybeParametric = true;
				}
			});

			const props = {
				...this.applyInternal(site, map),
				parentType: assertDefined(this.parentType.infer(site, map, null).asDataType),
				constrIndex: this.constrIndex
			};

			return isMaybeParametric ? new GenericParametricEnumMemberType(props) : new GenericEnumMemberType(props);
		}
	}
}

/**
 * @internal
 * @implements {Type}
 */
export class TypeClassImpl extends Common {
	/**
	 * @type {string}
	 */
	#name;

	/**
	 * @type {null | ParameterI}
	 */
	#parameter;

	/**
	 * @type {InstanceMembers}
	 */
	#instanceMembers;

	/**
	 * @type {TypeMembers}
	 */
	#typeMembers;

	/**
	 * @param {TypeClass} typeClass
	 * @param {string} name
	 * @param {null | ParameterI} parameter - reference to original parameter, which is more unique than name
	 */
	constructor(typeClass, name, parameter) {
		super();
		this.#name = name;
		this.#parameter = parameter;
        this.#instanceMembers = typeClass.genInstanceMembers(this);
		this.#typeMembers = typeClass.genTypeMembers(this);
    }

	/**
	 * @returns {boolean}
	 */
	isParametric() {
		return true;
	}

    /**
	 * @type {InstanceMembers}
	 */
	get instanceMembers() {
		return this.#instanceMembers;
	}

	/**
	 * @type {string}
	 */
	get name() {
		return this.#name;
	}

	/**
	 * @type {TypeMembers}
	 */
	get typeMembers() {
		return this.#typeMembers;
	}

    /**
     * @type {Type}
     */
    get asType() {
        return this;
    }

    /**
	 * @internal
	 * @param {Site} site 
	 * @param {InferenceMap} map 
	 * @param {null | Type} type
	 * @returns {Type}
	 */
	infer(site, map, type) {
		const p = assertDefined(this.#parameter, "unable to infer dummy TypeClass instantiation");

		const prev = map.get(p);

		if (!prev) {
			if (type) {
				map.set(p, type);

				return type;
			} else {
				// type not yet available: could be parametric func inside a parametric type
				return this;
			}
		} else {
			return prev;
		}
	}

	/**
	 * Returns 'true' if 'this' is a base-type of 'type'. Throws an error if 'this' isn't a Type.
	 * @param {Type} type
	 * @returns {boolean}
	 */
	isBaseOf(type) {
		if (type instanceof TypeClassImpl) {
			// we cans simply use name because name-shadowing isn't allowed
			return type.name == this.name;
		} else {
			return false;
		}
	}

    /**
	 * @returns {string}
	 */
	toString() {
		return this.name;
	}

	/**
	 * @returns {Typed}
	 */
	toTyped() {
		return new TypedEntity(this);
	}
}

/**
 * @internal
 * @implements {DataType}
 */
export class DataTypeClassImpl extends TypeClassImpl {
	/**
     * @type {string}
     */
	#path;

	/**
	 * @param {TypeClass} typeClass
	 * @param {string} name
	 * @param {string} path
	 * @param {null | ParameterI} parameter
	 */
	constructor(typeClass, name, path, parameter) {
		super(typeClass, name, parameter);

		this.#path = path;
    }

	/**
	 * @type {DataType}
	 */
	get asDataType() {
		return this;
	}

	/**
	 * @type {Named}
	 */
	get asNamed() {
		return this;
	}

	/**
	 * @type {string[]}
	 */
	get fieldNames() {
		return [];
	}

	/**
	 * @type {null | HeliosDataClass<HeliosData>}
	 */
	get offChainType() {
		return null;
	}

	/**
	 * @type {string}
	 */
	get path() {
		return this.#path;
	}

	/**
	 * @returns {Typed}
	 */
	toTyped() {
		return new DataEntity(this);
	}
}

/**
 * @internal
 * @implements {TypeClass}
 */
export class AnyTypeClass extends Common {
    constructor() {
        super();
    }

	/**
	 * @type {TypeClass}
	 */
	get asTypeClass() {
		return this;
	}

    /**
	 * @param {Type} impl
	 * @returns {TypeClassMembers}
	 */
	genInstanceMembers(impl) {
		return {};
	}

	/**
	 * @param {Type} impl
	 * @returns {TypeClassMembers}
	 */
	genTypeMembers(impl) {
		return {};
    }

	/**
	 * @param {Type} type 
	 * @returns {boolean}
	 */
	isImplementedBy(type) {
		return true;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return "Any";
	}

    /**
     * @param {string} name 
	 * @param {string} path
	 * @param {null | ParameterI} parameter
     * @returns {Type}
     */
    toType(name, path, parameter = null) {
		return new TypeClassImpl(this, name, parameter);
    }
}

/**
 * @internal
 * @implements {TypeClass}
 */
export class DefaultTypeClass extends Common {
    constructor() {
        super();
    }

	/**
	 * @type {TypeClass}
	 */
	get asTypeClass() {
		return this;
	}

	/**
	 * @param {Type} impl
	 * @returns {TypeClassMembers}
	 */
	genTypeMembers(impl) {
		return {
            __eq: new FuncType([impl, impl], BoolType),
            __neq: new FuncType([impl, impl], BoolType),
			__to_data: new FuncType([impl], RawDataType),
			from_data: new FuncType([RawDataType], impl)
		}
	}

	/**	
	 * @param {Type} impl
	 * @returns {TypeClassMembers}
	 */
	genInstanceMembers(impl) {
		return {
            serialize: new FuncType([], ByteArrayType)
		}
	}

	/**
	 * @param {Type} type 
	 * @returns {boolean}
	 */
	isImplementedBy(type) {
		return Common.typeImplements(type, this);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return "";
	}

    /**
     * @param {string} name 
	 * @param {string} path
	 * @param {null | ParameterI} parameter
     * @returns {DataType}
     */
    toType(name, path, parameter = null) {
        return new DataTypeClassImpl(this, name, path, parameter);
    }
}


/**
 * @internal
 * @implements {TypeClass}
 */
export class SummableTypeClass extends Common {
    constructor() {
        super();
    }

	/**
	 * @type {TypeClass}
	 */
	get asTypeClass() {
		return this;
	}

	/**
	 * @param {Type} impl
	 * @returns {TypeClassMembers}
	 */
	genTypeMembers(impl) {
		return {
            __add: new FuncType([impl, impl], impl),
            __sub: new FuncType([impl, impl], impl)
		};
	}

	/**	
	 * @param {Type} impl
	 * @returns {TypeClassMembers}
	 */
	genInstanceMembers(impl) {
		return {};
	}

	/**
	 * @param {Type} type 
	 * @returns {boolean}
	 */
	isImplementedBy(type) {
		return Common.typeImplements(type, this);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return "Summable";
	}

    /**
     * @param {string} name 
	 * @param {string} path
	 * @param {null | ParameterI} parameter
     * @returns {DataType}
     */
    toType(name, path, parameter = null) {
        return new DataTypeClassImpl(this, name, path, parameter);
    }
}

/**
 * @internal
 * @implements {ParameterI}
 */
export class Parameter {
	/** 
	 * @type {string} 
	 */
	#name;

	/** 
	 * @type {string} 
	 */
	#path;

	/** 
	 * @type {TypeClass}
	 */
	#typeClass;

	/**
	 * @param {string} name - typically "a" or "b"
	 * @param {string} path - typicall "__T0" or "__F0"
	 * @param {TypeClass} typeClass
	 */
	constructor(name, path, typeClass) {
		this.#name = name;
		this.#path = path;
		this.#typeClass = typeClass
	}

	/**
	 * @type {string}
	 */
	get name() {
		return this.#name;
	}

	/**
	 * @type {Type}
	 */
	get ref() {
		return this.#typeClass.toType(this.#name, this.#path, this);
	}

	/**
	 * A null TypeClass matches any type
	 * @type {TypeClass}
	 */
	get typeClass() {
		return this.#typeClass;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		if (this.#typeClass && this.#typeClass.toString() != "") {
			return `${this.#name}: ${this.#typeClass.toString()}`
		} else {
			return this.#name;
		}
	}
}

/**
 * Only func instances can be parametrics instances,
 *  there are no other kinds of parametric instances
 * @internal
 * @implements {Parametric}
 */
export class ParametricFunc extends Common {
	#params;
	#fnType;

	/**
	 * @param {Parameter[]} params
	 * @param {FuncType} fnType
	 */
	constructor(params, fnType) {
		super();
		this.#params = params;
		this.#fnType = fnType;
	}

	/**
	 * @type {null | ((...any) => HeliosDataClass<HeliosData>)}
	 */
	get offChainType() {
		return null;
	}

	get params() {
		return this.#params;
	}

	get fnType() {
		return this.#fnType;
	}

	/**
	 * null TypeClasses aren't included
	 * @type {TypeClass[]}
	 */
	get typeClasses() {
		return this.#params.map(p => p.typeClass);
	}

	/**
	 * @param {Type[]} types 
	 * @param {Site} site
	 * @returns {EvalEntity}
	 */
	apply(types, site = Site.dummy()) {
		if (types.length != this.#params.length) {
			throw site.typeError("wrong number of parameter type arguments");
		}

		/**
		 * @type {InferenceMap}
		 */
		const map = new Map();

		this.#params.forEach((p, i) => {
			if (!p.typeClass.isImplementedBy(types[i])) {
				throw site.typeError("typeclass match failed")
			}

			map.set(p, types[i]);
		});

		const inferred = this.#fnType.infer(site, map, null);

		return new FuncEntity(assertClass(inferred, FuncType));
	}

	/**
	 * @type {Parametric}
	 */
	get asParametric() {
		return this;
	}

    /**
	 * Must infer before calling
	 * @param {Site} site 
	 * @param {Typed[]} args
	 * @param {{[name: string]: Typed}} namedArgs
	 * @param {Type[]} paramTypes - so that paramTypes can be accessed by caller
	 * @returns {Func}
	 */
	inferCall(site, args, namedArgs = {}, paramTypes = []) {
		/**
		 * @type {InferenceMap}
		 */
		const map = new Map();

		const fnType = this.#fnType.inferArgs(site, map, args.map(a => a.type));

		// make sure that each parameter is defined in the map
		this.#params.forEach(p => {
			const pt = map.get(p);

			if (!pt) {
				throw site.typeError(`failed to infer type of '${p.name}'  (hint: apply directly using [...])`);
			}

			paramTypes.push(pt);
		});

		return new FuncEntity(fnType);
	}
	
	/**
	 * @param {Site} site 
	 * @param {InferenceMap} map 
	 * @returns {Parametric}
	 */
	infer(site, map) {
		const fnType = assertClass(this.#fnType.infer(site, map, null), FuncType);

		return new ParametricFunc(this.#params, fnType);
	}

    /**
     * @returns {string}
     */
	toString() {
		return `[${this.#params.map(p => p.toString()).join(", ")}]${this.#fnType.toString()}`;
	}
}

/**
 * @internal
 * @implements {DataType}
 */
class AppliedType extends Common {
    #types;
    #apply;
    #inner;

    /**
     * @param {Type[]} types
     * @param {(types: Type[]) => DataType} apply
     * @param {DataType} inner 
     */
    constructor(types, apply, inner) {
        super();

        this.#types = types;
        this.#apply = apply;
        this.#inner = inner;
    }

    /**
     * @type {string[]}
     */
    get fieldNames() {
        return this.#inner.fieldNames;
    }

    /**
     * @type {InstanceMembers}
     */
    get instanceMembers() {
        return this.#inner.instanceMembers;
    }

    /**
     * @type {string}
     */
    get name() {
        return this.#inner.name;
    }

    /**
     * @type {null | HeliosDataClass<HeliosData>}
     */
    get offChainType() {
        return this.#inner.offChainType;
    }

    /**
     * @type {string}
     */
    get path() {
        return this.#inner.path;
    }

    /**
     * @type {TypeMembers}
     */
    get typeMembers() {
        return this.#inner.typeMembers;
    }

	/**
	 * @type {TypeDetails | undefined}
	 */
	get typeDetails() {
		return this.#inner.typeDetails;
	}

    /**
     * @type {DataType}
     */
    get asDataType() {
        return this;
    }

    /**
     * @type {Named}
     */
    get asNamed() {
        return this;
    }

    /**
     * @type {Type}
     */
    get asType() {
        return this;
    }

	/**
	 * @param {any} obj 
	 * @param {JsToUplcHelpers} helpers
	 * @returns {Promise<UplcData>}
	 */
	jsToUplc(obj, helpers) {
		return this.#inner.jsToUplc(obj, helpers);
	}

	/**
	 * @param {UplcData} data 
	 * @param {UplcToJsHelpers} helpers
	 * @returns {any | Promise<any>}
	 */
	uplcToJs(data, helpers) {
		return this.#inner.uplcToJs(data, helpers);
	}

    /**
     * @param {Site} site 
     * @param {InferenceMap} map 
     * @param {null | Type} type 
     * @returns {Type}
     */
    infer(site, map, type) {
        if (!type) {
            const infered = this.#types.map(t => t.infer(site, map, null));

            return new AppliedType(infered, this.#apply, this.#apply(infered));
		} else if (type instanceof AppliedType && type.#types.length == this.#types.length) {
            const infered = this.#types.map((t, i) => t.infer(site, map, type.#types[i]));

            const res = new AppliedType(infered, this.#apply, this.#apply(infered));

			if (!res.isBaseOf(type)) {
				throw site.typeError("unable to infer type");
			}

			return res;
        } else {
			throw site.typeError("unable to infer type");
		}
    }

    /**
     * @param {Type} other 
     * @returns {boolean}
     */
    isBaseOf(other) {
        return this.#inner.isBaseOf(other);
    }

    /**
     * @returns {string}
     */
    toString() {
        return this.#inner.toString();
    }

	/**
	 * @returns {Typed}
	 */
	toTyped() {
		return new DataEntity(this);
	}
}

/**
 * @internal
 * @implements {Parametric}
 */
export class ParametricType extends Common {
	#name;
    #offChainType;
    #parameters;
    #apply;

    /**
     * @param {{
	 * 	 name: string,
     *   offChainType?: ((...any) => HeliosDataClass<HeliosData>)
     *   parameters: Parameter[]
     *   apply: (types: Type[]) => DataType
     * }} props
     */
    constructor({name, offChainType, parameters, apply}) {
        super();
		this.#name = name;
        this.#offChainType = offChainType ?? null;
        this.#parameters = parameters;
        this.#apply = apply;
    }

    /**
     * @type {Parametric}
     */
    get asParametric() {
        return this;
    }

    /**
     * @type {null | ((...any) => HeliosDataClass<HeliosData>)}
     */
    get offChainType() {
        return this.#offChainType;
    }

	/**
	 * @type {TypeClass[]}
	 */
	get typeClasses() {
		return this.#parameters.map(p => p.typeClass);
	}

    /**
     * @param {Type[]} types 
     * @param {Site} site 
     * @returns {EvalEntity}
     */
    apply(types, site = Site.dummy()) {
        if (types.length != this.#parameters.length) {
			throw site.typeError(`expected ${this.#parameters.length} type parameter(s), got ${types.length}`);
		}

		this.#parameters.forEach((p, i) => {
			if (!p.typeClass.isImplementedBy(types[i])) {
				throw site.typeError(`${types[i].toString()} doesn't implement ${p.typeClass.toString()}`);
			}
		});

		// TODO: recursive problem, defer the implementation check
		return new AppliedType(types, this.#apply, this.#apply(types));
    }

	 /**
	 * Must infer before calling
	 * @param {Site} site 
	 * @param {Typed[]} args
	 * @param {{[name: string]: Typed}} namedArgs
	 * @param {Type[]} paramTypes - so that paramTypes can be accessed by caller
	 * @returns {Func}
	 */
	inferCall(site, args, namedArgs = {}, paramTypes = []) {
		throw site.typeError("not a parametric function");
	}

	/**
	 * @param {Site} site 
	 * @param {InferenceMap} map 
	 * @returns {Parametric}
	 */
	infer(site, map) {
		throw site.typeError("not a parametric function");
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `${this.#name}`;//[${this.#parameters.map(p => p.toString())}]`;
	}
}


/////////////////////////////////////
// Section 16: Eval builtin functions
/////////////////////////////////////

/**
 * Used by print, error, and assert
 * @internal
 * @implements {Func}
 * @implements {Named}
 */
export class BuiltinFunc extends Common {
	/**
	 * @type {string}
	 */
	#name;

	/**
	 * @type {FuncType}
	 */
	#type;

	/**
	 * 
	 * @param {{
	 *   name: string,
	 *   type: FuncType
	 * }} props
	 */
	constructor({name, type}) {
		super();
		this.#name = name;
		this.#type = type;
	}

	/**
	 * @type {string}
	 */
	get name() {
		return this.#name;
	}

	/**
	 * @type {string}
	 */
	get path() {
		return `__helios__${this.#name}`;
	}

	/**
	 * @type {Type}
	 */
	get type() {
		return this.#type;
	}

	/**
	 * @type {FuncType}
	 */
	get funcType() {
		return this.#type;
	}

	/**
	 * @type {Func}
	 */
	get asFunc() {
		return this;
	}

	/**
     * @type {Named}
     */
	get asNamed() {
        return this;
    }

	/**
	 * @type {Typed}
	 */
	get asTyped() {
		return this;
	}

	/**
	 * @param {Site} site 
	 * @param {Typed[]} args 
	 * @param {{[name: string]: Typed}} namedArgs
	 * @returns {null | Typed}
	 */
	call(site, args, namedArgs = {}) {
		const res = this.#type.checkCall(site, args, namedArgs);

		if (!res) {
			return null
		} else {
			return res.toTyped();
		}
	}

	/**
     * @returns {string}
     */
    toString() {
        return this.name;
    }
}

/**
 * Special builtin function that throws an error if condition is false and returns Void
 * @internal
 */
export const AssertFunc = new BuiltinFunc({
    name: "assert",
    type: new FuncType([BoolType, StringType], new VoidType())
});

/**
 * Special builtin function that throws an error and returns ErrorInstance (special case of Void)
 * @internal
 */
export const ErrorFunc = new BuiltinFunc({
	name: "error",
	type: new FuncType([StringType], new ErrorType())
});

/**
 * Special builtin function that prints a message and returns void
 * @internal
 */
export const PrintFunc = new BuiltinFunc({
	name: "print",
	type:  new FuncType([StringType], new VoidType())
});


///////////////////////////////////
// Section 17: Eval container types
///////////////////////////////////


/**
 * @internal
 * @param {Type[]} itemTypes
 * @returns {Type}
 */
export function IteratorType$(itemTypes) {
	const props = {
		name: `Iterator[${itemTypes.map(it => it.toString()).join(", ")}]`,
		path: `__helios__iterator__${itemTypes.length}`,
		genInstanceMembers: (self) => {
			// Note: to_list and to_map can't be part of Iterator because type information is lost (eg. we can map to an iterator over functions)

			const itemType = itemTypes.length == 1 ? itemTypes[0] : TupleType$(itemTypes);

			const members = {
				any: new FuncType([new FuncType(itemTypes, BoolType)], BoolType),
				drop: new FuncType([IntType], self),
				filter: new FuncType([new FuncType(itemTypes, BoolType)], self),
				find: new FuncType([new FuncType(itemTypes, BoolType)], itemType),
				for_each: new FuncType([new FuncType(itemTypes, new VoidType())], new VoidType()),
				fold: (() => {
					const a = new Parameter("a", `${FTPP}0`, new AnyTypeClass());
					return new ParametricFunc([a], new FuncType([new FuncType([a.ref].concat(itemTypes), a.ref), a.ref], a.ref));
				})(),
				head: itemType,
				get: new FuncType([IntType], itemType),
				get_singleton: new FuncType([], itemType),
				is_empty: new FuncType([], BoolType),
				map: (() => {
					const a = new Parameter("a", `${FTPP}0`, new AnyTypeClass());
					return new ParametricFunc([a], new FuncType([new FuncType(itemTypes, a.ref)], IteratorType$([a.ref])));
				})(),
				map2: (() => {
					const a = new Parameter("a", `${FTPP}0`, new AnyTypeClass());
					const b = new Parameter("b", `${FTPP}0`, new AnyTypeClass());

					return new ParametricFunc([a, b], new FuncType([new FuncType(itemTypes, TupleType$([a.ref, b.ref]))], IteratorType$([a.ref, b.ref])));
				})(),
				prepend: new FuncType(itemTypes, self),
				tail: self,
				take: new FuncType([IntType], self)
			}

			if (itemTypes.length < 10) {
				members.zip = (() => {
					const a = new Parameter("a", `${FTPP}0`, new DefaultTypeClass());
					return new ParametricFunc([a], new FuncType([ListType$(a.ref)], IteratorType$(itemTypes.concat([a.ref]))));
				})();
			}

			return members;
		},
		genTypeMembers: (self) => ({})
	};

	// if any of the item type is parametric, this return type must also be parametric so that the item type inference methods are called correctly
	//  (i.e. the inference method of this Iterator type calls the inference methods of the itemtypes)
	return itemTypes.some(it => it.isParametric()) ? new GenericParametricType(props) : new GenericType(props);
};

/**
 * @internal
 * @template {HeliosData} T
 * @implements {DataType}
 */
export class TupleType extends GenericType {
	#itemTypes;

	/**
	 * @param {GenericTypeProps<T>} props
	 * @param {Type[]} itemTypes
	 */
	constructor(props, itemTypes) {
		super(props);

		this.#itemTypes = itemTypes;
	}
	
	get itemTypes() {
		return this.#itemTypes;
	}

	/**
     * @param {Type} other 
     * @returns {boolean}
     */
    isBaseOf(other) {
		if (other instanceof TupleType) {
			return other.#itemTypes.length == this.#itemTypes.length && this.#itemTypes.every((it, i) => it.isBaseOf(other.#itemTypes[i]));
		} else {
			return false;
		}
    }

	/**
	 * @internal
	 * @param {Site} site
	 * @param {InferenceMap} map 
	 * @param {null | Type} type 
	 * @returns {Type}
	 */
	infer(site, map, type) {
		if (!this.#itemTypes.some(it => it.isParametric())) {
			return this;
		}

		if (!type) {
			const itemTypes = this.#itemTypes.map(it => it.infer(site, map, null));

			return TupleType$(itemTypes);
		} else if (type instanceof TupleType && this.#itemTypes.length == type.#itemTypes.length) {
			const itemTypes = this.#itemTypes.map((it, i) => it.infer(site, map, type.#itemTypes[i]));

			return TupleType$(itemTypes);
		}

		throw site.typeError(`unable to infer type of ${this.toString()} (${type instanceof TupleType} ${type instanceof GenericType})`);
	}
}

/**
 * TODO: rename DataType to something else
 * @internal
 * @param {Type} type 
 * @return {boolean}
 */
export function isDataType(type) {
	const dt = type.asDataType;

	if (!dt) {
		return false;
	}

	// no need to check for primitives
	if (dt == IntType || dt == StringType || dt == ByteArrayType || dt == BoolType || dt == RealType) {
		return true;
	}

	const dataTypeClass = new DefaultTypeClass();

	return dataTypeClass.isImplementedBy(dt)
}

/**
 * @internal
 * @param {Type[]} itemTypes
 * @param {boolean | null} isAllDataTypes - if the all the itemTypes are known datatypes, then don't check that here (could lead to infinite recursion)
 * @returns {Type}
 */
export function TupleType$(itemTypes, isAllDataTypes = null) {
	const isData = isAllDataTypes !== null ? isAllDataTypes : itemTypes.every(it => {
		return isDataType(it);
	});

	const props = {
		name: `(${itemTypes.map(it => it.toString()).join(", ")})`,
		path: `__helios__tuple[${itemTypes.map(it => it.asDataType ? it.asDataType.path : "__helios__func").join("@")}]`,
		genInstanceMembers: (self) => {
			const members = isData ? genCommonInstanceMembers(self) : {};

			const getters = [
				"first",
				"second",
				"third",
				"fourth",
				"fifth"
			];

			for (let i = 0; i< 5 && i < itemTypes.length; i++) {
				const key = getters[i];
				members[key] = itemTypes[i]
			}

			const a = new Parameter("a", `${FTPP}0`, new AnyTypeClass());
			members.__to_func = new ParametricFunc([a], new FuncType([new FuncType(itemTypes, a.ref)], a.ref));
			
			return members;
		},
		genTypeMembers: (self) => {
			return isData ? genCommonTypeMembers(self) : {};
		}
	};

	return new TupleType(props, itemTypes);
}

/**
 * Returns null if `type` isn't a tuple
 * @internal
 * @param {Type} type 
 * @returns {null | Type[]}
 */
export function getTupleItemTypes(type) {
	if (type instanceof TupleType) {
		return type.itemTypes;
	} else {
		return null;
	}
}

/**
 * Builtin list type
 * @internal
 * @type {Parametric}
 */
export const ListType = new ParametricType({
	name: "[]",
	offChainType: HList,
	parameters: [new Parameter("ItemType", `${TTPP}0`, new DefaultTypeClass())],
	apply: ([itemType_]) => {
		const itemType = assertDefined(itemType_.asDataType);
		const offChainItemType = itemType.offChainType ?? null;
		const offChainType = offChainItemType ? HList(offChainItemType) : null;

		const props = {
			offChainType: offChainType,
			name: `[]${itemType.toString()}`,
			path: `__helios__list[${itemType.path}]`,
			genTypeDetails: (self) => ({
				inputType: `(${assertDefined(itemType.typeDetails?.inputType)})[]`,
				outputType: `(${assertDefined(itemType.typeDetails?.outputType)})[]`,
				internalType: {
					type: "List",
					itemType: assertDefined(itemType.typeDetails?.internalType)
				}
			}),
			jsToUplc: async (obj, helpers) => {
				if (Array.isArray(obj)) {
					return new ListData(await Promise.all(obj.map(item => itemType.jsToUplc(item, helpers))));
				} else {
					throw new Error("expected array");	
				}
			},
			uplcToJs: async (data, helpers) => {
				return await Promise.all(data.list.map(item => itemType.uplcToJs(item, helpers)));
			},
			genInstanceMembers: (self) => {
				/**
				 * @type {InstanceMembers}
				 */
				const specialMembers = {};

				if ((new SummableTypeClass()).isImplementedBy(itemType)) {
					specialMembers.sum = new FuncType([], itemType);
				} else if (StringType.isBaseOf(itemType)) {
					specialMembers.join = new FuncType([
						new ArgType(new Word(Site.dummy(), "separator"), StringType, true)
					], StringType);
				} else if (ByteArrayType.isBaseOf(itemType)) {
					specialMembers.join = new FuncType([
						new ArgType(new Word(Site.dummy(), "separator"), ByteArrayType, true)
					], ByteArrayType);
				} else if (itemType.asNamed?.name.startsWith("[]")) {
					specialMembers.flatten = new FuncType([], itemType);
				}

				return {
					...genCommonInstanceMembers(self),
					...specialMembers,
					all: new FuncType([new FuncType([itemType], BoolType)], BoolType),
					any: new FuncType([new FuncType([itemType], BoolType)], BoolType),
					append: new FuncType([itemType], self),
					drop: new FuncType([IntType], self),
					drop_end: new FuncType([IntType], self),
					filter: new FuncType([new FuncType([itemType], BoolType)], self),
					find: new FuncType([new FuncType([itemType], BoolType)], itemType),
					find_index: new FuncType([new FuncType([itemType], BoolType)], IntType),
					find_safe: new FuncType([new FuncType([itemType], BoolType)], OptionType$(itemType)),
					fold: (() => {
						const a = new Parameter("a", `${FTPP}0`, new AnyTypeClass());
						return new ParametricFunc([a], new FuncType([new FuncType([a.ref, itemType], a.ref), a.ref], a.ref));
					})(),
					fold2: (() => {
						const a = new Parameter("a", `${FTPP}0`, new AnyTypeClass());
						const b = new Parameter("b", `${FTPP}0`, new AnyTypeClass());
						return new ParametricFunc([a, b], new FuncType([new FuncType([a.ref, b.ref, itemType], TupleType$([a.ref, b.ref])), a.ref, b.ref], TupleType$([a.ref, b.ref])));
					})(),
					fold3: (() => {
						const a = new Parameter("a", `${FTPP}0`, new AnyTypeClass());
						const b = new Parameter("b", `${FTPP}0`, new AnyTypeClass());
						const c = new Parameter("c", `${FTPP}0`, new AnyTypeClass());
						return new ParametricFunc([a, b, c], new FuncType([new FuncType([a.ref, b.ref, c.ref, itemType], TupleType$([a.ref, b.ref, c.ref])), a.ref, b.ref, c.ref], TupleType$([a.ref, b.ref, c.ref])));
					})(),
					fold_lazy: (() => {
						const a = new Parameter("a", `${FTPP}0`, new AnyTypeClass());
						return new ParametricFunc([a], new FuncType([new FuncType([itemType, new FuncType([], a.ref)], a.ref), a.ref], a.ref));
					})(),
					fold2_lazy: (() => {
						const a = new Parameter("a", `${FTPP}0`, new AnyTypeClass());
						const b = new Parameter("b", `${FTPP}0`, new AnyTypeClass());
						return new ParametricFunc([a, b], new FuncType([new FuncType([itemType, new FuncType([], TupleType$([a.ref, b.ref]))], TupleType$([a.ref, b.ref])), a.ref, b.ref], TupleType$([a.ref, b.ref])));
					})(),
					for_each: new FuncType([new FuncType([itemType], new VoidType())], new VoidType()),
					get: new FuncType([IntType], itemType),
					get_singleton: new FuncType([], itemType),
					head: itemType,
					is_empty: new FuncType([], BoolType),
					length: IntType,
					map: (() => {
						const a = new Parameter("a", `${FTPP}0`, new DefaultTypeClass());
						return new ParametricFunc([a], new FuncType([new FuncType([itemType], a.ref)], ListType$(a.ref)));
					})(),
					map_option: (() => {
						const a = new Parameter("a", `${FTPP}0`, new DefaultTypeClass());
						return new ParametricFunc([a], new FuncType([new FuncType([itemType], OptionType$(a.ref))], ListType$(a.ref)));
					})(),
					prepend: new FuncType([itemType], self),
					set: new FuncType([IntType, itemType], self),
					sort: new FuncType([new FuncType([itemType, itemType], BoolType)], self),
					split_at: new FuncType([IntType], TupleType$([self, self], true)),
					tail: self,
					take: new FuncType([IntType], self),
					take_end: new FuncType([IntType], self),
					to_iterator: new FuncType([], IteratorType$([itemType])),
					zip: (() => {
						const a = new Parameter("a", `${FTPP}0`, new DefaultTypeClass());
						return new ParametricFunc([a], new FuncType([ListType$(a.ref)], IteratorType$([itemType, a.ref])));
					})()
				}
			},
			genTypeMembers: (self) => ({
				...genCommonTypeMembers(self),
				__add: new FuncType([self, self], self),
				new: new FuncType([IntType, new FuncType([IntType], itemType)], self),
				new_const: new FuncType([IntType, itemType], self),
				from_iterator: new FuncType([IteratorType$([itemType])], self)
			})
		};

		return itemType_.isParametric() ? new GenericParametricType(props) : new GenericType(props);
	}
});

/**
 * @internal
 * @param {Type} itemType 
 * @returns {DataType}
 */
export function ListType$(itemType) {
	return applyTypes(ListType, itemType);
}

/**
 * Builtin map type (in reality list of key-value pairs)
 * @internal
 * @type {Parametric}
 */
export const MapType = new ParametricType({
	name: "Map",
	offChainType: HMap,
	parameters: [
		new Parameter("KeyType", `${TTPP}0`, new DefaultTypeClass()), 
		new Parameter("ValueType", `${TTPP}1`, new DefaultTypeClass())
	],
	apply: ([keyType_, valueType_]) => {
		const keyType = assertDefined(keyType_.asDataType);
		const valueType = assertDefined(valueType_.asDataType);
		const offChainKeyType = keyType.offChainType ?? null;
		const offChainValueType = valueType.offChainType ?? null;
		const offChainType = offChainKeyType && offChainValueType ? HMap(offChainKeyType, offChainValueType) : null;

		const props = {
			offChainType: offChainType,
			name: `Map[${keyType.toString()}]${valueType.toString()}`,
			path: `__helios__map[${keyType.path}@${valueType.path}]`,
			genTypeDetails: (self) => ({
				inputType: `[${assertDefined(keyType.typeDetails?.inputType)}, ${assertDefined(valueType.typeDetails?.inputType)}][]`,
				outputType: `[${assertDefined(keyType.typeDetails?.outputType)}, ${assertDefined(valueType.typeDetails?.outputType)}][]`,
				internalType: {
					type: "Map",
					keyType: assertDefined(keyType.typeDetails?.internalType),
					valueType: assertDefined(valueType.typeDetails?.internalType)
				}
			}),
			genInstanceMembers: (self) => ({
				...genCommonInstanceMembers(self),
				all: new FuncType([new FuncType([keyType, valueType], BoolType)], BoolType),
				any: new FuncType([new FuncType([keyType, valueType], BoolType)], BoolType),
				append: new FuncType([keyType, valueType], self),
				delete: new FuncType([keyType], self),
				filter: new FuncType([new FuncType([keyType, valueType], BoolType)], self),
				find: new FuncType([new FuncType([keyType, valueType], BoolType)], TupleType$([keyType, valueType], true)),
				find_key: new FuncType([new FuncType([keyType], BoolType)], keyType),
				find_key_safe: new FuncType([new FuncType([keyType], BoolType)], OptionType$(keyType)),
				// TODO: convert return value of find_safe to an OptionType of a TupleType (requires changing the way options work internally)
				find_safe: new FuncType([new FuncType([keyType, valueType], BoolType)], TupleType$([new FuncType([], TupleType$([keyType, valueType])), BoolType], false)),
				find_value: new FuncType([new FuncType([valueType], BoolType)], valueType),
				find_value_safe: new FuncType([new FuncType([valueType], BoolType)], OptionType$(valueType)),
				fold: (() => {
					const a = new Parameter("a", `${FTPP}0`, new AnyTypeClass());
					return new ParametricFunc([a], new FuncType([new FuncType([a.ref, keyType, valueType], a.ref), a.ref], a.ref));
				})(),
				fold_lazy: (() => {
					const a = new Parameter("a", `${FTPP}0`, new AnyTypeClass());
					return new ParametricFunc([a], new FuncType([new FuncType([keyType, valueType, new FuncType([], a.ref)], a.ref), a.ref], a.ref));
				})(),
				for_each: new FuncType([new FuncType([keyType, valueType], new VoidType())], new VoidType()),
				get: new FuncType([keyType], valueType),
				get_safe: new FuncType([keyType], OptionType$(valueType)),
				head: TupleType$([keyType, valueType], true),
				head_key: keyType,
				head_value: valueType,
				is_empty: new FuncType([], BoolType),
				length: IntType,
				map: (() => {
					const a = new Parameter("a", `${FTPP}0`, new DefaultTypeClass());
					const b = new Parameter("b", `${FTPP}1`, new DefaultTypeClass());

					return new ParametricFunc([a, b], new FuncType([new FuncType([keyType, valueType], TupleType$([a.ref, b.ref], true))], MapType$(a.ref, b.ref)));
				})(),
				prepend: new FuncType([keyType, valueType], self),
				set: new FuncType([keyType, valueType], self),
				sort: new FuncType([new FuncType([keyType, valueType, keyType, valueType], BoolType)], self),
				tail: self,
				to_iterator: new FuncType([], IteratorType$([keyType, valueType])),
				update: new FuncType([keyType, new FuncType([valueType], valueType)], self),
				update_safe: new FuncType([keyType, new FuncType([valueType], valueType)], self)
			}),
			genTypeMembers: (self) => ({
				...genCommonTypeMembers(self),
				__add: new FuncType([self, self], self),
				from_iterator: new FuncType([IteratorType$([keyType, valueType])], self)
			})
		}

		return (keyType.isParametric() || valueType.isParametric()) ? new GenericParametricType(props) : new GenericType(props);
	}
});

/**
 * @internal
 * @param {Type} keyType 
 * @param {Type} valueType
 * @returns {DataType}
 */
export function MapType$(keyType, valueType) {
	return applyTypes(MapType, keyType, valueType);
}

/**
 * Builtin option type
 * @internal
 * @type {Parametric}
 */
const OptionType = new ParametricType({
	name: "Option",
	offChainType: Option,
	parameters: [new Parameter("SomeType", `${TTPP}0`, new DefaultTypeClass())],
	apply: ([someType_]) => {
		const someType = assertDefined(someType_.asDataType);
		const someOffChainType = someType.offChainType ?? null;
		const offChainType = someOffChainType ? Option(someOffChainType) : null;
		const someTypePath = someType.path;

		/**
		 * @type {null | EnumMemberType}
		 */
		let NoneType = null;

		/**
		 * @type {null | EnumMemberType}
		 */
		let SomeType = null;

		const appliedOptionTypeProps = {
			offChainType: offChainType,
			name: `Option[${someType.toString()}]`,
			path: `__helios__option[${someTypePath}]`,
			genTypeDetails: (self) => ({
				inputType: `null | ${assertDefined(someType.typeDetails?.inputType)}`,
				outputType: `null | ${assertDefined(someType.typeDetails?.outputType)}`,
				internalType: {
					type: "Option",
					someType: assertDefined(someType.typeDetails?.internalType)
				}
			}),
			genInstanceMembers: (self) => ({
				...genCommonInstanceMembers(self),
				map: (() => {
					const a = new Parameter("a", `${FTPP}0`, new DefaultTypeClass());
					return new ParametricFunc([a], new FuncType([new FuncType([someType], a.ref)], OptionType$(a.ref)));
				})(),
				unwrap: new FuncType([], someType)
			}),
			genTypeMembers: (self) => ({
				...genCommonTypeMembers(self),
           		None: assertDefined(NoneType),
            	Some: assertDefined(SomeType)
			})
		};

		const someTypeProps = {
			name: "Some",
			constrIndex: 0,
			fieldNames: ["some"],
			path: `__helios__option[${someTypePath}]__some`,
			genInstanceMembers: (self) => ({
				...genCommonInstanceMembers(self),
				some: someType
			}),
			genTypeMembers: (self) => ({
				...genCommonTypeMembers(self)
			})
		};

		const noneTypeProps = {
			name: "None",
			constrIndex: 1,
			path: `__helios__option[${someTypePath}]__none`,
			genInstanceMembers: (self) => ({
				...genCommonInstanceMembers(self)
			}),
			genTypeMembers: (self) => ({
				...genCommonTypeMembers(self)
			})
		};

		if (someType.isParametric()) {
			const AppliedOptionType = new GenericParametricType(appliedOptionTypeProps);

			SomeType = new GenericParametricEnumMemberType({
				...someTypeProps,
				parentType: AppliedOptionType
			});

			NoneType = new GenericParametricEnumMemberType({
				...noneTypeProps,
				parentType: AppliedOptionType
			});

			return AppliedOptionType;
		} else {
			const AppliedOptionType = new GenericType(appliedOptionTypeProps);

			SomeType = new GenericEnumMemberType({
				...someTypeProps,
				parentType: AppliedOptionType
			});

			NoneType = new GenericEnumMemberType({
				...noneTypeProps,
				parentType: AppliedOptionType
			});

			return AppliedOptionType;
		}
	}
});

/**
 * @internal
 * @param {Type} someType 
 * @returns {DataType}
 */
export function OptionType$(someType) {
	return applyTypes(OptionType, someType);
}


//////////////////////////////
// Section 18: Eval time types
//////////////////////////////

/**
 * Builtin Duration type
 * @internal
 * @type {DataType}
 */
export var DurationType = new GenericType({
    name: "Duration",
    offChainType: Duration,
    genTypeDetails: (self) => ({
        inputType: `number | bigint`,
        outputType: `number`,
        internalType: {
            type: "Duration"
        }
    }),
    jsToUplc: async (obj, helpers) => {
        return Duration.fromProps(obj)._toUplcData();
    },
    uplcToJs: async (data, helpers) => {
        return Number(Duration.fromUplcData(data).value);
    },
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self)
    }),
    genTypeMembers: (self) => {
        const selfInstance = new DataEntity(assertDefined(self.asDataType));

        return {
            ...genCommonTypeMembers(self),
            __add: new FuncType([self, self], self),
            __div: new FuncType([self, IntType], self),
            __div1: new FuncType([self, DurationType], IntType),
            __geq: new FuncType([self, DurationType], BoolType),
            __gt: new FuncType([self, DurationType], BoolType),
            __leq: new FuncType([self, DurationType], BoolType),
            __lt: new FuncType([self, DurationType], BoolType),
            __mod: new FuncType([self, self], self),
            __mul: new FuncType([self, IntType], self),
            __sub: new FuncType([self, self], self),
            new: new FuncType([IntType], self),
            SECOND: selfInstance,
            MINUTE: selfInstance,
            HOUR: selfInstance,
            DAY: selfInstance,
            WEEK: selfInstance
        }
    }
});

/**
 * Builtin Time type. Opaque alias of Int representing milliseconds since 1970
 * @internal
 * @type {DataType}
 */
export var TimeType = new GenericType({
    name: "Time",
    offChainType: Time,
    genTypeDetails: (self) => ({
        inputType: `number | bigint | string | Date`,
        outputType: `Date`,
        internalType: {
            type: "Time"
        }
    }),
    jsToUplc: async (obj, helpers) => {
        return Time.fromProps(obj)._toUplcData();
    },
    uplcToJs: async (data, helpers) => {
        return new Date(Number(Time.fromUplcData(data).value));
    },
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self)
    }),
    genTypeMembers: (self) => ({
        ...genCommonTypeMembers(self),
        __add: new FuncType([self, DurationType], TimeType),
        __geq: new FuncType([self, TimeType], BoolType),
        __gt: new FuncType([self, TimeType], BoolType),
        __leq: new FuncType([self, TimeType], BoolType),
        __lt: new FuncType([self, TimeType], BoolType),
	    __sub: new FuncType([self, TimeType], DurationType),
        __sub1: new FuncType([self, DurationType], TimeType),
        new: new FuncType([IntType], self)
    })
});

/**
 * Builtin TimeRange type
 * @internal
 * @type {DataType}
 */
export var TimeRangeType = new GenericType({
    name: "TimeRange",
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        contains: new FuncType([TimeType], BoolType),
        start: TimeType,
        end: TimeType,
        is_before: new FuncType([TimeType], BoolType),
        is_after: new FuncType([TimeType], BoolType)
    }),
    genTypeMembers: (self) => {
        const selfInstance = new DataEntity(assertDefined(self.asDataType));

        return {
            ...genCommonTypeMembers(self),
            new: new FuncType([TimeType, TimeType], self),
            ALWAYS: selfInstance,
            NEVER: selfInstance,
            from: new FuncType([TimeType], self),
            to: new FuncType([TimeType], self)
        };
    }
});


//////////////////////////////
// Section 19: Eval hash types
//////////////////////////////

/**
 * @param {Type} self
 * @returns {InstanceMembers}
 */
function genHashInstanceMembers(self) {
    return {
        ...genCommonInstanceMembers(self),
        bytes: ByteArrayType
    };
}

/**
 * @param {Type} self
 * @returns {TypeMembers}
 */
function genHashTypeMembers(self) {
    return {
        ...genCommonTypeMembers(self),
        __geq: new FuncType([self, self], BoolType),
        __gt: new FuncType([self, self], BoolType),
        __leq: new FuncType([self, self], BoolType),
        __lt: new FuncType([self, self], BoolType),
        new: new FuncType([ByteArrayType], self)
    };
}

/**
 * @param {HeliosDataClass<HeliosData>} offchainType 
 */
function genHashTypeProps(offchainType) {
    return {
        genTypeDetails: (self) => ({
            inputType: `number[] | string | helios.${offchainType.name}`,
            outputType: `helios.${offchainType.name}`,
            internalType: {
                type: offchainType.name
            }
        }),
        jsToUplc: async (obj, helpers) => {
            if (obj instanceof offchainType) {
                return obj._toUplcData();
            } else {
                const bytes = Array.isArray(obj) ? obj : hexToBytes(obj);

                return new ByteArrayData(bytes);
            }
        },
        uplcToJs: async (data, helpers) => {
            return new offchainType(data.bytes);
        }
    }
}

/**
 * @internal
 * @implements {DataType}
 */
export class ScriptHashType extends GenericType {
    /**
     * 
     * @param {null | string } name 
     * @param {null | HeliosDataClass<HeliosData>} offChainType 
     */
    constructor(name = null, offChainType = null) {
        if (offChainType && name) {
            super({
                ...genHashTypeProps(offChainType),
                name: name,
                offChainType: offChainType,
                genInstanceMembers: genHashInstanceMembers,
                genTypeMembers: (self) => ({
                    ...genHashTypeMembers(self),
                    from_script_hash: new FuncType([scriptHashType], self)
                })
            });
        } else {
            assert(name === null);

            super({
                name: "ScriptHash",
                genInstanceMembers: (self) => ({
                    ...genCommonInstanceMembers(self)
                }),
                genTypeMembers: (self) => ({
                    ...genCommonTypeMembers(self)
                })
            });
        }
    }
}

/**
 * @internal
 * @type {DataType}
 */
export const scriptHashType = new ScriptHashType();

/**
 * @internal
 * @type {DataType}
 */
export const DatumHashType = new GenericType({
    ...genHashTypeProps(DatumHash),
    name: "DatumHash",
    offChainType: DatumHash,
    genInstanceMembers: genHashInstanceMembers,
    genTypeMembers: genHashTypeMembers
});

/**
 * @internal
 * @type {ScriptHashType}
 */
export const MintingPolicyHashType = new ScriptHashType("MintingPolicyHash", MintingPolicyHash);

/**
 * Builtin PubKey type
 * @internal
 * @type {DataType}
 */
export const PubKeyType = new GenericType({
    name: "PubKey",
    offChainType: PubKey,
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        verify: new FuncType([ByteArrayType, ByteArrayType], BoolType)
    }),
    genTypeMembers: (self) => ({
        ...genCommonTypeMembers(self),
        new: new FuncType([ByteArrayType], self)
    })
});

/**
 * Builtin PubKeyHash type
 * @internal
 * @type {DataType}
 */
export const PubKeyHashType = new GenericType({
    ...genHashTypeProps(PubKeyHash),
    name: "PubKeyHash",
    offChainType: PubKeyHash,
    genInstanceMembers: genHashInstanceMembers,
    genTypeMembers: genHashTypeMembers
});

/**
 * Builtin StakingHash type
 * @internal
 * @type {DataType}
 */
export const StakingHashType = new GenericType({
    name: "StakingHash",
    genInstanceMembers: genCommonInstanceMembers,
    genTypeMembers: (self) => ({
        StakeKey: StakingHashStakeKeyType,
        Validator: StakingHashValidatorType,
        new_stakekey: new FuncType([PubKeyHashType], StakingHashStakeKeyType),
        new_validator: new FuncType([StakingValidatorHashType], StakingHashValidatorType)
    })
});

/**
 * @internal
 * @type {EnumMemberType}
 */
export const StakingHashStakeKeyType = new GenericEnumMemberType({
    name: "StakeKey",
    constrIndex: 0,
    parentType: StakingHashType,
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        hash: PubKeyHashType
    }),
    genTypeMembers: (self) => ({
        ...genCommonEnumTypeMembers(self, StakingHashType)
    })
});

/**
 * @internal
 * @type {EnumMemberType}
 */
export const StakingHashValidatorType = new GenericEnumMemberType({
    name: "Validator",
    constrIndex: 1,
    parentType: StakingHashType,
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        hash: StakingValidatorHashType
    }),
    genTypeMembers: (self) => ({
        ...genCommonEnumTypeMembers(self, StakingHashType)
    })
});

/**
 * @internal
 * @type {ScriptHashType}
 */
export const StakingValidatorHashType = new ScriptHashType("StakingValidatorHash", StakingValidatorHash);

/**
 * @internal
 * @type {ScriptHashType}
 */
export const ValidatorHashType = new ScriptHashType("ValidatorHash", ValidatorHash);



///////////////////////////////
// Section 20: Eval money types
///////////////////////////////

/**
 * Builtin AssetClass type
 * @internal
 * @type {DataType}
 */
export const AssetClassType = new GenericType({
    name: "AssetClass",
    offChainType: AssetClass,
    genTypeDetails: (self) => ({
        inputType: "string | {mph: number[] | string | helios.MintingPolicyHash, tokenName: number[] | string} | helios.AssetClass",
        outputType: "helios.AssetClass",
        internalType: {
            type: "AssetClass"
        }
    }),
    jsToUplc: async (obj, helpers) => {
        return AssetClass.fromProps(obj)._toUplcData();
    },
    uplcToJs: async (data, helpers) => {
        return AssetClass.fromUplcData(data);
    },
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        mph: MintingPolicyHashType,
        token_name: ByteArrayType
    }),
    genTypeMembers: (self) => {
        const selfInstance = new DataEntity(assertDefined(self.asDataType));

        return {
            ...genCommonTypeMembers(self),
            ADA: selfInstance,
            new: new FuncType([MintingPolicyHashType, ByteArrayType], self),
            __geq: new FuncType([self, self], BoolType),
            __gt: new FuncType([self, self], BoolType),
            __leq: new FuncType([self, self], BoolType),
            __lt: new FuncType([self, self], BoolType)
        }
    }
});


/**
 * Builtin money Value type
 * @internal
 * @type {DataType}
 */
export const ValueType = new GenericType({
    name: "Value",
    offChainType: Value,
    genTypeDetails: (self) => ({
        inputType: `number | bigint | {lovelace?: number | bigint, assets: [string, number | bigint][] | [number[] | string | helios.MintingPolicyHash, [number[] | string, number | bigint][]][] | helios.Assets} | helios.Value`,
        outputType: `helios.Value`,
        internalType: {
            type: "Value"
        }
    }),
    jsToUplc: async (obj, helpers) => {
        return Value.fromProps(obj)._toUplcData();
    },
    uplcToJs: async (data, helpers) => {
        return Value.fromUplcData(data);
    },
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        contains: new FuncType([self], BoolType),
        contains_policy: new FuncType([MintingPolicyHashType], BoolType),
        get: new FuncType([AssetClassType], IntType),
        get_assets: new FuncType([], ValueType),
        get_lovelace: new FuncType([], IntType),
        get_policy: new FuncType([MintingPolicyHashType], MapType$(ByteArrayType, IntType)),
        get_safe: new FuncType([AssetClassType], IntType),
        is_zero: new FuncType([], BoolType),
        to_map: new FuncType([], MapType$(MintingPolicyHashType, MapType$(ByteArrayType, IntType))),
        value: self // so that Value implements Valuable itself as well
    }),
    genTypeMembers: (self) => {
        const selfInstance = new DataEntity(assertDefined(self.asDataType));

        return {
            ...genCommonTypeMembers(self),
            __add: new FuncType([self, self], self),
            __div: new FuncType([self, IntType], ValueType),
            __geq: new FuncType([self, ValueType], BoolType),
            __gt: new FuncType([self, ValueType], BoolType),
            __leq: new FuncType([self, ValueType], BoolType),
            __lt: new FuncType([self, ValueType], BoolType),
            __mul: new FuncType([self, IntType], ValueType),
            __sub: new FuncType([self, self], self),
            from_map: new FuncType([MapType$(MintingPolicyHashType, MapType$(ByteArrayType, IntType))], self),
            lovelace: new FuncType([IntType], self),
            new: new FuncType([AssetClassType, IntType], self),
            sum: (() => {
                const a = new Parameter("a", `${FTPP}0`, new ValuableTypeClass());
                return new ParametricFunc([a], new FuncType([ListType$(a.ref)], self));
            })(),
            ZERO: selfInstance
        }
    }
});

/**
 * @internal
 * @implements {TypeClass}
 */
export class ValuableTypeClass extends DefaultTypeClass {
	/**
	 * @param {Type} impl
	 * @returns {TypeClassMembers}
	 */
	genTypeMembers(impl) {
		return {
            ...super.genTypeMembers(impl)
        };
	}

	/**	
	 * @param {Type} impl
	 * @returns {TypeClassMembers}
	 */
	genInstanceMembers(impl) {
		return {
            ...super.genInstanceMembers(impl),
            value: ValueType
		};
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return "Valuable";
	}
}


////////////////////////////
// Section 21: Eval tx types
////////////////////////////

/**
 * Buitin Address type
 * @internal
 * @type {DataType}
 */
export const AddressType = new GenericType({
    name: "Address",
    offChainType: Address,
    genTypeDetails: (self) => ({
        inputType: "string | helios.Address",
        outputType: "helios.Address",
        internalType: {
            type: "Address"
        }
    }),
    jsToUplc: async (obj, helpers) => {
        return (Address.fromProps(obj))._toUplcData();
    },
    uplcToJs: async (data, helpers) => {
        return Address.fromUplcData(data);
    },
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        credential: CredentialType,
        staking_credential: OptionType$(StakingCredentialType),
        to_bytes: new FuncType([], ByteArrayType),
        to_hex: new FuncType([], StringType)
    }),
    genTypeMembers: (self) => ({
        ...genCommonTypeMembers(self),
        new: new FuncType([CredentialType, OptionType$(StakingCredentialType)], self),
        new_empty: new FuncType([], self),
        from_bytes: new FuncType([ByteArrayType], self),
        from_hex: new FuncType([StringType], self)
    })
});

/**
 * @internal
 * @type {DataType}
 */
export const DCertType = new GenericType({
    name: "DCert",
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self)
    }),
    genTypeMembers: (self) => ({
        ...genCommonTypeMembers(self),
        Delegate: DCertDelegateType,
        Deregister: DCertDeregisterType,
        Register: DCertRegisterType,
        RegisterPool: DCertRegisterPoolType,
        RetirePool: DCertRetirePoolType,
        new_delegate: new FuncType([StakingCredentialType, PubKeyHashType], DCertDelegateType),
        new_deregister: new FuncType([StakingCredentialType], DCertDeregisterType),
        new_register: new FuncType([StakingCredentialType], DCertRegisterType),
        new_register_pool: new FuncType([PubKeyHashType, PubKeyHashType], DCertRegisterPoolType),
        new_retire_pool: new FuncType([PubKeyHashType, IntType], DCertRetirePoolType)
    })
});

/**
 * @internal
 * @type {EnumMemberType}
 */
const DCertDelegateType = new GenericEnumMemberType({
    name: "Delegate",
    constrIndex: 2,
    parentType: DCertType,
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        delegator: StakingCredentialType,
		pool_id: PubKeyHashType
    }),
    genTypeMembers: (self) => ({
        ...genCommonEnumTypeMembers(self, DCertType)
    })
});

/**
 * @internal
 * @type {EnumMemberType}
 */
const DCertDeregisterType = new GenericEnumMemberType({
    name: "Deregister",
    constrIndex: 1,
    parentType: DCertType,
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        credential: StakingCredentialType
    }),
    genTypeMembers: (self) => ({
        ...genCommonEnumTypeMembers(self, DCertType)
    })
});

/**
 * @internal
 * @type {EnumMemberType}
 */
const DCertRegisterType = new GenericEnumMemberType({
    name: "Register",
    constrIndex: 0,
    parentType: DCertType,
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        credential: StakingCredentialType
    }),
    genTypeMembers: (self) => ({
        ...genCommonEnumTypeMembers(self, DCertType)
    })
});

/**
 * @internal
 * @type {EnumMemberType}
 */
const DCertRegisterPoolType = new GenericEnumMemberType({
    name: "RegisterPool",
    constrIndex: 3,
    parentType: DCertType,
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        pool_id: PubKeyHashType,
        pool_vrf: PubKeyHashType
    }),
    genTypeMembers: (self) => ({
        ...genCommonEnumTypeMembers(self, DCertType)
    })
});

/**
 * @internal
 * @type {EnumMemberType}
 */
const DCertRetirePoolType = new GenericEnumMemberType({
    name: "RetirePool",
    constrIndex: 4,
    parentType: DCertType,
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        pool_id: PubKeyHashType,
        epoch: IntType
    }),
    genTypeMembers: (self) => ({
        ...genCommonEnumTypeMembers(self, DCertType)
    })
});


/**
 * Builtin Credential type
 * @internal
 * @type {DataType}
 */
export const CredentialType = new GenericType({
    name: "Credential",
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
    }),
    genTypeMembers: (self) => ({
        ...genCommonTypeMembers(self),
        PubKey: CredentialPubKeyType,
        Validator: CredentialValidatorType,
        new_pubkey: new FuncType([PubKeyHashType], CredentialPubKeyType),
        new_validator: new FuncType([ValidatorHashType], CredentialValidatorType)
    })
});


/**
 * Builtin Credential::PubKey
 */
const CredentialPubKeyType = new GenericEnumMemberType({
    name: "PubKey",
    constrIndex: 0,
    fieldNames: ["hash"],
    parentType: CredentialType,
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        hash: PubKeyHashType
    }),
    genTypeMembers: (self) => ({
        ...genCommonEnumTypeMembers(self, CredentialType)
    })
});

/**
 * Builtin Credential::Validator type
 */
const CredentialValidatorType = new GenericEnumMemberType({
    name: "Validator",
    constrIndex: 1,
    fieldNames: ["hash"],
    parentType: CredentialType,
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        hash: ValidatorHashType
    }),
    genTypeMembers: (self) => ({
        ...genCommonEnumTypeMembers(self, CredentialType)
    })
});

/**
 * @internal
 * @type {DataType}
 */
export const OutputDatumType = new GenericType({
    name: "OutputDatum",
    path: "__helios__outputdatum",
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        get_inline_data: new FuncType([], RawDataType)
    }),
    genTypeMembers: (self) => ({
        ...genCommonTypeMembers(self),
        Hash: OutputDatumHashType,
        Inline: OutputDatumInlineType,
        None: OutputDatumNoneType,
        new_hash: new FuncType([DatumHashType], OutputDatumHashType),
		new_inline: (() => {
            const a = new Parameter("a", `${FTPP}0`, new DefaultTypeClass());

            return new ParametricFunc([a], new FuncType([a.ref], OutputDatumInlineType))
        })(),
        new_none: new FuncType([], OutputDatumNoneType)
    })
});

/**
 * @internal
 * @type {EnumMemberType}
 */
const OutputDatumHashType = new GenericEnumMemberType({
    name: "Hash",
    constrIndex: 1,
    parentType: OutputDatumType,
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        hash: DatumHashType
    }),
    genTypeMembers: (self) => ({
        ...genCommonEnumTypeMembers(self, OutputDatumType)
    })
});

/**
 * @internal
 * @type {EnumMemberType}
 */
const OutputDatumInlineType = new GenericEnumMemberType({
    name: "Inline",
    constrIndex: 2,
    parentType: OutputDatumType,
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        data: RawDataType
    }),
    genTypeMembers: (self) => ({
        ...genCommonEnumTypeMembers(self, OutputDatumType)
    })
});

/**
 * @internal
 * @type {EnumMemberType}
 */
const OutputDatumNoneType = new GenericEnumMemberType({
    name: "None",
    constrIndex: 0,
    parentType: OutputDatumType,
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self)
    }),
    genTypeMembers: (self) => ({
        ...genCommonEnumTypeMembers(self, OutputDatumType)
    })
});

/**
 * Base class for ScriptContext, ContractContext, Scripts and other "macro"-types
 * @internal
 */
export class MacroType extends Common {
    /**
     * @type {string[]}
     */
    get fieldNames() {
        return [];
    }

    /**
     * @type {InstanceMembers}
     */
    get instanceMembers() {
        throw new Error("not yet implemented");
    }

    /**
     * @type {string}
     */
    get name() {
        throw new Error("not yet implemented");
    }

    /**
     * @type {null | HeliosDataClass<HeliosData>}
     */
    get offChainType() {
        return null;
    }

    /**
     * @type {string}
     */
    get path() {
        throw new Error("not yet implemented");
    }

	/**
	 * @type {TypeMembers}
	 */
	get typeMembers() {
        return {};
    }

    /**
     * @type {DataType}
     */
    get asDataType() {
        return this;
    }

    /**
     * @type {Named}
     */
    get asNamed() {
        return this;
    }

    /**
     * @type {Type}
     */
    get asType() {
        return this;
    }

    /**
     * @param {Site} site 
     * @param {InferenceMap} map 
     * @param {null | Type} type 
     * @returns {Type}
     */
    infer(site, map, type) {
        return this;
    }

    /**
     * @param {Type} other 
     * @returns {boolean}
     */
    isBaseOf(other) {
        throw new Error("not yet implemented");
    }

    /**
     * @returns {string}
     */
	toString() {
		return this.name;
	}

    /**
     * @returns {Typed}
     */
    toTyped() {
        return new DataEntity(this);
    }
}

/**
 * @internal
 * @typedef {{[name: string]: ScriptHashType}} ScriptTypes
 */

/**
 * @internal
 * @implements {DataType}
 */
export class ScriptsType extends MacroType {
    /**
     * @type {{[name: string]: Typed}}
     */
    #scripts;

    /**
     * @param {ScriptTypes} scripts 
     */
    constructor(scripts) {
        super();

        this.#scripts = {};
        
        for (let k in scripts) {
            this.#scripts[k] = scripts[k].toTyped();
        }
    }

    /**
     * @type {InstanceMembers}
     */
     get instanceMembers() {
        return {};
    }

    /**
     * @type {TypeMembers}
     */
    get typeMembers() {
        return {
            ...this.#scripts
        };
    }

    /**
     * @type {string}
     */
    get name() {
        return "Scripts";
    }

    /**
     * @type {string}
     */
    get path() {
        return "__helios__scripts";
    }

    /**
     * @param {Type} other 
     * @returns {boolean}
     */
    isBaseOf(other) {
        return other instanceof ScriptsType;
    }

    /**
     * @returns {boolean}
     */
    isEmpty() {
        return Object.keys(this.#scripts).length == 0;
    }
}

/**
 * Builtin ScriptContext type
 * @internal
 * @implements {DataType}
 */
export class ScriptContextType extends MacroType {
	constructor() {
		super();
	}

    /**
     * @type {string}
     */
    get name() {
        return "ScriptContext";
    }

    /**
	 * @type {InstanceMembers}
	 */
	get instanceMembers() {
        const members = {
            ...genCommonInstanceMembers(this),
            get_current_minting_policy_hash: new FuncType([], MintingPolicyHashType),
            get_current_input: new FuncType([], TxInputType),
            get_cont_outputs: new FuncType([], ListType$(TxOutputType)),
            get_current_validator_hash: new FuncType([], ValidatorHashType),
            get_spending_purpose_output_id: new FuncType([], TxOutputIdType),
            get_staking_purpose:new FuncType([], StakingPurposeType),
            get_script_purpose: new FuncType([], ScriptPurposeType),
            tx: TxType
        };
        
        return members;
	}

    /**
     * @type {string}
     */
    get path() {
		return "__helios__scriptcontext";
	}

	/**
	 * @type {TypeMembers}
	 */
	get typeMembers() {
        return {
            ...genCommonTypeMembers(this),
            new_certifying: new FuncType([TxType, DCertType], new ScriptContextType()),
            new_minting: new FuncType([TxType, MintingPolicyHashType], new ScriptContextType()),
            new_rewarding: new FuncType([TxType, StakingCredentialType], new ScriptContextType()),
            new_spending: new FuncType([TxType, TxOutputIdType], new ScriptContextType())
        };
	}

    /**
     * @param {Type} other 
     * @returns {boolean}
     */
    isBaseOf(other) {
        return other instanceof ScriptContextType;
    }
}

/**
 * Builtin ScriptContext type
 * @internal
 * @implements {DataType}
 */
export class ContractContextType extends MacroType {
    constructor() {
        super();
    }

    /**
	 * @type {InstanceMembers}
	 */
	get instanceMembers() {
        return {
            now: new FuncType([], TimeType),
            agent: WalletType,
            network: NetworkType,
            new_tx_builder: new FuncType([], TxBuilderType)
        };
	}

    /**
     * @type {string}
     */
    get name() {
        return "ContractContext";
    }

    /**
     * @type {string}
     */
    get path() {
        return "__helios__contractcontext";
    }

    /**
     * @param {Type} other 
     * @returns {boolean}
     */
    isBaseOf(other) {
        return other instanceof ContractContextType;
    }
}

/**
 * @internal
 */
export const WalletType = new GenericType({
    name: "Wallet",
    genInstanceMembers: (self) => ({
        address: AddressType,
        hash: PubKeyHashType,
        pick: new FuncType([ValueType], ListType$(TxInputType))
    }),
    genTypeMembers: (self) => ({})
});

/**
 * Does this really need to be a class? (i.e. will it be instantiated with some properties)
 * @internal
 */
export const NetworkType = new GenericType({
    name: "Network",
    genInstanceMembers: (self) => ({
        pick: new FuncType([AddressType, ValueType], ListType$(TxInputType)),
        get: new FuncType([TxOutputIdType], TxInputType),
        utxos_at: new FuncType([AddressType], IteratorType$([TxInputType]))
    }),
    genTypeMembers: (self) => ({}),
});

/**
 * Builtin ScriptPurpose type (Minting| Spending| Rewarding | Certifying)
 * @internal
 * @type {DataType}
 */
export const ScriptPurposeType = new GenericType({
    name: "ScriptPurpose",
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self)
    }),
    genTypeMembers: (self) => ({
        ...genCommonTypeMembers(self),
        Certifying: ScriptPurposeCertifyingType,
        Minting: ScriptPurposeMintingType,
        Rewarding: ScriptPurposeTypeRewarding,
        Spending: ScriptPurposeSpendingType,
        new_certifying: new FuncType([DCertType], ScriptPurposeCertifyingType),
        new_minting: new FuncType([MintingPolicyHashType], ScriptPurposeMintingType),
        new_rewarding: new FuncType([StakingCredentialType], ScriptPurposeTypeRewarding),
        new_spending: new FuncType([TxOutputIdType], ScriptPurposeSpendingType), 
    })
}); 

/**
 * Builtin ScriptPurpose::Certifying
 * @internal
 * @type {EnumMemberType}
 */
const ScriptPurposeCertifyingType = new GenericEnumMemberType({
    name: "Certifying",
    constrIndex: 3,
    parentType: ScriptPurposeType,
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        dcert: DCertType
    }),
    genTypeMembers: (self) => ({
        ...genCommonEnumTypeMembers(self, ScriptPurposeType)
    })
});

/**
 * Builtin ScriptPurpose::Minting
 * @internal
 * @type {EnumMemberType}
 */
const ScriptPurposeMintingType = new GenericEnumMemberType({
    name: "Minting",
    constrIndex: 0,
    parentType: ScriptPurposeType,
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        policy_hash: MintingPolicyHashType
    }),
    genTypeMembers: (self) => ({
        ...genCommonEnumTypeMembers(self, ScriptPurposeType)
    })
});

/**
 * Builtin ScriptPurpose::Rewarding
 * @internal
 * @type {EnumMemberType}
 */
const ScriptPurposeTypeRewarding = new GenericEnumMemberType({
    name: "Rewarding",
    constrIndex: 2,
    parentType: ScriptPurposeType,
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        credential: StakingCredentialType
    }),
    genTypeMembers: (self) => ({
        ...genCommonEnumTypeMembers(self, ScriptPurposeType)
    })
});

/**
 * Builtin ScriptPurpose::Spending
 * @internal
 * @type {EnumMemberType}
 */
const ScriptPurposeSpendingType = new GenericEnumMemberType({
    name: "Spending",
    constrIndex: 1,
    parentType: ScriptPurposeType,
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        output_id: TxOutputIdType
    }),
    genTypeMembers: (self) => ({
        ...genCommonEnumTypeMembers(self, ScriptPurposeType)
    })
});

/**
 * Builtin StakingCredential type
 * @internal
 * @type {DataType}
 */
export const StakingCredentialType = new GenericType({
    name: "StakingCredential",
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self)
    }),
    genTypeMembers: (self) => ({
        ...genCommonTypeMembers(self),
        Hash: StakingCredentialHashType,
        Ptr: StakingCredentialPtrType,
        new_hash: new FuncType([StakingHashType], StakingCredentialHashType),
        new_ptr: new FuncType([IntType, IntType, IntType], StakingCredentialPtrType)
    })
});

/**
 * Builtin StakingCredential::Hash
 * @internal
 * @type {EnumMemberType}
 */
const StakingCredentialHashType = new GenericEnumMemberType({
    name: "Hash",
    constrIndex: 0,
    parentType: StakingCredentialType,
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        hash: StakingHashType,
    }),
    genTypeMembers: (self) => ({
        ...genCommonEnumTypeMembers(self, StakingCredentialType)
    })
});

/**
 * Builtin StakingCredential::Ptr
 * @internal
 * @type {EnumMemberType}
 */
const StakingCredentialPtrType = new GenericEnumMemberType({
    name: "Ptr",
    constrIndex: 1,
    parentType: StakingCredentialType,
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self)
    }),
    genTypeMembers: (self) => ({
        ...genCommonEnumTypeMembers(self, StakingCredentialType)
    })
});

/**
 * Builtin StakingPurpose type (Rewarding or Certifying)
 * @internal
 * @type {DataType}
 */
export const StakingPurposeType = new GenericType({
    name : "StakingPurpose",
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self)
    }),
    genTypeMembers: (self) => ({
        ...genCommonTypeMembers(self),
        Certifying: StakingPurposeCertifyingType,
        Rewarding: StakingPurposeRewardingType
    })
});

/**
 * Builtin ScriptPurpose::Minting
 * @internal
 * @type {EnumMemberType}
 */
const StakingPurposeCertifyingType = new GenericEnumMemberType({
    name: "Certifying",
    constrIndex: 3,
    parentType: StakingPurposeType,
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        dcert: DCertType
    }),
    genTypeMembers: (self) => ({
        ...genCommonEnumTypeMembers(self, StakingPurposeType)
    })
});

/**
 * Builtin ScriptPurpose::Minting
 * @internal
 * @type {EnumMemberType}
 */
const StakingPurposeRewardingType = new GenericEnumMemberType({
    name: "Rewarding",
    constrIndex: 2,
    parentType: StakingPurposeType,
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        credential: StakingCredentialType,
    }),
    genTypeMembers: (self) => ({
        ...genCommonEnumTypeMembers(self, StakingPurposeType)
    })
});

/**
 * @internal
 */
export const TxBuilderType = new GenericType({
    name: "TxBuilder",
    path: "__helios__txbuilder",
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        add_output: new FuncType([TxOutputType], self),
        add_outputs: new FuncType([ListType$(TxOutputType)], self),
        add_ref_input: new FuncType([TxInputType], self),
        add_signer: new FuncType([PubKeyHashType], self),
        finalize: new FuncType([], TxType),
        pay: (() => {
            const a = new Parameter("a", `${FTPP}0`, new DefaultTypeClass());
            return new ParametricFunc([a], new FuncType([AddressType, ValueType, a.ref], self));
        })(),
        pay_if_true: (() => {
            const a = new Parameter("a", `${FTPP}0`, new DefaultTypeClass());
            return new ParametricFunc([a], new FuncType([BoolType, AddressType, ValueType, a.ref], self)); 
        })(),
        mint: (() => {
            const a = new Parameter("a", `${FTPP}0`, new DefaultTypeClass());
            return new ParametricFunc([a], new FuncType([ValueType, a.ref], self));
        })(),
        redeem: (() => {
            const a = new Parameter("a", `${FTPP}0`, new DefaultTypeClass());
            return new ParametricFunc([a], new FuncType([TxInputType, a.ref], self));
        })(),
        redeem_many: (() => {
            const a = new Parameter("a", `${FTPP}0`, new DefaultTypeClass());
            return new ParametricFunc([a], new FuncType([ListType$(TxInputType), a.ref], self));
        })(),
        spend: new FuncType([TxInputType], self),
        spend_many: new FuncType([ListType$(TxInputType)], self)
    }),
    genTypeMembers: (self) => ({
        ...genCommonTypeMembers(self)
    })
});

/**
 * Builtin Tx type
 * @internal
 * @type {DataType}
 */
export const TxType = new GenericType({
    name: "Tx",
    jsToUplc: async (obj, helpers) => {
        return helpers["Tx"](obj)
        //return obj.toTxData(assertDefined(networkParams));
    },
    uplcToJs: async (data, helpers) => {
        return helpers["Tx"](data);
        //return TxId.fromUplcData(data.fields[11]);
    },
    genTypeDetails: (self) => ({
        inputType: "helios.Tx",
        outputType: "helios.Tx",
        internalType: {
            type: "Tx"
        }
    }),
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        inputs: ListType$(TxInputType),
		ref_inputs: ListType$(TxInputType),
        outputs: ListType$(TxOutputType),
        fee: ValueType,
        minted: ValueType,
        dcerts: ListType$(DCertType),
        withdrawals: MapType$(StakingCredentialType, IntType),
		time_range: TimeRangeType,
		signatories: ListType$(PubKeyHashType),
        redeemers: MapType$(ScriptPurposeType, RawDataType),
        datums: MapType$(DatumHashType, RawDataType),
        id: TxIdType,
        find_datum_hash: (() => {
            const a = new Parameter("a", `${FTPP}0`, new DefaultTypeClass());

            return new ParametricFunc([a], new FuncType([a.ref], DatumHashType))
        })(),
        get_datum_data: new FuncType([TxOutputType], RawDataType),
        outputs_sent_to: new FuncType([PubKeyHashType], ListType$(TxOutputType)),
        outputs_sent_to_datum: (() => {
            const a = new Parameter("a", `${FTPP}0`, new DefaultTypeClass());

            return new ParametricFunc([a], new FuncType([PubKeyHashType, a.ref, BoolType], ListType$(TxOutputType)))
        })(),
        outputs_locked_by: new FuncType([ValidatorHashType], ListType$(TxOutputType)),
        outputs_locked_by_datum: (() => {
            const a = new Parameter("a", `${FTPP}0`, new DefaultTypeClass());

            return new ParametricFunc([a], new FuncType([ValidatorHashType, a.ref, BoolType], ListType$(TxOutputType)))
        })(),
        value_sent_to: new FuncType([PubKeyHashType], ValueType),
        value_sent_to_datum: (() => {
            const a = new Parameter("a", `${FTPP}0`, new DefaultTypeClass());

            return new ParametricFunc([a], new FuncType([PubKeyHashType, a.ref, BoolType], ValueType));
        })(),
        value_locked_by: new FuncType([ValidatorHashType], ValueType),
        value_locked_by_datum: (() => {
            const a = new Parameter("a", `${FTPP}0`, new DefaultTypeClass());

            return new ParametricFunc([a], new FuncType([ValidatorHashType, a.ref, BoolType], ValueType));
        })(),
        value_paid_to: (() => {
            const a = new Parameter("a", `${FTPP}0`, new DefaultTypeClass());

            return new ParametricFunc([a], new FuncType([AddressType, a.ref], ValueType));
        })(),
        is_signed_by: new FuncType([PubKeyHashType], BoolType)
    }),
    genTypeMembers: (self) => ({
        ...genCommonTypeMembers(self),
        new: (() => {
            const a = new Parameter("a", `${FTPP}0`, new DefaultTypeClass());
            const b = new Parameter("b", `${FTPP}1`, new DefaultTypeClass());
            
            return new ParametricFunc([a, b], new FuncType([
                ListType$(TxInputType), // 0
                ListType$(TxInputType), // 1
                ListType$(TxOutputType), // 2
                ValueType, // 3
                ValueType, // 4
                ListType$(DCertType), // 5
                MapType$(StakingCredentialType, IntType), // 6
                TimeRangeType, // 7
                ListType$(PubKeyHashType), // 8
                MapType$(ScriptPurposeType, a.ref), // 9
                MapType$(DatumHashType, b.ref), // 10
                TxIdType // 11
            ], self))
        })()
    })
});

/**
 * Builtin TxId type
 * @internal
 * @type {DataType}
 */
export const TxIdType = new GenericType({
    name: "TxId",
    offChainType: TxId,
    genTypeDetails: (self) => ({
        inputType: `number[] | string | helios.TxId`,
        outputType: `helios.TxId`,
        internalType: {
            type: "TxId"
        }
    }),
    jsToUplc: async (obj, helpers) => {
        return TxId.fromProps(obj)._toUplcData();
    },
    uplcToJs: async (data, helpers) => {
        return TxId.fromUplcData(data);
    },
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        bytes: ByteArrayType
    }),
    genTypeMembers: (self) => ({
        ...genCommonTypeMembers(self),
        __geq: new FuncType([self, self], BoolType),
        __gt: new FuncType([self, self], BoolType),
        __leq: new FuncType([self, self], BoolType),
        __lt: new FuncType([self, self], BoolType),
        new: new FuncType([ByteArrayType], self)
    })
});


/**
 * Builtin TxInput type
 * @internal
 * @type {DataType}
 */
export const TxInputType = new GenericType({
    name: "TxInput",
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        output_id: TxOutputIdType,
        output: TxOutputType,
        address: AddressType,
        value: ValueType,
        datum: OutputDatumType
    }),
    genTypeMembers: (self) => ({
        ...genCommonTypeMembers(self),
        new: new FuncType([TxOutputIdType, TxOutputType], self)
    })
});

/**
 * Builtin TxOutput type
 * @internal
 * @type {DataType}
 */
export const TxOutputType = new GenericType({
    name: "TxOutput",
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        address: AddressType,
        value: ValueType,
	    datum: OutputDatumType,
        ref_script_hash: OptionType$(scriptHashType)
    }),
    genTypeMembers: (self) => ({
        ...genCommonTypeMembers(self),
        new: new FuncType([AddressType, ValueType, OutputDatumType], self)
    })
});

/**
 * Builtin TxOutputId type
 * @internal
 * @type {DataType}
 */
export const TxOutputIdType = new GenericType({
    name: "TxOutputId",
    genTypeDetails: (self) => ({
        inputType: "{txId: number[] | string | helios.TxId, utxoId: number | bigint} | helios.TxOutputId",
        outputType: "helios.TxOutputId",
        internalType: {
            type: "TxOutputId"
        }
    }),
    jsToUplc: async (obj, helpers) => {
        return TxOutputId.fromProps(obj)._toUplcData();
    },
    uplcToJs: async (data, helpers) => {
        return TxOutputId.fromUplcData(data);
    },
    genInstanceMembers: (self) => ({
        ...genCommonInstanceMembers(self),
        tx_id: TxIdType,
        index: IntType
    }),
    genTypeMembers: (self) => ({
        ...genCommonTypeMembers(self),
        __geq: new FuncType([self, TxOutputIdType], BoolType),
        __gt: new FuncType([self, TxOutputIdType], BoolType),
        __leq: new FuncType([self, TxOutputIdType], BoolType),
        __lt: new FuncType([self, TxOutputIdType], BoolType),
        new: new FuncType([TxIdType, IntType], TxOutputIdType)
    })
});


/////////////////////
// Section 22: Scopes
/////////////////////

/**
 * @internal
 * @type {{[name: string]: DataType}}
 */
export const builtinTypes = {
	Address: AddressType,
	AssetClass: AssetClassType,
	Bool: BoolType,
	ByteArray: ByteArrayType,
	DCert: DCertType,
	Credential: CredentialType,
	DatumHash: DatumHashType,
	Data: RawDataType,
	Duration: DurationType,
	Int: IntType,
	MintingPolicyHash: MintingPolicyHashType,
	OutputDatum: OutputDatumType,
	PubKey: PubKeyType,
	PubKeyHash: PubKeyHashType,
	Real: RealType,
	ScriptHash: scriptHashType,
    ScriptPurpose: ScriptPurposeType,
    StakingCredential: StakingCredentialType,
    StakingHash: StakingHashType,
    StakingPurpose: StakingPurposeType,
    StakingValidatorHash: StakingValidatorHashType,
	String: StringType,
    Time: TimeType,
    TimeRange: TimeRangeType,
    Tx: TxType,
    TxId: TxIdType,
    TxInput: TxInputType,
    TxOutput: TxOutputType,
    TxOutputId: TxOutputIdType,
	ValidatorHash: ValidatorHashType,
    Value: ValueType
};

/**
 * GlobalScope sits above the top-level scope and contains references to all the builtin Values and Types
 * @internal
 */
export class GlobalScope {
	/**
	 * @type {[Word, EvalEntity][]}
	 */
	#values;

	constructor() {
		this.#values = [];
	}

	/**
	 * Checks if scope contains a name
	 * @param {Word} name 
	 * @returns {boolean}
	 */
	has(name) {
		for (let pair of this.#values) {
			if (pair[0].toString() == name.toString()) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Sets a global name, doesn't check for uniqueness
	 * Called when initializing GlobalScope
	 * @param {string | Word} name
	 * @param {EvalEntity} value
	 */
	set(name, value) {
		/** @type {Word} */
		let nameWord = !(name instanceof Word) ? Word.new(name) : name;

		this.#values.push([nameWord, value]);
	}

	/**
	 * Gets a named value from the scope.
	 * Throws an error if not found.
	 * @param {Word} name 
	 * @returns {null | EvalEntity}
	 */
	get(name) {
		for (let pair of this.#values) {
			if (pair[0].toString() == name.toString()) {
				return pair[1];
			}
		}

		name.referenceError(`'${name.toString()}' undefined`);
		return null;
	}

	/**
	 * @returns {boolean}
	 */
	isStrict() {
		throw new Error("should've been returned be TopScope");
	}

	/**
	 * Initialize the GlobalScope with all the builtins
	 * @param {ScriptTypes} scriptTypes - types of all the scripts in a contract/ensemble
	 * @returns {GlobalScope}
	 */
	static new(scriptTypes = {}) {
		let scope = new GlobalScope();

		// List (aka '[]'), Option, and Map types are accessed through special expressions

		// fill the global scope with builtin types
		for (let name in builtinTypes) {
			scope.set(name, builtinTypes[name])
		}

		scope.set("Any",         		  new AnyTypeClass());
		scope.set("Valuable",             new ValuableTypeClass());

		if (Object.keys(scriptTypes).length > 0) {
			scope.set("Scripts",     new ScriptsType(scriptTypes));
		}

        scope.set("ScriptContext",    new ScriptContextType());
		scope.set("ContractContext",  new ContractContextType());
		scope.set("TxBuilder",        TxBuilderType);
		scope.set("Wallet",           WalletType);
		scope.set("Network",          NetworkType);

        // builtin functions
        scope.set("assert",               AssertFunc);
		scope.set("error",                ErrorFunc);
        scope.set("print",                PrintFunc);

		return scope;
	}

	/**
	 * @param {(name: string, type: Type) => void} callback 
	 */
	loopTypes(callback) {
		for (let [k, v] of this.#values) {
			if (v.asType) {
				callback(k.value, v.asType);
			}
		}
	}
}

/**
 * User scope
 * @internal
 * @implements {EvalEntity}
 */
export class Scope extends Common {
	/** @type {GlobalScope | Scope} */
	#parent;

	/** 
	 * TopScope can elverage the #values to store ModuleScopes
	 * @type {[Word, (EvalEntity | Scope), boolean][]} 
	 */
	#values;

	/**
	 * @type {boolean}
	 */
	#allowShadowing;

	/**
	 * @param {GlobalScope | Scope} parent 
	 * @param {boolean} allowShadowing
	 */
	constructor(parent, allowShadowing = false) {
		super()
		this.#parent = parent;
		this.#values = []; // list of pairs
		this.#allowShadowing = allowShadowing;
	}

	/**
	 * @type {boolean}
	 */
	get allowShadowing() {
		return this.#allowShadowing;
	}

	/**
	 * Used by top-scope to loop over all the statements
	 */
	get values() {
		return this.#values.slice();
	}

	/**
	 * Checks if scope contains a name
	 * @param {Word} name 
	 * @returns {boolean}
	 */
	has(name) {
		for (let pair of this.#values) {
			if (pair[0].toString() == name.toString()) {
				return true;
			}
		}

		if (this.#parent !== null) {
			return this.#parent.has(name);
		} else {
			return false;
		}
	}

	/**
	 * Sets a named value. Throws an error if not unique
	 * @param {Word} name 
	 * @param {EvalEntity | Scope} value 
	 */
	setInternal(name, value, allowShadowing = false) {
		if (value instanceof Scope) {
			assert(name.value.startsWith("__scope__"));
		}

		if (this.has(name)) {
			const prevEntity = this.get(name, true);

			if (allowShadowing && value.asTyped && prevEntity && !(prevEntity instanceof Scope) && prevEntity.asTyped) {
				if (!(prevEntity.asTyped.type.isBaseOf(value.asTyped.type) && value.asTyped.type.isBaseOf(prevEntity.asTyped.type))) {
					name.syntaxError(`'${name.toString()}' already defined`);
				}
			} else {
				name.syntaxError(`'${name.toString()}' already defined`);
			}
		}

		this.#values.push([name, value, false]);
	}

	/**
	 * Sets a named value. Throws an error if not unique
	 * @param {Word} name 
	 * @param {EvalEntity | Scope} value 
	 */
	set(name, value) {
		this.setInternal(name, value, this.#allowShadowing);
	}

	/**
	 * @param {Word} name 
	 */
	remove(name) {
		this.#values = this.#values.filter(([n, _]) => n.value != name.value);
	}

	/**
	 * @param {Word} name 
	 * @returns {null | Scope}
	 */
	getScope(name) {
		assert(!name.value.startsWith("__scope__"));

		const entity = this.get(new Word(name.site, `__scope__${name.value}`));

		if (entity instanceof Scope) {
			return entity;
		} else if (!entity) {
			name.typeError(`expected Scope`);
			return null;
		} else {
			name.typeError(`expected Scope, got ${entity.toString()}`);
			return null;
		}
	}

	/**
	 * Gets a named value from the scope. Throws an error if not found
	 * @param {Word} name 
	 * @param {boolean} dryRun - if false -> don't set used flag
	 * @returns {null | EvalEntity | Scope}
	 */
	get(name, dryRun = false) {
		if (!(name instanceof Word)) {
			name = Word.new(name);
		}

		for (let i = this.#values.length - 1; i >= 0; i--) {
			const [key, entity, _] = this.#values[i];

			if (key.toString() == name.toString()) {
				if (!dryRun) {
					this.#values[i][2] = true;
				}
				return entity;
			}
		}

		if (this.#parent !== null) {
			return this.#parent.get(name, dryRun);
		} else {
			name.referenceError(`'${name.toString()}' undefined`);
			return null;
		}
	}

	/**
	 * @returns {boolean}
	 */
	isStrict() {
		return this.#parent.isStrict();
	}

	/**
	 * Asserts that all named values are user.
	 * Throws an error if some are unused.
	 * Check is only run if we are in strict mode
	 * @param {boolean} onlyIfStrict
	 */
	assertAllUsed(onlyIfStrict = true) {
		if (!onlyIfStrict || this.isStrict()) {
			for (let [name, entity, used] of this.#values) {
				if (!(entity instanceof Scope) && !used) {
					name.referenceError(`'${name.toString()}' unused`);
				}
			}
		}
	}

	/**
	 * @param {Word} name 
	 * @returns {boolean}
	 */
	isUsed(name) {
		for (let [checkName, entity, used] of this.#values) {
			if (name.value == checkName.value && !(entity instanceof Scope)) {
				return used;
			}
		}

		throw new Error(`${name.value} not found`);
	}

	dump() {
		console.log("DUMPING SCOPE", this.#values.length);
		this.#values.forEach(([w, v]) => {
			console.log(w.value, v);
		});
	}

	/**
	 * @param {(name: string, type: Type) => void} callback 
	 */
	loopTypes(callback) {
		this.#parent.loopTypes(callback);

		for (let [k, v] of this.#values) {
			if (v.asType) {
				callback(k.value, v.asType);
			}
		}
	}
}

/**
 * TopScope is a special scope that can contain UserTypes
 * @internal
 */
export class TopScope extends Scope {
	#strict;

	/**
	 * @param {GlobalScope} parent 
	 * @param {boolean} strict
	 */
	constructor(parent, strict = true) {
		super(parent);
		this.#strict = strict;
	}

	/**
	 * Prepends "__scope__" to name before actually setting scope
	 * @param {Word} name 
	 * @param {Scope} value 
	 */
	setScope(name, value) {
		assert(!name.value.startsWith("__scope__"));

		this.set(new Word(name.site, `__scope__${name.value}`), value);
	}

	/**
	 * @param {Word} name 
	 * @param {EvalEntity | Scope} value 
	 */
	set(name, value) {
		super.setInternal(name, value, false);
	}

	/**
	 * @param {boolean} s 
	 */
	setStrict(s) {
		this.#strict = s;
	}

	/**
	 * @returns {boolean}
	 */
	isStrict() {
		return this.#strict;
	}

	/**
	 * @param {Word} name 
	 * @returns {ModuleScope}
	 */
	getModuleScope(name) {
		assert(!name.value.startsWith("__scope__"));

		const maybeModuleScope = this.get(new Word(name.site, `__scope__${name.value}`));

		if (maybeModuleScope instanceof ModuleScope) {
			return maybeModuleScope;
		} else {
			throw new Error("expected ModuleScope");
		}
	}
}

/**
 * @internal
 */
export class ModuleScope extends Scope {
}


/////////////////////////////
// Section 23: IR definitions
/////////////////////////////
/**
 * For collecting test coverage statistics
 * @type {null | ((name: string, count: number) => void)}
 */
var onNotifyRawUsage = null;

/**
 * Set the statistics collector (used by the test-suite)
 * @internal
 * @param {(name: string, count: number) => void} callback 
 */
export function setRawUsageNotifier(callback) {
	onNotifyRawUsage = callback;
}

const RE_BUILTIN = new RegExp("(?<![@[])__helios[a-zA-Z0-9_@[\\]]*", "g");

/**
 * Wrapper for a builtin function (written in IR)
 * @internal
 */
class RawFunc {
	/**
	 * @type {string}
	 */
	#name;

	/**
	 * @type {((ttp: string[], ftp: string[]) => string)}
	 */
	#definition;

	/**
	 * Construct a RawFunc, and immediately scan the definition for dependencies
	 * @param {string} name 
	 * @param {string | ((ttp: string[], ftp: string[]) => string)} definition
	 */
	constructor(name, definition) {
		this.#name = name;
		assert(definition != undefined);
		this.#definition = typeof definition == "string" ? 
			(ttp, ftp) => {
				if (IRParametricName.matches(this.#name)) {
					// TODO: make sure definition is always a function for parametric names
					let pName = IRParametricName.parse(this.#name);
					pName = new IRParametricName(pName.base, ttp, pName.fn, ftp);
					const [def, _] = pName.replaceTemplateNames(new IR(definition)).generateSource();
					return def;
				} else {
					return definition;
				}
			} : 
			definition;	
	}

	/**
	 * @type {string}
	 */
	get name() {
		return this.#name;
	}

	/**
	 * @param {string[]} ttp
	 * @param {string[]} ftp
	 * @returns {IR}
	 */
	toIR(ttp = [], ftp = []) {
		return new IR(replaceTabs(this.#definition(ttp, ftp)));
	}

	/**
	 * Loads 'this.#dependecies' (if not already loaded), then load 'this'
	 * @param {Map<string, RawFunc>} db 
	 * @param {IRDefinitions} dst 
	 * @param {string[]} ttp
	 * @param {string[]} ftp
	 * @returns {void}
	 */
	load(db, dst, ttp = [], ftp = []) {
		if (onNotifyRawUsage !== null) {
			onNotifyRawUsage(this.#name, 1);
		}

		let name = this.#name;
		if (ttp.length > 0 || ftp.length > 0){
			let pName = IRParametricName.parse(name);
			pName = new IRParametricName(pName.base, ttp, pName.fn, ftp);
			name = pName.toString()
		}

		if (dst.has(name)) {
			return;
		} else {
			const ir = this.toIR(ttp, ftp);

			const [def, _] = ir.generateSource();
			const deps = new Set();
			def.match(RE_BUILTIN)?.forEach(match => deps.add(match));

			for (let dep of deps) {
				if (!db.has(dep)) {
					if (IRParametricName.matches(dep)) {
						const pName = IRParametricName.parse(dep);
						const genericName = pName.toTemplate(true);

						let fn = db.get(genericName);

						if (fn) {
							fn.load(db, dst, pName.ttp, pName.ftp);
						} else {
							// TODO: make sure all templated defs use the functional approach instead of the replacement approach
							fn = db.get(pName.toTemplate(false));

							if (fn) {
								const ir = pName.replaceTemplateNames(fn.toIR());
								fn = new RawFunc(dep, ir.toString());
								fn.load(db, dst);
							} else {
								throw new Error(`InternalError: dependency ${dep} not found`);	
							}
						}
					} else {
						throw new Error(`InternalError: dependency ${dep} not found`);
					}				
				} else {
					assertDefined(db.get(dep)).load(db, dst);
				}
			}

			dst.set(name, ir);
		}
	}
}

/**
 * Initializes the db containing all the builtin functions
 * @param {boolean} simplify
 * @param {boolean} isTestnet // needed for Address.to_bytes() and Address.to_hex()
 * @returns {Map<string, RawFunc>}
 */
// only need to wrap these source in IR right at the very end
function makeRawFunctions(simplify, isTestnet = config.IS_TESTNET) {
	/** @type {Map<string, RawFunc>} */
	let db = new Map();

	// local utility functions

	/**
	 * @param {RawFunc} fn 
	 */
	function add(fn) {
		if (db.has(fn.name)) {
			throw new Error(`builtin ${fn.name} duplicate`);
		}
		db.set(fn.name, fn);
	}

	/**
	 * @param {string} ns 
	 */
	function addNeqFunc(ns) {
		add(new RawFunc(`${ns}____neq`, 
		`(self, other) -> {
			__helios__bool____not(${ns}____eq(self, other))
		}`));
	}

	/**
	 * @param {string} ns 
	 */
	function addDataLikeEqFunc(ns) {
		add(new RawFunc(`${ns}____eq`, 
		`(self, other) -> {
			__core__equalsData(${ns}____to_data(self), ${ns}____to_data(other))
		}`));
	}

	/**
	 * @param {string} ns 
	 */
	function addSerializeFunc(ns) {
		add(new RawFunc(`${ns}__serialize`, 
		`(self) -> {
			() -> {
				__core__serialiseData(${ns}____to_data(self))
			}
		}`));
	}

	/**
	 * @param {string} ns 
	 */
	function addIntLikeFuncs(ns) {
		add(new RawFunc(`${ns}____eq`, "__helios__int____eq"));
		add(new RawFunc(`${ns}____neq`, "__helios__int____neq"));
		add(new RawFunc(`${ns}__serialize`, "__helios__int__serialize"));
		add(new RawFunc(`${ns}__from_data`, "__helios__int__from_data"));
		add(new RawFunc(`${ns}__from_data_safe`, "__helios__int__from_data_safe"));
		add(new RawFunc(`${ns}____to_data`, "__helios__int____to_data"));
	}

	/**
	 * @param {string} ns 
	 */
	function addByteArrayLikeFuncs(ns) {
		add(new RawFunc(`${ns}____eq`, "__helios__bytearray____eq"));
		add(new RawFunc(`${ns}____neq`, "__helios__bytearray____neq"));
		add(new RawFunc(`${ns}__serialize`, "__helios__bytearray__serialize"));
		add(new RawFunc(`${ns}__from_data`, "__helios__bytearray__from_data"));
		add(new RawFunc(`${ns}__from_data_safe`, "__helios__bytearray__from_data_safe"));
		add(new RawFunc(`${ns}____to_data`, "__helios__bytearray____to_data"));
		add(new RawFunc(`${ns}____lt`, "__helios__bytearray____lt"));
		add(new RawFunc(`${ns}____leq`, "__helios__bytearray____leq"));
		add(new RawFunc(`${ns}____gt`, "__helios__bytearray____gt"));
		add(new RawFunc(`${ns}____geq`, "__helios__bytearray____geq"));
		add(new RawFunc(`${ns}__new`, `__helios__common__identity`));
		add(new RawFunc(`${ns}__bytes`, "__helios__common__identity"));
		add(new RawFunc(`${ns}__show`, "__helios__bytearray__show"));
	}

	/**
	 * Adds basic auto members to a fully named type
	 * TODO: many types that are currently treated as Data could in fact be treated as something slighly better (eg. lists or pairs)
	 * @param {string} ns 
	 * @param {{
	 *   eq?: string, 
	 *   neq?: string, 
	 *   serialize?: string, 
	 *   from_data?: string, 
	 *   from_data_safe?: string, 
	 *   to_data?: string
	 * }} custom
	 */
	function addDataFuncs(ns, custom = {}) {
		add(new RawFunc(`${ns}____eq`, custom?.eq ?? "__helios__common____eq"));
		add(new RawFunc(`${ns}____neq`, custom?.neq ?? "__helios__common____neq"));
		add(new RawFunc(`${ns}__serialize`, custom?.serialize ?? "__helios__common__serialize"));
		add(new RawFunc(`${ns}__from_data`, custom?.from_data ?? "__helios__common__identity"));
		add(new RawFunc(`${ns}__from_data_safe`, custom?.from_data_safe  ?? `(data) -> {__helios__option__SOME_FUNC(data)}`));
		add(new RawFunc(`${ns}____to_data`, custom?.to_data ?? "__helios__common__identity"));
	}

	/**
	 * Adds basic auto members to a fully named enum type
	 * @param {string} ns 
	 * @param {number} constrIndex
	 */
	function addEnumDataFuncs(ns, constrIndex) {
		add(new RawFunc(`${ns}____eq`, "__helios__common____eq"));
		add(new RawFunc(`${ns}____neq`, "__helios__common____neq"));
		add(new RawFunc(`${ns}__serialize`, "__helios__common__serialize"));
		add(new RawFunc(`${ns}____to_data`, "__helios__common__identity"));
		add(new RawFunc(`${ns}__from_data`, 
		`(data) -> {
			__helios__common__assert_constr_index(data, ${constrIndex})
		}`));
		add(new RawFunc(`${ns}__from_data_safe`,
		`(data) -> {
			__core__chooseData(
				data,
				() -> {
					__core__ifThenElse(
						__core__equalsInteger(${constrIndex}, __core__fstPair(__core__unConstrData__safe(data))),
						() -> {
							__helios__option__SOME_FUNC(data)
						},
						() -> {
							__helios__option__NONE_FUNC
						}
					)()
				},
				() -> {__helios__option__NONE_FUNC},
				() -> {__helios__option__NONE_FUNC},
				() -> {__helios__option__NONE_FUNC},
				() -> {__helios__option__NONE_FUNC}
			)()
		}`));
	}

	/**
	 * Generates the IR needed to unwrap a Plutus-core constrData
	 * @param {string} dataExpr
	 * @param {number} iConstr 
	 * @param {number} iField 
	 * @param {string} errorExpr 
	 * @returns {string}
	 */
	function unData(dataExpr, iConstr, iField, errorExpr = "error(\"unexpected constructor index\")") {
		let inner = "__core__sndPair(pair)";
		for (let i = 0; i < iField; i++) {
			inner = `__core__tailList(${inner})`;
		}

		// deferred evaluation of ifThenElse branches
		return `(pair) -> {__core__ifThenElse(__core__equalsInteger(__core__fstPair(pair), ${iConstr}), () -> {__core__headList(${inner})}, () -> {${errorExpr}})()}(__core__unConstrData(${dataExpr}))`;
	}

	/**
	 * Generates verbose IR for unwrapping a Plutus-core constrData.
	 * If config.DEBUG === false then returns IR without print statement
	 * @param {string} dataExpr
	 * @param {string} constrName
	 * @param {number} iConstr
	 * @param {number} iField
	 * @returns {string}
	 */
	function unDataVerbose(dataExpr, constrName, iConstr, iField) {
		if (!config.DEBUG) {
			return unData(dataExpr, iConstr, iField);
		} else {
			return unData(dataExpr, iConstr, iField, `__helios__error(__core__appendString("bad constr for ${constrName}, want ${iConstr.toString()} but got ", __helios__int__show(__core__fstPair(pair))()))`)
		}
	}

	/**
	 * Generates IR for constructing a list.
	 * By default the result is kept as list, and not converted to data
	 * @param {string[]} args 
	 * @param {boolean} toData 
	 * @returns 
	 */
	function makeList(args, toData = false) {
		let n = args.length;
		let inner = "__core__mkNilData(())";

		for (let i = n - 1; i >= 0; i--) {
			inner = `__core__mkCons(${args[i]}, ${inner})`;
		}

		if (toData) {
			inner = `__core__listData(${inner})`
		}

		return inner;
	}


	// Common builtins
	add(new RawFunc("__helios__common__assert_constr_index",
	`(data, i) -> {
		__core__ifThenElse(
			__core__equalsInteger(__core__fstPair(__core__unConstrData(data)), i),
			() -> {data},
			() -> {__helios__error("unexpected constructor index")}
		)()
	}`));
	add(new RawFunc("__helios__common__identity",
	`(self) -> {self}`));
	add(new RawFunc("__helios__common____eq", "__core__equalsData"));
	add(new RawFunc("__helios__common____neq",
	`(a, b) -> {
		__helios__bool____not(__core__equalsData(a, b))
	}`));
	add(new RawFunc("__helios__common__serialize",
	`(self) -> {
		() -> {
			__core__serialiseData(self)
		}
	}`));
	add(new RawFunc("__helios__common__any",
	`(self, fn) -> {
		(recurse) -> {
			recurse(recurse, self, fn)
		}(
			(recurse, self, fn) -> {
				__core__chooseList(
					self, 
					() -> {false}, 
					() -> {
						__core__ifThenElse(
							fn(__core__headList__safe(self)),
							() -> {true}, 
							() -> {recurse(recurse, __core__tailList__safe(self), fn)}
						)()
					}
				)()
			}
		)
	}`));
	add(new RawFunc("__helios__common__all", 
	`(self, fn) -> {
		(recurse) -> {
			recurse(recurse, self, fn)
		}(
			(recurse, self, fn) -> {
				__core__chooseList(
					self,
					() -> {true},
					() -> {
						__core__ifThenElse(
							fn(__core__headList__safe(self)),
							() -> {recurse(recurse, __core__tailList__safe(self), fn)},
							() -> {false}
						)()
					}
				)()
			}
		)
	}`));
	add(new RawFunc("__helios__common__map",
	`(self, fn, init) -> {
		(recurse) -> {
			recurse(recurse, self, init)
		}(
			(recurse, rem, lst) -> {
				__core__chooseList(
					rem,
					() -> {lst},
					() -> {
						__core__mkCons(
							fn(__core__headList__safe(rem)), 
							recurse(recurse, __core__tailList__safe(rem), lst)
						)
					}
				)()
			}
		)
	}`));
	add(new RawFunc("__helios__common__filter", 
	`(self, fn, nil) -> {
		(recurse) -> {
			recurse(recurse, self, fn)
		}(
			(recurse, self, fn) -> {
				__core__chooseList(
					self, 
					() -> {nil}, 
					() -> {
						(head) -> {
							__core__ifThenElse(
								fn(head),
								() -> {__core__mkCons(head, recurse(recurse, __core__tailList__safe(self), fn))}, 
								() -> {recurse(recurse, __core__tailList__safe(self), fn)}
							)()
						}(__core__headList__safe(self))
					}
				)()
			}
		)
	}`));
	add(new RawFunc("__helios__common__filter_list", 
	`(self, fn) -> {
		__helios__common__filter(self, fn, __helios__common__list_0)
	}`));
	add(new RawFunc("__helios__common__filter_map",
	`(self, fn) -> {
		__helios__common__filter(self, fn, __core__mkNilPairData(()))
	}`));
	add(new RawFunc("__helios__common__find",
	`(self, fn) -> {
		(recurse) -> {
			recurse(recurse, self, fn)
		}(
			(recurse, self, fn) -> {
				__core__chooseList(
					self, 
					() -> {__helios__error("not found")}, 
					() -> {
						(head) -> {
							__core__ifThenElse(
								fn(head), 
								() -> {head}, 
								() -> {recurse(recurse, __core__tailList__safe(self), fn)}
							)()
						}(__core__headList__safe(self))
					}
				)()
			}
		)
	}`));
	add(new RawFunc("__helios__common__find_safe",
	`(self, fn, callback) -> {
		(recurse) -> {
			recurse(recurse, self, fn)
		}(
			(recurse, self, fn) -> {
				__core__chooseList(
					self, 
					() -> {__core__constrData(1, __helios__common__list_0)}, 
					() -> {
						(head) -> {
							__core__ifThenElse(
								fn(head), 
								() -> {__core__constrData(0, __helios__common__list_1(callback(head)))}, 
								() -> {recurse(recurse, __core__tailList__safe(self), fn)}
							)()
						}(__core__headList__safe(self))
					}
				)()
			}
		)
	}`));
	add(new RawFunc("__helios__common__fold",
	`(self, fn, z) -> {
		(recurse) -> {
			recurse(recurse, self, z)
		}(
			(recurse, self, z) -> {
				__core__chooseList(
					self, 
					() -> {z}, 
					() -> {recurse(recurse, __core__tailList__safe(self), fn(z, __core__headList__safe(self)))}
				)()
			}
		)
	}`));
	add(new RawFunc("__helios__common__fold_lazy",
	`(self, fn, z) -> {
		(recurse) -> {
			recurse(recurse, self)
		}(
			(recurse, self) -> {
				__core__chooseList(
					self, 
					() -> {z}, 
					() -> {fn(__core__headList__safe(self), () -> {recurse(recurse, __core__tailList__safe(self))})}
				)()
			}
		)
	}`));
	add(new RawFunc("__helios__common__insert_in_sorted",
	`(x, lst, comp) -> {
		(recurse) -> {
			recurse(recurse, lst)
		}(
			(recurse, lst) -> {
				__core__chooseList(
					lst,
					() -> {__core__mkCons(x, lst)},
					() -> {
						(head) -> {
							__core__ifThenElse(
								comp(x, head),
								() -> {__core__mkCons(x, lst)},
								() -> {__core__mkCons(head, recurse(recurse, __core__tailList__safe(lst)))}
							)()
						}(__core__headList__safe(lst))
					}
				)()
			}
		)
	}`));
	add(new RawFunc("__helios__common__sort", 
	`(lst, comp) -> {
		(recurse) -> {
			recurse(recurse, lst)
		}(
			(recurse, lst) -> {
				__core__chooseList(
					lst,
					() -> {lst},
					() -> {
						(head, tail) -> {
							__helios__common__insert_in_sorted(head, tail, comp)
						}(__core__headList__safe(lst), recurse(recurse, __core__tailList__safe(lst)))
					}
				)()
			}
		)
	}`));
	add(new RawFunc("__helios__common__map_get",
	`(self, key, fnFound, fnNotFound) -> {
		(recurse) -> {
			recurse(recurse, self, key)
		}(
			(recurse, self, key) -> {
				__core__chooseList(
					self, 
					fnNotFound, 
					() -> {
						(head) -> {
							__core__ifThenElse(
								__core__equalsData(key, __core__fstPair(head)), 
								() -> {fnFound(__core__sndPair(head))}, 
								() -> {recurse(recurse, __core__tailList__safe(self), key)}
							)()
						}(__core__headList__safe(self))
					}
				)()
			}
		)
	}`));
	add(new RawFunc("__helios__common__is_in_bytearray_list",
	`(lst, key) -> {
		__helios__common__any(lst, (item) -> {__core__equalsData(item, key)})
	}`));
	add(new RawFunc("__helios__common__length", 
	`(lst) -> {
		(recurse) -> {
			recurse(recurse, lst)
		}(
			(recurse, lst) -> {
				__core__chooseList(
					lst, 
					() -> {0}, 
					() -> {__core__addInteger(recurse(recurse, __core__tailList__safe(lst)), 1)}
				)()
			}
		)
	}`));
	add(new RawFunc("__helios__common__concat", 
	`(a, b) -> {
		(recurse) -> {
			recurse(recurse, b, a)
		}(
			(recurse, lst, rem) -> {
				__core__chooseList(
					rem,
					() -> {lst},
					() -> {__core__mkCons(__core__headList__safe(rem), recurse(recurse, lst, __core__tailList__safe(rem)))}
				)()
			}
		)
	}`));
	add(new RawFunc("__helios__common__slice_bytearray",
	`(self, selfLengthFn) -> {
		(start, end) -> {
			(normalize) -> {
				(fn) -> {
					fn(normalize(start))
				}(
					(start) -> {
						(fn) -> {
							fn(normalize(end))
						}(
							(end) -> {
								__core__sliceByteString(start, __core__subtractInteger(end, __helios__int__max(start, 0)), self)
							}
						)
					}
				)
			}(
				(pos) -> {
					__core__ifThenElse(
						__core__lessThanInteger(pos, 0),
						() -> {
							__core__addInteger(__core__addInteger(selfLengthFn(self), 1), pos)
						},
						() -> {
							pos
						}
					)()
				}
			)
		}
	}`));
	add(new RawFunc("__helios__common__starts_with", 
	`(self, selfLengthFn) -> {
		(prefix) -> {
			(n, m) -> {
				__core__ifThenElse(
					__core__lessThanInteger(n, m),
					() -> {false},
					() -> {
						__core__equalsByteString(prefix, __core__sliceByteString(0, m, self))
					}
				)()
			}(selfLengthFn(self), __core__lengthOfByteString(prefix))
		}
	}`));
	add(new RawFunc("__helios__common__ends_with",
	`(self, selfLengthFn) -> {
		(suffix) -> {
			n = selfLengthFn(self);
			m = __core__lengthOfByteString(suffix);
			__core__ifThenElse(
				__core__lessThanInteger(n, m),
				() -> {
					false
				},
				() -> {
					__core__equalsByteString(suffix, __core__sliceByteString(__core__subtractInteger(n, m), m, self))
				}
			)()
		}
	}`));
	// TODO: inline __core__sndPair(head)
	add(new RawFunc("__helios__common__cip68_field",
	`(self, name) -> {
		name_data = __core__bData(name);
		map = __core__unMapData(__core__headList(__core__sndPair(__core__unConstrData(self))));
		recurse = (recurse, map) -> {
			__core__chooseList(
				map,
				() -> {
					__helios__error(
						__core__appendString(
							"field ",
							__core__appendString(
								__helios__bytearray__show(name)(),
								" not found in struct"
							)
						)
					)
				},
				() -> {
					head = __core__headList__safe(map);
					key = __core__fstPair(head);
					value = __core__sndPair(head);
					__core__ifThenElse(
						__core__equalsData(key, name_data),
						() -> {
							value
						},
						() -> {
							recurse(recurse, __core__tailList__safe(map))
						}
					)()
				}
			)()
		};
		recurse(recurse, map)
	}`));
	// map is expected to already have been extracted
	add(new RawFunc("__helios__common__cip68_field_safe",
	`(map, name) -> {
		name = __core__bData(name);
		recurse = (recurse, map) -> {
			__core__chooseList(
				map,
				() -> {
					__helios__option__NONE_FUNC
				},
				() -> {
					head = __core__headList__safe(map);
					key = __core__fstPair(head);
					__core__ifThenElse(
						__core__equalsData(key, name),
						() -> {
							__helios__option__SOME_FUNC(__core__sndPair(head))
						},
						() -> {
							recurse(recurse, __core__tailList__safe(map))
						}
					)()
				}
			)()
		};
		recurse(recurse, map)
	}`));
	add(new RawFunc("__helios__common__test_cip68_field",
	`(self, name, inner_test) -> {
		__core__chooseData(
			self,
			() -> {
				fields = __core__sndPair(__core__unConstrData__safe(self));
				__core__chooseList(
					fields,
					() -> {false},
					() -> {
						head = __core__headList__safe(fields);
						__core__chooseData(
							head,
							() -> {false},
							() -> {
								map = __core__unMapData__safe(head);
								recurse = (recurse, map) -> {
									__core__chooseList(
										map,
										() -> {false},
										() -> {
											head = __core__headList__safe(map);
											key = __core__fstPair(head);
											value = __core__sndPair(head);
											__core__ifThenElse(
												__core__equalsData(key, name),
												() -> {
													inner_test(value)
												},
												() -> {
													recurse(recurse, __core__tailList__safe(map))
												}
											)()
										}
									)()
								};
								recurse(recurse, map)
							},
							() -> {false},
							() -> {false},
							() -> {false}
						)()
					}
				)()
			},
			() -> {false},
			() -> {false},
			() -> {false},
			() -> {false}
		)()
	}`));
	add(new RawFunc("__helios__common__enum_fields", 
	`(self) -> {
		__core__sndPair(__core__unConstrData(self))
	}`));
	add(new RawFunc("__helios__common__enum_field_0", 
	`(self) -> {
		__core__headList(__helios__common__enum_fields(self))
	}`));
	add(new RawFunc("__helios__common__enum_fields_after_0",
	`(self) -> {
		__core__tailList(__helios__common__enum_fields(self))
	}`));
	for (let i = 1; i < 20; i++) {
		add(new RawFunc(`__helios__common__enum_field_${i.toString()}`,
	`(self) -> {
		__core__headList(__helios__common__enum_fields_after_${(i-1).toString()}(self))
	}`));
		add(new RawFunc(`__helios__common__enum_fields_after_${i.toString()}`,
	`(self) -> {
		__core__tailList(__helios__common__enum_fields_after_${(i-1).toString()}(self))
	}`));
	}
	add(new RawFunc("__helios__common__struct_field_0", "__core__headList"));
	add(new RawFunc("__helios__common__struct_fields_after_0", "__core__tailList"));
	for (let i = 1; i < 20; i++) {
		add(new RawFunc(`__helios__common__struct_field_${i.toString()}`,
	`(self) -> {
		__core__headList(__helios__common__struct_fields_after_${(i-1).toString()}(self))
	}`));
		add(new RawFunc(`__helios__common__struct_fields_after_${i.toString()}`,
	`(self) -> {
		__core__tailList(__helios__common__struct_fields_after_${(i-1).toString()}(self))
	}`));
	}
	add(new RawFunc("__helios__common__list_0", "__core__mkNilData(())"));
	add(new RawFunc("__helios__common__list_1", 
	`(a) -> {
		__core__mkCons(a, __helios__common__list_0)
	}`));
	for (let i = 2; i < 20; i++) {
		/**
		 * @type {string[]}
		 */
		let args = [];

		for (let j = 0; j < i; j++) {
			args.push(`arg${j.toString()}`);
		}

		let woFirst = args.slice()
		let first = assertDefined(woFirst.shift());

		add(new RawFunc(`__helios__common__list_${i.toString()}`,
	`(${args.join(", ")}) -> {
		__core__mkCons(${first}, __helios__common__list_${(i-1).toString()}(${woFirst.join(", ")}))
	}`));
	}
	add(new RawFunc(`__helios__common__hash_datum_data[${FTPP}0]`, 
	`(data) -> {
		__core__blake2b_256(${FTPP}0__serialize(data)())
	}`));
	add(new RawFunc(`__helios__common__test_constr_data_2`,
	`(data, index, test_a, test_b) -> {
		__core__chooseData(
			data,
			() -> {
				pair = __core__unConstrData__safe(data);
				__core__ifThenElse(
					__core__equalsInteger(__core__fstPair(pair), index),
					() -> {
						fields = __core__sndPair(pair);
						__core__chooseList(
							fields,
							() -> {
								false
							},
							() -> {
								__core__ifThenElse(
									test_a(__core__headList__safe(fields)),
									() -> {
										tail = __core__tailList__safe(fields);
										__core__chooseList(
											tail,
											() -> {
												false
											},
											() -> {
												__core__ifThenElse(
													test_b(__core__headList__safe(tail)),
													() -> {
														__core__chooseList(
															__core__tailList__safe(tail), 
															() -> {
																true
															},
															() -> {
																false
															}
														)()
													},
													() -> {
														false
													}
												)()
											}
										)()
									},
									() -> {
										false
									}
								)()
							}
						)()
					},
					() -> {
						false
					}
				)()
			},
			() -> {false},
			() -> {false},
			() -> {false},
			() -> {false}
		)()
	}`));


	// Global builtin functions
	add(new RawFunc("__helios__print", 
	`(msg) -> {
		__core__trace(msg, ())
	}`));
	add(new RawFunc("__helios__error",
	`(msg) -> {
		__core__trace(
			msg, 
			() -> {
				error()
			}
		)()
	}`));
	add(new RawFunc("__helios__assert",
	`(cond, msg) -> {
		__core__ifThenElse(
			cond,
			() -> {
				()
			},
			() -> {
				__core__trace(
					msg,
					() -> {
						error()
					}
				)()
			}
		)()
	}`));


	// Int builtins
	add(new RawFunc("__helios__int____eq", "__core__equalsInteger"));
	add(new RawFunc("__helios__int__from_data", "__core__unIData"));
	add(new RawFunc("__helios__int__from_data_safe",
	`(data) -> {
		__core__chooseData(
			data,
			() -> {__helios__option__NONE_FUNC},
			() -> {__helios__option__NONE_FUNC},
			() -> {__helios__option__NONE_FUNC},
			() -> {
				__helios__option__SOME_FUNC(__core__unIData__safe(data))
			},
			() -> {__helios__option__NONE_FUNC}
		)()
	}`));
	add(new RawFunc("__helios__int__is_valid_data", `(data) -> {
		__core__chooseData(data, false, false, false, true, false)
	}`));
	add(new RawFunc("__helios__int____to_data", "__core__iData"));
	addNeqFunc("__helios__int");
	addSerializeFunc("__helios__int");
	add(new RawFunc("__helios__int____neg",
	`(self) -> {
		__core__multiplyInteger(self, -1)
	}`));
	add(new RawFunc("__helios__int____pos", "__helios__common__identity"));
	add(new RawFunc("__helios__int____add", "__core__addInteger"));
	add(new RawFunc("__helios__int____sub", "__core__subtractInteger"));
	add(new RawFunc("__helios__int____mul", "__core__multiplyInteger"));
	add(new RawFunc("__helios__int____div", "__core__divideInteger"));
	add(new RawFunc("__helios__int____mod", "__core__modInteger"));
	add(new RawFunc("__helios__int____add1",
	`(a, b) -> {
		__core__addInteger(
			__core__multiplyInteger(a, __helios__real__ONE),
			b
		)
	}`));
	add(new RawFunc("__helios__int____sub1",
	`(a, b) -> {
		__core__subtractInteger(
			__core__multiplyInteger(a, __helios__real__ONE),
			b
		)
	}`));
	add(new RawFunc("__helios__int____mul1", "__helios__int____mul"));
	add(new RawFunc("__helios__int____div1",
	`(a, b) -> {
		__core__divideInteger(
			__core__multiplyInteger(a, __helios__real__ONESQ),
			b
		)
	}`));
	add(new RawFunc("__helios__int____geq",
	`(a, b) -> {
		__helios__bool____not(__core__lessThanInteger(a, b))
	}`));
	add(new RawFunc("__helios__int____gt",
	`(a, b) -> {
		__helios__bool____not(__core__lessThanEqualsInteger(a, b))
	}`));
	add(new RawFunc("__helios__int____leq", "__core__lessThanEqualsInteger"));
	add(new RawFunc("__helios__int____lt", "__core__lessThanInteger"));
	add(new RawFunc("__helios__int____geq1",
	`(a, b) -> {
		__helios__bool____not(
			__core__lessThanInteger(
				__core__multiplyInteger(a, __helios__real__ONE),
				b
			)
		)
	}`));
	add(new RawFunc("__helios__int____gt1",
	`(a, b) -> {
		__helios__bool____not(
			__core__lessThanEqualsInteger(
				__core__multiplyInteger(a, __helios__real__ONE),
				b
			)
		)
	}`));
	add(new RawFunc("__helios__int____leq1",
	`(a, b) -> {
		__core__lessThanEqualsInteger(
			__core__multiplyInteger(a, __helios__real__ONE),
			b
		)
	}`));
	add(new RawFunc("__helios__int____lt1",
	`(a, b) -> {
		__core__lessThanInteger(
			__core__multiplyInteger(a, __helios__real__ONE),
			b
		)
	}`));
	add(new RawFunc("__helios__int__min",
	`(a, b) -> {
		__core__ifThenElse(
			__core__lessThanInteger(a, b),
			a,
			b
		)
	}`));
	add(new RawFunc("__helios__int__max",
	`(a, b) -> {
		__core__ifThenElse(
			__core__lessThanInteger(a, b),
			b,
			a
		)
	}`));
	add(new RawFunc("__helios__int__bound_min",
	`(self) -> {
		(other) -> {
			__helios__int__max(self, other)
		}
	}`));
	add(new RawFunc("__helios__int__bound_max",
	`(self) -> {
		(other) -> {
			__helios__int__min(self, other)
		}
	}`));
	add(new RawFunc("__helios__int__bound",
	`(self) -> {
		(min, max) -> {
			__helios__int__max(__helios__int__min(self, max), min)
		}
	}`));
	add(new RawFunc("__helios__int__abs",
	`(self) -> {
		() -> {
			__core__ifThenElse(
				__core__lessThanInteger(self, 0),
				() -> {
					__core__multiplyInteger(self, -1)
				},
				() -> {
					self
				}
			)()
		}
	}`));
	add(new RawFunc("__helios__int__encode_zigzag",
	`(self) -> {
		() -> {
			__core__ifThenElse(
				__core__lessThanInteger(self, 0),
				() -> {
					__core__subtractInteger(__core__multiplyInteger(self, -2), 1)
				},
				() -> {
					__core__multiplyInteger(self, 2)
				}
			)()
		}
	}`));
	add(new RawFunc("__helios__int__decode_zigzag",
	`(self) -> {
		() -> {
			__core__ifThenElse(
				__core__lessThanInteger(self, 0),
				() -> {
					__helios__error("expected positive int")
				},
				() -> {
					__core__ifThenElse(
						__core__equalsInteger(__core__modInteger(self, 2), 0),
						() -> {
							__core__divideInteger(self, 2)
						},
						() -> {
							__core__divideInteger(__core__addInteger(self, 1), -2)
						}
					)()
				}
			)()
		}
	}`));
	add(new RawFunc("__helios__int__to_bool",
	`(self) -> {
		() -> {
			__core__ifThenElse(__core__equalsInteger(self, 0), false, true)
		}
	}`));
	add(new RawFunc("__helios__int__to_real",
	`(self) -> {
		() -> {
			__core__multiplyInteger(self, __helios__real__ONE)
		}
	}`));
	add(new RawFunc("__helios__int__to_hex",
	`(self) -> {
		() -> {
			recurse = (recurse, self, bytes) -> {
				digit = __core__modInteger(self, 16);
				bytes = __core__consByteString(
					__core__ifThenElse(
						__core__lessThanInteger(digit, 10), 
						__core__addInteger(digit, 48), 
						__core__addInteger(digit, 87)
					), 
					bytes
				);
				__core__ifThenElse(
					__core__lessThanInteger(self, 16),
					() -> {bytes},
					() -> {
						recurse(recurse, __core__divideInteger(self, 16), bytes)
					}
				)()
			};
			__core__decodeUtf8__safe(
				__core__ifThenElse(
					__core__lessThanInteger(self, 0),
					() -> {
						__core__consByteString(
							45,
							recurse(recurse, __core__multiplyInteger(self, -1), #)
						)
					},
					() -> {
						recurse(recurse, self, #)
					}
				)()
			)
		}
	}`));
	add(new RawFunc("__helios__common__BASE58_ALPHABET", "#31323334353637383941424344454647484a4b4c4d4e505152535455565758595a6162636465666768696a6b6d6e6f707172737475767778797a"))
	add(new RawFunc("__helios__int__to_base58",
	`(self) -> {
		() -> {
			__core__decodeUtf8(
				__core__ifThenElse(
					__core__lessThanInteger(self, 0),
					() -> {
						__helios__error("expected positive number")
					},
					() -> {
						recurse = (recurse, self, bytes) -> {
							digit = __core__modInteger(self, 58);
							bytes = __core__consByteString(
								__core__indexByteString(__helios__common__BASE58_ALPHABET, digit),
								bytes
							);
							__core__ifThenElse(
								__core__lessThanInteger(self, 58),
								() -> {
									bytes
								},
								() -> {
									recurse(recurse, __core__divideInteger(self, 58), bytes)
								}
							)()
						};
						recurse(recurse, self, #)
					}
				)()
			)
		}
	}`));
	add(new RawFunc("__helios__int__BASE58_INVERSE_ALPHABET_1", "#ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000102030405060708ffffffffffff"));
	add(new RawFunc("__helios__int__BASE58_INVERSE_ALPHABET_2", "#ff090a0b0c0d0e0f10ff1112131415ff161718191a1b1c1d1e1f20ffffffffffff2122232425262728292a2bff2c2d2e2f30313233343536373839ffffffffff"));
	add(new RawFunc("__helios__int__invert_base58_char", 
	`(char) -> {
		digit = __core__ifThenElse(
			__core__lessThanInteger(char, 64),
			() -> {
				__core__indexByteString(__helios__int__BASE58_INVERSE_ALPHABET_1, char)
			},
			() -> {
				__core__ifThenElse(
					__core__lessThanInteger(char, 128),
					() -> {
						__core__indexByteString(
							__helios__int__BASE58_INVERSE_ALPHABET_2,
							__core__subtractInteger(char, 64)
						)
					},
					() -> {
						0xff
					}
				)()
			}
		)();
		__core__ifThenElse(
			__core__equalsInteger(digit, 0xff),
			() -> {
				__helios__error("invalid base58 character")
			},
			() -> {
				digit
			}
		)()
	}`));
	add(new RawFunc("__helios__int__from_base58",
	`(str) -> {
		bytes = __core__encodeUtf8(str);
		n = __core__lengthOfByteString(bytes);
		recurse = (recurse, acc, pow, i) -> {
			__core__ifThenElse(
				__core__equalsInteger(i, -1),
				() -> {
					acc
				},
				() -> {
					new_acc = __core__addInteger(
						acc,
						__core__multiplyInteger(
							__helios__int__invert_base58_char(
								__core__indexByteString(bytes, i)
							),
							pow
						)
					);
					recurse(recurse, new_acc, __core__multiplyInteger(pow, 58), __core__subtractInteger(i, 1))
				}
			)()
		};
		recurse(recurse, 0, 1, __core__subtractInteger(n, 1))
	}`));
	add(new RawFunc("__helios__int__show_digit",
	`(x) -> {
		__core__addInteger(__core__modInteger(x, 10), 48)
	}`));
	add(new RawFunc("__helios__int__show",
	`(self) -> {
		() -> {
			__core__decodeUtf8__safe(
				recurse = (recurse, i, bytes) -> {
					(bytes) -> {
						__core__ifThenElse(
							__core__lessThanInteger(i, 10),
							() -> {
								bytes
							},
							() -> {
								recurse(recurse, __core__divideInteger(i, 10), bytes)
							}
						)()
					}(__core__consByteString(__helios__int__show_digit(i), bytes))
				};
				__core__ifThenElse(
					__core__lessThanInteger(self, 0),
					() -> {__core__consByteString(45, recurse(recurse, __core__multiplyInteger(self, -1), #))},
					() -> {recurse(recurse, self, #)}
				)()
			)
		}
	}`));
	// not exposed, assumes positive number
	add(new RawFunc("__helios__int__show_padded",
	`(self, n) -> {
		recurse = (recurse, x, pos, bytes) -> {
			__core__ifThenElse(
				__core__equalsInteger(x, 0),
				() -> {
					__core__ifThenElse(
						__core__lessThanEqualsInteger(n, pos),
						() -> {
							bytes
						},
						() -> {
							recurse(
								recurse,
								0,
								__core__addInteger(pos, 1),
								__core__consByteString(48, bytes)
							)
						}
					)()
				},
				() -> {
					recurse(
						recurse,
						__core__divideInteger(x, 10),
						__core__addInteger(pos, 1),
						__core__consByteString(__helios__int__show_digit(x), bytes)
					)
				}
			)()
		};
		recurse(recurse, self, 0, #)
	}`));
	
	add(new RawFunc("__helios__int__parse_digit",
	`(digit) -> {
		__core__ifThenElse(
			__core__lessThanEqualsInteger(digit, 57),
			() -> {
				__core__ifThenElse(
					__core__lessThanEqualsInteger(48, digit),
					() -> {
						__core__subtractInteger(digit, 48)
					},
					() -> {
						__helios__error("not a digit")
					}
				)()
			},
			() -> {
				__helios__error("not a digit")
			}
		)()
	}`));
	add(new RawFunc("__helios__int__parse_hex_digit",
	`(hex) -> {
		__core__ifThenElse(
			__core__lessThanEqualsInteger(hex, 57),
			() -> {
				__core__ifThenElse(
					__core__lessThanEqualsInteger(48, hex),
					() -> {
						__core__subtractInteger(hex, 48)
					},
					() -> {
						__helios__error("not a hex digit")
					}
				)()
			},
			() -> {
				__core__ifThenElse(
					__core__lessThanEqualsInteger(hex, 70),
					() -> {
						__core__ifThenElse(
							__core__lessThanEqualsInteger(65, hex),
							() -> {
								__core__subtractInteger(hex, 55)
							}, 
							() -> {
								__helios__error("not a hex digit")
							}
						)()
					},
					() -> {
						__core__ifThenElse(
							__core__lessThanEqualsInteger(hex, 102),
							() -> {
								__core__ifThenElse(
									__core__lessThanEqualsInteger(97, hex),
									() -> {
										__core__subtractInteger(hex, 87)
									},
									() -> {
										__helios__error("not a hex digit")
									}
								)()
							},
							() -> {
								__helios__error("not a hex digit")
							}
						)()
					}
				)()
			}
		)()
	}`));
	add(new RawFunc("__helios__int__parse",
	`(string) -> {
		bytes = __core__encodeUtf8(string);
		n = __core__lengthOfByteString(bytes);
		b0 = __core__indexByteString(bytes, 0);
		recurse = (recurse, acc, i) -> {
			__core__ifThenElse(
				__core__equalsInteger(i, n),
				() -> {
					acc
				},
				() -> {
					new_acc = __core__addInteger(
						__core__multiplyInteger(acc, 10), 
						__helios__int__parse_digit(__core__indexByteString(bytes, i))
					);
					recurse(recurse, new_acc, __core__addInteger(i, 1))
				}
			)()
		};
		__core__ifThenElse(
			__core__equalsInteger(b0, 48),
			() -> {
				__core__ifThenElse(
					__core__equalsInteger(n, 1),
					() -> {
						0
					},
					() -> {
						__helios__error("zero padded integer can't be parsed")
					}
				)()
			},
			() -> {
				__core__ifThenElse(
					__core__equalsInteger(b0, 45),
					() -> {
						__core__ifThenElse(
							__core__equalsInteger(__core__indexByteString(bytes, 1), 48),
							() -> {
								__helios__error("-0 not allowed")
							},
							() -> {
								__core__multiplyInteger(
									recurse(recurse, 0, 1),
									-1
								)
							}
						)()
					},
					() -> {
						recurse(recurse, 0, 0)
					}
				)()
			}
		)()
	}`));
	add(new RawFunc("__helios__int__from_big_endian",
	`(bytes) -> {
		n = __core__lengthOfByteString(bytes);
		recurse = (recurse, acc, pow, i) -> {
			__core__ifThenElse(
				__core__equalsInteger(i, -1),
				() -> {
					acc
				},
				() -> {
					new_acc = __core__addInteger(
						acc,
						__core__multiplyInteger(__core__indexByteString(bytes, i), pow)
					);
					recurse(recurse, new_acc, __core__multiplyInteger(pow, 256), __core__subtractInteger(i, 1))
				}
			)()
		};
		recurse(recurse, 0, 1, __core__subtractInteger(n, 1))
	}`));
	add(new RawFunc("__helios__int__from_little_endian", 
	`(bytes) -> {
		n = __core__lengthOfByteString(bytes);
		recurse = (recurse, acc, pow, i) -> {
			__core__ifThenElse(
				__core__equalsInteger(i, n),
				() -> {
					acc
				},
				() -> {
					new_acc = __core__addInteger(
						acc,
						__core__multiplyInteger(__core__indexByteString(bytes, i), pow)
					);
					recurse(recurse, new_acc, __core__multiplyInteger(pow, 256), __core__addInteger(i, 1))
				}
			)()
		};
		recurse(recurse, 0, 1, 0)
	}`));
	add(new RawFunc("__helios__int__to_big_endian",
	`(self) -> {
		() -> {
			__core__ifThenElse(
				__core__lessThanInteger(self, 0),
				() -> {
					__helios__error("can't convert negative number to big endian bytearray")
				},
				() -> {
					recurse = (recurse, self, bytes) -> {
						bytes = __core__consByteString(__core__modInteger(self, 256), bytes);
						__core__ifThenElse(
							__core__lessThanInteger(self, 256),
							() -> {
								bytes
							},
							() -> {
								recurse(
									recurse,
									__core__divideInteger(self, 256),
									bytes
								)
							}
						)()
					};
					recurse(recurse, self, #)
				}
			)()
		}
	}`));
	add(new RawFunc("__helios__int__to_little_endian",
	`(self) -> {
		() -> {
			__core__ifThenElse(
				__core__lessThanInteger(self, 0),
				() -> {
					__helios__error("can't convert negative number to little endian bytearray")
				},
				() -> {
					recurse = (recurse, self) -> {
						__core__consByteString(
							__core__modInteger(self, 256),
							__core__ifThenElse(
								__core__lessThanInteger(self, 256),
								() -> {
									#
								},
								() -> {
									recurse(recurse, __core__divideInteger(self, 256))
								}
							)()
						)
					};
					recurse(recurse, self)
				}
			)()
		}
	}`));
	add(new RawFunc("__helios__int__sqrt",
	`(x) -> {
		__core__ifThenElse(
			__core__lessThanInteger(x, 2),
			() -> {
				__core__ifThenElse(
					__core__equalsInteger(x, 1),
					() -> {
						1
					},
					() -> {
						__core__ifThenElse(
							__core__equalsInteger(x, 0),
							() -> {
								0
							},
							() -> {
								__helios__error("negative number in sqrt")
							}
						)()
					}
				)()
			},
			() -> {
				recurse = (recurse, x0) -> {
					x1 = __core__divideInteger(
						__core__addInteger(
							x0,
							__core__divideInteger(x, x0)
						),
						2
					);
					__core__ifThenElse(
						__core__lessThanEqualsInteger(x0, x1),
						() -> {
							x0
						},
						() -> {
							recurse(recurse, x1)
						}
					)()
				};
				recurse(recurse, __core__divideInteger(x, 2))
			}
		)()
	}`));


	// Real builtins
	addIntLikeFuncs("__helios__real");
	add(new RawFunc("__helios__real__is_valid_data", "__helios__int__is_valid_data"));
	add(new RawFunc("__helios__real__PRECISION", REAL_PRECISION.toString()));
	add(new RawFunc("__helios__real__ONE", '1' + new Array(REAL_PRECISION).fill('0').join('')));
	add(new RawFunc("__helios__real__HALF", '5' + new Array(REAL_PRECISION-1).fill('0').join('')));
	add(new RawFunc("__helios__real__NEARLY_ONE", new Array(REAL_PRECISION).fill('9').join('')));
	add(new RawFunc("__helios__real__ONESQ", '1' + new Array(REAL_PRECISION*2).fill('0').join('')));
	add(new RawFunc("__helios__real____neg", "__helios__int____neg"));
	add(new RawFunc("__helios__real____pos", "__helios__int____pos"));
	add(new RawFunc("__helios__real____add", "__helios__int____add"));
	add(new RawFunc("__helios__real____add1", 
	`(a, b) -> {
		__core__addInteger(
			a,
			__core__multiplyInteger(b, __helios__real__ONE)
		)
	}`));
	add(new RawFunc("__helios__real____sub", "__helios__int____sub"));
	add(new RawFunc("__helios__real____sub1", 
	`(a, b) -> {
		__core__subtractInteger(
			a,
			__core__multiplyInteger(b, __helios__real__ONE)
		)
	}`));
	add(new RawFunc("__helios__real____mul",
	`(a, b) -> {
		__core__divideInteger(
			__core__multiplyInteger(a, b),
			__helios__real__ONE
		)
	}`));
	add(new RawFunc("__helios__real____mul1", "__helios__int____mul"));
	add(new RawFunc("__helios__real____div",
	`(a, b) -> {
		__core__divideInteger(
			__core__multiplyInteger(a, __helios__real__ONE),
			b
		)
	}`));
	add(new RawFunc("__helios__real____div1", "__helios__int____div"));
	add(new RawFunc("__helios__real____geq", "__helios__int____geq"));
	add(new RawFunc("__helios__real____gt", "__helios__int____gt"));
	add(new RawFunc("__helios__real____leq", "__helios__int____leq"));
	add(new RawFunc("__helios__real____lt", "__helios__int____lt"));
	add(new RawFunc("__helios__real____eq1",
	`(a, b) -> {
		__core__equalsInteger(a,
			__core__multiplyInteger(
				b,
				__helios__real__ONE
			)
		)
	}`));
	add(new RawFunc("__helios__real____neq1",
	`(a, b) -> {
		__helios__bool____not(
			__core__equalsInteger(
				a,
				__core__multiplyInteger(b, __helios__real__ONE)
			)
		)
	}`));
	add(new RawFunc("__helios__real____geq1", 
	`(a, b) -> {
		__helios__bool____not(
			__core__lessThanInteger(
				a,
				__core__multiplyInteger(b, __helios__real__ONE)
			)
		)
	}`));
	add(new RawFunc("__helios__real____gt1", 
	`(a, b) -> {
		__helios__bool____not(
			__core__lessThanEqualsInteger(
				a, 
				__core__multiplyInteger(b, __helios__real__ONE)
			)
		)
	}`));
	add(new RawFunc("__helios__real____leq1",
	`(a, b) -> {
		__core__lessThanEqualsInteger(
			a, 
			__core__multiplyInteger(b, __helios__real__ONE)
		)
	}`));
	add(new RawFunc("__helios__real____lt1", 
	`(a, b) -> {
		__core__lessThanInteger(
			a,
			__core__multiplyInteger(b, __helios__real__ONE)
		)
	}`));
	add(new RawFunc("__helios__real__abs", "__helios__int__abs"));
	add(new RawFunc("__helios__real__sqrt", 
	`(self) -> {
		__helios__int__sqrt(
			__helios__int____mul(self, __helios__real__ONE)
		)
	}`));
	add(new RawFunc("__helios__real__floor", 
	`(self) -> {
		() -> {
			__core__divideInteger(self, __helios__real__ONE)
		}
	}`));
	add(new RawFunc("__helios__real__trunc",
	`(self) -> {
		() -> {
			__core__quotientInteger(self, __helios__real__ONE)
		}
	}`));
	add(new RawFunc("__helios__real__ceil",
	`(self) -> {
		() -> {
			__core__divideInteger(
				__core__addInteger(self, __helios__real__NEARLY_ONE),
				__helios__real__ONE
			)
		}
	}`));
	add(new RawFunc("__helios__real__round",
	`(self) -> {
		() -> {
			__core__divideInteger(
				__core__addInteger(self, __helios__real__HALF),
				__helios__real__ONE
			)
		}
	}`));
	add(new RawFunc("__helios__real__show",
	`(self) -> {
		() -> {
			__helios__string____add(
				__helios__string____add(
					__core__ifThenElse(__core__lessThanInteger(self, 0), "-", ""),
					__helios__int__show(
						__helios__real__floor(
							__helios__real__abs(self)()
						)()
					)()
				),
				__helios__string____add(
					".",
					__core__decodeUtf8(
						__helios__int__show_padded(
							__helios__int____mod(self, __helios__real__ONE),
							__helios__real__PRECISION
						)
					)
				)
			)
		}
	}`));


	// Bool builtins
	addSerializeFunc("__helios__bool");
	add(new RawFunc("__helios__bool__is_valid_data",
	`(data) -> {
		__core__chooseData(
			data,
			() -> {
				pair = __core__unConstrData__safe(data);
				index = __core__fstPair(pair);
				fields = __core__sndPair(pair);
				__core__chooseList(
					fields,
					() -> {
						__core__ifThenElse(
							__core__equalsInteger(0, index),
							() -> {
								true
							},
							() -> {
								__core__ifThenElse(
									__core__equalsInteger(1, index),
									() -> {
										true
									},
									() -> {
										false
									}
								)()
							}
						)()
					},
					() -> {
						false
					}
				)()
			},
			() -> {false},
			() -> {false},
			() -> {false},
			() -> {false}
		)()
	}`));
	add(new RawFunc("__helios__bool____eq", 
	`(a, b) -> {
		__core__ifThenElse(a, b, __helios__bool____not(b))
	}`));
	add(new RawFunc("__helios__bool____neq",
	`(a, b) -> {
		__core__ifThenElse(a, __helios__bool____not(b), b)
	}`));
	// TODO: optimize this drastically by simply returning the comparison to 1
	add(new RawFunc("__helios__bool__from_data", 
	`(d) -> {
		__core__ifThenElse(
			__core__equalsInteger(__core__fstPair(__core__unConstrData(d)), 0), 
			false, 
			true
		)
	}`));
	add(new RawFunc("__helios__bool__from_data_safe",
	`(data) -> {
		__core__chooseData(
			data,
			() -> {
				__helios__option__SOME_FUNC(
					__core__equalsInteger(
						__core__fstPair(
							__core__unConstrData__safe(data)
						),
						1
					)
				)
			},
			() -> {__helios__option__NONE_FUNC},
			() -> {__helios__option__NONE_FUNC},
			() -> {__helios__option__NONE_FUNC},
			() -> {__helios__option__NONE_FUNC}
		)()
	}`));
	add(new RawFunc("__helios__bool____to_data",  
	`(b) -> {
		__core__constrData(__core__ifThenElse(b, 1, 0), __helios__common__list_0)
	}`));
	add(new RawFunc("__helios__bool__and",
	`(a, b) -> {
		__core__ifThenElse(
			a(), 
			() -> {b()}, 
			() -> {false}
		)()
	}`));
	add(new RawFunc("__helios__bool__or",
	`(a, b) -> {
		__core__ifThenElse(
			a(), 
			() -> {true},
			() -> {b()}
		)()
	}`));
	add(new RawFunc("__helios__bool____not", 
	`(b) -> {
		__core__ifThenElse(b, false, true)
	}`));
	add(new RawFunc("__helios__bool__to_int",
	`(self) -> {
		() -> {
			__core__ifThenElse(self, 1, 0)
		}
	}`));
	add(new RawFunc("__helios__bool__show",
	`(self) -> {
		() -> {
			__core__ifThenElse(self, "true", "false")
		}
	}`));
	add(new RawFunc("__helios__bool__trace",
	`(self) -> {
		(prefix) -> {
			__core__trace(
				__helios__string____add(
					prefix,
					__helios__bool__show(self)()
				), 
				self
			)
		}
	}`));
	add(new RawFunc("__helios__bool__trace_if_false",
	`(self) -> {
		(msg) -> {
			__core__ifThenElse(
				self,
				() -> {
					self
				},
				() -> {
					__core__trace(msg, self)
				}
			)()
		}
	}`));
	add(new RawFunc("__helios__bool__trace_if_true",
	`(self) -> {
		(msg) -> {
			__core__ifThenElse(
				self,
				() -> {
					__core__trace(msg, self)
				},
				() -> {
					self
				}
			)()
		}
	}`));


	// String builtins
	addSerializeFunc("__helios__string");
	addNeqFunc("__helios__string");
	add(new RawFunc("__helios__string____eq", "__core__equalsString"));
	add(new RawFunc("__helios__string__from_data", 
	`(d) -> {
		__core__decodeUtf8(__core__unBData(d))
	}`));
	add(new RawFunc("__helios__string__from_data_safe",
	`(data) -> {
		__core__chooseData(
			data,
			() -> {__helios__option__NONE_FUNC},
			() -> {__helios__option__NONE_FUNC},
			() -> {__helios__option__NONE_FUNC},
			() -> {__helios__option__NONE_FUNC},
			() -> {
				bytes = __core__unBData__safe(data);
				__core__ifThenElse(
					__helios__string__is_valid_utf8(bytes),
					() -> {
						__helios__option__SOME_FUNC(__core__decodeUtf8__safe(bytes))
					},
					() -> {
						__helios__option__NONE_FUNC
					}
				)()	
			}
		)()
	}`));
	add(new RawFunc("__helios__string__show", 
	`(self) -> {
		() -> {
			__core__appendString(
				"'",
				__core__appendString(
					self,
					"'"
				)
			)
		}
	}`));
	add(new RawFunc("__helios__string__parse_utf8_cont_byte",
	`(byte, callback) -> {
		__core__ifThenElse(
			__core__equalsInteger(__core__divideInteger(byte, 64), 2),
			() -> {
				callback(true, __core__modInteger(byte, 64))
			},
			() -> {
				callback(false, 0)
			}
		)()
	}`));
	add(new RawFunc("__helios__string__is_valid_utf8",
	`(bytes) -> {
		n = __core__lengthOfByteString(bytes);
		recurse = (recurse, i) -> {
			__core__ifThenElse(
				__core__equalsInteger(i, n),
				() -> {
					true
				},
				() -> {
					b0 = __core__indexByteString__safe(bytes, i);
					__core__ifThenElse(
						__core__lessThanEqualsInteger(b0, 127),
						() -> {
							recurse(recurse, __core__addInteger(i, 1))
						},
						() -> {
							__core__ifThenElse(
								__core__equalsInteger(__core__divideInteger(b0, 32), 6),
								() -> {
									inext = __core__addInteger(i, 2);
									__core__ifThenElse(
										__core__lessThanEqualsInteger(inext, n),
										() -> {
											__helios__string__parse_utf8_cont_byte(
												__core__indexByteString__safe(bytes, __core__addInteger(i, 1)), 
												(valid, c1) -> {
													__core__ifThenElse(
														valid,
														() -> {
															c = __core__addInteger(
																__core__multiplyInteger(__core__modInteger(b0, 32), 64),
																c1
															);
															__core__ifThenElse(
																__helios__bool__and(
																	() -> {__core__lessThanEqualsInteger(128, c)},
																	() -> {__core__lessThanEqualsInteger(c, 2047)}
																),
																() -> {
																	recurse(recurse, inext)
																},
																() -> {
																	false
																}
															)()
														},
														() -> {
															false
														}
													)()
												}
											)
										},
										() -> {
											false
										}
									)()
								},
								() -> {
									__core__ifThenElse(
										__core__equalsInteger(__core__divideInteger(b0, 16), 14),
										() -> {
											inext = __core__addInteger(i, 3);
											__core__ifThenElse(
												__core__lessThanEqualsInteger(inext, n),
												() -> {
													__helios__string__parse_utf8_cont_byte(
														__core__indexByteString__safe(bytes, __core__addInteger(i, 1)),
														(valid, c1) -> {
															__core__ifThenElse(
																valid,
																() -> {
																	__helios__string__parse_utf8_cont_byte(
																		__core__indexByteString__safe(bytes, __core__addInteger(i, 2)),
																		(valid, c2) -> {
																			__core__ifThenElse(
																				valid,
																				() -> {
																					c = __core__addInteger(
																						__core__multiplyInteger(__core__modInteger(b0, 16), 4096),
																						__core__addInteger(
																							__core__multiplyInteger(c1, 64),
																							c2
																						)
																					);
																					__core__ifThenElse(
																						__helios__bool__and(
																							() -> {__core__lessThanEqualsInteger(2048, c)},
																							() -> {__core__lessThanEqualsInteger(c, 65535)}
																						),
																						() -> {
																							recurse(recurse, inext)
																						},
																						() -> {
																							false
																						}
																					)()
																				},
																				() -> {
																					false
																				}
																			)()
																		}
																	)
																},
																() -> {
																	false
																}
															)()
														}
													)
												},
												() -> {
													false
												}
											)()
										},
										() -> {
											__core__ifThenElse(
												__core__equalsInteger(__core__divideInteger(b0, 8), 30),
												() -> {
													inext = __core__addInteger(i, 4);
													__core__ifThenElse(
														__core__lessThanEqualsInteger(inext, n),
														() -> {
															__helios__string__parse_utf8_cont_byte(
																__core__indexByteString__safe(bytes, __core__addInteger(i, 1)),
																(valid, c1) -> {
																	__core__ifThenElse(
																		valid,
																		() -> {
																			__helios__string__parse_utf8_cont_byte(
																				__core__indexByteString__safe(bytes, __core__addInteger(i, 2)),
																				(valid, c2) -> {
																					__core__ifThenElse(
																						valid,
																						() -> {
																							__helios__string__parse_utf8_cont_byte(
																								__core__indexByteString__safe(bytes, __core__addInteger(i, 3)),
																								(valid, c3) -> {
																									__core__ifThenElse(
																										valid,
																										() -> {
																											c = __core__addInteger(
																												__core__multiplyInteger(__core__modInteger(b0, 8), 262144),
																												__core__addInteger(
																													__core__multiplyInteger(c1, 4096),
																													__core__addInteger(
																														__core__multiplyInteger(c2, 64),
																														c3
																													)
																												)
																											);
																											__core__ifThenElse(
																												__helios__bool__and(
																													() -> {__core__lessThanEqualsInteger(65536, c)},
																													() -> {__core__lessThanEqualsInteger(c, 2097151)}
																												),
																												() -> {
																													recurse(recurse, inext)
																												},
																												() -> {
																													false
																												}
																											)()
																										},
																										() -> {
																											false
																										}
																									)()
																								}
																							)
																						},
																						() -> {
																							false
																						}
																					)()
																				}
																			)
																		},
																		() -> {
																			false
																		}
																	)()
																}
															)
														},
														() -> {
															false
														}
													)()
												},
												() -> {
													false
												}
											)()
										}
									)()
								}
							)()
						}
					)()
				}
			)()
		};
		recurse(recurse, 0)
	}`));
	add(new RawFunc("__helios__string__is_valid_data",
	`(data) -> {
		__core__chooseData(
			data, 
			() -> {false}, 
			() -> {false},
			() -> {false},
			() -> {false},
			() -> {
				__helios__string__is_valid_utf8(__core__unBData__safe(data))
			}
		)()
	}`));
	add(new RawFunc("__helios__string____to_data", 
	`(s) -> {
		__core__bData(__core__encodeUtf8(s))
	}`));
	add(new RawFunc("__helios__string____add", "__core__appendString"));
	add(new RawFunc("__helios__string__starts_with", 
	`(self) -> {
		(prefix) -> {
			__helios__bytearray__starts_with(
				__core__encodeUtf8(self)
			)(__core__encodeUtf8(prefix))
		}
	}`));
	add(new RawFunc("__helios__string__ends_with", 
	`(self) -> {
		(suffix) -> {
			__helios__bytearray__ends_with(
				__core__encodeUtf8(self)
			)(__core__encodeUtf8(suffix))
		}
	}`));
	add(new RawFunc("__helios__string__encode_utf8",
	`(self) -> {
		() -> {
			__core__encodeUtf8(self)
		}
	}`));


	// ByteArray builtins
	addSerializeFunc("__helios__bytearray");
	addNeqFunc("__helios__bytearray");
	add(new RawFunc("__helios__bytearray__parse",
	`(string) -> {
		hex = __core__encodeUtf8(string);
		i = __core__subtractInteger(__core__lengthOfByteString(hex), 1);
		recurse = (recurse, tail, i) -> {
			__core__ifThenElse(
				__core__equalsInteger(i, -1),
				() -> {
					tail
				},
				() -> {
					byte = __core__addInteger(
						__helios__int__parse_hex_digit(__core__indexByteString(hex, i)),
						__core__multiplyInteger(
							16,
							__helios__int__parse_hex_digit(__core__indexByteString(hex, __core__subtractInteger(i, 1)))
						)
					);
					recurse(
						recurse, 
						__core__consByteString(byte, tail), 
						__core__subtractInteger(i, 2)
					)
				}
			)()
		};
		recurse(recurse, #, i)
	}`));
	add(new RawFunc("__helios__bytearray____eq", "__core__equalsByteString"));
	add(new RawFunc("__helios__bytearray__from_data", "__core__unBData"));
	add(new RawFunc("__helios__bytearray__from_data_safe",
	`(data) -> {
		__core__chooseData(
			data,
			() -> {__helios__option__NONE_FUNC},
			() -> {__helios__option__NONE_FUNC},
			() -> {__helios__option__NONE_FUNC},
			() -> {__helios__option__NONE_FUNC},
			() -> {__helios__option__SOME_FUNC(__core__unBData__safe(data))}
		)()
	}`));
	add(new RawFunc("__helios__bytearray__is_valid_data",
	`(data) -> {
		__core__chooseData(data, false, false, false, false, true)
	}`));
	add(new RawFunc(`__helios__bytearray__is_valid_data_fixed_length`,
	`(data, n) -> {
		__core__chooseData(
			data,
			() -> {false},
			() -> {false},
			() -> {false},
			() -> {false},
			() -> {
				bytes = __core__unBData__safe(data);
				__core__ifThenElse(
					__core__equalsInteger(__core__lengthOfByteString(bytes), n),
					() -> {
						true
					},
					() -> {
						false
					}
				)()
			}
		)()
	}`));
	add(new RawFunc("__helios__bytearray____to_data", "__core__bData"));
	add(new RawFunc("__helios__bytearray____add", "__core__appendByteString"));
	add(new RawFunc("__helios__bytearray____geq",
	`(a, b) -> {
		__helios__bool____not(__core__lessThanByteString(a, b))
	}`));
	add(new RawFunc("__helios__bytearray____gt",
	`(a, b) -> {
		__helios__bool____not(__core__lessThanEqualsByteString(a, b))
	}`));
	add(new RawFunc("__helios__bytearray____leq", "__core__lessThanEqualsByteString"));
	add(new RawFunc("__helios__bytearray____lt", "__core__lessThanByteString"));
	add(new RawFunc("__helios__bytearray__length", "__core__lengthOfByteString"));
	add(new RawFunc("__helios__bytearray__slice",
	`(self) -> {
		__helios__common__slice_bytearray(self, __core__lengthOfByteString)
	}`));
	add(new RawFunc("__helios__bytearray__starts_with", 
	`(self) -> {
		__helios__common__starts_with(self, __core__lengthOfByteString)
	}`));
	add(new RawFunc("__helios__bytearray__ends_with",
	`(self) -> {
		__helios__common__ends_with(self, __core__lengthOfByteString)
	}`));
	add(new RawFunc("__helios__bytearray__prepend", 
	`(self) -> {
		(byte) -> {
			__core__consByteString(byte, self)
		}
	}`));
	add(new RawFunc("__helios__bytearray__sha2",
	`(self) -> {
		() -> {
			__core__sha2_256(self)
		}
	}`));
	add(new RawFunc("__helios__bytearray__sha3",
	`(self) -> {
		() -> {
			__core__sha3_256(self)
		}
	}`));
	add(new RawFunc("__helios__bytearray__blake2b",
	`(self) -> {
		() -> {
			__core__blake2b_256(self)
		}
	}`));
	add(new RawFunc("__helios__bytearray__decode_utf8",
	`(self) -> {
		() -> {
			__core__decodeUtf8(self)
		}
	}`));
	add(new RawFunc("__helios__bytearray__show",
	`(self) -> {
		() -> {
			recurse = (recurse, self) -> {
				n = __core__lengthOfByteString(self);
				__core__ifThenElse(
					__core__lessThanInteger(0, n),
					() -> {
						__core__appendString(
							__core__decodeUtf8__safe(
								hex_bytes = (
									__core__encodeUtf8(
										__helios__int__to_hex(
											__core__indexByteString__safe(self, 0)
										)()
									)
								);
								__core__ifThenElse(
									__core__equalsInteger(__core__lengthOfByteString(hex_bytes), 1),
									__core__consByteString(48, hex_bytes),
									hex_bytes
								)
							), 
							recurse(recurse, __core__sliceByteString(1, n, self))
						)
					},
					() -> {
						""
					}
				)()
			};
			recurse(recurse, self)
		}
	}`));


	// Iterator builtins (lazy lists)
	// many iterator methods must be generated for different number of arguments
	for (let n = 1; n <= 10; n++) {
		const basePath = `__helios__iterator__${n}`;
		const head = (new Array(n)).fill("").map((_, i) => `head${i}`).join(", ");
		const unit = (new Array(n)).fill("").map((_, i) => "()").join(", ");
		const returnHead = n == 1 ? `${head}` : `(callback) -> {callback(${head})}`;

		add(new RawFunc(`${basePath}__drop`,
	`(self) -> {
		(n) -> {
			recurse = (recurse, iterator, i) -> {
				__core__ifThenElse(
					__core__lessThanEqualsInteger(i, 0),
					() -> {
						iterator
					},
					() -> {
						iterator(
							(is_null, ${head}, next_iterator) -> {
								__core__ifThenElse(
									is_null,
									() -> {
										iterator
									},
									() -> {
										recurse(recurse, next_iterator, __core__subtractInteger(i, 1))
									}
								)()
							}
						)
					}
				)()
			};
			recurse(recurse, self, n)
		}
	}`
		));

		add(new RawFunc(`${basePath}__is_empty`,
	`(self) -> {
		() -> {
			self(
				(is_null, ${head}, next_iterator) -> {
					is_null
				}
			)
		}
	}`
		));

		add(new RawFunc(`${basePath}__head`,
	`(self) -> {
		self(
			(is_null, ${head}, next_iterator) -> {
				${returnHead}
			}
		)
	}`
		));

		add(new RawFunc(`${basePath}__tail`,
	`(self) -> {
		self(
			(is_null, ${head}, next_iterator) -> {
				next_iterator
			}
		)
	}`
		));

		add(new RawFunc(`${basePath}__get`,
	`(self) -> {
		(i) -> {
			__core__ifThenElse(
				__core__lessThanInteger(i, 0),
				() -> {
					__helios__error("negative index in iterator.get()")
				},
				() -> {
					recurse = (recurse, iterator, i) -> {
						iterator(
							(is_null, ${head}, next_iterator) -> {
								__core__ifThenElse(
									is_null,
									() -> {
										__helios__error("index out of range")
									},
									() -> {
										__core__ifThenElse(
											__core__equalsInteger(i, 0),
											() -> {
												${returnHead}
											},
											() -> {
												recurse(recurse, next_iterator, __core__subtractInteger(i, 1))
											}
										)()
									}
								)()
								
							}
						)
					};
					recurse(recurse, self, i)
				}
			)()
		}
	}`
		));

		add(new RawFunc(`${basePath}__get_singleton`,
	`(self) -> {
		() -> {
			self(
				(is_null, ${head}, next_iterator) -> {
					__core__ifThenElse(
						is_null,
						() -> {
							__helios__error("empty iterator, not a singleton")
						},
						() -> {
							__core__ifThenElse(
								${basePath}__is_empty(next_iterator)(),
								() -> {
									${returnHead}
								},
								() -> {
									__helios__error("not a singleton iterator")
								}
							)()
						}
					)()
				}
			)
		}
	}`
		));

		add(new RawFunc(`${basePath}__take`,
	`(self) -> {
		(n) -> {
			recurse = (recurse, iterator, i) -> {
				__core__ifThenElse(
					__core__lessThanEqualsInteger(i, 0),
					() -> {
						(callback) -> {
							callback(true, ${unit}, ())
						}
					},
					() -> {
						iterator(
							(is_null, ${head}, next_iterator) -> {
								__core__ifThenElse(
									is_null,
									() -> {
										iterator
									},
									() -> {
										(callback) -> {
											callback(false, ${head}, recurse(recurse, next_iterator, __core__subtractInteger(i, 1)))
										}
									}
								)()	
							}
						)
					}
				)()
				
			};
			recurse(recurse, self, n)
		}
	}`
		));

		add(new RawFunc(`${basePath}__for_each`,
	`(self) -> {
		(fn) -> {
			recurse = (recurse, iterator) -> {
				iterator(
					(is_null, ${head}, next_iterator) -> {
						__core__ifThenElse(
							is_null,
							() -> {
								()
							},
							() -> {
								__core__chooseUnit(
									fn(${head}),
									recurse(recurse, next_iterator)
								)
							}
						)()
					}
				)
			};
			recurse(recurse, self)
		}
	}`
		));

		add(new RawFunc(`${basePath}__fold[${FTPP}0]`,
	`(self) -> {
		(fn, z0) -> {
			recurse = (recurse, iterator, z) -> {
				iterator(
					(is_null, ${head}, next_iterator) -> {
						__core__ifThenElse(
							is_null,
							() -> {
								z
							},
							() -> {
								recurse(recurse, next_iterator, fn(z, ${head}))
							}
						)()
					}
				)
			};
			recurse(recurse, self, z0)
		}
	}`
		));

		add(new RawFunc(`${basePath}__find`,
	`(self) -> {
		(fn) -> {
			recurse = (recurse, iterator) -> {
				iterator(
					(is_null, ${head}, next_iterator) -> {
						__core__ifThenElse(
							is_null,
							() -> {
								__helios__error("not found")
							},
							() -> {
								__core__ifThenElse(
									fn(${head}),
									() -> {
										${returnHead}
									},
									() -> {
										recurse(recurse, next_iterator)
									}
								)()
							}
						)()
					}
				)
			};
			recurse(recurse, self)
		}
	}`
		));

		add(new RawFunc(`${basePath}__any`,
	`(self) -> {
		(fn) -> {
			recurse = (recurse, iterator) -> {
				iterator(
					(is_null, ${head}, next_iterator) -> {
						__core__ifThenElse(
							is_null,
							() -> {
								false
							},
							() -> {
								__core__ifThenElse(
									fn(${head}),
									() -> {
										true
									},
									() -> {
										recurse(recurse, next_iterator)
									}
								)()
							}
						)()
					}
				)
			};
			recurse(recurse, self)
		}
	}`
		));

		add(new RawFunc(`${basePath}__prepend`,
	`(self) -> {
		(${head}) -> {
			(callback) -> {
				callback(false, ${head}, self)
			}
		}
	}`
		));

		add(new RawFunc(`${basePath}__filter`,
	`(self) -> {
		(fn) -> {
			recurse = (recurse, iterator) -> {
				iterator(
					(is_null, ${head}, next_iterator) -> {
						(callback) -> {
							__core__ifThenElse(
								is_null,
								() -> {
									callback(true, ${unit}, ())
								},
								() -> {
									__core__ifThenElse(
										fn(${head}),
										() -> {
											callback(false, ${head}, recurse(recurse, next_iterator))
										},
										() -> {
											recurse(recurse, next_iterator)(callback)
										}
									)()
								}
							)()
						}
					}
				)
			};
			recurse(recurse, self)
		}
	}`
		));
		add(new RawFunc(`${basePath}__map[${FTPP}0]`,
	`(self) -> {
		(fn) -> {
			recurse = (recurse, iterator) -> {
				iterator(
					(is_null, ${head}, next_iterator) -> {
						(callback) -> {
							__core__ifThenElse(
								is_null,
								() -> {
									callback(true, (), ())
								},
								() -> {
									callback(false, fn(${head}), recurse(recurse, next_iterator))
								}
							)()
						}
					}
				)
			};
			recurse(recurse, self)
		}
	}`
		));
		add(new RawFunc(`${basePath}__map2[${FTPP}0@${FTPP}1]`,
	`(self) -> {
		(fn) -> {
			recurse = (recurse, iterator) -> {
				iterator(
					(is_null, ${head}, next_iterator) -> {
						(callback) -> {
							__core__ifThenElse(
								is_null,
								() -> {
									callback(true, (), (), ())
								},
								() -> {
									fn(${head})(
										(new_head0, new_head1) -> {
											callback(false, new_head0, new_head1, recurse(recurse, next_iterator))
										}
									)
								}
							)()
						}
					}
				)
			};
			recurse(recurse, self)
		}
	}`
		));
		add(new RawFunc(`${basePath}__zip[${FTPP}0]`,
	`(self) -> {
		(lst) -> {
			recurse = (recurse, iterator, lst) -> {
				iterator(
					(is_null, ${head}, next_iterator) -> {
						__core__ifThenElse(
							is_null,
							(callback) -> {
								callback(true, ${unit}, (), ())
							},
							(callback) -> {
								__core__chooseList(
									lst,
									() -> {
										callback(true, ${unit}, (), ())
									},
									() -> {
										callback(
											false,
											${head},
											${FTPP}0__from_data(__core__headList__safe(lst)),
											recurse(recurse, next_iterator, __core__tailList__safe(lst))
										)
									}
								)()
							}
						)
					}
				)	
			};
			recurse(recurse, self, lst)
		}
	}`
		));
	}


	// Struct (list of data, which is used by structs which have more than 1 field, otherwise the internal of that single field is used directly)
	addSerializeFunc("__helios__struct");
	addNeqFunc("__helios__struct");
	addDataLikeEqFunc("__helios__struct");
	add(new RawFunc("__helios__struct__from_data", "__core__unListData"));
	add(new RawFunc("__helios__struct____to_data", "__core__listData"));


	// Tuple builtins
	add(new RawFunc("__helios__tuple[]____to_func", (ttp) => `__helios__common__identity`));
	add(new RawFunc("__helios__tuple[]__from_data", (ttp) => {
		assert(ttp.length >= 2);

		return `(data) -> {
			fields = __core__unListData(data);
			(callback) -> {
				callback(${ttp.map((tp, i) => {
					let inner = "fields";

					for (let j = 0; j < i; j++) {
						inner = `__core__tailList(${inner})`
					}

					return `${tp}__from_data(__core__headList(${inner}))`;
				}).join(", ")})
			}
		}`;
	}));
	add(new RawFunc("__helios__tuple[]__from_data_safe", (ttp) => {
		assert(ttp.length >= 2);

		let inner = `__helios__option__SOME_FUNC(
			(callback) -> {
				callback(${ttp.map((_, i) => `opt${i}`).join(", ")})
			}
		}`;

		for (let i = ttp.length - 1; i >= 0; i--) {
			inner = `opt${i}(
				(valid, value${i}) -> {
					__core__ifThenElse(
						valid,
						() -> {
							${inner}
						},
						() -> {
							__helios__option__NONE_FUNC
						}
					)()
				}
			)`;
		}

		for (let i = ttp.length - 1; i >= 0; i--) {
			inner = `(fields) -> {
				__core__chooseList(
					fields,
					() -> {
						__helios__option__NONE_FUNC
					},
					() -> {
						(opt${i}) -> {
							${i == ttp.length - 1 ? inner : `${inner}(__core__tailList__safe(fields))`}
						}(${ttp[i]}__from_data_safe(__core__headList__safe(fields)))
					}
				)()
			}`;
		}

		return `(data) -> {
			__core__chooseData(
				data,
				() -> {__helios__option__NONE_FUNC},
				() -> {__helios__option__NONE_FUNC},
				() -> {
					${inner}(__core__unListData__safe(data))
				},
				() -> {__helios__option__NONE_FUNC},
				() -> {__helios__option__NONE_FUNC}
			)
		}`;
	}));
	add(new RawFunc("__helios__tuple[]__show", (ttp) => {
		let inner = `${ttp[ttp.length-1]}__show(x${ttp.length-1})()`;

		for (let i = ttp.length - 2; i >= 0; i--) {
			inner = `__core__appendString(
				${ttp[i]}__show(x${i})(),
				__core__appendString(
					", ",
					${inner}
				)
			)`;
		}

		return `(tuple) -> {
			() -> {
				tuple(
					(${ttp.map((_, i) => `x${i}`).join(", ")}) -> {
						__core__appendString(
							"(",
							__core__appendString(
								${inner},
								")"
							)
						)
					}
				)
			}
		}`;
	}));
	add(new RawFunc("__helios__tuple[]____to_data", (ttp) => {
		assert(ttp.length >= 2);

		let inner = `__core__mkNilData(())`;

		for (let i = ttp.length - 1; i >= 0; i--) {
			inner = `__core__mkCons(${ttp[i]}____to_data(x${i}), ${inner})`;
		}

		return `(tuple) -> {
			tuple(
				(${ttp.map((tp, i) => `x${i}`).join(", ")}) -> {
					__core__listData(${inner})
				}
			)
		}`;
	}));
	add(new RawFunc("__helios__tuple[]__is_valid_data", (ttp) => {
		assert(ttp.length >= 2);

		let inner = `__core__chooseList(
			list,
			() -> {true},
			() -> {false}
		)()`;

		for (let i = ttp.length - 1; i >= 0; i--) {
			const tp = ttp[i];
			inner = `__core__chooseList(
				list,
				() -> {false},
				() -> {
					head = __core__headList__safe(list);
					list = __core__tailList__safe(list);
					__helios__bool__and(
						() -> {${tp}__is_valid_data(head)},
						() -> {
							${inner}
						}
					)
				}
			)()`;
		}


		return `(data) -> {
			__core__chooseData(
				data,
				() -> {false},
				() -> {false},
				() -> {
					list = __core__unListData__safe(list);
					${inner}
				},
				() -> {false},
				() -> {false}
			)()
		}`;
	}));
	add(new RawFunc("__helios__tuple[]__serialize", (ttp) => {
		assert(ttp.length >= 2);

		return `(tuple) -> {
			__helios__common__serialize(__helios__tuple[${ttp.join("@")}]____to_data(tuple))
		}`
	}));
	add(new RawFunc("__helios__tuple[]____eq", (ttp) => {
		assert(ttp.length >= 2);

		return `(a, b) -> {
			__helios__common____eq(
				__helios__tuple[${ttp.join("@")}]____to_data(a),
				__helios__tuple[${ttp.join("@")}]____to_data(b)
			)
		}`;
	}));
	add(new RawFunc("__helios__tuple[]____neq", (ttp) => {
		assert(ttp.length >= 2);
		
		return `(a, b) -> {
			__helios__common____neq(
				__helios__tuple[${ttp.join("@")}]____to_data(a),
				__helios__tuple[${ttp.join("@")}]____to_data(b)
			)
		}`;
	}));
	["first", "second", "third", "fourth", "fifth"].forEach((getter, i) => {
		add(new RawFunc(`__helios__tuple[]__${getter}`, (ttp) => {
			assert(ttp.length >= 2);
	
			return `(tuple) -> {
				tuple(
					(${ttp.map((tp, j) => `x${j}`).join(", ")}) -> {
						x${i}
					}
				)
			}`
		}));
	});
	

	// List builtins
	addSerializeFunc(`__helios__list[${TTPP}0]`);
	addNeqFunc(`__helios__list[${TTPP}0]`);
	addDataLikeEqFunc(`__helios__list[${TTPP}0]`);
	add(new RawFunc(`__helios__list[${TTPP}0]__is_valid_data_internal`,
	`(lst) -> {
		recurse = (recurse, lst) -> {
			__core__chooseList(
				lst,
				() -> {
					true
				},
				() -> {
					__core__ifThenElse(
						${TTPP}0__is_valid_data(__core__headList__safe(lst)),
						() -> {
							recurse(recurse, __core__tailList__safe(lst))
						},
						() -> {
							false
						}
					)()
				}
			)()
		};
		recurse(recurse, lst)
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__from_data`, 
	`(data) -> {
		lst = __core__unListData(data);
		_ = __core__ifThenElse(
			__helios__list[${TTPP}0]__is_valid_data_internal(lst),
			() -> {
				()
			},
			() -> {
				__core__trace("Warning: invalid list data", ())
			}
		)();
		lst
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__from_data_safe`,
	`(data) -> {
		__core__chooseData(
			data,
			() -> {__helios__option__NONE_FUNC},
			() -> {__helios__option__NONE_FUNC},
			() -> {
				__helios__option__SOME_FUNC(__core__unListData__safe(data))
			},
			() -> {__helios__option__NONE_FUNC},
			() -> {__helios__option__NONE_FUNC}
		)()
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__is_valid_data`,
	`(data) -> {
		__core__chooseData(
			data,
			() -> {false},
			() -> {false},
			() -> {
				__helios__list[${TTPP}0]__is_valid_data_internal(__core__unListData__safe(data))
			},
			() -> {false},
			() -> {false}
		)()
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]____to_data`, "__core__listData"));
	add(new RawFunc(`__helios__list[${TTPP}0]__show`, 
	`(self) -> {
		() -> {
			recurse = (recurse, self, first) -> {
				__core__chooseList(
					self,
					() -> {
						""
					},
					() -> {
						__core__appendString(
							__core__ifThenElse(
								first,
								() -> {
									""
								},
								() -> {
									", "
								}
							)(),
							head = ${TTPP}0__from_data_safe(__core__headList__safe(self));
							__core__appendString(
								head(
									(valid, value) -> {
										__core__ifThenElse(
											valid,
											() -> {
												${TTPP}0__show(value)()
											},
											() -> {
												"<n/a>"
											}
										)()
									}
								),
								recurse(recurse, __core__tailList__safe(self), false)
							)
						)
					}
				)()
			};
			__core__appendString(
				"[",
				__core__appendString(
					recurse(recurse, self, true),
					"]"
				)
			)
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__new`,
	`(n, fn) -> {
		recurse = (recurse, i) -> {
			__core__ifThenElse(
				__core__lessThanInteger(i, n),
				() -> {__core__mkCons(${TTPP}0____to_data(fn(i)), recurse(recurse, __core__addInteger(i, 1)))},
				() -> {__core__mkNilData(())}
			)()
		};
		recurse(recurse, 0)
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__new_const`,
	`(n, item) -> {
		__helios__list[${TTPP}0]__new(n, (i) -> {item})
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]____add`, "__helios__common__concat"));
	add(new RawFunc(`__helios__list[${TTPP}0]__length`, "__helios__common__length"));
	add(new RawFunc(`__helios__list[${TTPP}0]__head`, 
	`(self) -> {
		${TTPP}0__from_data(__core__headList(self))
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__tail`, "__core__tailList"));
	add(new RawFunc(`__helios__list[${TTPP}0]__is_empty`,
	`(self) -> {
		() -> {
			__core__nullList(self)
		}
	}`));
	add(new RawFunc(`__helios__list[__helios__data]__to_iterator`,
	`(self) -> {
		() -> {
			recurse = (recurse, lst) -> {
				(callback) -> {
					__core__chooseList(
						lst,
						() -> {
							callback(true, (), ())
						},
						() -> {
							callback(
								false, 
								__core__headList__safe(lst),
								recurse(recurse, __core__tailList__safe(lst))
							)
						}
					)()
				}
			};
			recurse(recurse, self)
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__to_iterator`,
	`(self) -> {
		() -> {
			recurse = (recurse, lst) -> {
				(callback) -> {
					__core__chooseList(
						lst,
						() -> {
							callback(true, (), ())
						},
						() -> {
							callback(
								false, 
								${TTPP}0__from_data(__core__headList__safe(lst)),
								recurse(recurse, __core__tailList__safe(lst))
							)
						}
					)()
				}
			};
			recurse(recurse, self)
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__from_iterator`,
	`(iterator) -> {
		recurse = (recurse, iterator) -> {
			iterator(
				(is_null, head, next_iterator) -> {
					__core__ifThenElse(
						is_null,
						() -> {
							__core__mkNilData(())
						},
						() -> {
							__core__mkCons(
								${TTPP}0____to_data(head),
								recurse(recurse, next_iterator)
							)
						}
					)()
				}
			)
		};
		recurse(recurse, iterator)
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__zip[${FTPP}0]`, 
	`(self) -> {
		(other) -> {
			recurse = (recurse, lst1, lst2) -> {
				__core__chooseList(
					lst1,
					(callback) -> {
						callback(true, (), (), ())
					},
					(callback) -> {
						__core__chooseList(
							lst2,
							() -> {
								callback(true, (), (), ())
							},
							() -> {
								callback(
									false,
									${TTPP}0__from_data(__core__headList__safe(lst1)),
									${FTPP}0__from_data(__core__headList__safe(lst2)),
									recurse(recurse, __core__tailList__safe(lst1), __core__tailList__safe(lst2))
								)
							}
						)()
					}
				)
			};
			recurse(recurse, self, other)
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__get`,
	`(self) -> {
		(index) -> {
			${TTPP}0__from_data(__helios__list[__helios__data]__get(self)(index))
		}
	}`));
	add(new RawFunc("__helios__list[__helios__data]__get",
	`(self) -> {
		(index) -> {
			recurse = (recurse, self, i) -> {
				__core__chooseList(
					self, 
					() -> {
						__helios__error("index out of range")
					}, 
					() -> {
						__core__ifThenElse(
							__core__equalsInteger(index, i), 
							() -> {
								__core__headList__safe(self)
							}, 
							() -> {
								recurse(recurse, __core__tailList__safe(self), __core__addInteger(i, 1))
							}
						)()
					}
				)()
			};
			recurse(recurse, self, 0)
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__get_singleton`,
	`(self) -> {
		() -> {
			${TTPP}0__from_data(
				__helios__list[__helios__data]__get_singleton(self)()
			)
		}
	}`));
	add(new RawFunc("__helios__list[__helios__data]__get_singleton",
	`(self) -> {
		() -> {
			__core__chooseUnit(
				__helios__assert(
					__core__nullList(__core__tailList(self)),
					"not a singleton list"
				),
				__core__headList(self)
			)
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__set`,
	`(self) -> {
		(index, item) -> {
			__helios__list[__helios__data]__set(self)(index, ${TTPP}0____to_data(item))
		}
	}`));
	add(new RawFunc(`__helios__list[__helios__data]__set`,
	`(self) -> {
		(index, item) -> {
			recurse = (recurse, lst, i) -> {
				__core__chooseList(
					lst,
					() -> {
						__helios__error("index out of range")
					},
					() -> {
						__core__ifThenElse(
							__core__equalsInteger(i, index),
							() -> {
								__core__mkCons(item, __core__tailList__safe(lst))
							},
							() -> {
								__core__mkCons(
									__core__headList__safe(lst),
									recurse(recurse, __core__tailList__safe(lst), __core__addInteger(i, 1))
								)
							}
						)()
					}
				)()
			};
			recurse(recurse, self, 0)
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__split_at`, "__helios__list[__helios__data]__split_at"));
	add(new RawFunc(`__helios__list[__helios__data]__split_at`, 
	`(self) -> {
		(index) -> {
			recurse = (recurse, lst, i, build_head) -> {
				__core__chooseList(
					lst,
					() -> {
						__helios__error("index out of range")
					},
					() -> {
						__core__ifThenElse(
							__core__equalsInteger(i, index),
							() -> {
								(callback) -> {
									callback(build_head(__core__mkNilData(())), lst)
								}
							},
							() -> {
								recurse(recurse, __core__tailList__safe(lst), __core__addInteger(i, 1), (h) -> {
									build_head(__core__mkCons(__core__headList__safe(lst), h))
								})
							}
						)()
					}
				)()
			};
			recurse(recurse, self, 0, (head) -> {head})
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__drop`, "__helios__list[__helios__data]__drop"));
	add(new RawFunc("__helios__list[__helios__data]__drop",
	`(self) -> {
		(n) -> {
			recurse = (recurse, lst, n) -> {
				__core__ifThenElse(
					__core__equalsInteger(n, 0),
					() -> {
						lst
					},
					() -> {
						recurse(
							recurse,
							__core__tailList(lst),
							__core__subtractInteger(n, 1)
						)
					}
				)()
			};
			__core__ifThenElse(
				__core__lessThanInteger(n, 0),
				() -> {
					__helios__error("negative n in drop")
				},
				() -> {
					recurse(recurse, self, n)
				}
			)()
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__drop_end`, "__helios__list[__helios__data]__drop_end"));
	add(new RawFunc("__helios__list[__helios__data]__drop_end",
	`(self) -> {
		(n) -> {
			recurse = (recurse, lst) -> {
				__core__chooseList(
					lst,
					() -> {
						(callback) -> {callback(0, lst)}
					},
					() -> {
						recurse(recurse, __core__tailList__safe(lst))(
							(count, result) -> {
								__core__ifThenElse(
									__core__equalsInteger(count, n),
									() -> {
										(callback) -> {
											callback(
												count,
												__core__mkCons(
													__core__headList__safe(lst), 
													result
												)
											)
										}
									},
									() -> {
										(callback) -> {
											callback(
												__core__addInteger(count, 1),
												result
											)
										}
									}
								)()
							}
						)
					}
				)()
			};
			__core__ifThenElse(
				__core__lessThanInteger(n, 0),
				() -> {
					__helios__error("negative n in drop_end")
				},
				() -> {
					recurse(recurse, self)(
						(count, result) -> {
							__core__ifThenElse(
								__core__lessThanInteger(count, n),
								() -> {
									__helios__error("list too short")
								},
								() -> {
									result
								}
							)()
						}
					)
				}
			)()
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__take`, "__helios__list[__helios__data]__take"));
	add(new RawFunc("__helios__list[__helios__data]__take",
	`(self) -> {
		(n) -> {
			recurse = (recurse, lst, n) -> {
				__core__ifThenElse(
					__core__equalsInteger(n, 0),
					() -> {
						__core__mkNilData(())
					},
					() -> {
						__core__mkCons(
							__core__headList(lst),
							recurse(
								recurse,
								__core__tailList(lst),
								__core__subtractInteger(n, 1)
							)
						)
					}
				)()
			};
			__core__ifThenElse(
				__core__lessThanInteger(n, 0),
				() -> {
					__helios__error("negative n in take")
				},
				() -> {
					recurse(recurse, self, n)
				}
			)()
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__take_end`, "__helios__list[__helios__data]__take_end"));
	add(new RawFunc(`__helios__list[__helios__data]__take_end`,
	`(self) -> {
		(n) -> {
			recurse = (recurse, lst) -> {
				__core__chooseList(
					lst,
					() -> {
						(callback) -> {callback(0, lst)}
					},
					() -> {
						recurse(recurse, __core__tailList__safe(lst))(
							(count, tail) -> {
								__core__ifThenElse(
									__core__equalsInteger(count, n),
									() -> {
										(callback) -> {callback(count, tail)}
									},
									() -> {
										(callback) -> {
											callback(
												__core__addInteger(count, 1),
												lst
											)
										}
									}
								)()
							}
						)
					}
				)()
			};
			__core__ifThenElse(
				__core__lessThanInteger(n, 0),
				() -> {
					__helios__error("negative n in take_end")
				},
				() -> {
					recurse(recurse, self)(
						(count, result) -> {
							__core__ifThenElse(
								__core__lessThanInteger(count, n),
								() -> {
									__helios__error("list too short")
								},
								() -> {
									result
								}
							)()
						}
					)
				}
			)()
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__any`,
	`(self) -> {
		(fn) -> {
			__helios__common__any(
				self, 
				(item) -> {
					fn(${TTPP}0__from_data(item))
				}
			)
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__all`,
	`(self) -> {
		(fn) -> {
			__helios__common__all(
				self, 
				(item) -> {
					fn(${TTPP}0__from_data(item))
				}
			)
		}
	}`));
	add(new RawFunc(`__helios__list[__helios__data]__append`,
	`(self) -> {
		(item) -> {
			recurse = (recurse, lst) -> {
				__core__chooseList(
					lst,
					() -> {
						__core__mkCons(item, lst)
					},
					() -> {
						__core__mkCons(__core__headList__safe(lst), recurse(recurse, __core__tailList__safe(lst)))
					}
				)()
			};
			recurse(recurse, self)
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__append`,
	`(self) -> {
		(item) -> {
			__helios__list[__helios__data]__append(self)(${TTPP}0____to_data(item))
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__prepend`,
	`(self) -> {
		(item) -> {
			__core__mkCons(${TTPP}0____to_data(item), self)
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__find`,
	`(self) -> {
		(fn) -> {
			recurse = (recurse, lst) -> {
				__core__chooseList(
					lst, 
					() -> {__helios__error("not found")}, 
					() -> {
						item = ${TTPP}0__from_data(__core__headList__safe(lst));
						__core__ifThenElse(
							fn(item), 
							() -> {item}, 
							() -> {recurse(recurse, __core__tailList__safe(lst))}
						)()
					}
				)()
			};
			recurse(recurse, self)
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__find_index`,
	`(self) -> {
		(fn) -> {
			recurse = (recurse, lst, i) -> {
				__core__chooseList(
					lst,
					() -> {-1},
					() -> {
						item = ${TTPP}0__from_data(__core__headList__safe(lst));
						__core__ifThenElse(
							fn(item),
							() -> {i},
							() -> {recurse(recurse, __core__tailList__safe(lst), __core__addInteger(i, 1))}
						)()
					}
				)()
			};
			recurse(recurse, self, 0)
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__find_safe`,
	`(self) -> {
		(fn) -> {
			__helios__common__find_safe(
				self,
				(item) -> {
					fn(${TTPP}0__from_data(item))
				},
				__helios__common__identity
			)
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__filter`,
	`(self) -> {
		(fn) -> {
			__helios__common__filter_list(
				self, 
				(item) -> {
					fn(${TTPP}0__from_data(item))
				}
			)
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__for_each`,
	`(self) -> {
		(fn) -> {
			recurse = (recurse, lst) -> {
				__core__chooseList(
					lst,
					() -> {
						()
					},
					() -> {
						__core__chooseUnit(
							fn(${TTPP}0__from_data(__core__headList__safe(lst))),
							recurse(recurse, __core__tailList__safe(lst))
						)
					}
				)()
			};
			recurse(recurse, self)
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__fold[${FTPP}0]`,
	`(self) -> {
		(fn, a0) -> {
			__helios__common__fold(
				self, 
				(prev, item) -> {
					fn(prev, ${TTPP}0__from_data(item))
				}, 
				a0
			)
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__fold2[${FTPP}0@${FTPP}1]`,
	`(self) -> {
		(fn, a0, b0) -> {
			__helios__common__fold(
				self,
				(prev, item) -> {
					prev(
						(a, b) -> {
							fn(a, b, ${TTPP}0__from_data(item))
						}
					)
				},
				(callback) -> {
					callback(a0, b0)
				}
			)
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__fold3[${FTPP}0@${FTPP}1@${FTPP}2]`,
	`(self) -> {
		(fn, a0, b0, c0) -> {
			__helios__common__fold(
				self,
				(prev, item) -> {
					prev(
						(a, b, c) -> {
							fn(a, b, c, ${TTPP}0__from_data(item))
						}
					)
				},
				(callback) -> {
					callback(a0, b0, c0)
				}
			)
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__fold_lazy[${FTPP}0]`,
	`(self) -> {
		(fn, a0) -> {
			__helios__common__fold_lazy(
				self, 
				(item, next) -> {
					fn(${TTPP}0__from_data(item), next)
				},
				a0
			)
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__fold2_lazy[${FTPP}0@${FTPP}1]`,
	`(self) -> {
		(fn, a0, b0) -> {
			__helios__common__fold_lazy(
				self, 
				(item, next) -> {
					fn(${TTPP}0__from_data(item), next)
				},
				(callback) -> {
					callback(a0, b0)
				}
			)
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__map[${FTPP}0]`,
	`(self) -> {
		(fn) -> {
			__helios__common__map(
				self, 
				(item) -> {
					${FTPP}0____to_data(fn(${TTPP}0__from_data(item)))
				}, 
				__core__mkNilData(())
			)
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__map_option[${FTPP}0]`,
	`(self) -> {
		(fn) -> {
			recurse = (recurse, lst) -> {
				__core__chooseList(
					lst,
					() -> {
						lst
					},
					() -> {
						head = ${TTPP}0__from_data(__core__headList__safe(lst));
						tail = recurse(recurse, __core__tailList__safe(lst));
						opt = __core__unConstrData(fn(head));
						__core__ifThenElse(
							__core__equalsInteger(__core__fstPair(opt), 0),
							() -> {
								__core__mkCons(
									__core__headList(__core__sndPair(opt)),
									tail
								)
							},	
							() -> {
								tail
							}
						)()
						
					}
				)()
			};
			recurse(recurse, self)
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__sort`,
	`(self) -> {
		(comp) -> {
			__helios__common__sort(
				self, 
				(a, b) -> {
					comp(${TTPP}0__from_data(a), ${TTPP}0__from_data(b))
				}
			)
		}
	}`));


	// List specials
	add(new RawFunc(`__helios__list[${TTPP}0]__sum`,
	`(self) -> {
		() -> {
			recurse = (recurse, lst) -> {
				__core__chooseList(
					lst,
					() -> {
						0
					},
					() -> {
						${TTPP}0____add(
							${TTPP}0__from_data(__core__headList__safe(lst)),
							recurse(recurse, __core__tailList__safe(lst))
						)
					}
				)()
			};
			recurse(recurse, self)
		}
	}`));
	add(new RawFunc("__helios__list[__helios__string]__join",
	`(self) -> {
		(__useopt__separator, separator) -> {
			separator = __core__ifThenElse(__useopt__separator, separator, "");
			recurse = (recurse, lst, sep) -> {
				__core__chooseList(
					lst,
					() -> {
						""
					},
					() -> {
						__helios__string____add(
							__helios__string____add(
								sep,
								__helios__string__from_data(__core__headList__safe(lst))
							),
							recurse(recurse, __core__tailList__safe(lst), separator)
						)
					}
				)()
			};
			recurse(recurse, self, "")
		}
	}`));
	add(new RawFunc("__helios__list[__helios__bytearray]__join",
	`(self) -> {
		(__useopt__separator, separator) -> {
			separator = __core__ifThenElse(__useopt__separator, separator, #);
			recurse = (recurse, lst, sep) -> {
				__core__chooseList(
					lst,
					() -> {
						#
					},
					() -> {
						__helios__bytearray____add(
							__helios__bytearray____add(
								sep,
								__core__unBData(__core__headList__safe(lst))
							),
							recurse(recurse, __core__tailList__safe(lst), separator)
						)
					}
				)()
			};
			recurse(recurse, self, #)
		}
	}`));
	add(new RawFunc(`__helios__list[${TTPP}0]__flatten`,
	`(self) -> {
		() -> {
			recurse = (recurse, lst) -> {
				__core__chooseList(
					lst,
					() -> {
						__core__mkNilData(())
					},
					() -> {
						__helios__list[${TTPP}0]____add(
							__core__unListData(__core__headList__safe(lst)),
							recurse(recurse, __core__tailList__safe(lst))
						)
					}
				)()
			};
			recurse(recurse, self)
		}
	}`));


	// Map builtins
	addSerializeFunc(`__helios__map[${TTPP}0@${TTPP}1]`);
	addNeqFunc(`__helios__map[${TTPP}0@${TTPP}1]`);
	addDataLikeEqFunc(`__helios__map[${TTPP}0@${TTPP}1]`);
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__is_valid_data_internal`,
	`(map) -> {
		recurse = (recurse, map) -> {
			__core__chooseList(
				map,
				() -> {
					true
				},
				() -> {
					head = __core__headList__safe(map);
					__core__ifThenElse(
						${TTPP}0__is_valid_data(__core__fstPair(head)),
						() -> {
							__core__ifThenElse(
								${TTPP}1__is_valid_data(__core__sndPair(head)),
								() -> {
									recurse(recurse, __core__tailList__safe(map))
								},
								() -> {
									false
								}
							)()
						},
						() -> {
							false
						}
					)()
				}
			)()
		};
		recurse(recurse, map)
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__from_data`, 
	`(data) -> {
		map = __core__unMapData(data);
		_ = __core__ifThenElse(
			__helios__map[${TTPP}0@${TTPP}1]__is_valid_data_internal(map),
			() -> {
				()
			},
			() -> {
				__core__trace("Warning: invalid map data", ())
			}
		)();
		map
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__from_data_safe`,
	`(data) -> {
		__core__chooseData(
			data,
			() -> {__helios__option__NONE_FUNC},
			() -> {
				__helios__option__SOME_FUNC(__core__unMapData__safe(data))
			},
			() -> {__helios__option__NONE_FUNC},
			() -> {__helios__option__NONE_FUNC},
			() -> {__helios__option__NONE_FUNC}
		)()
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__show`,
	`(self) -> {
		() -> {
			recurse = (recurse, self, first) -> {
				__core__chooseList(
					self,
					() -> {
						""
					},
					() -> {
						__core__appendString(
							__core__ifThenElse(
								first,
								() -> {""},
								() -> {", "}
							)(),
							head = __core__headList__safe(self);
							key = ${TTPP}0__from_data_safe(__core__fstPair(head));
							value = ${TTPP}1__from_data_safe(__core__sndPair(head));
							__core__appendString(
								__core__appendString(
									__core__appendString(
										key(
											(valid, key) -> {
												__core__ifThenElse(
													valid,
													() -> {
														${TTPP}0__show(key)()
													},
													() -> {
														"<n/a>"
													}
												)()
											}
										),
										": "
									),
									value(
										(valid, value) -> {
											__core__ifThenElse(
												valid,
												() -> {
													${TTPP}1__show(value)()
												},
												() -> {
													"<n/a>"
												}
											)
										}
									)
								),
								recurse(recurse, __core__tailList__safe(self), false)
							)
						)
					}
				)()
			};
			__core__appendString(
				"{",
				__core__appendString(
					recurse(recurse, self, true),
					"}"
				)
			)
		}
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__is_valid_data`,
	`(data) -> {
		__core__chooseData(
			data,
			() -> {false},
			() -> {
				__helios__map[${TTPP}0@${TTPP}1]__is_valid_data_internal(__core__unMapData__safe(data))
			},
			() -> {false},
			() -> {false},
			() -> {false}
		)()
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]____to_data`, "__core__mapData"));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]____add`, "__helios__common__concat"));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__prepend`,
	`(self) -> {
		(key, value) -> {
			__core__mkCons(__core__mkPairData(${TTPP}0____to_data(key), ${TTPP}1____to_data(value)), self)
		}
	}`));
	add(new RawFunc(`__helios__map[__helios__data@__helios__data]__append`,
	`(self) -> {
		(key, value) -> {
			__helios__list[__helios__data]__append(self)(__core__mkPairData(key, value))
		}
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__append`,
	`(self) -> {
		(key, value) -> {
			__helios__map[__helios__data@__helios__data]__append(self)(${TTPP}0____to_data(key), ${TTPP}1____to_data(value))
		}
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__head`,
	`(self) -> {
		head = __core__headList(self);
		(callback) -> {
			callback(${TTPP}0__from_data(__core__fstPair(head)), ${TTPP}1__from_data(__core__sndPair(head)))
		}
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__head_key`,
	`(self) -> {
		${TTPP}0__from_data(__core__fstPair(__core__headList(self)))
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__head_value`,
	`(self) -> {
		${TTPP}1__from_data(__core__sndPair(__core__headList(self)))
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__length`,
	`(self) -> {
		__helios__common__length(self)
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__tail`, "__core__tailList"));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__is_empty`,
	`(self) -> {
		() -> {
			__core__nullList(self)
		}
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__get`,
	`(self) -> {
		(key) -> {
			__helios__common__map_get(
				self, 
				${TTPP}0____to_data(key), 
				(x) -> {${TTPP}1__from_data(x)}, 
				() -> {__helios__error("key not found")}
			)
		}
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__get_safe`,
	`(self) -> {
		(key) -> {
			__helios__common__map_get(
				self, 
				${TTPP}0____to_data(key), 
				(x) -> {
					__core__constrData(0, __helios__common__list_1(x))
				}, 
				() -> {
					__core__constrData(1, __helios__common__list_0)
				}
			)
		}
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__all`,
	`(self) -> {
		(fn) -> {
			fn = (pair) -> {
				fn(${TTPP}0__from_data(__core__fstPair(pair)), ${TTPP}1__from_data(__core__sndPair(pair)))
			};
			__helios__common__all(self, fn)
		}
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__any`,
	`(self) -> {
		(fn) -> {
			fn = (pair) -> {
				fn(${TTPP}0__from_data(__core__fstPair(pair)), ${TTPP}1__from_data(__core__sndPair(pair)))
			};
			__helios__common__any(self, fn)
		}
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__delete`,
	`(self) -> {
		(key) -> {
			key = ${TTPP}0____to_data(key);
			recurse = (recurse, self) -> {
				__core__chooseList(
					self,
					() -> {self},
					() -> {
						head = __core__headList__safe(self);
						tail = __core__tailList__safe(self);
						__core__ifThenElse(
							__core__equalsData(key, __core__fstPair(head)),
							() -> {recurse(recurse, tail)},
							() -> {__core__mkCons(head, recurse(recurse, tail))}
						)()
					}
				)()
			};
			recurse(recurse, self)
		}
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__filter`,
	`(self) -> {
		(fn) -> {
			__helios__common__filter_map(
				self, 
				(pair) -> {
					fn(${TTPP}0__from_data(__core__fstPair(pair)), ${TTPP}1__from_data(__core__sndPair(pair)))
				}
			)
		}
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__find`,
	`(self) -> {
		(fn) -> {
			recurse = (recurse, self) -> {
				__core__chooseList(
					self, 
					() -> {__helios__error("not found")}, 
					() -> {
						head = __core__headList__safe(self);
						key = ${TTPP}0__from_data(__core__fstPair(head));
						value = ${TTPP}1__from_data(__core__sndPair(head));
						__core__ifThenElse(
							fn(key, value), 
							() -> {
								(callback) -> {
									callback(key, value)
								}
							}, 
							() -> {
								recurse(recurse, __core__tailList__safe(self))
							}
						)()
					}
				)()
			};
			recurse(recurse, self)
		}
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__find_safe`,
	`(self) -> {
		(fn) -> {
			recurse = (recurse, self, fn) -> {
				__core__chooseList(
					self, 
					() -> {
						(callback) -> {
							callback(() -> {__helios__error("not found")}, false)
						}
					}, 
					() -> {
						head = __core__headList__safe(self);
						key = ${TTPP}0__from_data(__core__fstPair(head));
						value = ${TTPP}1__from_data(__core__sndPair(head));
						__core__ifThenElse(
							fn(key, value), 
							() -> {
								(callback) -> {
									callback(
										() -> {
											(callback) -> {
												callback(key, value)
											}
										},
										true
									)
								}
							}, 
							() -> {
								recurse(recurse, __core__tailList__safe(self), fn)
							}
						)()
					}
				)()
			};
			recurse(recurse, self, fn)
		}
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__find_key`,
	`(self) -> {
		(fn) -> {
			recurse = (recurse, map) -> {
				__core__chooseList(
					map, 
					() -> {__helios__error("not found")}, 
					() -> {
						item = ${TTPP}0__from_data(__core__fstPair(__core__headList__safe(map)));
						__core__ifThenElse(
							fn(item), 
							() -> {item}, 
							() -> {recurse(recurse, __core__tailList__safe(map))}
						)()
					}
				)()
			};
			recurse(recurse, self)
		}
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__find_key_safe`,
	`(self) -> {
		(fn) -> {
			__helios__common__find_safe(
				self,
				(pair) -> {
					fn(${TTPP}0__from_data(__core__fstPair(pair)))
				},
				__core__fstPair
			)
		}
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__find_value`,
	`(self) -> {
		(fn) -> {
			recurse = (recurse, map) -> {
				__core__chooseList(
					map, 
					() -> {__helios__error("not found")}, 
					() -> {
						item = ${TTPP}1__from_data(__core__sndPair(__core__headList__safe(map)));
						__core__ifThenElse(
							fn(item), 
							() -> {item}, 
							() -> {recurse(recurse, __core__tailList__safe(map))}
						)()
					}
				)()
			};
			recurse(recurse, self)
		}
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__find_value_safe`,
	`(self) -> {
		(fn) -> {
			__helios__common__find_safe(
				self,
				(pair) -> {
					fn(${TTPP}1__from_data(__core__sndPair(pair)))
				},
				__core__sndPair
			)
		}
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__map[${FTPP}0@${FTPP}1]`,
	`(self) -> {
		(fn) -> {
			__helios__common__map(
				self,
				(pair) -> {
					mapped_pair = fn(${TTPP}0__from_data(__core__fstPair(pair)), ${TTPP}1__from_data(__core__sndPair(pair)));
					mapped_pair(
						(key, value) -> {
							__core__mkPairData(${FTPP}0____to_data(key), ${FTPP}1____to_data(value))
						}
					)
				}, 
				__core__mkNilPairData(())
			)
		}
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__fold[${FTPP}0]`,
	`(self) -> {
		(fn, z) -> {
			__helios__common__fold(self,
				(z, pair) -> {
					fn(z, ${TTPP}0__from_data(__core__fstPair(pair)), ${TTPP}1__from_data(__core__sndPair(pair)))
				}, 
				z
			)
		}
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__fold_lazy[${FTPP}0]`,
	`(self) -> {
		(fn, z) -> {
			__helios__common__fold_lazy(self, 
				(pair, next) -> {
					fn(${TTPP}0__from_data(__core__fstPair(pair)), ${TTPP}1__from_data(__core__sndPair(pair)), next)
				}, 
				z
			)
		}
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__for_each`,
	`(self) -> {
		(fn) -> {
			recurse = (recurse, map) -> {
				__core__chooseList(
					map,
					() -> {
						()
					},
					() -> {
						head = __core__headList__safe(map);
						__core__chooseUnit(
							fn(${TTPP}0__from_data(__core__fstPair(head)), ${TTPP}1__from_data(__core__sndPair(head))),
							recurse(recurse, __core__tailList__safe(map))
						)
					}
				)()
			};
			recurse(recurse, self)
		}
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__set`, 
	`(self) -> {
		(key, value) -> {
			key = ${TTPP}0____to_data(key);
			value = ${TTPP}1____to_data(value);
			recurse = (recurse, self) -> {
				__core__chooseList(
					self,
					() -> {
						__core__mkCons(__core__mkPairData(key, value), __core__mkNilPairData(()))
					},
					() -> {
						head = __core__headList__safe(self);
						tail = __core__tailList__safe(self);
						__core__ifThenElse(
							__core__equalsData(key, __core__fstPair(head)),
							() -> {
								__core__mkCons(__core__mkPairData(key, value), tail)
							},
							() -> {
								__core__mkCons(head, recurse(recurse, tail))
							}
						)()
					}
				)()
			};
			recurse(recurse, self)
		}
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__to_iterator`,
	`(self) -> {
		() -> {
			recurse = (recurse, map) -> {
				(callback) -> {
					__core__chooseList(
						map,
						() -> {
							callback(true, (), (), ())
						},
						() -> {
							head = __core__headList__safe(map);
							callback(
								false, 
								${TTPP}0__from_data(__core__fstPair(head)),
								${TTPP}1__from_data(__core__sndPair(head)),
								recurse(recurse, __core__tailList__safe(map))
							)
						}
					)()
				}
			};
			recurse(recurse, self)
		}
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__from_iterator`,
	`(iterator) -> {
		recurse = (recurse, iterator) -> {
			iterator(
				(is_null, head0, head1, next_iterator) -> {
					__core__ifThenElse(
						is_null,
						() -> {
							__core__mkNilPairData(())
						},
						() -> {
							__core__mkCons(
								__core__mkPairData(${TTPP}0____to_data(head0), ${TTPP}1____to_data(head1)),
								recurse(recurse, next_iterator)
							)
						}
					)()
				}
			)
		};
		recurse(recurse, iterator)
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__update`,
	`(self) -> {
		(key, fn) -> {
			key = ${TTPP}0____to_data(key);
			recurse = (recurse, map) -> {
				__core__chooseList(
					map,
					() -> {
						__helios__error("key not found")
					},
					() -> {
						pair = __core__headList__safe(map);
						__core__ifThenElse(
							__core__equalsData(key, __core__fstPair(pair)),
							() -> {
								__core__mkCons(
									__core__mkPairData(
										key,
										${TTPP}1____to_data(fn(${TTPP}1__from_data(__core__sndPair(pair))))
									),
									__core__tailList(map)
								)
							},
							() -> {
								__core__mkCons(pair, recurse(recurse, __core__tailList__safe(map)))
							}
						)()
					}
				)()
			};
			recurse(recurse, self)
		}
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__update_safe`,
	`(self) -> {
		(key, fn) -> {
			key = ${TTPP}0____to_data(key);
			__helios__common__map(
				self,
				(pair) -> {
					oldKey = __core__fstPair(pair);
					oldValue = __core__sndPair(pair);
					newValue = __core__ifThenElse(
						__core__equalsData(oldKey, key),
						() -> {
							${TTPP}1____to_data(fn(${TTPP}1__from_data(oldValue)))
						},
						() -> {
							oldValue
						}
					)();
					__core__mkPairData(oldKey, newValue)
				}, 
				__core__mkNilPairData(())
			)
		}
	}`));
	add(new RawFunc(`__helios__map[${TTPP}0@${TTPP}1]__sort`,
	`(self) -> {
		(comp) -> {
			__helios__common__sort(
				self, 
				(a, b) -> {
					comp(
						${TTPP}0__from_data(__core__fstPair(a)), 
						${TTPP}1__from_data(__core__sndPair(a)), 
						${TTPP}0__from_data(__core__fstPair(b)),
						${TTPP}1__from_data(__core__sndPair(b))
					)
				}
			)
		}
	}`));


	// Option[T] builtins
	add(new RawFunc(`__helios__option[${TTPP}0]__is_valid_data`,
	`(data) -> {
		__core__chooseData(
			data,
			() -> {
				pair = __core__unConstrData__safe(data);
				index = __core__fstPair(pair);
				fields = __core__sndPair(pair);
				__core__ifThenElse(
					__core__equalsInteger(index, 0),
					() -> {
						__core__chooseList(
							fields,
							() -> {
								false
							},
							() -> {
								__core__chooseList(
									__core__tailList__safe(fields),
									() -> {
										${TTPP}0__is_valid_data(__core__headList__safe(fields))
									},
									() -> {
										false
									}
								)()
							}
						)()
					},
					() -> {
						__core__ifThenElse(
							__core__equalsInteger(index, 1),
							() -> {
								__core__chooseList(
									fields,
									true,
									false
								)
							},
							() -> {
								false
							}
						)()
					}
				)()
			},
			() -> {false},
			() -> {false},
			() -> {false},
			() -> {false}
		)()
	}`))
	addDataFuncs(`__helios__option[${TTPP}0]`, {
		from_data: `(data) -> {
			_ = __core__ifThenElse(
				__helios__option[${TTPP}0]__is_valid_data(data),
				() -> {
					()
				},
				() -> {
					__core__trace("Warning: invalid option data", ())
				}
			)();
			data
		}`,
		from_data_safe: `(data) -> {
			__core__chooseData(
				data,
				() -> {
					__helios__option__SOME_FUNC(data)
				},
				() -> {__helios__option__NONE_FUNC},
				() -> {__helios__option__NONE_FUNC},
				() -> {__helios__option__NONE_FUNC},
				() -> {__helios__option__NONE_FUNC}
			)()
		}`
	});
	add(new RawFunc(`__helios__option[${TTPP}0]__map[${FTPP}0]`, 
	`(self) -> {
		(fn) -> {
			pair = __core__unConstrData(self);
			__core__ifThenElse(
				__core__equalsInteger(__core__fstPair(pair), 0),
				() -> {
					__helios__option[${FTPP}0]__some__new(
						fn(
							${TTPP}0__from_data(
								__core__headList(__core__sndPair(pair))
							)
						)
					)
				},
				() -> {
					__helios__option[${FTPP}0]__none__new()
				}
			)()
		}
	}`));
	add(new RawFunc(`__helios__option[${TTPP}0]__unwrap`, 
	`(self) -> {
		() -> {
			${TTPP}0__from_data(__helios__common__enum_field_0(self))
		}
	}`));
	add(new RawFunc(`__helios__option[${TTPP}0]__show`,
	`(self) -> {
		__core__chooseData(
			self,
			() -> {
				pair = __core__unConstrData__safe(self);
				index = __core__fstPair(pair);
				__core__ifThenElse(
					__core__equalsInteger(index, 0),
					() -> {
						fields = __core__sndPair(pair);
						__core__chooseList(
							fields,
							() -> {
								"Option::Some{<n/a>}"
							},
							() -> {
								some = ${TTPP}0__from_data_safe(__core__headList__safe(fields));
								some(
									(valid, value) -> {
										__core__ifThenElse(
											valid,
											() -> {
												__core__appendString(
													"Option::Some{",
													__core__appendString(
														${TTPP}0__show(value)(),
														"}"
													)
												)
											},
											() -> {
												"Option::Some{<n/a>}"
											}
										)()
									}
								)
							}
						)()
					},
					() -> {
						"Option::None"
					}
				)()
			},
			() -> {"Option{<n/a>}"},
			() -> {"Option{<n/a>}"},
			() -> {"Option{<n/a>}"},
			() -> {"Option{<n/a>}"}
		)
	}`))


	// Option[T]::Some
	addEnumDataFuncs(`__helios__option[${TTPP}0]__some`, 0);
	add(new RawFunc("__helios__option__SOME_FUNC", 
	`(some) -> {
		(callback) -> {callback(true, some)}
	}`));
	add(new RawFunc(`__helios__option[${TTPP}0]__some____new`,
	`(some) -> {
		__core__constrData(0, __helios__common__list_1(${TTPP}0____to_data(some)))
	}`));
	add(new RawFunc(`__helios__option[${TTPP}0]__some__new`, `__helios__option[${TTPP}0]__some____new`));
	add(new RawFunc(`__helios__option[${TTPP}0]__some__cast`,
	`(data) -> {
		__helios__common__assert_constr_index(data, 0)
	}`));
	add(new RawFunc(`__helios__option[${TTPP}0]__some__some`, 
	`(self) -> {
		${TTPP}0__from_data(__helios__common__enum_field_0(self))
	}`));
	add(new RawFunc(`__helios__option__is_some`,
	`(data) -> {
		__core__equalsInteger(__core__fstPair(__core__unConstrData(data)), 0)
	}`));
	

	// Option[T]::None
	addEnumDataFuncs(`__helios__option[${TTPP}0]__none`, 1);
	add(new RawFunc("__helios__option__NONE", "__core__constrData(1, __helios__common__list_0)"));
	add(new RawFunc("__helios__option__NONE_FUNC", `(callback) -> {callback(false, ())}`));
	add(new RawFunc(`__helios__option[${TTPP}0]__none____new`,
	`() -> {
		__helios__option__NONE
	}`));
	add(new RawFunc(`__helios__option[${TTPP}0]__none__new`, `__helios__option[${TTPP}0]__none____new`));
	add(new RawFunc(`__helios__option[${TTPP}0]__none__cast`,
	`(data) -> {
		__helios__common__assert_constr_index(data, 1)
	}`));


	// ScriptHash builtin
	addByteArrayLikeFuncs("__helios__scripthash");
	add(new RawFunc("__helios__scripthash__is_valid_data", `(data) -> {__helios__bytearray__is_valid_data_fixed_length(data, 28)}`));

	
	for (let hash of ["pubkeyhash", "validatorhash", "mintingpolicyhash", "stakingvalidatorhash", "datumhash"]) {
	// Hash builtins
		addByteArrayLikeFuncs(`__helios__${hash}`);
		add(new RawFunc(`__helios__${hash}__is_valid_data`,
		`(data) -> {
			__helios__bytearray__is_valid_data_fixed_length(data, ${hash == "datumhash" ? 32 : 28})
		}`));
		add(new RawFunc(`__helios__${hash}__from_script_hash`, "__helios__common__identity"));
	}

	
	// PubKey builtin
	addByteArrayLikeFuncs("__helios__pubkey");
	add(new RawFunc("__helios__pubkey__is_valid_data", `(data) -> {__helios__bytearray__is_valid_data_fixed_length(data, 32)}`));
	add(new RawFunc("__helios__pubkey__verify", 
	`(self) -> {
		(message, signature) -> {
			__core__verifyEd25519Signature(self, message, signature)
		}
	}`));


	// ScriptContext builtins
	addDataFuncs("__helios__scriptcontext");
	// TODO: test fields
	add(new RawFunc("__helios__scriptcontext__is_valid_data",
	`(data) -> {
		__core__chooseData(
			data,
			() -> {
				true
			},
			() -> {false},
			() -> {false},
			() -> {false},
			() -> {false}
		)()
	}`));
	add(new RawFunc("__helios__scriptcontext__new_spending",
	`(tx, output_id) -> {
		__core__constrData(0, __helios__common__list_2(
			tx,
			__core__constrData(1, __helios__common__list_1(output_id))
		))
	}`));
	add(new RawFunc("__helios__scriptcontext__new_minting",
	`(tx, mph) -> {
		__core__constrData(0, __helios__common__list_2(
			tx,
			__core__constrData(
				0, 
				__helios__common__list_1(
					__helios__mintingpolicyhash____to_data(mph)
				)
			)
		))
	}`));
	add(new RawFunc("__helios__scriptcontext__new_rewarding",
	`(tx, cred) -> {
		__core__constrData(0, __helios__common__list_2(
			tx,
			__core__constrData(2, __helios__common__list_1(cred))
		))
	}`));
	add(new RawFunc("__helios__scriptcontext__new_certifying",
	`(tx, dcert) -> {
		__core__constrData(0, __helios__common__list_2(
			tx,
			__core__constrData(3, __helios__common__list_1(dcert))
		))
	}`));
	add(new RawFunc("__helios__scriptcontext__tx", "__helios__common__enum_field_0"));
	add(new RawFunc("__helios__scriptcontext__purpose", "__helios__common__enum_field_1"));
	add(new RawFunc("__helios__scriptcontext__get_current_input",
	`(self) -> {
		() -> {
			id = __helios__scriptcontext__get_spending_purpose_output_id(self)();
			recurse = (recurse, lst) -> {
				__core__chooseList(
					lst, 
					() -> {__helios__error("not found")}, 
					() -> {
						item = __core__headList__safe(lst);
						__core__ifThenElse(
							__core__equalsData(__helios__txinput__output_id(item), id), 
							() -> {item}, 
							() -> {recurse(recurse, __core__tailList__safe(lst))}
						)()
					}
				)()
			};
			recurse(recurse, __helios__tx__inputs(__helios__scriptcontext__tx(self)))
		}
	}`));
	add(new RawFunc("__helios__scriptcontext__get_cont_outputs",
	`(self) -> {
		() -> {
			vh = __helios__scriptcontext__get_current_validator_hash(self)();
			outputs = __helios__tx__outputs(__helios__scriptcontext__tx(self));
			__helios__common__filter_list(
				outputs,
				(output) -> {
					credential = __helios__address__credential(__helios__txoutput__address(output));
					pair = __core__unConstrData(credential);
					__core__ifThenElse(
						__core__equalsInteger(__core__fstPair(pair), 0),
						() -> {
							false
						},
						() -> {
							__core__equalsByteString(__core__unBData(__core__headList(__core__sndPair(pair))), vh)
						}
					)()
				}
			)
		}
	}`));
	add(new RawFunc("__helios__scriptcontext__get_spending_purpose_output_id",
	`(self) -> {
		() -> {
			__helios__common__enum_field_0(__helios__common__enum_field_1(self))
		}
	}`));
	add(new RawFunc("__helios__scriptcontext__get_current_validator_hash",
	`(self) -> {
		() -> {
			__helios__credential__validator__hash(
				__helios__credential__validator__cast(
					__helios__address__credential(
						__helios__txoutput__address(
							__helios__txinput__output(
								__helios__scriptcontext__get_current_input(self)()
							)
						)
					)
				)
			)
		}
	}`));
	add(new RawFunc("__helios__scriptcontext__get_current_minting_policy_hash", 
	`(self) -> {
		() -> {
			__helios__mintingpolicyhash__from_data(__helios__scriptcontext__get_spending_purpose_output_id(self)())
		}
	}`));
	add(new RawFunc("__helios__scriptcontext__get_staking_purpose", 
	`(self) -> {
		() -> {
			__helios__scriptcontext__purpose(self)
		}
	}`));
	add(new RawFunc("__helios__scriptcontext__get_script_purpose", 
	`(self) -> {
		() -> {
			__helios__scriptcontext__purpose(self)
		}
	}`));

	
	// ContractContext builtin
	addDataFuncs("__helios__contractcontext");
	add(new RawFunc("__helios__contractcontext__now", 
	`(self) -> {
		() -> {
			__core__macro__now(())
		}
	}`));
	add(new RawFunc("__helios__contractcontext__agent", `(self) -> {self}`));
	add(new RawFunc("__helios__contractcontext__network", `(self) -> {()}`));
	add(new RawFunc("__helios__contractcontext__new_tx_builder", `(self) -> {__helios__txbuilder__new_empty}`));


	// Network builtin
	add(new RawFunc("__helios__network__pick", 
	`(self) -> {
		(address, value) -> {
			__core__macro__pick(__helios__address____to_data(address), __helios__value____to_data(value), ())
		}
	}`));
	add(new RawFunc("__helios__network__get",
	`(self) -> {
		(id) -> {
			__core__macro__get_utxo(__helios__txoutputid____to_data(id), ())
		}
	}`));
	add(new RawFunc("__helios__network__utxos_at",
	`(self) -> {
		(addr) -> {
			__helios__list[__helios__data]__to_iterator(
				__core__macro__utxos_at(__helios__address____to_data(addr), ())
			)()
		}
	}`))


	// Wallet builtin
	addDataFuncs("__helios__wallet");
	add(new RawFunc("__helios__wallet__address", `(self) -> {__helios__common__enum_field_1(self)}`));
	add(new RawFunc("__helios__wallet__hash", 
	`(self) -> {
		__helios__credential__pubkey__hash(
			__helios__credential__pubkey__cast(
				__helios__address__credential(
					__helios__common__enum_field_0(self)
				)
			)
		)
	}`));
	add(new RawFunc("__helios__wallet__pick", 
	`(self) -> {
		(value) -> {
			__core__macro__pick(__helios__common__enum_field_0(self), __helios__value____to_data(value), ())
		}
	}`));


	// StakingPurpose builtins
	addDataFuncs("__helios__stakingpurpose");
	add(new RawFunc("__helios__stakingpurpose__testdata", 
	`(data) -> {
		__core__chooseData(
			data,
			() -> {
				true
			},
			() -> {false},
			() -> {false},
			() -> {false},
			() -> {false}
		)()
	}`));


	// StakingPurpose::Rewarding builtins
	addEnumDataFuncs("__helios__stakingpurpose__rewarding", 2);
	add(new RawFunc("__helios__stakingpurpose__rewarding__credential", "__helios__common__enum_field_0"));

	
	// StakingPurpose::Certifying builtins
	addEnumDataFuncs("__helios__stakingpurpose__certifying", 3);
	add(new RawFunc("__helios__stakingpurpose__certifying__dcert", "__helios__common__enum_field_0"));


	// ScriptPurpose builtins
	addDataFuncs("__helios__scriptpurpose");
	// TODO: test fields
	add(new RawFunc("__helios__scriptpurpose__is_valid_data",
	`(data) -> {
		__core__chooseData(
			data,
			() -> {
				true
			},
			() -> {false},
			() -> {false},
			() -> {false},
			() -> {false}
		)()
	}`));
	add(new RawFunc("__helios__scriptpurpose__new_minting",
	`(mph) -> {
		__core__constrData(0, __helios__common__list_1(__helios__mintingpolicyhash____to_data(mph)))
	}`));
	add(new RawFunc("__helios__scriptpurpose__new_spending",
	`(output_id) -> {
		__core__constrData(1, __helios__common__list_1(output_id))
	}`));
	add(new RawFunc("__helios__scriptpurpose__new_rewarding",
	`(cred) -> {
		__core__constrData(2, __helios__common__list_1(cred))
	}`));
	add(new RawFunc("__helios__scriptpurpose__new_certifying",
	`(action) -> {
		__core__constrData(3, __helios__common__list_1(action))
	}`));


	// ScriptPurpose::Minting builtins
	addEnumDataFuncs("__helios__scriptpurpose__minting", 0);
	add(new RawFunc("__helios__scriptpurpose__minting__policy_hash", 
	`(self) -> {
		__helios__mintingpolicyhash__from_data(__helios__common__enum_field_0(self))
	}`));

	
	// ScriptPurpose::Spending builtins
	addEnumDataFuncs("__helios__scriptpurpose__spending", 1);
	add(new RawFunc("__helios__scriptpurpose__spending__output_id", "__helios__common__enum_field_0"));

	
	// ScriptPurpose::Rewarding builtins
	addEnumDataFuncs("__helios__scriptpurpose__rewarding", 2);
	add(new RawFunc("__helios__scriptpurpose__rewarding__credential", "__helios__common__enum_field_0"));

	
	// ScriptPurpose::Certifying builtins
	addEnumDataFuncs("__helios__scriptpurpose__certifying", 3);
	add(new RawFunc("__helios__scriptpurpose__certifying__dcert", "__helios__common__enum_field_0"));


	// DCert builtins
	addDataFuncs("__helios__dcert");
	// TODO: test each enum variant
	add(new RawFunc("__helios__dcert__is_valid_data",
	`(data) -> {
		__core__chooseData(
			data,
			() -> {
				true
			},
			() -> {false},
			() -> {false},
			() -> {false},
			() -> {false}
		)()
	}`));
	add(new RawFunc("__helios__dcert__new_register",
	`(cred) -> {
		__core__constrData(0, __helios__common__list_1(cred))
	}`));
	add(new RawFunc("__helios__dcert__new_deregister",
	`(cred) -> {
		__core__constrData(1, __helios__common__list_1(cred))
	}`));
	add(new RawFunc("__helios__dcert__new_delegate",
	`(cred, pool_id) -> {
		__core__constrData(2, __helios__common__list_2(cred, __helios__pubkeyhash____to_data(pool_id)))
	}`));
	add(new RawFunc("__helios__dcert__new_register_pool",
	`(id, vrf) -> {
		__core__constrData(3, __helios__common__list_2(__helios__pubkeyhash____to_data(id), __helios__pubkeyhash____to_data(vrf)))
	}`));
	add(new RawFunc("__helios__dcert__new_retire_pool",
	`(id, epoch) -> {
		__core__constrData(4, __helios__common__list_2(__helios__pubkeyhash____to_data(id), __helios__int____to_data(epoch)))
	}`));


	// DCert::Register builtins
	addEnumDataFuncs("__helios__dcert__register", 0);
	add(new RawFunc("__helios__dcert__register__credential", "__helios__common__enum_field_0"));


	// DCert::Deregister builtins
	addEnumDataFuncs("__helios__dcert__deregister", 1);
	add(new RawFunc("__helios__dcert__deregister__credential", "__helios__common__enum_field_0"));


	// DCert::Delegate builtins
	addEnumDataFuncs("__helios__dcert__delegate", 2);
	add(new RawFunc("__helios__dcert__delegate__delegator", "__helios__common__enum_field_0"));
	add(new RawFunc("__helios__dcert__delegate__pool_id", 
	`(self) -> {
		__helios__pubkeyhash__from_data(__helios__common__enum_field_1(self))
	}`));


	// DCert::RegisterPool builtins
	addEnumDataFuncs("__helios__dcert__registerpool", 3);
	add(new RawFunc("__helios__dcert__registerpool__pool_id", 
	`(self) -> {
		__helios__pubkeyhash__from_data(__helios__common__enum_field_0(self))
	}`));
	add(new RawFunc("__helios__dcert__registerpool__pool_vrf", 
	`(self) -> {
		__helios__pubkeyhash__from_data(__helios__common__enum_field_1(self))
	}`));


	// DCert::RetirePool builtins
	addEnumDataFuncs("__helios__dcert__retirepool", 4);
	add(new RawFunc("__helios__dcert__retirepool__pool_id", 
	`(self) -> {
		__helios__pubkeyhash__from_data(__helios__common__enum_field_0(self))
	}`));
	add(new RawFunc("__helios__dcert__retirepool__epoch", 
	`(self) -> {
		__helios__int__from_data(__helios__common__enum_field_1(self))
	}`));


	// TxBuilder builtins
	addDataFuncs("__helios__txbuilder");
	add(new RawFunc(`__helios__txbuilder__new`,
	`(inputs, ref_inputs, outputs, fee, minted, dcerts, withdrawals, validity, signatories, redeemers, datums) -> {
		__core__constrData(0, __helios__common__list_12(
			__core__listData(inputs),
			__core__listData(ref_inputs),
			__core__listData(outputs),
			__core__mapData(fee),
			__core__mapData(minted),
			__core__listData(dcerts),
			__core__mapData(withdrawals),
			__helios__timerange____to_data(validity),
			__core__listData(signatories),
			__core__mapData(redeemers),
			__core__mapData(datums),
			__helios__txid__new(#00010203040506070809101112131415161718192021222324252627)
		))
	}`));
	add(new RawFunc(`__helios__txbuilder__new_empty`,
	`() -> {
		__helios__txbuilder__new(
			__core__mkNilData(()),
			__core__mkNilData(()),
			__core__mkNilData(()),
			__core__mkNilPairData(()),
			__core__mkNilPairData(()),
			__core__mkNilData(()),
			__core__mkNilPairData(()),
			__helios__timerange__ALWAYS,
			__core__mkNilData(()),
			__core__mkNilPairData(()),
			__core__mkNilPairData(())
		)
	}`));
	add(new RawFunc(`__helios__txbuilder__unwrap`, 
	`(self, callback) -> {
		fields = __core__sndPair(__core__unConstrData(self));
		inputs = __core__unListData(__core__headList(fields));
		fields = __core__tailList(fields);
		ref_inputs = __core__unListData(__core__headList(fields));
		fields = __core__tailList(fields);
		outputs = __core__unListData(__core__headList(fields));
		fields = __core__tailList(fields);
		fee = __core__unMapData(__core__headList(fields));
		fields = __core__tailList(fields);
		minted = __core__unMapData(__core__headList(fields));
		fields = __core__tailList(fields);
		dcerts = __core__unListData(__core__headList(fields));
		fields = __core__tailList(fields);
		withdrawals = __core__unMapData(__core__headList(fields));
		fields = __core__tailList(fields);
		validity = __helios__timerange__from_data(__core__headList(fields));
		fields = __core__tailList(fields);
		signatories = __core__unListData(__core__headList(fields));
		fields = __core__tailList(fields);
		redeemers = __core__unMapData(__core__headList(fields));
		fields = __core__tailList(fields);
		datums = __core__unMapData(__core__headList(fields));
		fields = __core__tailList(fields);
		callback(inputs, ref_inputs, outputs, fee, minted, dcerts, withdrawals, validity, signatories, redeemers, datums)
	}`));
	add(new RawFunc(`__helios__txbuilder__spend`, 
	`(self) -> {
		(input) -> {
			__helios__txbuilder__unwrap(self, (inputs, ref_inputs, outputs, fee, minted, dcerts, withdrawals, validity, signatories, redeemers, datums) -> {
				__helios__txbuilder__new(
					__helios__list[__helios__data]__append(inputs)(__helios__txinput____to_data(input)),
					ref_inputs,
					outputs, 
					fee, 
					minted, 
					dcerts, 
					withdrawals, 
					validity, 
					signatories, 
					redeemers, 
					datums
				)
			})
		}
	}`));
	add(new RawFunc(`__helios__txbuilder__spend_many`,
	`(self) -> {
		(extra_inputs) -> {
			__helios__txbuilder__unwrap(self, (inputs, ref_inputs, outputs, fee, minted, dcerts, withdrawals, validity, signatories, redeemers, datums) -> {
				__helios__txbuilder__new(
					__helios__common__concat(inputs, extra_inputs),
					ref_inputs,
					outputs, 
					fee, 
					minted, 
					dcerts, 
					withdrawals, 
					validity, 
					signatories, 
					redeemers, 
					datums
				)
			})
		}
	}`))
	add(new RawFunc(`__helios__txbuilder__redeem[${FTPP}0]`, 
	`(self) -> {
		(input, redeemer) -> {
			__helios__txbuilder__unwrap(self, (inputs, ref_inputs, outputs, fee, minted, dcerts, withdrawals, validity, signatories, redeemers, datums) -> {
				__helios__txbuilder__new(
					__helios__list[__helios__data]__append(inputs)(
						__helios__txinput____to_data(input)
					),
					ref_inputs, 
					outputs, 
					fee, 
					minted, 
					dcerts, 
					withdrawals, 
					validity, 
					signatories,
					__helios__map[__helios__data@__helios__data]__append(redeemers)(
						__helios__scriptpurpose__new_spending(__helios__txinput__output_id(input)),
						${FTPP}0____to_data(redeemer)
					),
					datums
				)
			})
		}
	}`));
	add(new RawFunc(`__helios__txbuilder__redeem_many[${FTPP}0]`,
	`(self) -> {
		(inputs, redeemer) -> {
			recurse = (recurse, inputs) -> {
				__core__chooseList(
					inputs,
					() -> {
						self
					},
					() -> {
						__helios__txbuilder__redeem[${FTPP}0](
							recurse(recurse, __core__tailList__safe(inputs))
						)(
							__core__headList__safe(inputs),
							redeemer
						)
					}
				)()
			};
			recurse(recurse, inputs)
		}
	}`));
	add(new RawFunc(`__helios__txbuilder__add_output`,
	`(self) -> {
		(output) -> {
			__helios__txbuilder__unwrap(self, (inputs, ref_inputs, outputs, fee, minted, dcerts, withdrawals, validity, signatories, redeemers, datums) -> {
				__helios__txbuilder__new(
					inputs,
					ref_inputs,
					__helios__list[__helios__data]__append(outputs)(
						__helios__txoutput____to_data(output)
					),
					fee,
					minted,
					dcerts,
					withdrawals,
					validity,
					signatories,
					redeemers,
					datums
				)
			})
		}
	}`));
	add(new RawFunc(`__helios__txbuilder__add_outputs`,
	`(self) -> {
		(extra_outputs) -> {
			__helios__txbuilder__unwrap(self, (inputs, ref_inputs, outputs, fee, minted, dcerts, withdrawals, validity, signatories, redeemers, datums) -> {
				__helios__txbuilder__new(
					inputs,
					ref_inputs,
					__helios__common__concat(outputs, extra_outputs),
					fee,
					minted,
					dcerts,
					withdrawals,
					validity,
					signatories,
					redeemers,
					datums
				)
			})
		}
	}`));
	add(new RawFunc(`__helios__txbuilder__pay[${FTPP}0]`,
	`(self) -> {
		(address, value, datum) -> {
			__helios__txbuilder__unwrap(self, (inputs, ref_inputs, outputs, fee, minted, dcerts, withdrawals, validity, signatories, redeemers, datums) -> {
				__helios__txbuilder__new(
					inputs,
					ref_inputs,
					__helios__list[__helios__data]__append(outputs)(
						__helios__txoutput____to_data(
							__helios__txoutput__new(
								address, 
								value, 
								__helios__outputdatum__new_inline[__helios__data](
									${FTPP}0____to_data(datum)
								)
							)
						)
					),
					fee,
					minted,
					dcerts,
					withdrawals,
					validity,
					signatories,
					redeemers,
					datums
				)
			})
		}
	}`));
	add(new RawFunc(`__helios__txbuilder__pay_if_true[${FTPP}0]`,
	`(self) -> {
		(cond, address, value, datum) -> {
			__core__ifThenElse(
				cond,
				() -> {
					__helios__txbuilder__unwrap(self, (inputs, ref_inputs, outputs, fee, minted, dcerts, withdrawals, validity, signatories, redeemers, datums) -> {
						__helios__txbuilder__new(
							inputs,
							ref_inputs,
							__helios__list[__helios__data]__append(outputs)(
								__helios__txoutput____to_data(
									__helios__txoutput__new(
										address, 
										value, 
										__helios__outputdatum__new_inline[__helios__data](
											${FTPP}0____to_data(datum)
										)
									)
								)
							),
							fee,
							minted,
							dcerts,
							withdrawals,
							validity,
							signatories,
							redeemers,
							datums
						)
					})
				},
				() -> {
					self
				}
			)()
		}
	}`));
	add(new RawFunc(`__helios__txbuilder__mint[${FTPP}0]`,
	`(self) -> {
		(value, redeemer) -> {
			__core__chooseUnit(
				__helios__assert(
					__core__equalsInteger(__helios__common__length(value), 1),
					__helios__string____add("expected a single mph in mint value, got ", __helios__int__show(__helios__common__length(value))())
				),
				mph = __helios__mintingpolicyhash__from_data(__core__fstPair(__core__headList(value)));
				__helios__txbuilder__unwrap(self, (inputs, ref_inputs, outputs, fee, minted, dcerts, withdrawals, validity, signatories, redeemers, datums) -> {
					__core__chooseUnit(
						__helios__assert(
							__helios__bool____not(
								__helios__value__contains_policy(minted)(mph)
							),
							"already minted before"
						),
						__helios__txbuilder__new(
							inputs,
							ref_inputs,
							outputs,
							fee,
							__helios__value____add(minted, value),
							dcerts,
							withdrawals,
							validity,
							signatories,
							__helios__map[__helios__data@__helios__data]__append(redeemers)(
								__helios__scriptpurpose__new_minting(mph),
								${FTPP}0____to_data(redeemer)
							),
							datums
						)
					)
				})
			)
		}
	}`));
	add(new RawFunc(`__helios__txbuilder__add_ref_input`, 
	`(self) -> {
		(ref_input) -> {
			__helios__txbuilder__unwrap(self, (inputs, ref_inputs, outputs, fee, minted, dcerts, withdrawals, validity, signatories, redeemers, datums) -> {
				__helios__txbuilder__new(
					inputs,
					__helios__list[__helios__data]__append(ref_inputs)(__helios__txinput____to_data(ref_input)),
					outputs,
					fee,
					minted,
					dcerts,
					withdrawals,
					validity,
					signatories,
					redeemers,
					datums
				)
			})
		}
	}`));
	add(new RawFunc(`__helios__txbuilder__add_signer`,
	`(self) -> {
		(pk) -> {
			__helios__txbuilder__unwrap(self, (inputs, ref_inputs, outputs, fee, minted, dcerts, withdrawals, validity, signatories, redeemers, datums) -> {
				__helios__txbuilder__new(
					inputs,
					ref_inputs,
					outputs,
					fee,
					minted,
					dcerts,
					withdrawals,
					validity,
					__helios__list[__helios__data]__append(signatories)(__helios__pubkeyhash____to_data(pk)),
					redeemers,
					datums
				)
			})
		}
	}`));
	add(new RawFunc(`__helios__txbuilder__finalize`,
	`(self) -> {
		() -> {
			__core__macro__finalize(self, ())
		}
	}`));


	// Tx builtins
	addDataFuncs("__helios__tx");
	// TODO: test fields
	add(new RawFunc("__helios__tx__is_valid_data",
	`(data) -> {
		__core__chooseData(
			data,
			() -> {
				true
			},
			() -> {false},
			() -> {false},
			() -> {false},
			() -> {false}
		)()
	}`));
	add(new RawFunc(`__helios__tx__new[${FTPP}0@${FTPP}1]`,
	`(inputs, ref_inputs, outputs, fee, minted, dcerts, withdrawals, validity, signatories, redeemers, datums, txId) -> {
		__core__constrData(0, __helios__common__list_12(
			__core__listData(inputs),
			__core__listData(ref_inputs),
			__core__listData(outputs),
			__core__mapData(fee),
			__core__mapData(minted),
			__core__listData(dcerts),
			__core__mapData(withdrawals),
			__helios__timerange____to_data(validity),
			__core__listData(signatories),
			__core__mapData(redeemers),
			__core__mapData(datums),
			__helios__txid____to_data(txId)
		))
	}`));
	add(new RawFunc("__helios__tx__inputs", 
	`(self) -> {
		__core__unListData(__helios__common__enum_field_0(self))
	}`));
	add(new RawFunc("__helios__tx__ref_inputs", 
	`(self) -> {
		__core__unListData(__helios__common__enum_field_1(self))
	}`))
	add(new RawFunc("__helios__tx__outputs", 
	`(self) -> {
		__core__unListData(__helios__common__enum_field_2(self))
	}`));
	add(new RawFunc("__helios__tx__fee", 
	`(self) -> {
		__core__unMapData(__helios__common__enum_field_3(self))
	}`));
	add(new RawFunc("__helios__tx__minted", 
	`(self) -> {
		__core__unMapData(__helios__common__enum_field_4(self))
	}`));
	add(new RawFunc("__helios__tx__dcerts", 
	`(self) -> {
		__core__unListData(__helios__common__enum_field_5(self))
	}`));
	add(new RawFunc("__helios__tx__withdrawals", 
	`(self) -> {
		__core__unMapData(__helios__common__enum_field_6(self))
	}`));
	add(new RawFunc("__helios__tx__time_range", "__helios__common__enum_field_7"));
	add(new RawFunc("__helios__tx__signatories", 
	`(self) -> {
		__core__unListData(__helios__common__enum_field_8(self))
	}`));
	add(new RawFunc("__helios__tx__redeemers", 
	`(self) -> {
		__core__unMapData(__helios__common__enum_field_9(self))
	}`));
	add(new RawFunc("__helios__tx__datums", 
	`(self) -> {
		__core__unMapData(__helios__common__enum_field_10(self))
	}`));
	add(new RawFunc("__helios__tx__id", "__helios__common__enum_field_11"));
	add(new RawFunc(`__helios__tx__find_datum_hash[${FTPP}0]`,
	`(self) -> {
		(datum) -> {
			__helios__datumhash__from_data(
				__core__fstPair(
					__helios__common__find(
						__helios__tx__datums(self),
						(pair) -> {
							__core__equalsData(__core__sndPair(pair), datum)
						}
					)
				)
			)
		}
	}`));
	add(new RawFunc("__helios__tx__get_datum_data",
	`(self) -> {
		(output) -> {
			output = __core__unConstrData(__helios__txoutput__datum(output));
			idx = __core__fstPair(output);
			__core__ifThenElse(
				__core__equalsInteger(idx, 1),
				() -> {
					__helios__common__map_get(
						__helios__tx__datums(self), 
						__core__headList(__core__sndPair(output)),
						__helios__common__identity,
						() -> {__helios__error("datumhash not found")}
					)
				},
				() -> {
					__core__ifThenElse(
						__core__equalsInteger(idx, 2),
						() -> {
							__core__headList(__core__sndPair(output))
						},
						() -> {__helios__error("output doesn't have a datum")}
					)()
				}
			)()
		}
	}`));
	add(new RawFunc("__helios__tx__filter_outputs",
	`(self, fn) -> {
		__helios__common__filter_list(
			__helios__tx__outputs(self), 
			fn
		)
	}`));
	add(new RawFunc("__helios__tx__outputs_sent_to",
	`(self) -> {
		(pkh) -> {
			__helios__tx__filter_outputs(self, (output) -> {
				__helios__txoutput__is_sent_to(output)(pkh)
			})
		}
	}`));
	add(new RawFunc(`__helios__tx__outputs_sent_to_datum[${FTPP}0]`,
	`(self) -> {
		(pkh, datum, isInline) -> {
			__core__ifThenElse(
				isInline,
				() -> {
					__helios__tx__outputs_sent_to_inline_datum[${FTPP}0](self, pkh, datum)
				},
				() -> {
					__helios__tx__outputs_sent_to_datum_hash[${FTPP}0](self, pkh, datum)
				}
			)()
		}
	}`));
	add(new RawFunc(`__helios__tx__outputs_sent_to_datum_hash[${FTPP}0]`,
	`(self, pkh, datum) -> {
		datumHash = __helios__common__hash_datum_data[${FTPP}0](datum);
		__helios__tx__filter_outputs(
			self, 
			(output) -> {
				__helios__bool__and(
					() -> {
						__helios__txoutput__is_sent_to(output)(pkh)
					},
					() -> {
						__helios__txoutput__has_datum_hash(output, datumHash)
					}
				)
			}
		)
	}`));
	add(new RawFunc(`__helios__tx__outputs_sent_to_inline_datum[${FTPP}0]`,
	`(self, pkh, datum) -> {
		__helios__tx__filter_outputs(
			self, 
			(output) -> {
				__helios__bool__and(
					() -> {
						__helios__txoutput__is_sent_to(output)(pkh)
					},
					() -> {
						__helios__txoutput__has_inline_datum[${FTPP}0](output, datum)
					}
				)
			}
		)
	}`));
	add(new RawFunc("__helios__tx__outputs_locked_by",
	`(self) -> {
		(vh) -> {
			__helios__tx__filter_outputs(self, (output) -> {
				__helios__txoutput__is_locked_by(output)(vh)
			})
		}
	}`));
	add(new RawFunc(`__helios__tx__outputs_locked_by_datum[${FTPP}0]`,
	`(self) -> {
		(vh, datum, isInline) -> {
			__core__ifThenElse(
				isInline,
				() -> {
					__helios__tx__outputs_locked_by_inline_datum[${FTPP}0](self, vh, datum)
				},
				() -> {
					__helios__tx__outputs_locked_by_datum_hash[${FTPP}0](self, vh, datum)
				}
			)()
		}
	}`));
	add(new RawFunc(`__helios__tx__outputs_locked_by_datum_hash[${FTPP}0]`,
	`(self, vh, datum) -> {
		datumHash = __helios__common__hash_datum_data[${FTPP}0](datum);
		__helios__tx__filter_outputs(
			self, 
			(output) -> {
				__helios__bool__and(
					() -> {
						__helios__txoutput__is_locked_by(output)(vh)
					},
					() -> {
						__helios__txoutput__has_datum_hash(output, datumHash)
					}
				)
			}
		)
	}`));
	add(new RawFunc(`__helios__tx__outputs_locked_by_inline_datum[${FTPP}0]`,
	`(self, vh, datum) -> {
		__helios__tx__filter_outputs(
			self, 
			(output) -> {
				__helios__bool__and(
					() -> {
						__helios__txoutput__is_locked_by(output)(vh)
					},
					() -> {
						__helios__txoutput__has_inline_datum[${FTPP}0](output, datum)
					}
				)
			}
		)
	}`));
	add(new RawFunc(`__helios__tx__outputs_paid_to[${FTPP}0]`,
	`(self) -> {
		(addr, datum) -> {
			__helios__tx__filter_outputs(
				self, 
				(output) -> {
					__helios__bool__and(
						() -> {
							__helios__address____eq(__helios__txoutput__address(output), addr)
						},
						() -> {
							__helios__txoutput__has_inline_datum[${FTPP}0](output, datum)
						}
					)
				}
			)
		}
	}`));
	add(new RawFunc("__helios__tx__value_sent_to",
	`(self) -> {
		(pkh) -> {
			__helios__txoutput__sum_values(__helios__tx__outputs_sent_to(self)(pkh))
		}
	}`));
	add(new RawFunc(`__helios__tx__value_sent_to_datum[${FTPP}0]`,
	`(self) -> {
		(pkh, datum, isInline) -> {
			__helios__txoutput__sum_values(__helios__tx__outputs_sent_to_datum[${FTPP}0](self)(pkh, datum, isInline))
		}
	}`));
	add(new RawFunc("__helios__tx__value_locked_by",
	`(self) -> {
		(vh) -> {
			__helios__txoutput__sum_values(__helios__tx__outputs_locked_by(self)(vh))
		}
	}`));
	add(new RawFunc(`__helios__tx__value_locked_by_datum[${FTPP}0]`,
	`(self) -> {
		(vh, datum, isInline) -> {
			__helios__txoutput__sum_values(__helios__tx__outputs_locked_by_datum[${FTPP}0](self)(vh, datum, isInline))
		}
	}`));
	add(new RawFunc(`__helios__tx__value_paid_to[${FTPP}0]`,
	`(self) -> {
		(addr, datum) -> {
			__helios__txoutput__sum_values(__helios__tx__outputs_paid_to[${FTPP}0](self)(addr, datum))
		}
	}`));
	add(new RawFunc("__helios__tx__is_signed_by",
	`(self) -> {
		(hash) -> {
			hash = __helios__pubkeyhash____to_data(hash);
			__helios__common__any(
				__helios__tx__signatories(self),
				(signatory) -> {
					__core__equalsData(signatory, hash)
				}
			)
		}
	}`));


	// TxId builtins
	addDataFuncs("__helios__txid");
	add(new RawFunc("__helios__txid__bytes",
	`(self) -> {
		__core__unBData(__core__headList(__core__sndPair(__core__unConstrData(self))))
	}`));
	add(new RawFunc("__helios__txid____lt", 
	`(a, b) -> {
		__helios__bytearray____lt(__helios__txid__bytes(a), __helios__txid__bytes(b))
	}`));
	add(new RawFunc("__helios__txid____leq", 
	`(a, b) -> {
		__helios__bytearray____leq(__helios__txid__bytes(a), __helios__txid__bytes(b))
	}`));
	add(new RawFunc("__helios__txid____gt", 
	`(a, b) -> {
		__helios__bytearray____gt(__helios__txid__bytes(a), __helios__txid__bytes(b))
	}`));
	add(new RawFunc("__helios__txid____geq", 
	`(a, b) -> {
		__helios__bytearray____geq(__helios__txid__bytes(a), __helios__txid__bytes(b))
	}`));
	add(new RawFunc("__helios__txid__new",
	`(bytes) -> {
		__core__constrData(0, __helios__common__list_1(__core__bData(bytes))) 
	}`));
	add(new RawFunc("__helios__txid__is_valid_data",
	`(data) -> {
		__core__chooseData(
			data,
			() -> {
				pair = __core__unConstrData__safe(data);
				index = __core__fstPair(pair);
				fields = __core__sndPair(pair);
				__core__ifThenElse(
					__core__equalsInteger(0, index),
					() -> {
						__core__chooseList(
							fields,
							() -> {
								false
							},
							() -> {
								__core__chooseList(
									__core__tailList__safe(fields),
									() -> {
										__helios__bytearray__is_valid_data_fixed_length(__core__headList__safe(fields), 32)
									},
									() -> {
										false
									}
								)()
							}
						)()
					},
					() -> {
						false
					}
				)()
			},
			() -> {false},
			() -> {false},
			() -> {false},
			() -> {false}
		)()
	}`));
	add(new RawFunc("__helios__txid__show",
	`(self) -> {
		__helios__bytearray__show(__helios__txid__bytes(self))
	}`));


	// TxInput builtins
	addDataFuncs("__helios__txinput");
	add(new RawFunc("__helios__txinput__is_valid_data", 
	`(data) -> {
		__helios__common__test_constr_data_2(data 0, __helios__txoutputid__is_valid_data, __helios__txoutput__is_valid_data)
	}`));
	add(new RawFunc("__helios__txinput__new",
	`(output_id, output) -> {
		__core__constrData(0, __helios__common__list_2(output_id, output))
	}`));
	add(new RawFunc("__helios__txinput__output_id", "__helios__common__enum_field_0"));
	add(new RawFunc("__helios__txinput__output", "__helios__common__enum_field_1"));
	add(new RawFunc("__helios__txinput__address",
	`(self) -> {
		__helios__txoutput__address(__helios__txinput__output(self))
	}`));
	add(new RawFunc("__helios__txinput__value", 
	`(self) -> {
		__helios__txoutput__value(__helios__txinput__output(self))
	}`));
	add(new RawFunc("__helios__txinput__datum",
	`(self) -> {
		__helios__txoutput__datum(__helios__txinput__output(self))
	}`));
	

	// TxOutput builtins
	addDataFuncs("__helios__txoutput");
	// TODO: test fields
	add(new RawFunc("__helios__txoutput__is_valid_data",
	`(data) -> {
		__core__chooseData(
			data,
			() -> {
				true
			},
			() -> {false},
			() -> {false},
			() -> {false},
			() -> {false}
		)()
	}`));
	add(new RawFunc("__helios__txoutput__new", 
	`(address, value, datum) -> {
		__core__constrData(0, __helios__common__list_4(address, __core__mapData(value), datum, __helios__option__NONE))
	}`));
	add(new RawFunc("__helios__txoutput__address", "__helios__common__enum_field_0"));
	add(new RawFunc("__helios__txoutput__value", `(self) -> {
		__core__unMapData(__helios__common__enum_field_1(self))
	}`));
	add(new RawFunc("__helios__txoutput__datum", "__helios__common__enum_field_2"));
	add(new RawFunc("__helios__txoutput__ref_script_hash", "__helios__common__enum_field_3"));
	add(new RawFunc("__helios__txoutput__get_datum_hash",
	`(self) -> {
		() -> {
			pair = __core__unConstrData(__helios__txoutput__datum(self));
			__core__ifThenElse(
				__core__equalsInteger(__core__fstPair(pair), 1),
				() -> {
					__helios__datumhash__from_data(
						__core__headList(__core__sndPair(pair))
					)
				},
				() -> {#}
			)()
		}
	}`));
	add(new RawFunc("__helios__txoutput__has_datum_hash",
	`(self, datumHash) -> {
		__helios__datumhash____eq(__helios__txoutput__get_datum_hash(self)(), datumHash)
	}`));
	add(new RawFunc(`__helios__txoutput__has_inline_datum[${FTPP}0]`,
	`(self, datum) -> {
		pair = __core__unConstrData(__helios__txoutput__datum(self));
		__core__ifThenElse(
			__core__equalsInteger(__core__fstPair(pair), 2),
			() -> {
				__core__equalsData(
					${FTPP}0____to_data(datum),
					__core__headList(__core__sndPair(pair))
				)
			},
			() -> {false}
		)()
	}`));
	add(new RawFunc("__helios__txoutput__is_locked_by",
	`(self) -> {
		(hash) -> {
			credential = __helios__address__credential(__helios__txoutput__address(self));
			__core__ifThenElse(
				__helios__credential__is_validator(credential),
				() -> {
					__helios__validatorhash____eq(
						hash, 
						__helios__credential__validator__hash(
							__helios__credential__validator__cast(credential)
						)
					)
				},
				() -> {false}
			)()
		}
	}`));
	add(new RawFunc("__helios__txoutput__is_sent_to",
	`(self) -> {
		(pkh) -> {
			credential = __helios__address__credential(__helios__txoutput__address(self));
			__core__ifThenElse(
				__helios__credential__is_pubkey(credential),
				() -> {
					__helios__pubkeyhash____eq(
						pkh, 
						__helios__credential__pubkey__hash(
							__helios__credential__pubkey__cast(credential)
						)
					)
				},
				() -> {false}
			)()
		}
	}`));
	add(new RawFunc("__helios__txoutput__sum_values",
	`(outputs) -> {
		__helios__common__fold(
			outputs, 
			(prev, txOutput) -> {
				__helios__value____add(
					prev,
					__helios__txoutput__value(txOutput)
				)
			}, 
			__helios__value__ZERO
		)
	}`));


	// OutputDatum
	addDataFuncs("__helios__outputdatum");
	// TODO: test each enum variant
	add(new RawFunc("__helios__outputdatum__is_valid_data", 
	`(data) -> {
		__core__chooseData(
			data,
			() -> {
				true
			},
			() -> {false},
			() -> {false},
			() -> {false},
			() -> {false}
		)()
	}`));
	add(new RawFunc("__helios__outputdatum__new_none",
	`() -> {
		__core__constrData(0, __helios__common__list_0)
	}`));
	add(new RawFunc("__helios__outputdatum__new_hash",
	`(hash) -> {
		__core__constrData(1, __helios__common__list_1(__helios__datumhash____to_data(hash)))
	}`));
	add(new RawFunc(`__helios__outputdatum__new_inline[__helios__data]`,
	`(data) -> {
		__core__constrData(2, __helios__common__list_1(data))
	}`));
	add(new RawFunc(`__helios__outputdatum__new_inline[${FTPP}0]`,
	`(data) -> {
		__helios__outputdatum__new_inline[__helios__data](${FTPP}0____to_data(data))
	}`));
	add(new RawFunc("__helios__outputdatum__get_inline_data",
	`(self) -> {
		() -> {
			pair = __core__unConstrData(self);
			index = __core__fstPair(pair);
			fields = __core__sndPair(pair);
			__core__ifThenElse(
				__core__equalsInteger(index, 2),
				() -> {
					__core__headList(fields)
				},
				() -> {
					__helios__error("not an inline datum")
				}
			)()
		}
	}`));


	// OutputDatum::None
	addEnumDataFuncs("__helios__outputdatum__none", 0);
	

	// OutputDatum::Hash
	addEnumDataFuncs("__helios__outputdatum__hash", 1);
	add(new RawFunc("__helios__outputdatum__hash__hash", 
	`(self) -> {
		__helios__datumhash__from_data(__helios__common__enum_field_0(self))
	}`));


	// OutputDatum::Inline
	addEnumDataFuncs("__helios__outputdatum__inline", 2);
	add(new RawFunc("__helios__outputdatum__inline__data", "__helios__common__enum_field_0"));


	// RawData
	addDataFuncs("__helios__data");
	add(new RawFunc("__helios__data__is_valid_data", `(data) -> {true}`));
	add(new RawFunc("__helios__data__tag", 
	`(self) -> {
		__core__fstPair(__core__unConstrData(self))
	}`));


	// TxOutputId
	addDataFuncs("__helios__txoutputid");
	add(new RawFunc("__helios__txoutputid__is_valid_data",
	`(data) -> {
		__helios__common__test_constr_data_2(data, 0, __helios__txid__is_valid_data, __helios__int__is_valid_data)
	}`));
	add(new RawFunc("__helios__txoutputid__tx_id", "__helios__common__enum_field_0"));
	add(new RawFunc("__helios__txoutputid__index", 
	`(self) -> {
		__helios__int__from_data(__helios__common__enum_field_1(self))
	}`));
	add(new RawFunc("__helios__txoutputid__comp", 
	`(a, b, comp_txid, comp_index) -> {
		a_txid = __helios__txoutputid__tx_id(a);
		a_index = __helios__txoutputid__index(a);
		b_txid = __helios__txoutputid__tx_id(b);
		b_index = __helios__txoutputid__index(b);
		__core__ifThenElse(
			__core__equalsData(a_txid, b_txid),
			() -> {
				comp_index(a_index, b_index)
			},
			() -> {
				comp_txid(a_txid, b_txid)
			}
		)()
	}`));
	add(new RawFunc("__helios__txoutputid____lt", 
	`(a, b) -> {
		__helios__txoutputid__comp(a, b, __helios__txid____lt, __helios__int____lt)
	}`));
	add(new RawFunc("__helios__txoutputid____leq", 
	`(a, b) -> {
		__helios__txoutputid__comp(a, b, __helios__txid____leq, __helios__int____leq)
	}`));
	add(new RawFunc("__helios__txoutputid____gt", 
	`(a, b) -> {
		__helios__txoutputid__comp(a, b, __helios__txid____gt, __helios__int____gt)
	}`));
	add(new RawFunc("__helios__txoutputid____geq", 
	`(a, b) -> {
		__helios__txoutputid__comp(a, b, __helios__txid____geq, __helios__int____geq)
	}`));
	add(new RawFunc("__helios__txoutputid__new",
	`(tx_id, idx) -> {
		__core__constrData(0, __helios__common__list_2(tx_id, __helios__int____to_data(idx)))
	}`));
	add(new RawFunc("__helios__txoutputid__show",
	`(self) -> {
		() -> {
			__helios__string____add(
				__helios__txid__show(__helios__txoutputid__tx_id(self))(),
				__helios__string____add(
					"#",
					__helios__int__show(__helios__txoutputid__index(self))()
				)
			)
		}
	}`));


	// Address
	addDataFuncs("__helios__address");
	add(new RawFunc("__helios__address__to_hex", 
	`(self) -> {
		__helios__bytearray__show(__helios__address__to_bytes(self)())
	}`));
	add(new RawFunc("__helios__address__show", "__helios__address__to_hex"));
	add(new RawFunc("__helios__address__header",
	`(self) -> {
		() -> {
			credential = __helios__address__credential(self);
			staking_credential = __helios__address__staking_credential(self);
			__core__ifThenElse(
				__helios__credential__is_pubkey(credential),
				() -> {
					staking_option_pair = __core__unConstrData(staking_credential);
					__core__ifThenElse(
						__core__equalsInteger(__core__fstPair(staking_option_pair), 0),
						() -> {
							staking_credential = __core__headList(__core__sndPair(__core__unConstrData(__helios__stakingcredential__hash__cast(__core__headList(__core__sndPair(staking_option_pair))))));
							__core__ifThenElse(
								__helios__credential__is_pubkey(staking_credential),
								() -> {
									${isTestnet ? "0x00" : "0x01"}
								},
								() -> {
									${isTestnet ? "0x20" : "0x21"}
								}
							)()
						},
						() -> {
							${isTestnet ? "0x60" : "0x61"}
						}
					)()
				},
				() -> {
					staking_option_pair = __core__unConstrData(staking_credential);
					__core__ifThenElse(
						__core__equalsInteger(__core__fstPair(staking_option_pair), 0)
						() -> {
							staking_credential = __core__headList(__core__sndPair(__core__unConstrData(__helios__stakingcredential__hash__cast(__core__headList(__core__sndPair(staking_option_pair))))));
							__core__ifThenElse(
								__helios__credential__is_pubkey(staking_credential),
								() -> {
									${isTestnet ? "0x10" : "0x11"}
								},
								() -> {
									${isTestnet ? "0x30" : "0x31"}
								}
							)()
						},
						() -> {
							${isTestnet ? "0x70" : "0x71"}
						}
					)()
				}
			)()
		}
	}`));
	add(new RawFunc("__helios__address__to_bytes",
	`(self) -> {
		() -> {
			credential = __helios__address__credential(self);
			staking_credential = __helios__address__staking_credential(self);
			__core__ifThenElse(
				__helios__credential__is_pubkey(credential),
				() -> {
					staking_option_pair = __core__unConstrData(staking_credential);
					__core__ifThenElse(
						__core__equalsInteger(__core__fstPair(staking_option_pair), 0),
						() -> {
							staking_credential = __core__headList(__core__sndPair(__core__unConstrData(__helios__stakingcredential__hash__cast(__core__headList(__core__sndPair(staking_option_pair))))));
							__core__ifThenElse(
								__helios__credential__is_pubkey(staking_credential),
								() -> {
									__core__consByteString(
										${isTestnet ? "0x00" : "0x01"},
										__core__appendByteString(
											__helios__credential__pubkey__hash(credential),
											__helios__credential__pubkey__hash(staking_credential)
										)
									)
								},
								() -> {
									__core__consByteString(
										${isTestnet ? "0x20" : "0x21"},
										__core__appendByteString(
											__helios__credential__pubkey__hash(credential),
											__helios__credential__validator__hash(staking_credential)
										)
									)
								}
							)()
						},
						() -> {
							__core__consByteString(
								${isTestnet ? "0x60" : "0x61"},
								__helios__credential__pubkey__hash(credential)
							)
						}
					)()
				},
				() -> {
					staking_option_pair = __core__unConstrData(staking_credential);
					__core__ifThenElse(
						__core__equalsInteger(__core__fstPair(staking_option_pair), 0),
						() -> {
							staking_credential = __core__headList(__core__sndPair(__core__unConstrData(__helios__stakingcredential__hash__cast(__core__headList(__core__sndPair(staking_option_pair))))));
							__core__ifThenElse(
								__helios__credential__is_pubkey(staking_credential),
								() -> {
									__core__consByteString(
										${isTestnet ? "0x10" : "0x11"},
										__core__appendByteString(
											__helios__credential__validator__hash(credential),
											__helios__credential__pubkey__hash(staking_credential)
										)
									)
								},
								() -> {
									__core__consByteString(
										${isTestnet ? "0x30" : "0x31"},
										__core__appendByteString(
											__helios__credential__validator__hash(credential),
											__helios__credential__validator__hash(staking_credential)
										)
									)
								}
							)()
						},
						() -> {
							__core__consByteString(
								${isTestnet ? "0x70" : "0x71"},
								__helios__credential__validator__hash(credential)
							)
						}
					)()
				}
			)()
		}
	}`));
	add(new RawFunc("__helios__address__from_bytes",
	`(bytes) -> {
		header = __core__indexByteString(bytes, 0);
		__core__ifThenElse(
			__core__equalsInteger(__core__modInteger(header, 2), ${isTestnet ? "0" : "1"}),
			() -> {
				is_pubkey_spending = __core__equalsInteger(__core__modInteger(__core__divideInteger(header, 16), 2), 0);
				staking_type = __core__divideInteger(header, 32);
				__core__ifThenElse(
					is_pubkey_spending,
					() -> {
						__core__ifThenElse(
							__core__equalsInteger(staking_type, 0),
							() -> {
								__helios__address__new(
									__helios__credential__new_pubkey(__core__sliceByteString(1, 28, bytes)),
									__core__constrData(0, __helios__common__list_1(
										__helios__stakingcredential__new_hash(
											__helios__credential__new_pubkey(__core__sliceByteString(29, 28, bytes))
										)
									))
								)
							},
							() -> {
								__core__ifThenElse(
									__core__equalsInteger(staking_type, 1),
									() -> {
										__helios__address__new(
											__helios__credential__new_pubkey(__core__sliceByteString(1, 28, bytes)),
											__core__constrData(0, __helios__common__list_1(
												__helios__stakingcredential__new_hash(
													__helios__credential__new_validator(__core__sliceByteString(29, 28, bytes))
												)
											))
										)
									},
									() -> {
										__core__ifThenElse(
											__core__equalsInteger(staking_type, 3),
											() -> {
												__helios__address__new(
													__helios__credential__new_pubkey(__core__sliceByteString(1, 28, bytes)),
													__helios__option__NONE
												)
											},
											() -> {
												__helios__error("unhandled staking type")
											}
										)()
									}
								)()
							}
						)()
					},
					() -> {
						__core__ifThenElse(
							__core__equalsInteger(staking_type, 0),
							() -> {
								__helios__address__new(
									__helios__credential__new_validator(__core__sliceByteString(1, 28, bytes)),
									__core__constrData(0, __helios__common__list_1(
										__helios__stakingcredential__new_hash(
											__helios__credential__new_pubkey(__core__sliceByteString(29, 28, bytes))
										)
									))
								)
							},
							() -> {
								__core__ifThenElse(
									__core__equalsInteger(staking_type, 1),
									() -> {
										__helios__address__new(
											__helios__credential__new_validator(__core__sliceByteString(1, 28, bytes)),
											__core__constrData(0, __helios__common__list_1(
												__helios__stakingcredential__new_hash(
													__helios__credential__new_validator(__core__sliceByteString(29, 28, bytes))
												)
											))
										)
									},
									() -> {
										__core__ifThenElse(
											__core__equalsInteger(staking_type, 3),
											() -> {
												__helios__address__new(
													__helios__credential__new_validator(__core__sliceByteString(1, 28, bytes)),
													__helios__option__NONE
												)
											},
											() -> {
												__helios__error("unhandled staking type")
											}
										)()
									}
								)()
							}
						)()
					}
				)()
			},
			() -> {
				__helios__error(
					__core__appendString(
						"not a ${isTestnet ? "testnet" : "mainnet"} address (header: ",
						__core__appendString(
							__helios__int__show(header)(),
							")"
						)
					)
				)
			}
		)()
	}`));
	add(new RawFunc("__helios__address__from_hex",
	`(hex) -> {
		__helios__address__from_bytes(__helios__bytearray__parse(hex))
	}`));
	add(new RawFunc("__helios__address__is_valid_data", 
	`(data) -> {
		__helios__common__test_constr_data_2(data, 0, __helios__credential__is_valid_data, __helios__option[__helios__stakingcredential]__is_valid_data)
	}`));
	add(new RawFunc("__helios__address__new", 
	`(cred, staking_cred) -> {
		__core__constrData(0, __helios__common__list_2(cred, staking_cred))
	}`));
	add(new RawFunc("__helios__address__new_empty",
	`() -> {
		__core__constrData(0, __helios__common__list_2(__helios__credential__new_pubkey(#), __helios__option__NONE))
	}`))
	add(new RawFunc("__helios__address__credential", "__helios__common__enum_field_0"));
	add(new RawFunc("__helios__address__staking_credential", "__helios__common__enum_field_1"));
	add(new RawFunc("__helios__address__is_staked",
	`(self) -> {
		() -> {
			__core__equalsInteger(__core__fstPair(__core__unConstrData(__helios__common__enum_field_1(self))), 0)
		}
	}`));


	// Credential builtins
	addDataFuncs("__helios__credential");
	add(new RawFunc("__helios__credential__is_valid_data",
	`(data) -> {
		__core__chooseData(
			data,
			() -> {
				pair = __core__unConstrData__safe(data);
				index = __core__fstPair(pair);
				fields = __core__sndPair(pair);
				__core__ifThenElse(
					__core__equalsInteger(index, 0),
					() -> {
						__core__chooseList(
							fields,
							() -> {
								false
							},
							() -> {
								__core__chooseList(
									__core__tailList__safe(fields),
									() -> {
										__helios__validatorhash__is_valid_data(__core__headList__safe(fields))
									}, 
									() -> {
										false
									}
								)()
							}
						)()
					},
					() -> {
						__core__ifThenElse(
							__core__equalsInteger(index, 1),
							() -> {
								__core__chooseList(
									fields,
									() -> {
										false
									},
									() -> {
										__core__chooseList(
											__core__tailList__safe(fields),
											() -> {
												__helios__pubkeyhash__is_valid_data(__core__headList__safe(fields))
											}, 
											() -> {
												false
											}
										)()
									}
								)()
							},
							() -> {
								false
							}
						)()
					}
				)()
			},
			() -> {false},
			() -> {false},
			() -> {false},
			() -> {false}
		)()
	}`));
	add(new RawFunc("__helios__credential__new_pubkey",
	`(hash) -> {
		__core__constrData(0, __helios__common__list_1(__helios__pubkeyhash____to_data(hash)))
	}`));
	add(new RawFunc("__helios__credential__new_validator",
	`(hash) -> {
		__core__constrData(1, __helios__common__list_1(__helios__validatorhash____to_data(hash)))
	}`));
	add(new RawFunc("__helios__credential__is_pubkey",
	`(self) -> {
		__core__equalsInteger(__core__fstPair(__core__unConstrData(self)), 0)
	}`));
	add(new RawFunc("__helios__credential__is_validator",
	`(self) -> {
		__core__equalsInteger(__core__fstPair(__core__unConstrData(self)), 1)
	}`));


	// Credential::PubKey builtins
	addEnumDataFuncs("__helios__credential__pubkey", 0);
	add(new RawFunc("__helios__credential__pubkey__cast",
	`(data) -> {
		__helios__common__assert_constr_index(data, 0)
	}`));
	add(new RawFunc("__helios__credential__pubkey__hash", 
	`(self) -> {
		__helios__pubkeyhash__from_data(__helios__common__enum_field_0(self))
	}`));


	// Credential::Validator builtins
	addEnumDataFuncs("__helios__credential__validator", 1);
	add(new RawFunc("__helios__credential__validator____new", "__helios__credential__new_validator"));
	add(new RawFunc("__helios__credential__validator__cast",
	`(data) -> {
		__helios__common__assert_constr_index(data, 1)
	}`));
	add(new RawFunc("__helios__credential__validator__hash", 
	`(self) -> {
		__helios__validatorhash__from_data(__helios__common__enum_field_0(self))
	}`));


	// StakingHash builtins
	addDataFuncs("__helios__stakinghash");
	add(new RawFunc("__helios__stakinghash__is_valid_data", "__helios__credential__is_valid_data"));
	add(new RawFunc("__helios__stakinghash__new_stakekey", "__helios__credential__new_pubkey"));
	add(new RawFunc("__helios__stakinghash__new_validator", "__helios__credential__new_validator"));
	add(new RawFunc("__helios__stakinghash__is_stakekey", "__helios__credential__is_stakekey"));
	add(new RawFunc("__helios__stakinghash__is_validator", "__helios__credential__is_validator"));


	// StakingHash::StakeKey builtins
	addEnumDataFuncs("__helios__stakinghash__stakekey", 0);
	add(new RawFunc("__helios__stakinghash__stakekey__is_valid_data", "__helios__credential__pubkey__is_valid_data"));
	add(new RawFunc("__helios__stakinghash__stakekey__cast", "__helios__credential__pubkey__cast"));
	add(new RawFunc("__helios__stakinghash__stakekey__hash", "__helios__credential__pubkey__hash"));


	// StakingHash::Validator builtins
	addEnumDataFuncs("__helios__stakinghash__validator", 1);
	add(new RawFunc("__helios__stakinghash__validator__is_valid_data", "__helios__credential__validator__is_valid_data"));
	add(new RawFunc("__helios__stakinghash__validator__cast", "__helios__credential__validator__cast"));
	add(new RawFunc("__helios__stakinghash__validator__hash", "__helios__credential__validator__hash"));


	// StakingCredential builtins
	addDataFuncs("__helios__stakingcredential");
	// TODO: test fields
	add(new RawFunc("__helios__stakingcredential__is_valid_data",
	`(data) -> {
		__core__chooseData(
			data,
			() -> {
				true
			},
			() -> {false},
			() -> {false},
			() -> {false},
			() -> {false}
		)()
	}`));
	add(new RawFunc("__helios__stakingcredential__new_hash", 
	`(cred) -> {
		__core__constrData(0, __helios__common__list_1(cred))
	}`));
	add(new RawFunc("__helios__stakingcredential__hash__cast",
	`(data) -> {
		__helios__common__assert_constr_index(data, 0)
	}`));
	add(new RawFunc("__helios__stakingcredential__new_ptr", 
	`(i, j, k) -> {
		__core__constrData(1, __helios__common__list_3(
			__helios__int____to_data(i), 
			__helios__int____to_data(j), 
			__helios__int____to_data(k)
		))
	}`));
	add(new RawFunc("__helios__stakingcredential__ptr__cast",
	`(data) -> {
		__helios__common__assert_constr_index(data, 1)
	}`));

	
	// StakingCredential::Hash builtins
	addEnumDataFuncs("__helios__stakingcredential__hash", 0);
	add(new RawFunc("__helios__stakingcredential__hash__hash", "__helios__common__enum_field_0"));


	// StakingCredential::Ptr builtins
	addEnumDataFuncs("__helios__stakingcredential__ptr", 1);


	// Time builtins
	addIntLikeFuncs("__helios__time");
	add(new RawFunc("__helios__time__is_valid_data", `__helios__int__is_valid_data`));
	add(new RawFunc("__helios__time__new", `__helios__common__identity`));
	add(new RawFunc("__helios__time____add", `__helios__int____add`));
	add(new RawFunc("__helios__time____sub", `__helios__int____sub`));
	add(new RawFunc("__helios__time____sub1", `__helios__int____sub`));
	add(new RawFunc("__helios__time____geq", `__helios__int____geq`));
	add(new RawFunc("__helios__time____gt", `__helios__int____gt`));
	add(new RawFunc("__helios__time____leq", `__helios__int____leq`));
	add(new RawFunc("__helios__time____lt", `__helios__int____lt`));
	add(new RawFunc("__helios__time__show", `__helios__int__show`));


	// Duratin builtins
	addIntLikeFuncs("__helios__duration");
	add(new RawFunc("__helios__duration__is_valid_data", `__helios__int__is_valid_data`));
	add(new RawFunc("__helios__duration__new", `__helios__common__identity`));
	add(new RawFunc("__helios__duration__show", `__helios__int__show`));
	add(new RawFunc("__helios__duration____add", `__helios__int____add`));
	add(new RawFunc("__helios__duration____sub", `__helios__int____sub`));
	add(new RawFunc("__helios__duration____mul", `__helios__int____mul`));
	add(new RawFunc("__helios__duration____div", `__helios__int____div`));
	add(new RawFunc("__helios__duration____div1", `__helios__int____div`));
	add(new RawFunc("__helios__duration____mod", `__helios__int____mod`));
	add(new RawFunc("__helios__duration____geq", `__helios__int____geq`));
	add(new RawFunc("__helios__duration____gt", `__helios__int____gt`));
	add(new RawFunc("__helios__duration____leq", `__helios__int____leq`));
	add(new RawFunc("__helios__duration____lt", `__helios__int____lt`));
	add(new RawFunc("__helios__duration__SECOND", "1000"));
	add(new RawFunc("__helios__duration__MINUTE", "60000"));
	add(new RawFunc("__helios__duration__HOUR", "3600000"));
	add(new RawFunc("__helios__duration__DAY", "86400000"));
	add(new RawFunc("__helios__duration__WEEK", "604800000"));


	// TimeRange builtins
	addDataFuncs("__helios__timerange");
	// TODO: test fields
	add(new RawFunc("__helios__timerange__is_valid_data",
	`(data) -> {
		__core__chooseData(
			data,
			() -> {
				true
			},
			() -> {false},
			() -> {false},
			() -> {false},
			() -> {false}
		)()
	}`));
	add(new RawFunc("__helios__timerange__new", `
	(a, b) -> {
		a = __helios__time____to_data(a);
		b = __helios__time____to_data(b);
		__core__constrData(0, __helios__common__list_2(
			__core__constrData(0, __helios__common__list_2(
				__core__constrData(1, __helios__common__list_1(a)),
				__helios__bool____to_data(true)
			)),
			__core__constrData(0, __helios__common__list_2(
				__core__constrData(1, __helios__common__list_1(b)),
				__helios__bool____to_data(true)
			))
		))
	}`));
	add(new RawFunc("__helios__timerange__ALWAYS", `
	__core__constrData(0, __helios__common__list_2(
		__core__constrData(0, __helios__common__list_2(
			__core__constrData(0, __helios__common__list_0),
			__helios__bool____to_data(true)
		)),
		__core__constrData(0, __helios__common__list_2(
			__core__constrData(2, __helios__common__list_0),
			__helios__bool____to_data(true)
		))
	))`));
	add(new RawFunc("__helios__timerange__NEVER", `
	__core__constrData(0, __helios__common__list_2(
		__core__constrData(0, __helios__common__list_2(
			__core__constrData(2, __helios__common__list_0),
			__helios__bool____to_data(true)
		)),
		__core__constrData(0, __helios__common__list_2(
			__core__constrData(0, __helios__common__list_0),
			__helios__bool____to_data(true)
		))
	))`));
	add(new RawFunc("__helios__timerange__from", `
	(a) -> {
		a = __helios__time____to_data(a);
		__core__constrData(0, __helios__common__list_2(
			__core__constrData(0, __helios__common__list_2(
				__core__constrData(1, __helios__common__list_1(a)),
				__helios__bool____to_data(true)
			)),
			__core__constrData(0, __helios__common__list_2(
				__core__constrData(2, __helios__common__list_0),
				__helios__bool____to_data(true)
			))
		))
	}`));
	add(new RawFunc("__helios__timerange__to", `
	(b) -> {
		b = __helios__time____to_data(b);
		__core__constrData(0, __helios__common__list_2(
			__core__constrData(0, __helios__common__list_2(
				__core__constrData(0, __helios__common__list_0),
				__helios__bool____to_data(true)
			)),
			__core__constrData(0, __helios__common__list_2(
				__core__constrData(1, __helios__common__list_1(b)),
				__helios__bool____to_data(true)
			))
		))
	}`));
	add(new RawFunc("__helios__timerange__is_before", 
	`(self) -> {
		(t) -> {
			upper = __helios__common__enum_field_1(self);
			extended = __helios__common__enum_field_0(upper);
			closed = __helios__bool__from_data(__helios__common__enum_field_1(upper));
			extType = __core__fstPair(__core__unConstrData(extended));
			__core__ifThenElse(
				__core__equalsInteger(extType, 2),
				() -> {false},
				() -> {
					__core__ifThenElse(
						__core__equalsInteger(extType, 0),
						() -> {true},
						() -> {
							__core__ifThenElse(
								closed,
								() -> {__core__lessThanInteger(__core__unIData(__core__headList(__core__sndPair(__core__unConstrData(extended)))), t)},
								() -> {__core__lessThanEqualsInteger(__core__unIData(__core__headList(__core__sndPair(__core__unConstrData(extended)))), t)}
							)()
						}
					)()
				}
			)()
		}
	}`));
	add(new RawFunc("__helios__timerange__is_after",
	`(self) -> {
		(t) -> {
			lower = __helios__common__enum_field_0(self);
			extended = __helios__common__enum_field_0(lower);
			closed = __helios__bool__from_data(__helios__common__enum_field_1(lower));
			extType = __core__fstPair(__core__unConstrData(extended));
			__core__ifThenElse(
				__core__equalsInteger(extType, 0),
				() -> {false},
				() -> {
					__core__ifThenElse(
						__core__equalsInteger(extType, 2),
						() -> {true},
						() -> {
							__core__ifThenElse(
								closed,
								() -> {__core__lessThanInteger(t, __core__unIData(__core__headList(__core__sndPair(__core__unConstrData(extended)))))},
								() -> {__core__lessThanEqualsInteger(t, __core__unIData(__core__headList(__core__sndPair(__core__unConstrData(extended)))))}
							)()
						}
					)()
				}
			)()
		}
	}`));
	add(new RawFunc("__helios__timerange__contains",
	`(self) -> {
		(t) -> {
			lower = __helios__common__enum_field_0(self);
			extended = __helios__common__enum_field_0(lower);
			closed = __helios__bool__from_data(__helios__common__enum_field_1(lower));
			lowerExtType = __core__fstPair(__core__unConstrData(extended));
			checkUpper = () -> {
				upper = __helios__common__enum_field_1(self);
				extended = __helios__common__enum_field_0(upper);
				closed = __helios__bool__from_data(__helios__common__enum_field_1(upper));
				upperExtType = __core__fstPair(__core__unConstrData(extended));
				__core__ifThenElse(
					__core__equalsInteger(upperExtType, 0),
					() -> {false},
					() -> {
						__core__ifThenElse(
							__core__equalsInteger(upperExtType, 2),
							() -> {true},
							() -> {
								__core__ifThenElse(
									__core__ifThenElse(
										closed,
										() -> {__core__lessThanEqualsInteger(t, __core__unIData(__core__headList(__core__sndPair(__core__unConstrData(extended)))))},
										() -> {__core__lessThanInteger(t, __core__unIData(__core__headList(__core__sndPair(__core__unConstrData(extended)))))}
									)(),
									true,
									false
								)
							}
						)()
					}
				)()
			};
			__core__ifThenElse(
				__core__equalsInteger(lowerExtType, 2),
				() -> {false},
				() -> {
					__core__ifThenElse(
						__core__equalsInteger(lowerExtType, 0),
						() -> {checkUpper()},
						() -> {
							__core__ifThenElse(
								__core__ifThenElse(
									closed,
									() -> {__core__lessThanEqualsInteger(__core__unIData(__core__headList(__core__sndPair(__core__unConstrData(extended)))), t)},
									() -> {__core__lessThanInteger(__core__unIData(__core__headList(__core__sndPair(__core__unConstrData(extended)))), t)}
								)(),
								() -> {checkUpper()},
								() -> {false}
							)()
						}
					)()
				}
			)()
		}
	}`));
	add(new RawFunc("__helios__timerange__start",
	`(self) -> {
		__helios__time__from_data(__helios__common__enum_field_0(__helios__common__enum_field_0(__helios__common__enum_field_0(self))))
	}`));
	add(new RawFunc("__helios__timerange__end",
	`(self) -> {
		__helios__time__from_data(__helios__common__enum_field_0(__helios__common__enum_field_0(__helios__common__enum_field_1(self))))
	}`));
	add(new RawFunc("__helios__timerange__show",
	`(self) -> {
		() -> {
			show_extended = (extended) -> {
				extType = __core__fstPair(__core__unConstrData(extended));
				__core__ifThenElse(
					__core__equalsInteger(extType, 0),
					() -> {"-inf"},
					() -> {
						__core__ifThenElse(
							__core__equalsInteger(extType, 2),
							() -> {"+inf"},
							() -> {
								fields = __core__sndPair(__core__unConstrData(extended));
								__helios__int__show(
									__helios__int__from_data(__core__headList(fields))
								)()
							}
						)()
					}
				)()
			};
			__helios__string____add(
				lower = __helios__common__enum_field_0(self);
				extended = __helios__common__enum_field_0(lower);
				closed = __helios__bool__from_data(__helios__common__enum_field_1(lower));
				__helios__string____add(
					__core__ifThenElse(closed, "[", "("),
					show_extended(extended)
				),
				__helios__string____add(
					",",
					upper = __helios__common__enum_field_1(self);
					extended = __helios__common__enum_field_0(upper);
					closed = __helios__bool__from_data(__helios__common__enum_field_1(upper));
					__helios__string____add(
						show_extended(extended),
						__core__ifThenElse(closed, "]", ")")
					)
				)
			)
		}
	}`))


	// AssetClass builtins
	addDataFuncs("__helios__assetclass");
	add(new RawFunc("__helios__assetclass__is_valid_data",
	`(data) -> {
		__helios__common__test_constr_data_2(data, 0, __helios__mintingpolicyhash__is_valid_data, __helios__bytearray__is_valid_data)
	}`));
	add(new RawFunc("__helios__assetclass__ADA", `__helios__assetclass__new(#, #)`));
	add(new RawFunc("__helios__assetclass__new",
	`(mph, token_name) -> {
		__core__constrData(0, __helios__common__list_2(
			__helios__mintingpolicyhash____to_data(mph), 
			__helios__bytearray____to_data(token_name)
		))
	}`));
	add(new RawFunc("__helios__assetclass__mph", 
	`(self) -> {
		__helios__mintingpolicyhash__from_data(__helios__common__enum_field_0(self))
	}`));
	add(new RawFunc("__helios__assetclass__token_name", 
	`(self) -> {
		__helios__bytearray__from_data(__helios__common__enum_field_1(self))
	}`));
	add(new RawFunc("__helios__assetclass__show",
	`(self) -> {
		__core__chooseData(
			self,
			() -> {
				fields = __core__sndPair(__core__unConstrData__safe(self));
				__core__chooseList(
					fields,
					() -> {
						"N/A"
					},
					() -> {
						mph = __core__headList__safe(fields);
						__core__appendString(
							__core__chooseData(
								mph,
								() -> {"N/A"},
								() -> {"N/A"},
								() -> {"N/A"},
								() -> {"N/A"},
								() -> {
									__helios__bytearray__show(__core__unBData__safe(mph))()
								}
							)(),
							__core__appendString(
								".",
								fields = __core__tailList__safe(fields);
								__core__chooseList(
									fields,
									() -> {
										"N/A"
									},
									() -> {
										token_name = __core__headList__safe(fields);
										__core__chooseData(
											token_name,
											() -> {"N/A"},
											() -> {"N/A"},
											() -> {"N/A"},
											() -> {"N/A"},
											() -> {
												__helios__bytearray__show(__core__unBData__safe(token_name))()
											}
										)()
									}
								)()
							)
						)
					}
				)()
			},
			() -> {"N/A"},
			() -> {"N/A"},
			() -> {"N/A"},
			() -> {"N/A"}
		)
	}`));
	add(new RawFunc("__helios__assetclass____lt",
	`(a, b) -> {
		mpha = __helios__assetclass__mph(a);
		mphb = __helios__assetclass__mph(b);
		__core__ifThenElse(
			__helios__bytearray____eq(mpha, mphb),
			() -> {
				__helios__bytearray____lt(
					__helios__assetclass__token_name(a),
					__helios__assetclass__token_name(b)
				)
			},
			() -> {
				__helios__bytearray____lt(mpha, mphb)
			}
		)()
	}`));
	add(new RawFunc("__helios__assetclass____leq",
	`(a, b) -> {
		mpha = __helios__assetclass__mph(a);
		mphb = __helios__assetclass__mph(b);
		__core__ifThenElse(
			__helios__bytearray____eq(mpha, mphb),
			() -> {
				__helios__bytearray____leq(
					__helios__assetclass__token_name(a),
					__helios__assetclass__token_name(b)
				)
			},
			() -> {
				__helios__bytearray____lt(mpha, mphb)
			}
		)()
	}`));
	add(new RawFunc("__helios__assetclass____gt",
	`(a, b) -> {
		mpha = __helios__assetclass__mph(a);
		mphb = __helios__assetclass__mph(b);
		__core__ifThenElse(
			__helios__bytearray____eq(mpha, mphb),
			() -> {
				__helios__bytearray____gt(
					__helios__assetclass__token_name(a),
					__helios__assetclass__token_name(b)
				)
			},
			() -> {
				__helios__bytearray____gt(mpha, mphb)
			}
		)()
	}`));
	add(new RawFunc("__helios__assetclass____geq",
	`(a, b) -> {
		mpha = __helios__assetclass__mph(a);
		mphb = __helios__assetclass__mph(b);
		__core__ifThenElse(
			__helios__bytearray____eq(mpha, mphb),
			() -> {
				__helios__bytearray____geq(
					__helios__assetclass__token_name(a),
					__helios__assetclass__token_name(b)
				)
			},
			() -> {
				__helios__bytearray____gt(mpha, mphb)
			}
		)()
	}`));


	// Value builtins
	addSerializeFunc("__helios__value");
	// TODO: test each entry in the map
	add(new RawFunc("__helios__value__is_valid_data",
	`(data) -> {
		__core__chooseData(
			data,
			() -> {false},
			() -> {
				map = __core__unMapData__safe(data);
				recurse = (recurse, map) -> {
					__core__chooseList(
						map,
						() -> {
							true
						},
						() -> {
							head = __core__headList__safe(map);
							key = __core__fstPair(head);
							value = __core__sndPair(head);
							__core__ifThenElse(
								__helios__mintingpolicyhash__is_valid_data(key),
								() -> {
									__core__chooseData(
										value,
										() -> {false},
										() -> {
											inner = __core__unMapData__safe(value);
											__core__chooseList(
												inner,
												() -> {
													false
												},
												() -> {
													recurse_inner = (recurse_inner, inner) -> {
														__core__chooseList(
															inner,
															() -> {
																true
															},
															() -> {
																head = __core__headList__safe(inner);
																key = __core__fstPair(head);
																value = __core__sndPair(head);
																__core__ifThenElse(
																	__helios__bytearray__is_valid_data(key),
																	() -> {
																		__core__ifThenElse(
																			__helios__int__is_valid_data(value),
																			() -> {
																				true
																			},
																			() -> {
																				false
																			}
																		)()
																	},
																	() -> {
																		false
																	}
																)()
															}
														)()
													};
													recurse_inner(recurse_inner, inner)
												}
											)()
										},
										() -> {false},
										() -> {false},
										() -> {false}
									)()
								},
								() -> {
									false
								}
							)()
						}
					)()
				};
				recurse(recurse, map)
			},
			() -> {false},
			() -> {false},
			() -> {false}
		)()
	}`));
	add(new RawFunc("__helios__value__from_data", "__core__unMapData"));
	add(new RawFunc("__helios__value__from_data_safe",
	`(data) -> {
		__core__chooseData(
			data,
			() -> {__helios__option__NONE_FUNC},
			() -> {
				__helios__option__SOME_FUNC(__core__unMapData__safe(data))
			},
			() -> {__helios__option__NONE_FUNC},
			() -> {__helios__option__NONE_FUNC},
			() -> {__helios__option__NONE_FUNC}
		)()
	}`));
	add(new RawFunc("__helios__value____to_data", "__core__mapData"));
	add(new RawFunc("__helios__value__value", "__helios__common__identity"));
	add(new RawFunc("__helios__value__ZERO", "__core__mkNilPairData(())"));
	add(new RawFunc("__helios__value__lovelace",
	`(i) -> {
		__helios__value__new(__helios__assetclass__ADA, i)
	}`));
	add(new RawFunc("__helios__value__new",
	`(assetClass, i) -> {
		__core__ifThenElse(
			__core__equalsInteger(0, i),
			() -> {
				__helios__value__ZERO
			},
			() -> {
				mph = __helios__common__enum_field_0(assetClass);
				tokenName = __helios__common__enum_field_1(assetClass);
				__core__mkCons(
					__core__mkPairData(
						mph, 
						__core__mapData(
							__core__mkCons(
								__core__mkPairData(tokenName, __helios__int____to_data(i)), 
								__core__mkNilPairData(())
							)
						)
					), 
					__core__mkNilPairData(())
				)
			}
		)()
	}`));
	add(new RawFunc("__helios__value__from_map", "__helios__common__identity"));
	add(new RawFunc("__helios__value__to_map", 
	`(self) -> {
		() -> {
			self
		}
	}`));
	add(new RawFunc("__helios__value__get_map_keys",
	`(map) -> {
		recurse = (recurse, map) -> {
			__core__chooseList(
				map, 
				() -> {__helios__common__list_0}, 
				() -> {__core__mkCons(__core__fstPair(__core__headList__safe(map)), recurse(recurse, __core__tailList__safe(map)))}
			)()
		};
		recurse(recurse, map)
	}`));
	add(new RawFunc("__helios__value__merge_map_keys",
	`(a, b) -> {
		aKeys = __helios__value__get_map_keys(a);
		recurse = (recurse, keys, map) -> {
			__core__chooseList(
				map, 
				() -> {__helios__common__list_0}, 
				() -> {
					key = __core__fstPair(__core__headList__safe(map));
					__core__ifThenElse(
						__helios__common__is_in_bytearray_list(aKeys, key), 
						() -> {recurse(recurse, keys, __core__tailList__safe(map))},
						() -> {__core__mkCons(key, recurse(recurse, keys, __core__tailList__safe(map)))}
					)()
				}
			)()
		};
		uniqueBKeys = recurse(recurse, aKeys, b);
		__helios__common__concat(aKeys, uniqueBKeys)	
	}`));

	add(new RawFunc("__helios__value__get_inner_map",
	`(map, mph) -> {
		recurse = (recurse, map) -> {
			__core__chooseList(
				map, 
				() -> {__core__mkNilPairData(())},
				() -> {
					__core__ifThenElse(
						__core__equalsData(__core__fstPair(__core__headList__safe(map)), mph), 
						() -> {__core__unMapData(__core__sndPair(__core__headList__safe(map)))},
						() -> {recurse(recurse, __core__tailList__safe(map))}
					)()
				}
			)()
		};
		recurse(recurse, map)
	}`));
	add(new RawFunc("__helios__value__get_inner_map_int",
	`(map, key) -> {
		recurse = (recurse, map, key) -> {
			__core__chooseList(
				map, 
				() -> {0}, 
				() -> {
					__core__ifThenElse(
						__core__equalsData(__core__fstPair(__core__headList__safe(map)), key), 
						() -> {__core__unIData(__core__sndPair(__core__headList__safe(map)))}, 
						() -> {recurse(recurse, __core__tailList__safe(map), key)}
					)()
				}
			)()
		};
		recurse(recurse, map, key)
	}`));
	add(new RawFunc("__helios__value__add_or_subtract_inner",
	`(op) -> {
		(a, b) -> {
			recurse = (recurse, keys, result) -> {
				__core__chooseList(
					keys, 
					() -> {result}, 
					() -> {
						key = __core__headList__safe(keys);
						tail = recurse(recurse, __core__tailList__safe(keys), result);
						sum = op(__helios__value__get_inner_map_int(a, key), __helios__value__get_inner_map_int(b, key));
						__core__ifThenElse(
							__core__equalsInteger(sum, 0), 
							() -> {tail}, 
							() -> {__core__mkCons(__core__mkPairData(key, __core__iData(sum)), tail)}
						)()
					}
				)()
			};
			recurse(recurse, __helios__value__merge_map_keys(a, b), __core__mkNilPairData(()))
		}
	}`));
	add(new RawFunc("__helios__value__add_or_subtract",
	`(a, b, op) -> {
		recurse = (recurse, keys, result) -> {
			__core__chooseList(
				keys, 
				() -> {result}, 
				() -> {
					key = __core__headList__safe(keys);
					tail = recurse(recurse, __core__tailList__safe(keys), result);
					item = __helios__value__add_or_subtract_inner(op)(__helios__value__get_inner_map(a, key), __helios__value__get_inner_map(b, key));
					__core__chooseList(
						item, 
						() -> {tail}, 
						() -> {__core__mkCons(__core__mkPairData(key, __core__mapData(item)), tail)}
					)()
				}
			)()
		};
		recurse(recurse, __helios__value__merge_map_keys(a, b), __core__mkNilPairData(()))
	}`));
	add(new RawFunc("__helios__value__map_quantities",
	`(self, op) -> {
		recurseInner = (recurseInner, inner) -> {
			__core__chooseList(
				inner,
				() -> {__core__mkNilPairData(())},
				() -> {
					head = __core__headList__safe(inner);
					__core__mkCons(
						__core__mkPairData(
							__core__fstPair(head),
							__core__iData(op(__core__unIData(__core__sndPair(head))))
						),
						recurseInner(recurseInner, __core__tailList__safe(inner))
					)
				}
			)()
		};
		recurseOuter = (recurseOuter, outer) -> {
			__core__chooseList(
				outer,
				() -> {__core__mkNilPairData(())},
				() -> {
					head = __core__headList__safe(outer);
					__core__mkCons(
						__core__mkPairData(
							__core__fstPair(head), 
							__core__mapData(recurseInner(recurseInner, __core__unMapData(__core__sndPair(head))))
						),  
						recurseOuter(recurseOuter, __core__tailList__safe(outer))
					)
				}
			)()
		};
		recurseOuter(recurseOuter, self)
	}`));
	add(new RawFunc("__helios__value__compare_inner",
	`(comp, a, b) -> {
		recurse = (recurse, keys) -> {
			__core__chooseList(
				keys, 
				() -> {true}, 
				() -> {
					key = __core__headList__safe(keys);
					__core__ifThenElse(
						__helios__bool____not(
							comp(
								__helios__value__get_inner_map_int(a, key), 
								__helios__value__get_inner_map_int(b, key)
							)
						), 
						() -> {false}, 
						() -> {recurse(recurse, __core__tailList__safe(keys))}
					)()
				}
			)()
		};
		recurse(recurse, __helios__value__merge_map_keys(a, b))
	}`));
	add(new RawFunc("__helios__value__compare",
	`(a, b, comp) -> {
		recurse = (recurse, keys) -> {
			__core__chooseList(
				keys, 
				() -> {true}, 
				() -> {
					key = __core__headList__safe(keys);
					__core__ifThenElse(
						__helios__bool____not(
							__helios__value__compare_inner(
								comp, 
								__helios__value__get_inner_map(a, key), 
								__helios__value__get_inner_map(b, key)
							)
						), 
						() -> {false}, 
						() -> {recurse(recurse, __core__tailList__safe(keys))}
					)()
				}
			)()
		};
		recurse(recurse, __helios__value__merge_map_keys(a, b))
	}`));
	add(new RawFunc("__helios__value____eq",
	`(a, b) -> {
		__helios__value__compare(a, b, __core__equalsInteger)
	}`));
	add(new RawFunc("__helios__value____neq",
	`(a, b) -> {
		__helios__bool____not(__helios__value____eq(a, b))
	}`));
	add(new RawFunc("__helios__value____add",
	`(a, b) -> {
		__helios__value__add_or_subtract(a, b, __core__addInteger)
	}`));
	add(new RawFunc("__helios__value____sub",
	`(a, b) -> {
		__helios__value__add_or_subtract(a, b, __core__subtractInteger)
	}`));
	add(new RawFunc("__helios__value____mul",
	`(a, scale) -> {
		__helios__value__map_quantities(a, (qty) -> {__core__multiplyInteger(qty, scale)})
	}`));
	add(new RawFunc("__helios__value____div",
	`(a, den) -> {
		__helios__value__map_quantities(a, (qty) -> {__core__divideInteger(qty, den)})
	}`));
	add(new RawFunc("__helios__value____geq",
	`(a, b) -> {
		__helios__value__compare(
			a, 
			b, 
			(a_qty, b_qty) -> {
				__helios__bool____not(
					__core__lessThanInteger(a_qty, b_qty)
				)
			}
		)
	}`));
	add(new RawFunc("__helios__value__contains", `
	(self) -> {
		(value) -> {
			__helios__value____geq(self, value)
		}
	}`));
	add(new RawFunc("__helios__value____gt",
	`(a, b) -> {
		__helios__bool__and(
			() -> {
				__helios__bool____not(
					__helios__bool__and(
						__helios__value__is_zero(a),
						__helios__value__is_zero(b)
					)
				)
			},
			() -> {
				__helios__value__compare(
					a, 
					b,
					(a_qty, b_qty) -> {
						__helios__bool____not(__core__lessThanEqualsInteger(a_qty, b_qty))
					}
				)
			}
		)
	}`));
	add(new RawFunc("__helios__value____leq",
	`(a, b) -> {
		__helios__value__compare(a, b, __core__lessThanEqualsInteger)
	}`));
	add(new RawFunc("__helios__value____lt",
	`(a, b) -> {
		__helios__bool__and(
			() -> {
				__helios__bool____not(
					__helios__bool__and(
						__helios__value__is_zero(a),
						__helios__value__is_zero(b)
					)
				)
			},
			() -> {
				__helios__value__compare( 
					a, 
					b,
					(a_qty, b_qty) -> {
						__core__lessThanInteger(a_qty, b_qty)
					}
				)
			}
		)
	}`));
	add(new RawFunc("__helios__value__is_zero_inner",
	`(tokens) -> {
		recurse = (recurse, tokens) -> {
			__core__chooseList(
				tokens,
				() -> {
					true
				},
				() -> {
					__helios__bool__and(
						() -> {
							__core__equalsInteger(__core__unIData(__core__sndPair(__core__headList__safe(tokens))), 0)
						},
						() -> {
							recurse(recurse, __core__tailList__safe(tokens))
						}
					)
				}
			)()
		};
		recurse(recurse, tokens)
	}`));
	add(new RawFunc("__helios__value__is_zero",
	`(self) -> {
		() -> {
			recurse = (recurse, map) -> {
				__core__chooseList(
					map,
					() -> {
						true
					},
					() -> {
						__helios__bool__and(
							() -> {
								__helios__value__is_zero_inner(__core__unMapData(__core__sndPair(__core__headList__safe(map))))
							},
							() -> {
								recurse(recurse, __core__tailList__safe(map))
							}
						)
					}
				)()
			};
			recurse(recurse, self)
		}
	}`));
	// TODO: core__unBData__safe(mph)
	add(new RawFunc("__helios__value__get",
	`(self) -> {
		(assetClass) -> {
			mph = __helios__common__enum_field_0(assetClass);
			tokenName = __helios__common__enum_field_1(assetClass);
			outer = (outer, inner, map) -> {
				__core__chooseList(
					map, 
					() -> {
						__helios__error(
							__helios__string____add(
								__helios__string____add(
									"policy ", 
									__helios__mintingpolicyhash__show(__core__unBData(mph))()
								),
								" not found"
							)
						)
					},
					() -> {
						__core__ifThenElse(
							__core__equalsData(__core__fstPair(__core__headList__safe(map)), mph), 
							() -> {inner(inner, __core__unMapData(__core__sndPair(__core__headList__safe(map))))}, 
							() -> {outer(outer, inner, __core__tailList__safe(map))}
						)()
					}
				)()
			};
			inner = (inner, map) -> {
				__core__chooseList(
					map, 
					() -> {__helios__error("tokenName not found")}, 
					() -> {
						__core__ifThenElse(
							__core__equalsData(__core__fstPair(__core__headList__safe(map)), tokenName),
							() -> {
								__core__unIData(__core__sndPair(__core__headList__safe(map)))
							},
							() -> {
								inner(inner, __core__tailList__safe(map))
							}
						)()
					}
				)()
			};
			outer(outer, inner, self)
		}
	}`));
	add(new RawFunc("__helios__value__get_safe",
	`(self) -> {
		(assetClass) -> {
			mintingPolicyHash = __helios__common__enum_field_0(assetClass);
			tokenName = __helios__common__enum_field_1(assetClass);
			outer = (outer, inner, map) -> {
				__core__chooseList(
					map, 
					() -> {0}, 
					() -> {
						__core__ifThenElse(
							__core__equalsData(__core__fstPair(__core__headList__safe(map)), mintingPolicyHash), 
							() -> {inner(inner, __core__unMapData(__core__sndPair(__core__headList__safe(map))))}, 
							() -> {outer(outer, inner, __core__tailList__safe(map))}
						)()
					}
				)()
			};
			inner = (inner, map) -> {
				__core__chooseList(
					map, 
					() -> {0}, 
					() -> {
						__core__ifThenElse(
							__core__equalsData(__core__fstPair(__core__headList__safe(map)), tokenName),
							() -> {
								__core__unIData(__core__sndPair(__core__headList__safe(map)))
							},
							() -> {
								inner(inner, __core__tailList__safe(map))
							}
						)()
					}
				)()
			};
			outer(outer, inner, self)
		}
	}`));
	add(new RawFunc("__helios__value__get_lovelace",
	`(self) -> {
		() -> {
			__helios__value__get_safe(self)(__helios__assetclass__ADA)
		}
	}`));
	add(new RawFunc("__helios__value__get_assets",
	`(self) -> {
		() -> {
			__helios__common__filter_map(
				self,
				(pair) -> {
					__helios__bool____not(__core__equalsByteString(__core__unBData(__core__fstPair(pair)), #))
				}
			)
		}
	}`));
	add(new RawFunc("__helios__value__get_policy", 
	`(self) -> {
		(mph) -> {
			mph = __helios__mintingpolicyhash____to_data(mph);
			recurse = (recurse, map) -> {
				__core__chooseList(
					map,
					() -> {__helios__error("policy not found")},
					() -> {
						__core__ifThenElse(
							__core__equalsData(__core__fstPair(__core__headList__safe(map)), mph),
							() -> {
								__core__unMapData(__core__sndPair(__core__headList__safe(map)))
							},
							() -> {
								recurse(recurse, __core__tailList__safe(map))
							}
						)()
					}
				)()
			};
			recurse(recurse, self)
		} 
	}`));
	add(new RawFunc("__helios__value__contains_policy",
	`(self) -> {
		(mph) -> {
			mph = __helios__mintingpolicyhash____to_data(mph);
			recurse = (recurse, map) -> {
				__core__chooseList(
					map,
					() -> {false},
					() -> {
						__core__ifThenElse(
							__core__equalsData(__core__fstPair(__core__headList__safe(map)), mph),
							() -> {true},
							() -> {recurse(recurse, __core__tailList__safe(map))}
						)()
					}
				)()
			};
			recurse(recurse, self)
		}
	}`));
	add(new RawFunc("__helios__value__show",
	`(self) -> {
		() -> {
			__helios__common__fold(
				self,
				(prev, pair) -> {
					mph = __core__unBData__safe(__core__fstPair(pair));
					tokens = __core__unMapData__safe(__core__sndPair(pair));
					__helios__common__fold(
						tokens,
						(prev, pair) -> {
							token_name = __core__unBData__safe(__core__fstPair(pair));
							qty = __core__unIData__safe(__core__sndPair(pair));
							__helios__string____add(
								prev,
								__core__ifThenElse(
									__helios__mintingpolicyhash____eq(mph, #),
									() -> {
										__helios__string____add(
											"lovelace: ",
											__helios__string____add(
												__helios__int__show(qty)(),
												"\\n"
											)
										)
									},
									() -> {
										__helios__string____add(
											__helios__mintingpolicyhash__show(mph)(),
											__helios__string____add(
												".",
												__helios__string____add(
													__helios__bytearray__show(token_name)(),
													__helios__string____add(
														": ",
														__helios__string____add(
															__helios__int__show(qty)(),
															"\\n"
														)
													)
												)
											)
										)
									}
								)()
							)
						},
						prev
					)
				},
				""
			)
		}
	}`))
	add(new RawFunc(`__helios__value__sum[${FTPP}0]`,
	`(self) -> {
		recurse = (recurse, lst) -> {
			__core__chooseList(
				lst,
				() -> {
					__helios__value__ZERO
				},
				() -> {
					__helios__value____add(
						${FTPP}0__value(${FTPP}0__from_data(__core__headList__safe(lst))),
						recurse(recurse, __core__tailList__safe(lst))
					)
				}
			)()
		};
		recurse(recurse, self)
	}`));

	return db;
}

/**
 * @internal
 */
export class ToIRContext {
	/**
	 * @readonly
	 * @type {boolean}
	 */
	simplify;

	/**
	 * @readonly
	 * @type {string}
	 */
	indent; 

	/**
	 * @type {Map<string, RawFunc>}
	 */
	#db;
	
	/**
	 * @param {boolean} simplify 
	 * @param {string} indent
	 * @param {Map<string, RawFunc>} db
	 */
	constructor(simplify, indent = "", db = new Map()) {
		this.simplify = simplify;
		this.indent = indent;

		this.#db = db;
	}

	/**
	 * @returns {ToIRContext}
	 */
	tab() {
		return new ToIRContext(this.simplify, this.indent + TAB, this.#db);
	}

	/**
	 * @type {Map<string, RawFunc>}
	 */
	get db() {
		if (this.#db.size == 0) {
			this.#db = makeRawFunctions(this.simplify);
		}

		return this.#db;
	}
}


/**
 * Load all raw generics so all possible implementations can be generated correctly during type parameter injection phase
 * @internal
 * @param {ToIRContext} ctx
 * @returns {Map<string, ((ttp: string[], ftp: string[]) => IR)>}
 */
export function fetchRawGenerics(ctx) {
	/**
	 * @type {Map<string, ((ttp: string[], ftp: string[]) => IR)>}
	 */
	const map = new Map();

	for (let [k, v] of ctx.db) {
		if (IRParametricName.matches(k)) {
			// load without dependencies
			/**
			 * 
			 * @param {string[]} ttp 
			 * @param {string[]} ftp 
			 * @returns {IR}
			 */
			const fn = (ttp, ftp) => v.toIR(ttp, ftp);
			map.set(k, fn)
		}
	}

	return map;
}

/**
 * Doesn't add templates
 * @internal
 * @param {ToIRContext} ctx
 * @param {IR} ir 
 * @param {null | IRDefinitions} userDefs - some userDefs might have the __helios prefix
 * @returns {IRDefinitions}
 */
export function fetchRawFunctions(ctx, ir, userDefs = null) {
	// notify statistics of existence of builtin in correct order
	if (onNotifyRawUsage !== null) {
		for (let [name, _] of ctx.db) {
			// don't add templates, as they will never actually be used (type parameters are substituted)
			if (!IRParametricName.isTemplate(name)) {
				onNotifyRawUsage(name, 0);
			}
		}
	}

	let [src, _] = ir.generateSource();

	let matches = src.match(RE_BUILTIN);

	/**
	 * @type {IRDefinitions}
	 */
	const map = new Map();

	if (matches !== null) {
		for (let m of matches) {
			if (!IRParametricName.matches(m) && !map.has(m) && (!userDefs || !userDefs.has(m))) {
				const builtin = ctx.db.get(m);

				if (!builtin) {
					console.log(src);
					throw new Error(`builtin ${m} not found`);
				}

				builtin.load(ctx.db, map);
			}
		}
	}

	return map;
}

/**
 * @internal
 * @param {ToIRContext} ctx
 * @param {IR} ir 
 * @returns {IR}
 */
export function wrapWithRawFunctions(ctx, ir) {
	const map = fetchRawFunctions(ctx, ir);
	
	return IR.wrapWithDefinitions(ir, map);
}



/////////////////////////////////////
// Section 24: Helios AST expressions
/////////////////////////////////////

/**
 * Base class of every Type and Instance expression.
 * @internal
 */
export class Expr extends Token {
	/**@type {null | EvalEntity} */
	#cache;

	/**
	 * @param {Site} site 
	 
	 */
	constructor(site) {
		super(site);
		this.#cache = null;
	}

	/**
	 * @type {null | EvalEntity}
	 */
	get cache() {
		return this.#cache;
	}

	/**
	 * Used in switch cases where initial typeExpr is used as memberName instead
	 * @param {null | EvalEntity} c
	 */
	set cache(c) {
		this.#cache = c;
	}

	/**
	 * @param {Scope} scope 
	 * @returns {null | EvalEntity}
	 */
	evalInternal(scope) {
		throw new Error("not yet implemented");
	}

	/**
	 * @param {Scope} scope 
	 * @returns {null | EvalEntity}
	 */
	eval(scope) {
		//if (this.#cache === null) {
			this.#cache = this.evalInternal(scope);
		//}

		return this.#cache;
	}

	/**
	 * @param {Scope} scope 
	 * @returns {null | DataType}
	 */
	evalAsDataType(scope) {
		const result_ = this.eval(scope);

		if (!result_) {
			return null;
		}

		const result = result_.asDataType;

		if (!result) {
			this.typeError("not a data type");
			return null;
		}

		return result;
	}

	/**
	 * @param {Scope} scope 
	 * @returns {null | Type}
	 */
	evalAsType(scope) {
		const r = this.eval(scope);

		if (!r) {
			return null;
		}

		const result = r.asType;

		if (!result) {
			this.typeError(`${r.toString()} isn't a type`);
			return null;
		}

		return result;
	}

	/**
	 * @param {Scope} scope 
	 * @returns {null | Typed}
	 */
	evalAsTyped(scope) {
		const r  = this.eval(scope);

		if (!r) {
			return null;
		}

		const result = r.asTyped;

		if (!result) {
			this.typeError(`${r.toString()} isn't a value`);
			return null;
		}

		return result;
	}

	/**
	 * @returns {boolean}
	 */
	isLiteral() {
		return false;
	}

	/**
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIR(ctx) {
		throw new Error("not yet implemented");
	}

	/**
	 * @returns {string}
	 */
	toString() {
		throw new Error("not yet implemented");
	}
}

/**
 * Simple reference class (i.e. using a Word)
 * @internal
 */
export class RefExpr extends Expr {
	#name;

	/**
	 * @param {Word} name
	 */
	constructor(name) {
		super(name.site);
		this.#name = name;
	}

	/**
	 * @param {Scope} scope 
	 * @returns {null | EvalEntity}
	 */
	evalInternal(scope) {
		return scope.get(this.#name);
	}

	/**
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIR(ctx) {
		const path = this.cache?.asNamed ? this.cache.asNamed.path : this.#name.value;

		let ir = new IR(path, this.site);
		
		return ir;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return this.#name.toString();
	}
}

/**
 * Name::Member expression
 * @internal
 */
export class PathExpr extends Expr {
	#baseExpr;
	#memberName;

	/**
	 * @param {Site} site 
	 * @param {Expr} baseExpr 
	 * @param {Word} memberName
	 */
	constructor(site, baseExpr, memberName) {
		super(site);
		this.#baseExpr = baseExpr;
		this.#memberName = memberName;
	}

	/**
	 * @type {Expr}
	 */
	get baseExpr() {
		return this.#baseExpr;
	}
	
	/**
	 * @param {Scope} scope 
	 * @returns {null | EvalEntity}
	 */
	evalInternal(scope) {
		const base = this.#baseExpr.eval(scope);

		if (!base) {
			return null;
		}

		/**
		 * @type {null | EvalEntity}
		 */
		let member = null;

		if (base.asNamespace) {
			member = base.asNamespace.namespaceMembers[this.#memberName.value];
		} else if (base.asType) {
			const typeMembers = base.asType.typeMembers;

			member = typeMembers[this.#memberName.value];
		}

		if (!member) {
			this.#memberName.referenceError(`${base.toString()}::${this.#memberName.value} not found`);
			return null;
		}

		if (member.asType?.toTyped().asFunc) {
			return member.asType.toTyped();
		} else {
			return member;
		}
	}

	/**
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIR(ctx) {
		const v = this.cache;

		if (v?.asNamed) {
			return new IR(`${v.asNamed.path}`, this.site);
		} else if (this.#baseExpr.cache?.asNamed) {
			return new IR(`${this.#baseExpr.cache.asNamed.path}__${this.#memberName.value}`, this.site);
		} else {
			throw new Error(`expected named value, ${v?.toString()}`);
		}
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `${this.#baseExpr.toString()}::${this.#memberName.toString()}`;
	}
}

/**
 * Name::Member expression which can instantiate zero field structs and enum members
 * @internal
 */
export class ValuePathExpr extends PathExpr {

	/**
	 * @param {Site} site 
	 * @param {Expr} baseExpr 
	 * @param {Word} memberName
	 */
	constructor(site, baseExpr, memberName) {
		super(site, baseExpr, memberName);
	}
	
	/**
	 * @param {Scope} scope 
	 * @returns {null | EvalEntity}
	 */
	evalInternal(scope) {
		const member = super.evalInternal(scope);

		if (!member) {
			return null;
		}

		if (member.asEnumMemberType && member.asEnumMemberType.fieldNames.length == 0) {
			return new DataEntity(member.asEnumMemberType);
		} else {
			return member;
		}
	}

	/**
	 * @returns {boolean}
	 */
	isLiteral() {
		return (this.cache?.asTyped?.type.asEnumMemberType?.fieldNames?.length ?? -1) == 0;
	}

	/**
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIR(ctx) {
		const v = this.cache;

		if (v?.asTyped?.type?.asEnumMemberType && v.asTyped.type.asEnumMemberType.fieldNames.length == 0) {
			return new IR([
				new IR(`${v.asTyped.type.asEnumMemberType.path}____new`, this.site),
				new IR("()")
			]);
		} else {
			return super.toIR(ctx);
		}
	}
}

/**
 * []ItemType
 * @internal
 */
export class ListTypeExpr extends Expr {
	#itemTypeExpr;

	/**
	 * @param {Site} site 
	 * @param {Expr} itemTypeExpr 
	 */
	constructor(site, itemTypeExpr) {
		super(site);
		this.#itemTypeExpr = itemTypeExpr;
	}
	
	/**
	 * @param {Scope} scope 
	 * @returns {null | Type}
	 */
	evalInternal(scope) {
		const itemType_ = this.#itemTypeExpr.eval(scope);

		if (!itemType_) {
			return null;
		}

		const itemType = itemType_.asType;

		if (!itemType) {
			this.#itemTypeExpr.typeError(`'${itemType_.toString()}' isn't a type`);
			return null;
		}

		return ListType$(itemType);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `[]${this.#itemTypeExpr.toString()}`;
	}
}

/**
 * Map[KeyType]ValueType expression
 * @internal
 */
export class MapTypeExpr extends Expr {
	#keyTypeExpr;
	#valueTypeExpr;

	/**
	 * @param {Site} site 
	 * @param {Expr} keyTypeExpr 
	 * @param {Expr} valueTypeExpr 
	 */
	constructor(site, keyTypeExpr, valueTypeExpr) {
		super(site);
		this.#keyTypeExpr = keyTypeExpr;
		this.#valueTypeExpr = valueTypeExpr;
	}

	/**
	 * @param {Scope} scope 
	 * @returns {null | EvalEntity}
	 */
	evalInternal(scope) {
		const keyType_ = this.#keyTypeExpr.eval(scope);
		if (!keyType_) {
			return null;
		}

		const keyType = keyType_.asType;
		if (!keyType) {
			this.#keyTypeExpr.typeError("map key type not a type");
			return null;
		}

		const valueType_ = this.#valueTypeExpr.eval(scope);
		if (!valueType_) {
			return null;
		}

		const valueType = valueType_.asType;
		if (!valueType) {
			this.#valueTypeExpr.typeError("map value type not a type");
			return null;
		}

		return MapType$(keyType, valueType);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `Map[${this.#keyTypeExpr.toString()}]${this.#valueTypeExpr.toString()}`;
	}
}

/**
 * Iterator[Type1, ...] expr
 * @internal
 */
export class IteratorTypeExpr extends Expr {
	#itemTypeExprs;

	/**
	 * @param {Site} site
	 * @param {Expr[]} itemTypeExprs
	 */
	constructor(site, itemTypeExprs) {
		super(site);

		this.#itemTypeExprs = itemTypeExprs;
	}

	/**
	 * @param {Scope} scope
	 * @returns {null | EvalEntity}
	 */
	evalInternal(scope) {
		const itemTypes_ = this.#itemTypeExprs.map(ite => {
			const ite_ = ite.eval(scope);

			if (!ite_) {
				return null;
			}

			const itemType = ite_.asType;

			if (!itemType) {
				ite.typeError("not a type");
				return null;
			}

			return itemType;
		});

		const itemTypes = reduceNull(itemTypes_);
		if (itemTypes === null) {
			return null;
		}

		if (itemTypes.length > 10) {
			this.site.typeError("too many Iterator type args (limited to 10)");
			return null;
		}

		return IteratorType$(itemTypes);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `Iterator[${this.#itemTypeExprs.map(ite => ite.toString()).join(", ")}]`;
	}
}

/**
 * Option[SomeType] expression
 * @internal
 */
export class OptionTypeExpr extends Expr {
	#someTypeExpr;

	/**
	 * @param {Site} site 
	 * @param {Expr} someTypeExpr 
	 */
	constructor(site, someTypeExpr) {
		super(site);
		this.#someTypeExpr = someTypeExpr;
	}

	/**
	 * @param {Scope} scope 
	 * @returns {null | Type}
	 */
	evalInternal(scope) {
		const someType = this.#someTypeExpr.evalAsType(scope);

		if (!someType) {
			return null;
		}

		return OptionType$(someType);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `Option[${this.#someTypeExpr.toString()}]`;
	}
}

/**
 * '()' which can only be used as return type of func
 * @internal
 */
export class VoidTypeExpr extends Expr {
	/**
	 * @param {Site} site 
	 */
	constructor(site) {
		super(site);
	}

	/**
	 * @param {Scope} scope 
	 * @returns {EvalEntity}
	 */
	evalInternal(scope) {
		return new VoidType();
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return "()";
	}
}

/**
 * @internal
 */
export class TupleTypeExpr extends Expr {
	#itemTypeExprs;

	/**
	 * @param {Site} site
	 * @param {Expr[]} itemTypeExprs
	 */
	constructor(site, itemTypeExprs) {
		super(site);
		this.#itemTypeExprs = itemTypeExprs;
	}

	/**
	 * @param {Scope} scope
	 * @returns {null | EvalEntity}
	 */
	evalInternal(scope) {
		const itemTypes_ = this.#itemTypeExprs.map(ite => {
			const ite_ = ite.eval(scope);

			if (!ite_) {
				return null;
			}

			const itemType = ite_.asType;

			if (!itemType) {
				ite.typeError("not a type");
				return null;
			}

			return itemType;
		});

		const itemTypes = reduceNull(itemTypes_);
		if (itemTypes === null) {
			return null;
		}

		if (itemTypes.length > 10) {
			this.site.typeError("too many Type type args (limited to 10)");
			return null;
		}

		return TupleType$(itemTypes);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `(${this.#itemTypeExprs.map(ite => ite.toString()).join(", ")})`;
	}
}

/**
 * @internal
 */
export class FuncArgTypeExpr extends Token {
	#name;
	#typeExpr;
	optional;

	/**
	 * @param {Site} site 
	 * @param {null | Word} name 
	 * @param {Expr} typeExpr 
	 * @param {boolean} optional 
	 */
	constructor(site, name, typeExpr, optional) {
		super(site);
		this.#name = name;
		this.#typeExpr = typeExpr;
		this.optional = optional;
	}

	/**
	 * @returns {boolean}
	 */
	isNamed() {
		return this.#name == null;
	}

	/**
	 * @returns {boolean}
	 */
	isOptional() {
		return this.optional;
	}

	/**
	 * @param {Scope} scope 
	 * @returns {null | ArgType}
	 */
	eval(scope) {
		const type_ = this.#typeExpr.eval(scope);

		if (!type_) {
			return null;
		}

		const type = type_.asType;
		if (!type) {
			this.#typeExpr.typeError(`'${type_.toString()}' isn't a type`);
			return null;
		}

		return new ArgType(this.#name, type, this.optional);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return [
			this.#name != null ? `${this.#name.toString()}: ` : "",
			this.optional ? "?" : "",
			this.#typeExpr.toString()
		].join("");
	}
}

/**
 * (ArgType1, ...) -> RetType expression
 * @internal
 */
export class FuncTypeExpr extends Expr {
	#argTypeExprs;
	#retTypeExpr;

	/**
	 * @param {Site} site
	 * @param {FuncArgTypeExpr[]} argTypeExprs 
	 * @param {Expr} retTypeExpr 
	 */
	constructor(site, argTypeExprs, retTypeExpr) {
		super(site);
		this.#argTypeExprs = argTypeExprs;
		this.#retTypeExpr = retTypeExpr;
	}

	/**
	 * @param {Scope} scope 
	 * @returns {null | Type}
	 */
	evalInternal(scope) {
		const argTypes_ = this.#argTypeExprs.map(a => a.eval(scope));

		const retType_ = this.#retTypeExpr.eval(scope);

		if (!retType_) {
			return null;
		}

		const retType = retType_.asType;
		if (!retType) {
			this.#retTypeExpr.typeError("return type isn't a type");
			return null;
		}

		const argTypes = reduceNull(argTypes_);

		if (argTypes === null) {
			return null;
		}

		return new FuncType(argTypes, retType);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `(${this.#argTypeExprs.map(a => a.toString()).join(", ")}) -> ${this.#retTypeExpr.toString()}`;
	}
}

/**
 * expr(...); ...
 * @internal
 */
export class ChainExpr extends Expr {
	/**
	 * @readonly
	 * @type {Expr}
	 */
	upstreamExpr;

	/**
	 * @readonly
	 * @type {Expr}
	 */
	downstreamExpr;

	/**
	 * @param {Site} site 
	 * @param {Expr} upstreamExpr 
	 * @param {Expr} downstreamExpr 
	 */
	constructor(site, upstreamExpr, downstreamExpr) {
		super(site);
		this.upstreamExpr = upstreamExpr;
		this.downstreamExpr = downstreamExpr;
	}

	toString() {
		return `${this.upstreamExpr.toString()}; ${this.downstreamExpr.toString()}`;
	}

	/**
	 * @param {Scope} scope
	 * @returns {null | EvalEntity}
	 */
	evalInternal(scope) {
		const upstreamVal_ = this.upstreamExpr.eval(scope);

		if (upstreamVal_) {
			const upstreamVal = upstreamVal_.asTyped;

			if (!upstreamVal) {
				this.upstreamExpr.typeError("upstream isn't typed");
			} else {
				if ((new ErrorType()).isBaseOf(upstreamVal.type)) {
					this.downstreamExpr.typeError("unreachable code (upstream always throws error)");
				} else if (!((new VoidType()).isBaseOf(upstreamVal.type))) {
					this.upstreamExpr.typeError("unexpected return value (hint: use '='");
				}
			}
		}

		return this.downstreamExpr.eval(scope);
	}

	/**
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIR(ctx) {
		return new IR([
			new IR("__core__chooseUnit(", this.site),
			this.upstreamExpr.toIR(ctx),
			new IR(", "),
			this.downstreamExpr.toIR(ctx),
			new IR(")")
		]);
	}
}

/**
 * '... = ... ; ...' expression
 * @internal
 */
export class AssignExpr extends ChainExpr {
	/**
	 * @type {DestructExpr}
	 */
	#nameType;

	/**
	 * @param {Site} site 
	 * @param {DestructExpr} nameType 
	 * @param {Expr} upstreamExpr 
	 * @param {Expr} downstreamExpr 
	 */
	constructor(site, nameType, upstreamExpr, downstreamExpr) {
		super(site, assertDefined(upstreamExpr), assertDefined(downstreamExpr));
		this.#nameType = assertDefined(nameType);
	}

	/**
	 * @param {Scope} scope 
	 * @returns {null | EvalEntity}
	 */
	evalInternal(scope) {
		const subScope = new Scope(scope, scope.allowShadowing);

		let upstreamVal = this.upstreamExpr.eval(scope);

		if (upstreamVal && upstreamVal.asTyped) {
			if (this.#nameType.hasType() || this.#nameType.isTuple()) {
				this.#nameType.evalInAssignExpr(subScope, assertDefined(upstreamVal.asTyped.type.asType), 0);
			} else if (this.upstreamExpr.isLiteral() || scope.has(this.#nameType.name)) {
				// enum variant type resulting from a constructor-like associated function must be cast back into its enum type
				if ((this.upstreamExpr instanceof CallExpr &&
					this.upstreamExpr.fnExpr instanceof PathExpr) || 
					(this.upstreamExpr instanceof PathExpr && 
					!this.upstreamExpr.isLiteral())) 
				{
					const upstreamType = upstreamVal.asTyped.type;

					if (upstreamType.asEnumMemberType) {
						upstreamVal = new DataEntity(upstreamType.asEnumMemberType.parentType);
					}
				}

				subScope.set(this.#nameType.name, upstreamVal);
			} else {
				this.typeError("unable to infer type of assignment rhs");
			}
		} else if (this.#nameType.hasType()) {
			this.#nameType.evalInAssignExpr(subScope, null, 0);
		} else {
			this.upstreamExpr.typeError("rhs isn't an instance");
			subScope.set(this.#nameType.name, new DataEntity(new AnyType()));
		}
		
		const downstreamVal = this.downstreamExpr.eval(subScope);

		subScope.assertAllUsed();

		return downstreamVal;
	}

	/**
	 * 
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIR(ctx) {
		let inner = this.downstreamExpr.toIR(ctx.tab());
		
		if (this.#nameType.isTuple() && this.#nameType.isIgnored()) {
			// TODO: get rid of this on the next major version release, while making sure the default approach is equally efficient (i.e. the callback call is properly inlined)
			// keep using the old way of creating the IR in order to assure backwards compatibility
			for (let i = this.#nameType.children.length - 1; i >= 0; i--) {
				// internally generates enum-member error IR
				inner = this.#nameType.children[i].wrapDestructIR(ctx, inner, i);
			}

			const ir = new IR([
				this.upstreamExpr.toIR(ctx),
				new IR(`(\n${ctx.indent + TAB}(`), new IR(this.#nameType.children.map((nt, i) => nt.toNameIR(i))).join(", "), new IR(") ->", this.site), new IR(` {\n${ctx.indent}${TAB}${TAB}`),
				inner,
				new IR(`\n${ctx.indent + TAB}}\n${ctx.indent})`)
			]);

			return ir;
		} else {
			inner = this.#nameType.wrapDestructIR(ctx, inner, 0);

			let upstream = this.upstreamExpr.toIR(ctx);

			// enum member run-time error IR
			if (this.#nameType.hasType()) {
				const t = assertDefined(this.#nameType.type);

				if (t.asEnumMemberType) {
					upstream = new IR([
						new IR("__helios__common__assert_constr_index("),
						upstream,
						new IR(`, ${t.asEnumMemberType.constrIndex})`)
					]);
				}
			}

			return new IR([
				new IR("("),
				this.#nameType.toNameIR(0), // wrapDestructIR depends on this name
				new IR(") "),
				new IR("->", this.site), new IR(` {\n${ctx.indent}${TAB}`),
				inner,
				new IR(`\n${ctx.indent}}(`),
				upstream,
				new IR(")")
			]);
		}
	}

	/**
	 * @returns {string}
	 */
	toString() {
		let downstreamStr = this.downstreamExpr.toString();
		assert(downstreamStr != undefined);

		return `${this.#nameType.toString()} = ${this.upstreamExpr.toString()}; ${downstreamStr}`;
	}
}

/**
 * Helios equivalent of unit
 * @internal
 */
export class VoidExpr extends Expr {
	/**
	 * @param {Site} site
	 */
	constructor(site) {
		super(site);
	}

	/**
	 * @param {Scope} scope 
	 * @returns {Instance}
	 */
	evalInternal(scope) {
		return new VoidEntity();
	}

	/**
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIR(ctx) {
		return new IR("()", this.site);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return "()";
	}
}

/**
 * Literal expression class (wraps literal tokens)
 * @internal
 */
export class PrimitiveLiteralExpr extends Expr {
	#primitive;

	/**
	 * @param {PrimitiveLiteral} primitive 
	 */
	constructor(primitive) {
		super(primitive.site);
		this.#primitive = primitive;
	}

	/**
	 * @type {DataType}
	 */
	get type() {
		if (this.#primitive instanceof IntLiteral) {
			return IntType;
		} else if (this.#primitive instanceof RealLiteral) {
			return RealType;
		} else if (this.#primitive instanceof BoolLiteral) {
			return BoolType;
		} else if (this.#primitive instanceof StringLiteral) {
			return StringType;
		} else if (this.#primitive instanceof ByteArrayLiteral) {
			return ByteArrayType;
		} else {
			throw new Error("unhandled primitive type");
		}	
	}

	/**
	 * @param {Scope} scope 
	 * @returns {EvalEntity}
	 */
	evalInternal(scope) {
		return new DataEntity(this.type);
	}

	/**
	 * @returns {boolean}
	 */
	isLiteral() {
		return true;
	}

	/**
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIR(ctx) {
		// all literals can be reused in their string-form in the IR
		return new IR(this.#primitive.toString(), this.#primitive.site);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return this.#primitive.toString();
	}
}

/**
 * Literal UplcData which is the result of parameter substitutions.
 * @internal
 */
export class LiteralDataExpr extends Expr {
	#type;
	#data;

	/**
	 * @param {Site} site 
	 * @param {DataType} type
	 * @param {UplcData} data
	 */
	constructor(site, type, data) {
		super(site);
		this.#type = type;
		this.#data = data;
	}

	/**
	 * @internal
	 * @type {DataType}
	 */
	get type() {
		return this.#type;
	}

	/**
	 * @returns {boolean}
	 */
	isLiteral() {
		return true;
	}

	/**
	 * @param {Scope} scope 
	 * @returns {EvalEntity}
	 */
	evalInternal(scope) {
		return new DataEntity(this.#type);
	}

	/**
	 * @type {EvalEntity}
	 */
	get cache() {
		return new DataEntity(this.#type);
	}

	/**
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIR(ctx) {
		return new IR(this.toString(), this.site);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `##${bytesToHex(this.#data.toCbor())}`;
	}
}

/**
 * Struct field (part of a literal struct constructor)
 * @internal
 */
export class StructLiteralField {
	#name;
	#value;

	/**
	 * @param {null | Word} name 
	 * @param {Expr} value 
	 */
	constructor(name, value) {
		this.#name = name;
		this.#value = value;
	}

	/**
	 * @type {Word}
	 */
	get name() {
		if (this.#name === null) {
			throw new Error("name of field not given");
		} else {
			return this.#name;
		}
	}

	get site() {
		if (this.#name === null) {
			return this.#value.site;
		} else {
			return this.#name.site;
		}
	}
	
	/**
	 * @param {Scope} scope 
	 * @returns {null | EvalEntity}
	 */
	eval(scope) {
		return this.#value.eval(scope);
	}

	/**
	 * @returns {boolean}
	 */
	isNamed() {
		return this.#name !== null;
	}

	/**
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIR(ctx) {
		return this.#value.toIR(ctx);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		if (this.#name === null) {
			return this.#value.toString();
		} else {
			return `${this.#name.toString()}: ${this.#value.toString()}`;
		}
	}
}

/**
 * Struct literal constructor
 * @internal
 */
export class StructLiteralExpr extends Expr {
	#typeExpr;
	#fields;

	/**
	 * @param {Expr} typeExpr 
	 * @param {StructLiteralField[]} fields 
	 */
	constructor(typeExpr, fields) {
		super(typeExpr.site);
		this.#typeExpr = typeExpr;
		this.#fields = fields;
	}

	/**
	 * @param {Scope} scope 
	 * @returns {null | EvalEntity}
	 */
	evalInternal(scope) {
		const type_ = this.#typeExpr.eval(scope);

		if (!type_) {
			return null;
		}

		const type = type_.asDataType;

		if (!type) {
			this.#typeExpr.typeError(`'${this.#typeExpr.toString()}' doesn't evaluate to a data type`);
			return null;
		}

		if (type.fieldNames.length != this.#fields.length) {
			this.typeError(`wrong number of fields for ${type.toString()}, expected ${type.fieldNames.length}, got ${this.#fields.length}`);
			return null;
		}

		/**
		 * @param {Word} name
		 * @returns {null | Type}
		 */
		const getMemberType = (name) => {
			const memberVal = type.instanceMembers[name.value];

			if (!memberVal) {
				name.typeError(`member '${name.value}' not defined`);
				return null;
			}

			const memberType = memberVal.asType;

			if (!memberType) {
				name.typeError(`member '${name.value}' isn't a type`);
				return null;
			}

			return memberType;
		};

		for (let i = 0; i < this.#fields.length; i++) {
			const f = this.#fields[i];
		
			const fieldVal_ = f.eval(scope);
			if (!fieldVal_) {
				return null
			}

			const fieldVal = fieldVal_.asTyped;
			if (!fieldVal) {
				f.site.typeError("not typed");
				return null;
			}

			if (f.isNamed()) {
				if (type.fieldNames.findIndex(n => n == f.name.value) == -1) {
					f.name.site.typeError("not a valid field");
					return null;
				}

				// check the named type
				const memberType = getMemberType(f.name);
				if (!memberType) {
					continue;
				}

				if (!memberType.isBaseOf(fieldVal.type)) {
					f.site.typeError(`wrong field type for '${f.name.toString()}', expected ${memberType.toString()}, got ${fieldVal.type.toString()}`);
					return null;
				}
			} else {
				// check the positional type
				const memberType = getMemberType(new Word(f.site, type.fieldNames[i]));

				if (!memberType) {
					continue;
				}
				
				if (!memberType.isBaseOf(fieldVal.type)) {
					f.site.typeError(`wrong field type for field ${i.toString()}, expected ${memberType.toString()}, got ${fieldVal.type.toString()}`);
					return null;
				}
			}
		}

		return new DataEntity(type);
	}

	/**
	 * @returns {boolean}
	 */
	isLiteral() {
		return true;
	}

	/**
	 * @returns {boolean}
	 */
	isNamed() {
		// the expression builder already checked that all fields are named or all or positional (i.e. not mixed)
		return this.#fields.length > 0 && this.#fields[0].isNamed();
	}

	/**
	 * @param {ToIRContext} ctx
	 * @param {Site} site
	 * @param {string} path
	 * @param {IR[]} fields
	 */
	static toIRInternal(ctx, site, path, fields) {
		return new IR([
			new IR(`${path}____new`),
			new IR("("),
			new IR(fields).join(", "),
			new IR(")")
		], site);
	}

	/**
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIR(ctx) {
		const type = assertDefined(this.#typeExpr.cache?.asDataType);

		const fields = this.#fields.slice();

		// sort fields by correct name
		if (this.isNamed()) {
			fields.sort((a, b) => type.fieldNames.findIndex(n => n == a.name.value) - type.fieldNames.findIndex(n => n == b.name.value));
		}

		const irFields = fields.map(f => f.toIR(ctx));

		return StructLiteralExpr.toIRInternal(ctx, this.site, type.path, irFields);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `${this.#typeExpr.toString()}{${this.#fields.map(f => f.toString()).join(", ")}}`;
	}
}

/**
 * []{...} expression
 * @internal
 */
export class ListLiteralExpr extends Expr {
	#itemTypeExpr;
	#itemExprs;

	/**
	 * @param {Site} site 
	 * @param {Expr} itemTypeExpr 
	 * @param {Expr[]} itemExprs 
	 */
	constructor(site, itemTypeExpr, itemExprs) {
		super(site);
		this.#itemTypeExpr = itemTypeExpr;
		this.#itemExprs = itemExprs;
	}

	/**
	 * @type {DataType}
	 */
	get itemType() {
		return assertDefined(this.#itemTypeExpr.cache?.asDataType);
	}

	/**
	 * @param {Scope} scope
	 * @returns {null | EvalEntity}
	 */
	evalInternal(scope) {
		const itemType_ = this.#itemTypeExpr.eval(scope);
		if (!itemType_) {
			return null;
		}

		const itemType = itemType_.asDataType;

		if (!itemType) {
			this.#itemTypeExpr.typeError("content of list can't be func");
			return null;
		}

		for (let itemExpr of this.#itemExprs) {
			const itemVal_ = itemExpr.eval(scope);
			if (!itemVal_) {
				continue;
			}

			const itemVal = itemVal_.asTyped;

			if (!itemVal) {
				itemExpr.typeError("not typed");
				continue;
			}

			if (!itemType.isBaseOf(itemVal.type)) {
				itemExpr.typeError(`expected ${itemType.toString()}, got ${itemVal.type.toString()}`);
				continue;
			}
		}

		return new DataEntity(ListType$(itemType));
	}

	/**
	 * @returns {boolean}
	 */
	isLiteral() {
		return true;
	}

	/**
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIR(ctx) {
		let ir = new IR("__core__mkNilData(())");

		// starting from last element, keeping prepending a data version of that item

		for (let i = this.#itemExprs.length - 1; i >= 0; i--) {

			let itemIR = new IR([
				new IR(`${this.itemType.path}____to_data`),
				new IR("("),
				this.#itemExprs[i].toIR(ctx),
				new IR(")"),
			]);

			ir = new IR([
				new IR("__core__mkCons"),
				new IR("("),
				itemIR,
				new IR(", "),
				ir,
				new IR(")")
			]);
		}

		return ir;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `[]${this.#itemTypeExpr.toString()}{${this.#itemExprs.map(itemExpr => itemExpr.toString()).join(', ')}}`;
	}
}

/**
 * Map[...]...{... : ...} expression
 * @internal
 */
export class MapLiteralExpr extends Expr {
	#keyTypeExpr;
	#valueTypeExpr;
	#pairExprs;

	/**
	 * @param {Site} site 
	 * @param {Expr} keyTypeExpr 
	 * @param {Expr} valueTypeExpr
	 * @param {[Expr, Expr][]} pairExprs 
	 */
	constructor(site, keyTypeExpr, valueTypeExpr, pairExprs) {
		super(site);
		this.#keyTypeExpr = keyTypeExpr;
		this.#valueTypeExpr = valueTypeExpr;
		this.#pairExprs = pairExprs;
	}

	/**
	 * @type {DataType}
	 */
	get keyType() {
		return assertDefined(this.#keyTypeExpr.cache?.asDataType);
	}

	/**
	 * @type {DataType}
	 */
	get valueType() {
		return assertDefined(this.#valueTypeExpr.cache?.asDataType);
	}

	/**
	 * @param {Scope} scope
	 * @returns {null | EvalEntity}
	 */
	evalInternal(scope) {
		const keyType_ = this.#keyTypeExpr.eval(scope);
		if (!keyType_) {
			return null;
		}

		const keyType = keyType_.asDataType;
		if (!keyType) {
			this.#keyTypeExpr.typeError("key-type of Map can't be func");
			return null;
		}

		const valueType_ = this.#valueTypeExpr.eval(scope);
		if (!valueType_) {
			return null;
		}

		const valueType = valueType_.asDataType;
		if (!valueType) {
			this.#valueTypeExpr.typeError("value-type of Map can't be func");
			return null;
		}

		for (let [keyExpr, valueExpr] of this.#pairExprs) {
			const keyVal_ = keyExpr.eval(scope);
			if (!keyVal_) {
				continue;
			}

			const keyVal = keyVal_.asTyped;
			if (!keyVal) {
				keyExpr.typeError("not typed");
				continue;
			}

			const valueVal_ = valueExpr.eval(scope); 
			if (!valueVal_) {
				continue;
			}

			const valueVal = valueVal_.asTyped;
			if (!valueVal) {
				valueExpr.typeError("not typed");
				continue;
			}

			if (!keyType.isBaseOf(keyVal.type)) {
				keyExpr.typeError(`expected ${keyType.toString()} for map key, got ${keyVal.toString()}`);
				continue;
			}
			
			if (!valueType.isBaseOf(valueVal.type)) {
				valueExpr.typeError(`expected ${valueType.toString()} for map value, got ${valueVal.toString()}`);
				continue;
			}
		}

		return new DataEntity(MapType$(keyType, valueType));
	}

	/**
	 * @returns {boolean}
	 */
	isLiteral() {
		return true;
	}

	/**
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIR(ctx) {
		let ir = new IR("__core__mkNilPairData(())");

		// starting from last element, keeping prepending a data version of that item

		for (let i = this.#pairExprs.length - 1; i >= 0; i--) {
			let [keyExpr, valueExpr] = this.#pairExprs[i];

			let keyIR = new IR([
				new IR(`${this.keyType.path}____to_data`),
				new IR("("),
				keyExpr.toIR(ctx),
				new IR(")"),
			]);

			let valueIR = new IR([
				new IR(`${this.valueType.path}____to_data`),
				new IR("("),
				valueExpr.toIR(ctx),
				new IR(")"),
			]);

			ir = new IR([
				new IR("__core__mkCons("),
				new IR("__core__mkPairData("),
				keyIR,
				new IR(","),
				valueIR,
				new IR(")"),
				new IR(", "),
				ir,
				new IR(")")
			], this.site);
		}

		return ir;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `Map[${this.#keyTypeExpr.toString()}]${this.#valueTypeExpr.toString()}{${this.#pairExprs.map(([keyExpr, valueExpr]) => `${keyExpr.toString()}: ${valueExpr.toString()}`).join(', ')}}`;
	}
}

/**
 * NameTypePair is base class of FuncArg and DataField (differs from StructLiteralField) 
 * @internal
 */
export class NameTypePair {
	#name;
	#typeExpr;

	/**
	 * @param {Word} name 
	 * @param {null | Expr} typeExpr 
	 */
	constructor(name, typeExpr) {
		this.#name = name;
		this.#typeExpr = typeExpr;
	}

	/**
	 * @type {Site}
	 */
	get site() {
		return this.#name.site;
	}

	/**
	 * @type {Word}
	 */
	get name() {
		return this.#name;
	}

	/**
	 * Throws an error if called before evalType()
	 * @type {Type}
	 */
	get type() {
		if (this.isIgnored()) {
			return new AllType();
		} else if (this.#typeExpr === null) {
			throw new Error("typeExpr not set in " + this.site.src.raw.split("\n")[0]);
		} else {
			// asDataType might be null if the evaluation of its TypeExpr threw a syntax error
			return this.#typeExpr.cache?.asType ?? new AllType();
		}
	}

	/**
	 * @type {null | Expr}
	 */
	get typeExpr() {
		return this.#typeExpr
	}

	/**
	 * @type {string}
	 */
	get typeName() {
		if (this.#typeExpr === null) {
			return "";
		} else {
			return this.#typeExpr.toString();
		}
	}

	/**
	 * @returns {boolean}
	 */
	isIgnored() {
		return this.name.value === "_";
	}

	/**
	 * @returns {boolean}
	 */
	hasType() {
		return this.#typeExpr !== null;
	}

	/**
	 * Evaluates the type, used by FuncLiteralExpr and DataDefinition
	 * @param {Scope} scope 
	 * @returns {null | Type}
	 */
	evalType(scope) {
		if (this.isIgnored()) {
			return new AllType();
		} else if (this.#typeExpr === null) {
			throw new Error("typeExpr not set in " + this.site.src.raw.split("\n")[0]);
		} else {
			const t = this.#typeExpr.eval(scope);

			if (!t) {
				return null;
			}

			if (!t.asType) {
				this.#typeExpr.typeError(`'${t.toString()} isn't a valid type`);
				return null;
			} else {
				return t.asType;
			}
		}
	}

	/**
	 * @returns {IR}
	 */
	toIR() {
		return new IR(this.#name.toString(), this.#name.site);
	}

	/**
	 * 
	 * @returns {string}
	 */
	toString() {
		if (this.#typeExpr === null) {
			return this.name.toString();
		} else {
			return `${this.name.toString()}: ${this.#typeExpr.toString()}`;
		}
	}
}

/**
 * Function argument class
 * @internal
 */
export class FuncArg extends NameTypePair {
	#defaultValueExpr;

	/**
	 * @param {Word} name 
	 * @param {null | Expr} typeExpr
	 * @param {null | Expr} defaultValueExpr
	 */
	constructor(name, typeExpr, defaultValueExpr = null) {
		super(name, typeExpr);

		this.#defaultValueExpr = defaultValueExpr;
	}

	/**
	 * @param {Scope} scope 
	 */
	evalDefault(scope) {
		if (this.#defaultValueExpr != null) {
			const v_ = this.#defaultValueExpr.eval(scope);
			if (!v_) {
				return;
			}

			const v = v_.asTyped;
			if (!v) {
				this.#defaultValueExpr.typeError("not typed");
				return;
			}

			const t = this.evalType(scope);
			if (!t) {
				return;
			}

			if (!t.isBaseOf(v.type)) {
				this.#defaultValueExpr.site.typeError(`expected ${t.toString()}, got ${v.type.toString()}`);
				return;
			}
		}
	}

	/**
	 * @param {Scope} scope 
	 * @returns {null | ArgType}
	 */
	evalArgType(scope) {
		const t = super.evalType(scope);

		if (!t) {
			return null;
		}

		return new ArgType(this.name, t, this.#defaultValueExpr != null);
	}

	/**
	 * @returns {IR}
	 */
	toIR() {
		const name = super.toIR();

		if (this.#defaultValueExpr == null) {
			return name;
		} else {
			return new IR([
				new IR(`__useopt__${this.name.toString()}`),
				new IR(", "),
				name
			]);
		}
	}

	/**
	 * @param {IR} bodyIR 
	 * @param {string} name 
	 * @param {IR} defaultIR 
	 * @returns {IR}
	 */
	static wrapWithDefaultInternal(bodyIR, name, defaultIR) {
		return new IR([
			new IR(`(${name}) -> {`),
			bodyIR,
			new IR([
				new IR(`}(__core__ifThenElse(__useopt__${name}, () -> {${name}}, () -> {`),
				defaultIR, 
				new IR("})())")
			])
		]);
	}

	/**
	 * (argName) -> {
	 *   <bodyIR>
	 * }(
	 *   ifThenElse(
	 * 		__useoptarg__argName,
	 *  	() -> {
	 *        argName
	 *      },
	 *      () -> {
	 *        <defaultValueExpr>
	 *      }
	 *   )()
	 * )
	 * TODO: indentation
	 * @param {ToIRContext} ctx
	 * @param {IR} bodyIR 
	 * @returns {IR}
	 */
	wrapWithDefault(ctx, bodyIR) {
		if (this.#defaultValueExpr == null) {
			return bodyIR;
		} else {
			const name = this.name.toString();

			return FuncArg.wrapWithDefaultInternal(bodyIR, name, this.#defaultValueExpr.toIR(ctx));
		}
	}
}

/**
 * (..) -> RetTypeExpr {...} expression
 * @internal
 */
export class FuncLiteralExpr extends Expr {
	#args;
	#retTypeExpr;
	#bodyExpr;

	/**
	 * @param {Site} site
	 * @param {FuncArg[]} args 
	 * @param {null | Expr} retTypeExpr 
	 * @param {Expr} bodyExpr 
	 */
	constructor(site, args, retTypeExpr, bodyExpr) {
		super(site);
		this.#args = args;
		this.#retTypeExpr = retTypeExpr;
		this.#bodyExpr = bodyExpr;
	}

	/**
	 * @type {number}
	 */
	get nArgs() {
		return this.#args.length;
	}

	/**
	 * @type {string[]}
	 */
	get argNames() {
		return this.#args.map(a => a.name.value);
	}

	/**
	 * @type {Type[]}
	 */
	get argTypes() {
		return this.#args.map(a => a.type);
	}

	/**
	 * @type {string[]}
	 */
	get argTypeNames() {
		return this.#args.map(a => a.typeName)
	}

	/**
	 * @type {Expr}
	 */
	get retExpr() {
		let expr = this.#bodyExpr;

		while (expr instanceof ChainExpr) {
			expr = expr.downstreamExpr;
		}

		return expr;
	}

	/**
	 * @type {Type}
	 */
	get retType() {
		if (this.#retTypeExpr === null) {
			return new AllType();
		} else {
			return assertDefined(this.#retTypeExpr.cache?.asType);
		}
	}
	
	/**
	 * @returns {boolean}
	 */
	isLiteral() {
		return true;
	}

	/**
	 * @param {Scope} scope 
	 * @returns {null | FuncType}
	 */
	evalType(scope) {
		let args = this.#args;
		if (this.isMethod()) {
			args = args.slice(1);
		}

		const argTypes = reduceNull(args.map(a => a.evalArgType(scope)));

		const retType = this.#retTypeExpr ? this.#retTypeExpr.evalAsType(scope) : new AllType();

		if (argTypes === null || retType === null) {
			return null;
		}

		return new FuncType(argTypes, retType);
	}

	/**
	 * @param {Scope} scope 
	 * @returns {null | EvalEntity}
	 */
	evalInternal(scope) {
		const fnType = this.evalType(scope);

		if (!fnType) {
			return null;
		}
		
		// argTypes is calculated separately again here so it includes self
		const argTypes = this.#args.map(a => a.evalType(scope));

		const subScope = new Scope(scope, true);

		argTypes.forEach((a, i) => {
			if (a && !this.#args[i].isIgnored()) {
				this.#args[i].evalDefault(subScope);

				subScope.set(this.#args[i].name, a.toTyped());
			}
		});

		let bodyVal = this.#bodyExpr.eval(subScope);

		if (!bodyVal) {
			return null;
		}

		if (this.#retTypeExpr == null) {
			if (bodyVal.asTyped) {
				return new FuncEntity(new FuncType(fnType.argTypes, bodyVal.asTyped.type));
			} else {
				this.#bodyExpr.typeError("expect multi or typed");
				return null;
			}
		} else if (bodyVal.asTyped) {
			if (!fnType.retType.isBaseOf(bodyVal.asTyped.type)) {
				this.#retTypeExpr.typeError(`wrong return type, expected ${fnType.retType.toString()} but got ${bodyVal.asTyped.type.toString()}`);
				return null;
			}
		} else {
			this.#bodyExpr.typeError("expect multi or typed");
			return null;
		}

		subScope.assertAllUsed();

		return new FuncEntity(fnType);
	}

	isMethod() {
		return this.#args.length > 0 && this.#args[0].name.toString() == "self";
	}

	/**
	 * @returns {IR}
	 */
	argsToIR() {
		let args = this.#args.map(a => a.toIR());
		if (this.isMethod()) {
			args = args.slice(1);
		}

		return (new IR(args)).join(", ");
	}

	/**
	 * In reverse order, because later opt args might depend on earlier args
	 * @param {ToIRContext} ctx
	 * @param {IR} innerIR 
	 * @returns {IR}
	 */
	wrapWithDefaultArgs(ctx, innerIR) {
		const args = this.#args.slice().reverse();

		for (let arg of args) {
			innerIR = arg.wrapWithDefault(ctx, innerIR);
		}

		return innerIR;
	}

	/**
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIRInternal(ctx) {
		let argsWithCommas = this.argsToIR();

		let innerIndent = ctx.indent;
		let methodIndent = ctx.indent;
		if (this.isMethod()) {
			innerIndent += TAB;
		}

		let innerIR = this.#bodyExpr.toIR(ctx.tab());

		innerIR = this.wrapWithDefaultArgs(ctx, innerIR);

		let ir = new IR([
			new IR("("),
			argsWithCommas,
			new IR(") "), new IR("->", this.site), new IR(` {\n${innerIndent}${TAB}`),
			innerIR,
			new IR(`\n${innerIndent}}`),
		]);

		// wrap with 'self'
		if (this.isMethod()) {
			ir = new IR([
				new IR(`(self) -> {\n${methodIndent}${TAB}`),
				ir,
				new IR(`\n${methodIndent}}`),
			]);
		}

		return ir;
	}

	/**
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIR(ctx) {
		return this.toIRInternal(ctx);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		if (this.#retTypeExpr == null) {
			return `(${this.#args.map(a => a.toString()).join(", ")}) -> {${this.#bodyExpr.toString()}}`;
		} else {
			return `(${this.#args.map(a => a.toString()).join(", ")}) -> ${this.#retTypeExpr.toString()} {${this.#bodyExpr.toString()}}`;
		}
	}
}

/**
 * value[...] expression
 * @internal
 */
export class ParametricExpr extends Expr {
	#baseExpr;
	#parameters;

	/**
	 * @param {Site} site - site of brackets
	 * @param {Expr} baseExpr
	 * @param {Expr[]} parameters
	 */
	constructor(site, baseExpr, parameters) {
		super(site);
		this.#baseExpr = baseExpr;
		this.#parameters = parameters;
	}

	/**
	 * @type {Type[]}
	 */
	get paramTypes() {
		return this.#parameters.map(p => {
			const pt = p.cache?.asType;

			if (!pt) {
				throw new Error("not a type");
			}

			return pt;
		})
	}

	/**
	 * @param {Scope} scope 
	 * @returns {null | EvalEntity}
	 */
	evalInternal(scope) {
		const paramTypes = reduceNull(this.#parameters.map(p => p.evalAsType(scope)));

		const baseVal = this.#baseExpr.eval(scope);

		if (!baseVal) {
			return null;
		}

		if (!baseVal.asParametric) {
			this.site.typeError(`'${baseVal.toString()}' isn't a parametric type`);
			return null;
		} 

		if (paramTypes === null) {
			return null
		}
		
		return baseVal.asParametric.apply(paramTypes, this.site);
	}

	/**
	 * Reused by CallExpr
	 * @param {Type[]} paramTypes
	 * @returns {string}
	 */
	static toApplicationIR(paramTypes) {
		return `[${paramTypes.map(pt => {
			if (pt instanceof FuncType) {
				return "__fn";
			} else {
				return assertDefined(pt.asNamed).path;
			}
		}).join("@")}]`;
	}

	/**
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIR(ctx) {
		const params = ParametricExpr.toApplicationIR(this.paramTypes);
		
		if (this.#baseExpr instanceof MemberExpr) {
			return this.#baseExpr.toIR(ctx, params);
		} else {
			return IR.new`${this.#baseExpr.toIR(ctx).toString()}${params}${this.site}`;
		}
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `${this.#baseExpr.toString()}[${this.#parameters.map(p => p.toString()).join(", ")}]`;
	}
}

/**
 * Unary operator expression
 * Note: there are no post-unary operators, only pre
 * @internal
 */
export class UnaryExpr extends Expr {
	#op;
	#a;

	/**
	 * @param {SymbolToken} op 
	 * @param {Expr} a 
	 */
	constructor(op, a) {
		super(op.site);
		this.#op = op;
		this.#a = a;
	}

	/**
	 * Turns an op symbol into an internal name
	 * @returns {Word}
	 */
	translateOp() {
		const op = this.#op.toString();
		const site = this.#op.site;

		if (op == "+") {
			return new Word(site, "__pos");
		} else if (op == "-") {
			return new Word(site, "__neg");
		} else if (op == "!") {
			return new Word(site, "__not");
		} else {
			throw new Error("unhandled unary op");
		}
	}

	/**
	 * @param {Scope} scope 
	 * @returns {null | EvalEntity}
	 */
	evalInternal(scope) {
		const a_ = this.#a.eval(scope);
		if (!a_) {
			return null;
		}

		const a = a_.asInstance;
		if (!a) {
			this.#a.site.typeError("not an instance");
			return null;
		}

		const op = this.translateOp().value;

		const fnVal = a.type.typeMembers[op]?.asType?.toTyped()?.asFunc;

		if (fnVal) {
			// immediately applied
			return fnVal.asFunc.call(this.#op.site, [a]);
		} else {
			this.#a.site.typeError(`'${this.#op.toString()} ${a.type.toString()}' undefined`);
			return null;
		}
	}

	/**
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIR(ctx) {
		const path = assertDefined(this.cache?.asTyped?.type?.asNamed).path;

		return IR.new`${path}__${this.translateOp().value}${this.site}(${this.#a.toIR(ctx)})`;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `${this.#op.toString()}${this.#a.toString()}`;
	}
}

/**
 * @internal
 * @type {{[name: string]: string}}
 */
export const BINARY_SYMBOLS_MAP = {
	"||": "__or",
	"&&": "__and",
	"==": "__eq",
	"!=": "__neq",
	"<": "__lt",
	"<=": "__leq",
	">": "__gt",
	">=": "__geq",
	"+": "__add",
	"-": "__sub",
	"*": "__mul",
	"/": "__div",
	"%": "__mod"
}

/**
 * Binary operator expression
 * @internal
 */
export class BinaryExpr extends Expr {
	#op;
	#a;
	#b;
	#swap; // swap a and b for commutative ops
	#alt; // use alt (each operator can have one overload)

	/**
	 * @param {SymbolToken} op 
	 * @param {Expr} a 
	 * @param {Expr} b 
	 */
	constructor(op, a, b) {
		super(op.site);
		this.#op = op;
		this.#a = a;
		this.#b = b;
		this.#swap = false;
		this.#alt = 0;
	}

	/** 
	 * @type {Expr}
	 */
	get first() {
		return this.#swap ? this.#b : this.#a;
	}

	/**
	 * @type {Expr} 
	 */
	get second() {
		return this.#swap ? this.#a : this.#b;
	}

	toString() {
		return `${this.#a.toString()} ${this.#op.toString()} ${this.#b.toString()}`;
	}

	/**
	 * Turns op symbol into internal name
	 * @param {number} alt
	 * @returns {Word}
	 */
	translateOp(alt = 0) {
		const op = this.#op.toString();
		const site = this.#op.site;

		let name = BINARY_SYMBOLS_MAP[op];

		if (!name) {
			throw new Error("unhandled");
		}

		if (alt > 0) {
			name += alt.toString();
		}

		return new Word(site, name);
	}

	/**
	 * @returns {boolean}
	 */
	isCommutative() {
		switch (this.#op.toString()) {
			case "+":
			case "*":
			case "==":
			case "!=":
				return true;
			default:
				return false;
		}
	}

	/**
	 * @param {Scope} scope 
	 * @returns {null | EvalEntity}
	 */
	evalInternal(scope) {
		const a_ = this.#a.eval(scope);
		const b_ = this.#b.eval(scope);
		
		if (!a_ || !b_) {
			return null;
		}

		const a = a_.asInstance;
		if (!a) {
			this.#a.typeError(`lhs of ${this.#op.toString()} not an instance`);
			return null;
		}

		const b = b_.asInstance;
		if (!b) {
			this.#b.typeError(`rhs of ${this.#op.toString()} not an instance`);
			return null;
		}

		for (let swap of (this.isCommutative() ? [false, true] : [false])) {
			for (let alt of [0, 1, 2]) {
				let first  = swap ? b : a;
				let second = swap ? a : b;

				const fnVal_ = first.type.typeMembers[this.translateOp(alt).value];

				let fnVal = fnVal_?.asType?.toTyped()?.asFunc;
				if (!fnVal) {
					continue;
				}

				if (fnVal.funcType.argTypes[0].isBaseOf(first.type) && fnVal.funcType.argTypes[1].isBaseOf(second.type)) {
					let res = fnVal.call(this.#op.site, [first, second]);

					this.#swap = swap;
					this.#alt  = alt;

					return res;
				}
			}
		}

		this.typeError(`'${a.type.toString()} ${this.#op.toString()} ${b.type.toString()}' undefined`);
		return null;
	}

	/**
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIR(ctx) {
		let path = assertDefined(this.first.cache?.asTyped?.type.asNamed).path;

		let op = this.translateOp(this.#alt).value;

		if (op == "__and" || op == "__or") {
			return new IR([
				new IR(`${path}${op}`, this.site), new IR(`(\n${ctx.indent}${TAB}() -> {`),
				this.first.toIR(ctx.tab()),
				new IR(`},\n${ctx.indent}${TAB}() -> {`),
				this.second.toIR(ctx.tab()),
				new IR(`}\n${ctx.indent})`)
			]);
		} else {
			return new IR([
				new IR(`${path}__${op}`, this.site), new IR("(", this.site),
				this.first.toIR(ctx),
				new IR(", "),
				this.second.toIR(ctx),
				new IR(")")
			]);
		}
	}
}

/**
 * Parentheses expression
 * @internal
 */
export class ParensExpr extends Expr {
	#exprs;

	/**
	 * @param {Site} site 
	 * @param {Expr[]} exprs
	 */
	constructor(site, exprs) {
		super(site);
		this.#exprs = exprs;
	}

	/**
	 * @returns {boolean}
	 */
	isLiteral() {
		return this.#exprs.every(e => e.isLiteral());
	}

	/**
	 * @param {Scope} scope 
	 * @returns {null | EvalEntity}
	 */
	evalInternal(scope) {
		if (this.#exprs.length === 1) {
			return this.#exprs[0].eval(scope);
		} else {
			const entries = reduceNull(this.#exprs.map(e => {
				const v_ = e.eval(scope);
				if (!v_) {
					return null;
				}

				const v = v_.asTyped;
				if (!v) {
					e.site.typeError("not typed");
					return null;
				} 
				
				if ((new ErrorType()).isBaseOf(v.type)) {
					e.site.typeError("unexpected error call in multi-valued expression");
					return null;
				}

				return v.type;
			}));

			if (entries === null) {
				return null;
			}

			//return new MultiEntity(entries);

			return TupleType$(entries).toTyped();
		}
	}

	/**
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIR(ctx) {
		if (this.#exprs.length === 1) {
			return this.#exprs[0].toIR(ctx);
		} else {
			return new IR(
				[new IR(`(callback) -> {\n${ctx.indent + TAB}callback(\n${ctx.indent + TAB + TAB}`, this.site)]
				.concat(new IR(this.#exprs.map(e => e.toIR(ctx.tab().tab()))).join(`,\n${ctx.indent + TAB + TAB}`))
				.concat([new IR(`\n${ctx.indent + TAB})\n${ctx.indent}}`)])
			);
		}
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `(${this.#exprs.map(e => e.toString()).join(", ")})`;
	}
}

/**
 * @internal
 */
export class CallArgExpr extends Token {
	#name;
	#valueExpr;

	/**
	 * @param {Site} site 
	 * @param {null | Word} name 
	 * @param {Expr} valueExpr 
	 */
	constructor(site, name, valueExpr) {
		super(site);

		this.#name = name;
		this.#valueExpr = valueExpr;
	}

	/**
	 * @type {string}
	 */
	get name() {
		return this.#name?.toString() ?? "";
	}

	/**
	 * @type {Expr}
	 */
	get valueExpr() {
		return this.#valueExpr;
	}

	/**
	 * @type {EvalEntity}
	 */
	get value() {
		return assertDefined(this.#valueExpr.cache);
	}

	/**
	 * @returns {boolean}
	 */
	isNamed() {
		return this.#name != null;
	}

	/**
	 * @returns {boolean}
	 */
	isLiteral() {
		return this.#valueExpr.isLiteral();
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return [
			this.#name != null ? `${this.#name.toString()}: `: "",
			this.#valueExpr.toString()
		].join("");
	}

	/**
	 * @param {Scope} scope 
	 * @returns {null | EvalEntity}
	 */
	eval(scope) {
		return this.#valueExpr.eval(scope);
	}
}

/**
 * ...(...) expression
 * @internal
 */
export class CallExpr extends Expr {
	#fnExpr;
	#argExprs;

	/**
	 * @type {Type[]}
	 */
	#paramTypes;

	/**
	 * @type {null | Func}
	 */
	#appliedFnVal;

	/**
	 * @param {Site} site 
	 * @param {Expr} fnExpr 
	 * @param {CallArgExpr[]} argExprs 
	 */
	constructor(site, fnExpr, argExprs) {
		super(site);
		this.#fnExpr = fnExpr;
		this.#argExprs = argExprs;
		this.#paramTypes = [];
		this.#appliedFnVal = null; // only for infered parametric funcions
	}

	get fnExpr() {
		return this.#fnExpr;
	}

	toString() {
		return `${this.#fnExpr.toString()}(${this.#argExprs.map(a => a.toString()).join(", ")})`;
	}

	/**
	 * @returns {boolean}
	 */
	isLiteral() {
		if (this.#fnExpr instanceof PathExpr && this.cache?.asTyped && this.#fnExpr.baseExpr.cache?.asType?.isBaseOf(this.cache.asTyped.type)) {
			return true;
		} else {
			return false;
		}
	}

	/**
	 * @param {Scope} scope 
	 * @returns {null | EvalEntity}
	 */
	evalInternal(scope) {
		const fnVal = this.#fnExpr.eval(scope);

		const argVals = reduceNull(this.#argExprs.map((ae, i) => {
			const av_ = ae.eval(scope);

			if (!av_) {
				return null;
			}
			
			const av = av_.asTyped;

			if (!av) {
				ae.typeError(`arg ${i+1} not an instance`);
				return null;
			}

			return av;
		}));

		if (!fnVal || argVals === null) {
			return null;
		}

		/**
		 * @type {Typed[]}
		 */
		const posArgVals = [];

		this.#argExprs.forEach((argExpr, i) => {
			if (!argExpr.isNamed()) {
				posArgVals.push(argVals[i]);
			}
		});

		/**
		 * @type {{[name: string]: Typed}}
		 */
		const namedArgVals = {};

		this.#argExprs.forEach((argExpr, i) => {
			if (argExpr.isNamed()) {
				const val = argVals[i];

				if (val.asTyped) {
					namedArgVals[argExpr.name] = val.asTyped;
				} else {
					throw new Error("unexpected");
				}
			}
		});

		assert(posArgVals.every(pav => pav != undefined));

		if (fnVal.asParametric) {
			this.#paramTypes = [];

			this.#appliedFnVal = fnVal.asParametric.inferCall(this.site, posArgVals, namedArgVals, this.#paramTypes);

			return this.#appliedFnVal.call(this.site, posArgVals, namedArgVals);
		} else if (fnVal.asFunc) {
			return fnVal.asFunc.call(this.site, posArgVals, namedArgVals);
		} else {
			this.#fnExpr.typeError(`unable to call ${fnVal.toString()} (returned by ${this.#fnExpr.toString()})`);
			return null;
		}
	}

	/**
	 * Don't call this inside eval() because param types won't yet be complete.
	 * @type {FuncType}
	 */
	get fn() {
		if (this.#fnExpr.cache?.asParametric) {
			return assertClass(this.#appliedFnVal?.type?.asType, FuncType);
		} else {
			return assertClass(this.#fnExpr.cache?.asTyped?.type.asType, FuncType);
		}
	}

	/**
	 * @param {ToIRContext} ctx
	 * @returns {[Expr[], IR[]]} - first list are positional args, second list named args and remaining opt args
	 */
	expandArgs(ctx) {
		const fn = this.fn;
		const nNonOptArgs = fn.nNonOptArgs;

		/**
		 * @type {Expr[]}
		 */
		const positional = [];

		this.#argExprs.forEach(ae => {
			if (!ae.isNamed()) {
				positional.push(ae.valueExpr);
			}
		});

		/**
		 * @type {IR[]}
		 */
		const namedOptional = [];

		this.#argExprs.forEach(ae => {
			if (ae.isNamed()) {
				const i = fn.getNamedIndex(ae.site, ae.name);

				if (i < nNonOptArgs) {
					positional[i] = ae.valueExpr;
				} else {
					namedOptional[i - nNonOptArgs] = new IR([
						new IR("true"),
						new IR(", "),
						ae.valueExpr.toIR(ctx)
					]);
				}
			}
		});

		for (let i = nNonOptArgs; i < fn.nArgs; i++) {
			if (namedOptional[i - nNonOptArgs] == undefined) {
				namedOptional[i - nNonOptArgs] = new IR([
					new IR("false"),
					new IR(", "),
					new IR("()")
				]);
			}
		}

		return [positional.filter(p => p != undefined), namedOptional];
	}

	/**
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toFnExprIR(ctx) {
		if (this.#fnExpr.cache?.asParametric instanceof ParametricFunc) {
			assert(this.#paramTypes.length > 0);

			const params = ParametricExpr.toApplicationIR(this.#paramTypes);

			if (this.#fnExpr instanceof MemberExpr) {
				return this.#fnExpr.toIR(ctx, params);
			} else {
				return new IR(`${this.#fnExpr.toIR(ctx).toString()}${params}`, this.#fnExpr.site);
			}
		} else {
			return this.#fnExpr.toIR(ctx);
		}
	}

	/**
	 * @private
	 * @param {Expr[]} posExprs 
	 * @returns {Map<Expr, number>}
	 */
	detectExpandedTuples(posExprs) {
		/**
		 * @type {Map<Expr, number>}
		 */
		const result = new Map();

		const posArgs = reduceNull(posExprs.map(e => e.cache?.asTyped ?? null));

		if (!posArgs) {
			posExprs.forEach(e => {
				result.set(e, 0);
			})

			return result;
		}

		const expandedPosArgs = this.fn.expandTuplesInPosArgs(posArgs);

		let j = 0;

		for (let i = 0; i < posArgs.length; i++) {
			if (j >= expandedPosArgs.length) {
				throw new Error("unexpected");
			}

			if (posArgs[i] == expandedPosArgs[j]) {
				result.set(posExprs[i], 0);
				j++;
			} else {
				const tupleItemTypes = getTupleItemTypes(posArgs[i].type);
				if (!tupleItemTypes) {
					throw new Error("unexpected");
				}

				result.set(posExprs[i], tupleItemTypes.length);
				j += tupleItemTypes.length;
			}
		}

		return result;
	}

	/**
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIR(ctx) {
		let fnIR = this.toFnExprIR(ctx);

		/**
		 * We need the func type for things like multivalued args and optional args 
		 * @type {FuncType} 
		 */
		const fn = this.fn;

		/**
		 * First step is to eliminate the named args
		 * @type {[Expr[], IR[]]}
		 */
		const [posExprs, namedOptExprs] = this.expandArgs(ctx);

		// some multiValued args (always positional)
		const isExpandedTuple = this.detectExpandedTuples(posExprs);

		if (posExprs.some(e => (isExpandedTuple.get(e) ?? 0) > 0 )) {
			// count the number of final args
			let n = 0;

			posExprs.forEach((e, i) => {
				if ((isExpandedTuple.get(e) ?? 0) > 0) {
					n += assertDefined(isExpandedTuple.get(e));
				} else {
					n += 1;
				}
			});

			n += namedOptExprs.length;

			if (n > fn.nArgs) {
				namedOptExprs.splice(0, n - fn.nArgs);
			}

			let names = [];

			for (let i = 0; i < fn.nArgs; i++) {
				if (i >= fn.nNonOptArgs) {
					names.push(`__useopt__x${i}`);
				}

				names.push(`x${i}`);
			}

			let ir = new IR([
				fnIR,
				new IR("("),
				new IR(names.map(n => new IR(n))).join(", "),
				new IR(")", this.site)
			]);

			for (let namedIR of namedOptExprs.slice().reverse()) {
				const n2 = assertDefined(names.pop());
				const n1 = assertDefined(names.pop());
				assert(n1.startsWith("__useopt__"));

				ir = new IR([
					new IR("("),
					new IR(n1),
					new IR(", "),
					new IR(n2),
					new IR(") -> {"),
					ir,
					new IR("}("),
					assertDefined(namedIR), // bool - val pair
					new IR(")")
				]);
			}

			for (let i = posExprs.length - 1; i >= 0; i--) {
				const e = posExprs[i];

				if ((isExpandedTuple.get(e) ?? 0) > 0) {
					const nMulti = assertDefined(isExpandedTuple.get(e));
					const multiNames = [];
					const multiOpt = [];

					while (multiNames.length < nMulti) {
						multiNames.unshift(assertDefined(names.pop()));

						if (names.length > 0 && names[names.length-1] == `__useopt__${multiNames[0]}`) {
							multiOpt.unshift(assertDefined(names.pop()));
						}
					}

					if (multiOpt.length > 0) {
						ir = new IR([
							new IR("("),
							new IR(multiOpt.map(n => new IR(n))).join(", "),
							new IR(") -> {"),
							ir,
							new IR("}("),
							new IR(multiOpt.map(n => new IR("true"))).join(", "),
							new IR(")")
						])
					}

					ir = new IR([
						e.toIR(ctx),
						new IR("(("),
						new IR(multiNames.map(n => new IR(n))).join(", "),
						new IR(") -> {"),
						ir,
						new IR("})")
					]);
				} else {
					const name = assertDefined(names.pop());

					if (names.length > 0 && names[names.length - 1] == `__useopt__${name}`) {
						ir = new IR([
							new IR("("),
							new IR(assertDefined(names.pop())),
							new IR(") -> {"),
							new IR("}(true)")
						]);
					}

					ir = new IR([
						new IR("("),
						new IR(name),
						new IR(") -> {"),
						ir,
						new IR("}("),
						e.toIR(ctx),
						new IR(")")
					]);
				}
			}

			return ir;
		} else /* no multivalued args */ {
			if (posExprs.length + namedOptExprs.length > fn.nArgs) {
				namedOptExprs.splice(0, posExprs.length + namedOptExprs.length - fn.nArgs);
			}

			let args = posExprs.map((a, i) => {
				let ir = a.toIR(ctx);

				if (i >= fn.nNonOptArgs) {
					ir = new IR([
						new IR("true, "),
						ir
					]);
				}

				return ir;
			}).concat(namedOptExprs);

			return new IR([
				fnIR,
				new IR("(", this.site),
				(new IR(args)).join(", "),
				new IR(")")
			]);
		}
	}
}

/**
 *  ... . ... expression
 * @internal
 */
export class MemberExpr extends Expr {
	#objExpr;
	#memberName;

	/**
	 * @param {Site} site 
	 * @param {Expr} objExpr 
	 * @param {Word} memberName 
	 */
	constructor(site, objExpr, memberName) {
		super(site);
		this.#objExpr = objExpr;
		this.#memberName = memberName;
	}

	/**
	 * @param {Scope} scope 
	 * @returns {null | EvalEntity}
	 */
	evalInternal(scope) {
		const objVal_ = this.#objExpr.eval(scope);
		if (!objVal_) {
			return null;
		}

		const objVal = objVal_.asInstance;
		if (!objVal) {
			this.#objExpr.site.typeError(`lhs of '.' not an instance`);
			return null;
		}

		let member = objVal.instanceMembers[this.#memberName.value];
		if (!member) {

			if (objVal?.type?.asEnumMemberType) {
				member = objVal.type.asEnumMemberType.parentType.instanceMembers[this.#memberName.value];
			}

			if (!member) {
				this.#memberName.referenceError(`'${objVal.type.toString()}.${this.#memberName.value}' undefined`);
				return null;
			}
		}

		if (member.asParametric) {
			return member;
		} else if (member.asType) {
			const memberVal = member.asType.toTyped();

			return memberVal;
		} else {
			throw new Error("expected type or parametric");
		}
	}

	/**
	 * @param {ToIRContext} ctx
	 * @param {string} params - applied type parameters must be inserted Before the call to self
	 * @returns {IR}
	 */
	toIR(ctx, params = "") {
		// members can be functions so, field getters are also encoded as functions for consistency

		const objType = assertDefined(this.#objExpr.cache?.asTyped?.type?.asNamed); 

		let objPath = objType.path;

		// if we are getting the member of an enum member we should check if it a field or method, because for a method we have to use the parent type
		if (objType.asEnumMemberType && (objType.asEnumMemberType.instanceMembers[this.#memberName.value] === undefined)) {
			objPath = objType.asEnumMemberType.parentType.path;
		}

		let ir = new IR(`${objPath}__${this.#memberName.toString()}${params}`, this.site);

		return new IR([
			ir, new IR("("),
			this.#objExpr.toIR(ctx),
			new IR(")"),
		]);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `${this.#objExpr.toString()}.${this.#memberName.toString()}`;
	}
}

/**
 * if-then-else expression 
 * @internal
 */
export class IfElseExpr extends Expr {
	#conditions;
	#branches;

	/**
	 * @param {Site} site 
	 * @param {Expr[]} conditions 
	 * @param {Expr[]} branches 
	 */
	constructor(site, conditions, branches) {
		assert(branches.length == conditions.length + 1);
		assert(branches.length > 1);

		super(site);
		this.#conditions = conditions;
		this.#branches = branches;
	}

	toString() {
		let s = "";
		for (let i = 0; i < this.#conditions.length; i++) {
			s += `if (${this.#conditions[i].toString()}) {${this.#branches[i].toString()}} else `;
		}

		s += `{${this.#branches[this.#conditions.length].toString()}}`;

		return s;
	}

	/**
	 * @param {Site} site
	 * @param {null | Type} prevType
	 * @param {Type} newType
	 * @returns {null | Type}
	 */
	static reduceBranchType(site, prevType, newType) {
		if (prevType === null || prevType instanceof ErrorType) {
			return newType;
		} else if (newType instanceof ErrorType) {
			return prevType;
		} else if (!prevType.isBaseOf(newType)) {
			if (newType.isBaseOf(prevType)) {
				return newType;
			} else {
				// check if enumparent is base of newType and of prevType
				if (newType.asEnumMemberType) {
					const parentType = newType.asEnumMemberType.parentType;

					if (parentType.isBaseOf(prevType) && parentType.isBaseOf(newType)) {
						return parentType;
					}
				}

				const prevTupleItems = getTupleItemTypes(prevType);
				const newTupleItems = getTupleItemTypes(newType);

				if (prevTupleItems && newTupleItems && prevTupleItems.length == newTupleItems.length) {
					const reducedTupleItems = reduceNull(prevTupleItems.map((prev, i) => IfElseExpr.reduceBranchType(site, prev, newTupleItems[i])));

					if (reducedTupleItems) {
						return TupleType$(reducedTupleItems);
					}
				}

				site.typeError("inconsistent types");
				return null;
			}
		} else {
			return prevType;
		}
	}

	/**
	 * @param {Site} site
	 * @param {null | Type} prevType
	 * @param {Typed} newValue
	 * @returns {null | Type}
	 */
	static reduceBranchMultiType(site, prevType, newValue) {
		if (newValue.asTyped && (new ErrorType()).isBaseOf(newValue.asTyped.type)) {
			return prevType;
		}

		const newType = assertDefined(newValue.asTyped).type;

		if (prevType === null) {
			return newType;
		} else {
			return IfElseExpr.reduceBranchType(site, prevType, newType);
		}
	}

	/**
	 * @param {Scope} scope 
	 * @returns {null | EvalEntity}
	 */
	evalInternal(scope) {
		for (let c of this.#conditions) {
			const cVal_ = c.eval(scope);
			if (!cVal_) {
				continue;
			}

			const cVal = cVal_.asTyped;

			if (!cVal || !BoolType.isBaseOf(cVal.type)) {
				c.typeError("expected bool");
				continue;
			}
		}

		/**
		 * @type {null | Type}
		 */
		let branchMultiType = null;

		for (let b of this.#branches) {
			// don't allow shadowing
			const branchScope = new Scope(scope, false);

			const branchVal = b.evalAsTyped(branchScope);

			if (!branchVal) {
				continue;
			}

			branchMultiType = IfElseExpr.reduceBranchMultiType(
				b.site, 
				branchMultiType, 
				branchVal
			);
		}

		if (branchMultiType === null) {
			// i.e. every branch throws an error
			return new ErrorEntity();
		} else  {
			return branchMultiType.toTyped();
		}
	}

	/**
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIR(ctx) {
		let n = this.#conditions.length;

		// each branch actually returns a function to allow deferred evaluation
		let res = new IR([
			new IR("() -> {"),
			this.#branches[n].toIR(ctx),
			new IR("}")
		]);

		// TODO: nice indentation
		for (let i = n - 1; i >= 0; i--) {
			res = new IR([
				new IR("__core__ifThenElse("),
				this.#conditions[i].toIR(ctx),
				new IR(", () -> {"),
				this.#branches[i].toIR(ctx),
				new IR("}, () -> {"),
				res,
				new IR("()})"),
			]);
		}

		return new IR([res, new IR("()", this.site)]);
	}
}

/**
 * DestructExpr is for the lhs-side of assignments and for switch cases
 * `NameExpr [':' TypeExpr ['{' ... '}']]`
 * @internal
 */
export class DestructExpr {
	/**
	 * @type {Word}
	 */
	#name;

	/**
	 * @type {null | Expr}
	 */
	#typeExpr;

	/**
	 * @type {DestructExpr[]}
	 */
	#destructExprs;

	/**
	 * @type {boolean}
	 */
	#isTuple;

	/**
	 * @param {Word} name - use an underscore as a sink
	 * @param {null | Expr} typeExpr 
	 * @param {DestructExpr[]} destructExprs
	 * @param {boolean} isTuple typeExpr must be `null` if isTuple is `true` and `destructExpr.length` must be `> 0`
	 */
	constructor(name, typeExpr, destructExprs = [], isTuple = false) {
		this.#name = assertDefined(name);
		this.#typeExpr = typeExpr;
		this.#destructExprs = destructExprs;
		this.#isTuple = isTuple;

		if (isTuple) {
			assert(this.#destructExprs.length > 0 && this.#typeExpr == null);
		} else {
			assert(!(this.#typeExpr == null && this.#destructExprs.length > 0), `unexpected syntax: ${this.toString()}`);
		}
	}

	/**
	 * @type {Site}
	 */
	get site() {
		return this.#name.site;
	}

	/**
	 * @type {Word}
	 */
	get name() {
		return this.#name;
	}

	/**
	 * @type {DestructExpr[]}
	 */
	get children() {
		return this.#destructExprs;
	}

	/**
	 * @returns {boolean}
	 */
	isTuple() {
		return this.#isTuple;
	}

	/**
	 * @returns {boolean}
	 */
	hasDestructExprs() {
		return this.#destructExprs.length > 0;
	}

	/**
	 * @returns {boolean}
	 */
	isIgnored() {
		return this.name.value === "_";
	}

	/**
	 * @returns {boolean}
	 */
	hasType() {
		return this.#typeExpr !== null;
	}

	/**
	 * Throws an error if called before evalType()
	 * @type {null | Type}
	 */
	get type() {
		if (this.#typeExpr === null) {
			if (this.#isTuple) {
				const nestedTypes = reduceNull(this.#destructExprs.map(e => e.type));

				if (!nestedTypes) {
					this.site.typeError(`invalid nested tuple in in destruct expression`);
					return null;
				}

				return TupleType$(nestedTypes);
			} else if (this.isIgnored()) {
				return new AllType();
			} else {
				return null;
			}
		} else {
			if (!this.#typeExpr.cache?.asType) {
				this.#typeExpr.typeError(`invalid type '${assertDefined(this.#typeExpr.cache, "cache unset").toString()}'`);
				return null;
			} else {
				return this.#typeExpr.cache.asType;
			}
		}
	}

	/**
	 * @type {Word}
	 */
	get typeName() {
		if (this.#typeExpr === null) {
			return new Word(this.site, "");
		} else {
			return new Word(this.#typeExpr.site, this.#typeExpr.toString());
		}
	}

	/**
	 * @returns {string}
	 */
	toString() {
		if (this.#typeExpr === null) {
			if (this.#destructExprs.length > 0 && this.#isTuple) {
				return `${this.name.toString()}: (${this.#destructExprs.map(de => de.toString()).join(", ")})`;
			} else {
				return this.name.toString();
			}
		} else {
			let destructStr = "";

			if (this.#destructExprs.length > 0) {
				destructStr = `{${this.#destructExprs.map(de => de.toString()).join(", ")}}`;
			}

			if (this.isIgnored()) {
				return `${this.#typeExpr.toString()}${destructStr}`;
			} else {
				return `${this.name.toString()}: ${this.#typeExpr.toString()}${destructStr}`;
			}
		}
	}

	/**
	 * Evaluates the type, used by FuncLiteralExpr and DataDefinition
	 * @param {Scope} scope 
	 * @param {null | Type} upstreamType
	 * @returns {null | Type}
	 */
	evalType(scope, upstreamType = null) {
		if (this.#typeExpr === null) {
			if (this.#isTuple) {
				const upstreamItemTypes = upstreamType ? getTupleItemTypes(upstreamType) : null;
				const nestedTypes = reduceNull(this.#destructExprs.map((e, i) => e.evalType(scope, upstreamItemTypes ? upstreamItemTypes[i] : null)));

				if (!nestedTypes) {
					this.site.typeError(`invalid nested tuple in in destruct expression`);
					return null;
				}

				return TupleType$(nestedTypes);
			} else if (this.isIgnored()) {
				return new AllType();
			} else {
				throw new Error("typeExpr not set in " + this.site.src.raw.split("\n")[0]);
			}
		} else {
			const t = this.#typeExpr.evalAsType(scope);

			if (t && upstreamType && !upstreamType.asEnumMemberType && t.asEnumMemberType) {
				return t.asEnumMemberType.parentType;
			} else {
				return t;
			}
		}
	}

	/**
	 * @param {Scope} scope 
	 * @param {Type} upstreamType 
	 */
	evalDestructExprs(scope, upstreamType) {
		if (this.#destructExprs.length > 0) {
			if (this.#isTuple) {
				const tupleItemTypes = getTupleItemTypes(upstreamType);

				if (!tupleItemTypes) {
					this.site.typeError("upstream value isn't a tuple, can't destruct");
					return;
				}

				if (tupleItemTypes.length != this.#destructExprs.length) {
					this.site.typeError(`wrong number of destruct tuple fields, expected ${tupleItemTypes.length}, got ${this.#destructExprs.length}`);
					return;
				}

				for (let i = 0; i < this.#destructExprs.length; i++) {
					this.#destructExprs[i].evalInternal(
						scope, 
						tupleItemTypes[i], 
						i
					);
				}
			} else {
				if (!upstreamType.asDataType) {
					this.site.typeError("can't destruct a function");
					return;
				}

				const upstreamFieldNames = upstreamType.asDataType.fieldNames;

				if (upstreamFieldNames.length != this.#destructExprs.length) {
					this.site.typeError(`wrong number of destruct fields, expected ${upstreamFieldNames.length}, got ${this.#destructExprs.length}`);
					return;
				}

				for (let i = 0; i < this.#destructExprs.length; i++) {
					this.#destructExprs[i].evalInternal(
						scope, 
						assertDefined(upstreamType.instanceMembers[upstreamFieldNames[i]].asDataType), // we `asDataType` because methods can't be destructed
						i
					);
				}
			}
		}
	}

	/**
	 * @param {Scope} scope 
	 * @param {Type} upstreamType
	 * @param {number} i
	 */
	evalInternal(scope, upstreamType, i) {
		if (this.hasType()) {
			const t = this.evalType(scope);
			if (!t) {
				return;
			}

			// differs from upstreamType because can be enum parent
			let checkType = t;

			// if t is enum variant, get parent instead (exact variant is checked at runtime instead)
			if (t.asEnumMemberType && !upstreamType.asEnumMemberType) {
				checkType = t.asEnumMemberType.parentType;
			}

			if (!checkType.isBaseOf(upstreamType)) {
				this.site.typeError(`expected ${checkType.toString()} for destructure field ${i+1}, got ${upstreamType.toString()}`);
				return null;
			}

			if (!this.isIgnored()) {
				// TODO: take into account ghost type parameters
				scope.set(this.name, t.toTyped());
			}

			this.evalDestructExprs(scope, t);
		} else {
			if (!this.isIgnored()) {
				// TODO: take into account ghost type parameters
				scope.set(this.name, upstreamType.toTyped());
			}

			this.evalDestructExprs(scope, upstreamType);
		}
	}

	/**
	 * @param {Scope} scope
	 * @param {DataType} caseType
	 */
	evalInSwitchCase(scope, caseType) {
		if (!this.isIgnored()) {
			scope.set(this.#name, caseType.toTyped());
		}

		if (this.#typeExpr) {
			this.#typeExpr.cache = caseType;
		}

		this.evalDestructExprs(scope, caseType);
	}

	/**
	 * @param {Scope} scope 
	 * @param {null | Type} upstreamType
	 * @param {number} i
	 */
	evalInAssignExpr(scope, upstreamType, i) {
		const t = this.evalType(scope);

		if (!t) {
			scope.set(this.name, new DataEntity(new AnyType()));
			return;
		}

		// differs from upstreamType because can be enum parent
		// if t is enum variant, get parent instead (exact variant is checked at runtime instead)
		// also do this for nested as well
		const checkType = this.evalType(scope, upstreamType);

		if (checkType && upstreamType) {
			if (!checkType.isBaseOf(upstreamType)) {
				this.site.typeError(`expected ${checkType.toString()} for rhs ${i+1}, got ${upstreamType.toString()}`);
			}
		}

		if (!this.isIgnored()) {
			// TODO: take into account ghost type parameters
			scope.set(this.name, t.toTyped());
		}

		this.evalDestructExprs(scope, t);
	}

	/**
	 * @param {number} argIndex 
	 * @returns {IR}
	 */
	toNameIR(argIndex) {
		if (this.isIgnored()) {
			return new IR(`__lhs_${argIndex}`);
		} else {
			return new IR(this.#name.toString(), this.#name.site)
		}
	}

	/**
	 * @param {number} fieldIndex
	 * @returns {string}
	 */
	getFieldFn(fieldIndex) {
		const type = assertDefined(this.type);

		if (type.asDataType) {
			return `${type.asDataType.path}__${type.asDataType.fieldNames[fieldIndex]}`;
		} else {
			return "";
		}
	}

	/**
	 * @private
	 * @param {ToIRContext} ctx
	 * @param {IR} inner 
	 * @param {string} objName 
	 * @param {number} fieldIndex 
	 * @param {string} fieldGetter
	 * @returns {IR}
	 */
	wrapDestructIRInternal(ctx, inner, objName, fieldIndex, fieldGetter) {
		if (this.isIgnored() && this.#destructExprs.length == 0) {
			return inner;
		} else {
			const baseName = this.isIgnored() ? `${objName}_${fieldIndex}` : this.#name.toString();

			for (let i = this.#destructExprs.length - 1; i >= 0; i--) {
				const de = this.#destructExprs[i];

				const innerGetter = this.#isTuple ? de.toNameIR(i).toString() : `${this.getFieldFn(i)}(${baseName})`;

				inner = de.wrapDestructIRInternal(ctx.tab(), inner, baseName, i, innerGetter);
			}

			if (this.#isTuple) {
				inner = IR.new`${baseName}(
					(${new IR(this.#destructExprs.map((de, i) => de.toNameIR(i))).join(", ")}) -> {
						${inner}
					}
				)`;
			}

			let getter = fieldGetter;

			const t = this.type;

			// assert correct constructor index
			if (this.#typeExpr && t && t.asEnumMemberType) {
				const constrIdx = t.asEnumMemberType.constrIndex;

				getter = `__helios__common__assert_constr_index(${getter}, ${constrIdx})`;
			}
			
			return new IR([
				new IR("("),
				new IR(baseName, this.#name.site),
				new IR(") "),
				new IR("->", this.site), new IR(` {\n${ctx.indent}${TAB}`),
				inner,
				new IR(`\n${ctx.indent}}(${getter})`),
			]);
		}
	}

	/**
	 * 
	 * @param {ToIRContext} ctx
	 * @param {IR} inner - downstream IR expression
	 * @param {number} argIndex 
	 * @returns {IR}
	 */
	wrapDestructIR(ctx, inner, argIndex) {
		if (this.#destructExprs.length == 0) {
			return inner;
		} else {
			/**
			 * same as this.toNameIR()
			 * TODO: can __lhs be changed to underscore?
			 */
			const baseName = this.isIgnored() ? `__lhs_${argIndex}` : this.#name.toString();

			for (let i = this.#destructExprs.length - 1; i >= 0; i--) {
				const de = this.#destructExprs[i];

				const getter = this.#isTuple ? de.toNameIR(i).toString() : `${this.getFieldFn(i)}(${baseName})`;

				inner = de.wrapDestructIRInternal(ctx.tab(), inner, baseName, i, getter);
			}

			if (this.#isTuple) {
				return IR.new`${baseName}(
					(${new IR(this.#destructExprs.map((de, i) => de.toNameIR(i))).join(", ")}) -> {
						${inner}
					}
				)`;
			} else {
				return inner;
			}
		}
	}

	/**
	 * @returns {IR}
	 */
	toIR() {
		return new IR(this.#name.toString(), this.#name.site);
	}
}

/**
 * Switch case for a switch expression
 * @internal
 */
export class SwitchCase extends Token {
	#lhs;
	#bodyExpr;

	/** 
	 * @type {null | number} 
	 */
	#constrIndex;

	/**
	 * @param {Site} site 
	 * @param {DestructExpr} lhs
	 * @param {Expr} bodyExpr 
	 */
	constructor(site, lhs, bodyExpr) {
		super(site);
		this.#lhs = lhs;
		this.#bodyExpr = bodyExpr;
		this.#constrIndex = null;
	}

	/**
	 * @type {Expr}
	 */
	get body() {
		return this.#bodyExpr;
	}

	/**
	 * Used by parser to check if typeExpr reference the same base enum
	 * @type {Word} - word representation of type
	 */
	get memberName() {
		return this.#lhs.typeName;
	}

	/**
	 * @returns {boolean}
	 */
	isDataMember() {
		switch (this.memberName.value) {
			case "Int":
			case "[]Data":
			case "ByteArray":
			case "Map[Data]Data":
				return true;
			default:
				return false;
		}
	}

	/**
	 * @type {number}
	 */
	get constrIndex() {
		if (this.#constrIndex === null) {
			throw new Error("constrIndex not yet set");
		} else {
			return this.#constrIndex;
		}
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `${this.#lhs.toString()} => ${this.#bodyExpr.toString()}`;
	}

	/**
	 * Evaluates the switch type and body value of a case.
	 * @param {Scope} scope 
	 * @param {DataType} enumType
	 * @returns {null | Typed}
	 */
	evalEnumMember(scope, enumType) {
		const caseType = enumType.typeMembers[this.memberName.value]?.asEnumMemberType;
		if (!caseType) {
			this.memberName.typeError(`${this.memberName.value} isn't a valid enum member of ${enumType.toString()}`);
			return null;
		}

		this.#constrIndex = caseType.constrIndex;

		assert(this.#constrIndex >= 0);

		const caseScope = new Scope(scope, false);

		this.#lhs.evalInSwitchCase(caseScope, caseType);

		const bodyVal_ = this.#bodyExpr.eval(caseScope);
		
		if (!bodyVal_) {
			return null;
		}

		const bodyVal = bodyVal_.asTyped;

		if (!bodyVal) {
			this.#bodyExpr.typeError("not typed");
			return null;
		}

		caseScope.assertAllUsed();

		return bodyVal;
	}

	/**
	 * Evaluates the switch type and body value of a case.
	 * @param {Scope} scope
	 * @returns {null | Typed}
	 */
	evalDataMember(scope) {
		/** @type {DataType} */
		let memberType;

		switch (this.memberName.value) {
			case "Int":
				memberType = IntType;
				break;
			case "ByteArray":
				memberType = ByteArrayType;
				break;
			case "[]Data":
				memberType = ListType$(RawDataType);
				break;
			case "Map[Data]Data":
				memberType = MapType$(RawDataType, RawDataType);
				break;
			default:
				const maybeMemberType_ = scope.get(this.memberName);
				if (!maybeMemberType_) {
					return null;
				}

				let maybeMemberType = maybeMemberType_.asDataType;
				if (!maybeMemberType) {
					this.memberName.typeError("expected a data type");
					return null;
				}
				memberType = maybeMemberType;

				if (!Common.isEnum(memberType)) {
					this.memberName.typeError("expected an enum type");
					return null;
				}
		}

		const caseScope = new Scope(scope, false);

		this.#lhs.evalInSwitchCase(caseScope, memberType);

		const bodyVal_ = this.#bodyExpr.eval(caseScope);

		if (!bodyVal_) {
			return null;
		}

		caseScope.assertAllUsed();

		const bodyVal = bodyVal_.asTyped;

		if (!bodyVal) {
			this.#bodyExpr.typeError("not typed");
			return null;
		}

		return bodyVal;
	}

	/**
	 * Accept an arg because will be called with the result of the controlexpr
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIR(ctx) {
		let inner = this.#bodyExpr.toIR(ctx.tab());

		inner = this.#lhs.wrapDestructIR(ctx, inner, 0);

		return new IR([
			new IR("("),
			this.#lhs.toNameIR(0), // wrapDestructIR depends on this name
			new IR(") "),
			new IR("->", this.site), new IR(` {\n${ctx.indent}${TAB}`),
			inner,
			new IR(`\n${ctx.indent}}`),
		]);
	}
}

/**
 * @internal
 */
export class UnconstrDataSwitchCase extends SwitchCase {
	#intVarName;
	#lstVarName;

	/**
	 * @param {Site} site 
	 * @param {?Word} intVarName 
	 * @param {?Word} lstVarName 
	 * @param {Expr} bodyExpr 
	 */
	constructor(site, intVarName, lstVarName, bodyExpr) {
		super(site, new DestructExpr(new Word(site, "_"), new RefExpr(new Word(site, "(Int, []Data)"))), bodyExpr);

		this.#intVarName = intVarName;
		this.#lstVarName = lstVarName;
	}

	isDataMember() {
		return true;
	}

	toString() {
		return `(${this.#intVarName === null ? "" : this.#intVarName.value + ": "}Int, ${this.#lstVarName === null ? "" : this.#lstVarName.value + ": "} []Data) => ${this.body.toString()}`;
	}

	/**
	 * @param {Scope} scope 
	 * @param {Type} enumType
	 * @returns {Instance}
	 */
	evalEnumMember(scope, enumType) {
		throw new Error("not available");
	}

	/**
	 * Evaluates the switch type and body value of a case.
	 * @param {Scope} scope
	 * @returns {null | Typed}
	 */
	evalDataMember(scope) {
		/**
		 * @type {null | Typed}
		 */
		let bodyVal = null;

		if (this.#intVarName !== null || this.#lstVarName !== null) {
			let caseScope = new Scope(scope, false);

			if (this.#intVarName !== null) {
				caseScope.set(this.#intVarName, new DataEntity(IntType));
			}

			if (this.#lstVarName !== null) {
				caseScope.set(this.#lstVarName, new DataEntity(ListType$(RawDataType)));
			}

			const bodyVal_ = this.body.eval(caseScope);

			if (!bodyVal_) {
				return null;
			}

			bodyVal = bodyVal_.asTyped;

			caseScope.assertAllUsed();
		} else {
			const bodyVal_ = this.body.eval(scope);

			if (!bodyVal_) {
				return null;
			}

			bodyVal = bodyVal_.asTyped;
		}

		if (!bodyVal) {
			this.body.typeError("not typed");
			return null;
		}

		return bodyVal;
	}

	/**
	 * Accepts two args
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIR(ctx) {
		return new IR([
			new IR(`(data) -> {\n${ctx.indent}${TAB}`),
			new IR(`(pair) -> {\n${ctx.indent}${TAB}${TAB}`),
			new IR(`(${this.#intVarName !== null ? this.#intVarName.toString() : "_"}, ${this.#lstVarName !== null ? this.#lstVarName.toString() : "_"}) `), new IR("->", this.site), new IR(` {\n${ctx.indent}${TAB}${TAB}${TAB}`),
			this.body.toIR(ctx.tab().tab().tab()),
			new IR(`\n${ctx.indent}${TAB}${TAB}}(__core__fstPair(pair), __core__sndPair(pair))`),
			new IR(`\n${ctx.indent}${TAB}}(__core__unConstrData(data))`),
			new IR(`\n${ctx.indent}}`)
		]);
	}
}

/**
 * Default switch case
 * @internal
 */
export class SwitchDefault extends Token {
	#bodyExpr;

	/**
	 * @param {Site} site
	 * @param {Expr} bodyExpr
	 */
	constructor(site, bodyExpr) {
		super(site);
		this.#bodyExpr = bodyExpr;
	}

	toString() {
		return `else => ${this.#bodyExpr.toString()}`;
	}

	/**
	 * @param {Scope} scope 
	 * @returns {null | Typed}
	 */
	eval(scope) {
		const bodyVal_ = this.#bodyExpr.eval(scope);

		if (!bodyVal_) {
			return null;
		}

		const bodyVal = bodyVal_.asTyped;

		if (!bodyVal) {
			this.#bodyExpr.typeError("not typed");
			return null;
		}

		return bodyVal;
	}

	/**
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIR(ctx) {
		return new IR([
			new IR(`(_) `), new IR("->", this.site), new IR(` {\n${ctx.indent}${TAB}`),
			this.#bodyExpr.toIR(ctx.tab()),
			new IR(`\n${ctx.indent}}`)
		]);
	}
}

/**
 * Parent class of EnumSwitchExpr and DataSwitchExpr
 */
class SwitchExpr extends Expr {
	#controlExpr;
	#cases;
	#defaultCase;

	/** 
	 * @param {Site} site
	 * @param {Expr} controlExpr - input value of the switch
	 * @param {SwitchCase[]} cases
	 * @param {null | SwitchDefault} defaultCase
	*/
	constructor(site, controlExpr, cases, defaultCase = null) {
		super(site);
		this.#controlExpr = controlExpr;
		this.#cases = cases;
		this.#defaultCase = defaultCase;
	}

	get controlExpr() {
		return this.#controlExpr;
	}

	get cases() {
		return this.#cases;
	}

	get defaultCase() {
		return this.#defaultCase;
	}

	/**
	 * If there isn't enough coverage then we can simply set the default case to void, so the other branches can be error, print or assert
	 */
	setDefaultCaseToVoid() {
		this.#defaultCase = new SwitchDefault(this.site, new VoidExpr(this.site));
	}

	toString() {
		return `${this.#controlExpr.toString()}.switch{${this.#cases.map(c => c.toString()).join(", ")}${this.#defaultCase === null ? "" : ", " + this.#defaultCase.toString()}}`;
	}
}

/**
 * Switch expression for Enum, with SwitchCases and SwitchDefault as children
 * @internal
 */
export class EnumSwitchExpr extends SwitchExpr {
	/**
	 * @param {Scope} scope 
	 * @returns {null | EvalEntity}
	 */
	evalInternal(scope) {
		const controlVal_ = this.controlExpr.eval(scope);
		if (!controlVal_) {
			return null;
		}

		const controlVal = controlVal_.asTyped;

		if (!controlVal) {
			this.controlExpr.typeError("not typed");
			return null;
		}

		let enumType = controlVal.type.asDataType;

		if (!enumType) {
			this.controlExpr.typeError("not an enum");
			return null;
		}

		if (controlVal.type.asEnumMemberType) {
			this.controlExpr.typeError(`${controlVal.type.toString()} is an enum variant, not an enum`);
			enumType = controlVal.type.asEnumMemberType.parentType; // continue with optimistic evaluation, even though compilation will fail
		}

		const nEnumMembers = Common.countEnumMembers(enumType);

		// check that we have enough cases to cover the enum members
		if (this.defaultCase === null && nEnumMembers > this.cases.length) {
			// mutate defaultCase to VoidExpr
			this.setDefaultCaseToVoid();
		}

		/** @type {null | Type} */
		let branchMultiType = null;

		for (let c of this.cases) {
			const branchVal = c.evalEnumMember(scope, enumType);

			if (!branchVal) {
				continue;
			}
	
			branchMultiType = IfElseExpr.reduceBranchMultiType(
				c.site, 
				branchMultiType, 
				branchVal
			);
		}

		if (this.defaultCase !== null) {
			const defaultVal = this.defaultCase.eval(scope);

			if (defaultVal) {
				branchMultiType = IfElseExpr.reduceBranchMultiType(
					this.defaultCase.site,
					branchMultiType, 
					defaultVal
				);
			}
		}

		if (branchMultiType === null) {
			return new ErrorEntity();
		} else {
			return branchMultiType.toTyped();
		}
	}

	/**
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIR(ctx) {
		let cases = this.cases.slice();

		/** @type {SwitchCase | SwitchDefault} */
		let last;
		if (this.defaultCase !== null) {
			last = this.defaultCase;
		} else {
			last = assertDefined(cases.pop());
		}

		let n = cases.length;

		let res = last.toIR(ctx.tab().tab().tab());

		// TODO: if constrIndex is null then use the case test that is defined as a builtin (needed to be able to treat StakingCredential as an enum)
		// TODO: once the null fallback has been implemented get rid of constrIndex
		for (let i = n - 1; i >= 0; i--) {
			const c = cases[i];
			
			const test = IR.new`__core__equalsInteger(i, ${c.constrIndex.toString()})`;

			res = IR.new`__core__ifThenElse(
				${test},
				() -> {
					${c.toIR(ctx.tab().tab().tab())}
				}, () -> {
					${res}
				}
			)()`;
		}

		return new IR([
			new IR(`(e) `), new IR("->", this.site), new IR(` {\n${ctx.indent}${TAB}(\n${ctx.indent}${TAB}${TAB}(i) -> {\n${ctx.indent}${TAB}${TAB}${TAB}`),
			res,
			new IR(`\n${ctx.indent}${TAB}${TAB}}(__core__fstPair(__core__unConstrData(e)))\n${ctx.indent}${TAB})(e)\n${ctx.indent}}(`),
			this.controlExpr.toIR(ctx),
			new IR(")"),
		]);
	}
}

/**
 * Switch expression for Data
 * @internal
 */
export class DataSwitchExpr extends SwitchExpr {
	/**
	 * @param {Scope} scope 
	 * @returns {null | EvalEntity}
	 */
	evalInternal(scope) {
		const controlVal_ = this.controlExpr.eval(scope);
		if (!controlVal_) {
			return null;
		}

		const controlVal = controlVal_.asTyped;
		if (!controlVal) {
			this.controlExpr.typeError("not typed");
			return null;
		}

		const dataType = controlVal.type.asDataType;
		if (!dataType) {
			this.controlExpr.typeError("not a data type");
			return null;
		}

		if (!RawDataType.isBaseOf(dataType)) {
			this.controlExpr.typeError(`expected Data type, got ${controlVal.type.toString()}`);
			return null;
		}

		// check that we have enough cases to cover the enum members
		if (this.defaultCase === null && this.cases.length < 5) {
			// mutate defaultCase to VoidExpr
			this.setDefaultCaseToVoid();
		}

		/** @type {null | Type} */
		let branchMultiType = null;

		for (let c of this.cases) {
			const branchVal = c.evalDataMember(scope);

			if (!branchVal) {
				continue;
			}

			branchMultiType = IfElseExpr.reduceBranchMultiType(
				c.site, 
				branchMultiType, 
				branchVal
			);
		}

		if (this.defaultCase !== null) {
			const defaultVal = this.defaultCase.eval(scope);

			if (defaultVal) {
				branchMultiType = IfElseExpr.reduceBranchMultiType(
					this.defaultCase.site, 
					branchMultiType, 
					defaultVal
				);
			}
		}

		if (branchMultiType === null) {
			// only possible if each branch is an error
			return new ErrorEntity();
		} else {
			return branchMultiType.toTyped();
		}
	}

	/**
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIR(ctx) {
		/** @type {[?IR, ?IR, ?IR, ?IR, ?IR]} */
		let cases = [null, null, null, null, null]; // constr, map, list, int, byteArray

		for (let c of this.cases) {
			let ir = c.toIR(ctx.tab().tab());

			switch (c.memberName.value) {
				case "ByteArray":
					cases[4] = new IR([
						new IR("("), new IR("e"), new IR(") -> {"), 
						ir,
						new IR("("),
						new IR("__helios__bytearray__from_data"),
						new IR("("), new IR("e"), new IR(")"),
						new IR(")"),
						new IR("}")
					]);
					break;
				case "Int":
					cases[3] = new IR([
						new IR("("), new IR("e"), new IR(") -> {"), 
						ir,
						new IR("("),
						new IR("__helios__int__from_data"),
						new IR("("), new IR("e"), new IR(")"),
						new IR(")"),
						new IR("}")
					]);
					break;
				case "[]Data":
					cases[2] = new IR([
						new IR("("), new IR("e"), new IR(") -> {"), 
						ir,
						new IR("("),
						new IR("__code__unListData"),
						new IR("("), new IR("e"), new IR(")"),
						new IR(")"),
						new IR("}")
					]);
					break;
				case "Map[Data]Data":
					cases[1] = new IR([
						new IR("("), new IR("e"), new IR(") -> {"), 
						ir,
						new IR("("),
						new IR("__code__unMapData"),
						new IR("("), new IR("e"), new IR(")"),
						new IR(")"),
						new IR("}")
					]);
					break;
				case "(Int, []Data)":
					// conversion from_data is handled by UnconstrDataSwitchCase
					cases[0] = ir;
					break;
				default:
					if (cases[0] !== null) {
						throw new Error("should've been caught before");
					}

					cases[0] = ir;
			}
		}

		if (this.defaultCase !== null) {
			for (let i = 0; i < 5; i++) {
				if (cases[i] === null) {
					cases[i] = new IR(`${ctx.indent}${TAB}def`);
				}
			}
		}

		let res = new IR([
			new IR(`${ctx.indent}__core__chooseData(e, `, this.site),
			new IR(cases.map(c => assertDefined(c))).join(", "),
			new IR(`${ctx.indent})`)
		]);

		if (this.defaultCase !== null) {
			res = new IR([
				new IR(`${ctx.indent}(def) -> {\n`),
				res,
				new IR(`\n${ctx.indent}}(`),
				this.defaultCase.toIR(ctx),
				new IR(`)`)
			]);
		}

		res = new IR([
			new IR(`${ctx.indent}(e) -> {\n`),
			res,
			new IR("(e)"),
			new IR(`${ctx.indent}}(`),
			this.controlExpr.toIR(ctx),
			new IR(")")
		]);

		return res;
	}
}


////////////////////////////////////
// Section 25: Helios AST statements
////////////////////////////////////

/**
 * Base class for all statements
 * Doesn't return a value upon calling eval(scope)
 * @internal
 */
export class Statement extends Token {
	#name;
	#basePath; // set by the parent Module

	/**
	 * @param {Site} site 
	 * @param {Word} name 
	 */
	constructor(site, name) {
		super(site);
		this.#name = name;
		this.#basePath = "__user";
	}

	/**
	 * @type {Word}
	 */
	get name() {
		return this.#name;
	}

	/**
	 * @type {string}
	 */
	get path() {
		return `${this.#basePath}__${this.name.toString()}`;
	}

	/**
	 * @param {ModuleScope} scope 
	 */
	eval(scope) {
		throw new Error("not yet implemented");
	}

	/**
	 * @param {string} namespace 
	 * @param {(name: string, cs: ConstStatement) => void} callback 
	 */
	loopConstStatements(namespace, callback) {
		throw new Error("not yet implemented");
	}

	/**
	 * @param {string} basePath 
	 */
	setBasePath(basePath) {
		this.#basePath = basePath;
	}

	/**
	 * Returns IR of statement.
	 * No need to specify indent here, because all statements are top-level
	 * @param {ToIRContext} ctx
	 * @param {IRDefinitions} map 
	 */
	toIR(ctx, map) {
		throw new Error("not yet implemented");
	}

	/**
	 * @returns {string}
	 */
	toString() {
		throw new Error("not yet implemented");
	}
}

/**
 * Each field in `import {...} from <ModuleName>` is given a separate ImportFromStatement
 * @internal
 */
export class ImportFromStatement extends Statement {
	#origName;
	#moduleName;

	/**
	 * @param {Site} site 
	 * @param {Word} name
	 * @param {Word} origName
	 * @param {Word} moduleName
	 */
	constructor(site, name, origName, moduleName) {
		super(site, name);
		this.#origName = origName;
		this.#moduleName = moduleName;
	}

	/**
	 * @type {Word}
	 */
	get moduleName() {
		return this.#moduleName;
	}

	/**
	 * @param {ModuleScope} scope
	 * @returns {null | EvalEntity}
	 */
	evalInternal(scope) {
		const importedScope = scope.getScope(this.#moduleName);

		if (!importedScope) {
			return null;
		}

		const importedEntity = importedScope.get(this.#origName);

		if (importedEntity instanceof Scope) {
			this.#origName.typeError(`can't import a module from a module`);
			return null;
		} else {
			return importedEntity;
		}
	}

	/**
	 * @param {ModuleScope} scope 
	 */
	eval(scope) {
		const v = this.evalInternal(scope);

		if (v) {
			scope.set(this.name, v);
		}
	}

	/**
	 * Do nothing
	 * @param {string} namespace 
	 * @param {(name: string, cs: ConstStatement) => void} callback 
	 */
	loopConstStatements(namespace, callback) {
	}

	/**
	 * @param {ToIRContext} ctx
	 * @param {IRDefinitions} map 
	 */
	toIR(ctx, map) {
		// import statements only have a scoping function and don't do anything to the IR
	}
}

/**
 * `import <ModuleName>`
 * @internal
 */
export class ImportModuleStatement extends Statement {
	/**
	 * @type {Map<string, EvalEntity>}
	 */
	#imported;

	/**
	 * @param {Site} site 
	 * @param {Word} moduleName
	 */
	constructor(site, moduleName) {
		super(site, moduleName);
		this.#imported = new Map();
	}

	/**
	 * @type {Word}
	 */
	get moduleName() {
		return this.name;
	}

	/**
	 * @param {ModuleScope} scope
	 * @returns {null | EvalEntity}
	 */
	evalInternal(scope) {
		const importedScope = scope.getScope(this.name);

		if (!importedScope) {
			return null;
		}
		
		/**
		 * @type {NamespaceMembers}
		 */
		const namespaceMembers = {};

		for (let [name, entity] of importedScope.values) {
			if (!(entity instanceof Scope)) {
				namespaceMembers[name.value] = entity;
			}
		}

		return new ModuleNamespace(namespaceMembers);
	}

	/**
	 * @param {ModuleScope} scope 
	 */
	eval(scope) {
		let v = this.evalInternal(scope);

		if (v) {
			scope.set(this.name, v);
		}
	}

	/**
	 * Do nothing
	 * @param {string} namespace 
	 * @param {(name: string, cs: ConstStatement) => void} callback 
	 */
	loopConstStatements(namespace, callback) {
	}

	/**
	 * @param {ToIRContext} ctx
	 * @param {IRDefinitions} map 
	 */
	toIR(ctx, map) {
		// import statements only have a scoping function and don't do anything to the IR
	}
}

/**
 * Const value statement
 * @internal
 */
export class ConstStatement extends Statement {
	/**
	 * @type {Expr}
	 */
	#typeExpr;

	/**
	 * @type {null | Expr}
	 */
	#valueExpr;

	/**
	 * @param {Site} site 
	 * @param {Word} name 
	 * @param {Expr} typeExpr - can be null in case of type inference
	 * @param {null | Expr} valueExpr 
	 */
	constructor(site, name, typeExpr, valueExpr) {
		super(site, name);
		this.#typeExpr = typeExpr;
		this.#valueExpr = valueExpr;
	}

	/**
	 * @type {DataType}
	 */
	get type() {
		return assertDefined(this.#typeExpr.cache?.asDataType, this.#typeExpr.cache?.toString() ?? this.#typeExpr.toString());
	}

	/**
	 * Include __const prefix in path so that mutual recursion injection isn't applied
	 * @type {string}
	 */
	get path() {
		return `__const${super.path}`;
	}

	/**
	 * @returns {boolean}
	 */
	isSet() {
		return this.#valueExpr !== null;
	}

	/**
	 * Use this to change a value of something that is already typechecked.
	 * @param {UplcData} data
	 */
	changeValueSafe(data) {
		const type = this.type;
		const site = this.#valueExpr ? this.#valueExpr.site : this.site;

		this.#valueExpr = new LiteralDataExpr(site, type, data);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `const ${this.name.toString()}${this.#typeExpr.toString()}${this.#valueExpr ? ` = ${this.#valueExpr.toString()}` : ""};`;
	}

	/**
	 * @param {Scope} scope 
	 * @returns {null | DataType}
	 */
	evalType(scope) {
		return this.#typeExpr.evalAsDataType(scope);
	}

	/**
	 * @param {Scope} scope 
	 * @returns {null | EvalEntity}
	 */
	evalInternal(scope) {
		const type = this.#typeExpr.evalAsDataType(scope);

		if (!type) {
			return null;
		}

		if (this.#valueExpr) {
			const value = this.#valueExpr.evalAsTyped(scope);

			if (!value) {
				return null;
			}

			if (!type.isBaseOf(value.type)) {
				this.#valueExpr.typeError("wrong type");
				return null;
			}
		}

		return new DataEntity(type);
	}

	/**
	 * Evaluates rhs and adds to scope
	 * @param {TopScope} scope 
	 */
	eval(scope) {
		const data = this.evalInternal(scope);

		if (!data) {
			scope.set(this.name, new NamedEntity(this.name.value, this.path, new DataEntity(new AllType())));
		} else {
			scope.set(this.name, new NamedEntity(this.name.value, this.path, data));
		}
	}

	/**
	 * @param {string} namespace 
	 * @param {(name: string, cs: ConstStatement) => void} callback 
	 */
	loopConstStatements(namespace, callback) {
		callback(`${namespace}${this.name.value}`, this);
	}

	/**
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIRInternal(ctx) {
		let ir = assertDefined(this.#valueExpr).toIR(ctx);

		if (this.#valueExpr instanceof LiteralDataExpr) {
			/*ir = new IR([
				new IR(`${this.#valueExpr.type.path}__from_data`),
				new IR("(", this.site),
				ir,
				new IR(")")
			]);*/

			ir = IR.new`${this.#valueExpr.type.path}__from_data${null}(${this.site}${ir})`;
		}

		return ir;
	}

	/**
	 * @param {ToIRContext} ctx
	 * @param {IRDefinitions} map 
	 */
	toIR(ctx, map) {
		if (this.#valueExpr) {
			map.set(this.path, this.toIRInternal(ctx));
		}
	}
}


/**
 * @internal
 */
export class TypeParameter {
	#name;
	#typeClassExpr;

	/**
	 * @param {Word} name 
	 * @param {null | Expr} typeClassExpr 
	 */
	constructor(name, typeClassExpr) {
		this.#name = name;
		this.#typeClassExpr = typeClassExpr;
	}

	/**
	 * @type {string}
	 */
	get name() {
		return this.#name.value;
	}

	/**
	 * @type {TypeClass}
	 */
	get typeClass() {
		if (this.#typeClassExpr) {
			return assertDefined(this.#typeClassExpr.cache?.asTypeClass);
		} else {
			return new DefaultTypeClass();
		}
	}

	/**
	 * @param {Scope} scope 
	 * @param {string} path
	 * @returns {null | Parameter}
	 */
	eval(scope, path) {
		/**
		 * @type {TypeClass}
		 */
		let typeClass = new DefaultTypeClass();

		if (this.#typeClassExpr) {
			const typeClass_ = this.#typeClassExpr.eval(scope);
			if (!typeClass_) {
				return null;
			}

			if (!typeClass_.asTypeClass) {
				this.#typeClassExpr?.typeError("not a typeclass");
				return null;
			} else {
				typeClass = typeClass_.asTypeClass;
			}
		}

		const parameter = new Parameter(this.name, path, typeClass);

		scope.set(this.#name, typeClass.toType(this.#name.value, path, parameter));

		return parameter;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		if (this.#typeClassExpr) {
			return `${this.#name}: ${this.#typeClassExpr.toString()}`;
		} else {
			return `${this.#name}`;
		}
	}
}

/**
 * @internal
 */
export class TypeParameters {
	#parameterExprs;
	#prefix;

	/**
	 * @type {null | Parameter[]}
	 */
	#parameters;

	/**
	 * @param {TypeParameter[]} parameterExprs 
	 * @param {boolean} isForFunc
	 */
	constructor(parameterExprs, isForFunc) {
		this.#parameterExprs = parameterExprs;
		this.#prefix = isForFunc ? FTPP : TTPP;
		this.#parameters = null;
	}

	/**
	 * @returns {boolean}
	 */
	hasParameters() {
		return this.#parameterExprs.length > 0;
	}

	/** 
	 * @type {string[]}
	 */
	get parameterNames() {
		return this.#parameterExprs.map(pe => pe.name);
	}

	/**
	 * @returns {Parameter[]}
	 */
	getParameters() {
		return assertDefined(this.#parameters, "parameters not yet evaluated");
	}

	/**
	 * Always include the braces, even if there aren't any type parameters, so that the mutual recursion injection function has an easier time figuring out what can depend on what
	 * @param {string} base
	 * @returns {string}
	 */
	genTypePath(base) {
		return `${base}[${this.#parameterExprs.map((_, i) => `${this.#prefix}${i}`).join("@")}]`;
	}

	/**
	 * Always include the braces, even if there aren't any type parameters, so that the mutual recursion injection function has an easier time figuring out what can depend on what
	 * @param {string} base
	 * @returns {string}
	 */
	genFuncPath(base) {
		if (this.hasParameters()) {
			return this.genTypePath(base);
		} else {
			return base;
		}
	}

	/**
	 * @returns {string}
	 */
	toString() {
		if (!this.hasParameters()) {
			return "";
		} else {
			return `[${this.#parameterExprs.map(p => p.toString()).join(", ")}]`;
		}
	}

	/**
	 * @param {Scope} scope 
	 * @returns {Scope}
	 */
	evalParams(scope) {
		const subScope = new Scope(scope);

		this.#parameters = [];

		this.#parameterExprs.forEach((pe, i) => {
			const p = pe.eval(subScope, `${this.#prefix}${i}`);

			if (p) {
				this.#parameters?.push(p);
			}
		});

		return subScope;
	}

	/**
	 * @param {Scope} scope 
	 * @param {(scope: Scope) => (null | FuncType)} evalConcrete
	 * @returns {null | ParametricFunc | FuncType}
	 */
	evalParametricFuncType(scope, evalConcrete, impl = null) {
		const typeScope = this.evalParams(scope);

		const type = evalConcrete(typeScope);

		if (!type) {
			return null;
		}

		typeScope.assertAllUsed();

		return this.hasParameters() ? new ParametricFunc(this.getParameters(), type) : type;
	}

	/**
	 * @param {Scope} scope 
	 * @param {(scope: Scope) => (null | FuncType)} evalConcrete 
	 * @returns {null | EvalEntity}
	 */
	evalParametricFunc(scope, evalConcrete) {
		const type = this.evalParametricFuncType(scope, evalConcrete);

		if (!type) {
			return null;
		}

		if (type.asType) {
			return type.asType.toTyped();
		} else {
			return type;
		}
	}

	/**
	 * @param {Scope} scope
	 * @param {Site} site
	 * @param {(scope: Scope) => DataType} evalConcrete
	 * @returns {[DataType | ParametricType, Scope]}
	 */
	createParametricType(scope, site, evalConcrete) {
		const typeScope = this.evalParams(scope);

		const type = evalConcrete(new Scope(typeScope));

		if (!this.hasParameters()) {
			return [type, typeScope];
		} else {
			const paramType = new ParametricType({
				name: type.name,
				parameters: this.getParameters(),
				apply: (paramTypes) => {
					/**
					 * @type {InferenceMap}
					 */
					const map = new Map();

					paramTypes.forEach((pt, i) => {
						const p = this.getParameters()[i];

						map.set(p, pt);
					});

					const appliedType = assertDefined(type.infer(site, map, null).asDataType);

					const appliedPath = IRParametricName.parse(type.path, true).toImplementation(paramTypes.map(pt => assertDefined(pt.asDataType).path)).toString();

					if (appliedType instanceof GenericType) {
						return appliedType.changeNameAndPath(
							`${type.name}[${paramTypes.map(pt => pt.toString()).join(",")}]`,
							appliedPath
						);
					} else {
						throw new Error("unexpected");
					}
				}
			});

			return [paramType, typeScope];
		}
	}
}

/**
 * Single field in struct or enum member
 * @internal
 */
export class DataField extends NameTypePair {
	#tag;

	/**
	 * @param {Word} name 
	 * @param {Expr} typeExpr 
	 * @param {null | StringLiteral} tag
	 */
	constructor(name, typeExpr, tag = null) {
		super(name, typeExpr);
		this.#tag = tag;
	}

	/**
	 * Throws an error if called before evalType()
	 * @type {DataType}
	 */
	get type() {
		return assertDefined(super.type.asDataType);
	}

	/**
	 * @returns {boolean}
	 */
	hasTag() {
		return this.#tag !== null;
	}

	/**
	 * @type {string}
	 */
	get tag() {
		return this.#tag ? this.#tag.value : this.name.value;
	}

	/**
	 * Evaluates the type, used by FuncLiteralExpr and DataDefinition
	 * @param {Scope} scope 
	 * @returns {null | DataType}
	 */
	eval(scope) {
		if (this.typeExpr === null) {
			throw new Error("typeExpr not set in " + this.site.src.raw.split("\n")[0]);
		} else {
			const t = this.typeExpr.eval(scope);

			if (!t) {
				return null;
			}

			if (t.asDataType) {
				const dt = t.asDataType;

				if (isDataType(dt)) {
					return dt;
				}
			}

			this.typeExpr.typeError(`'${t.toString()}' isn't a valid data field type`);
			return null;
		}
	}
}

/**
 * Base class for struct and enum member
 * @internal
 */
export class DataDefinition {
	#site;
	#name;
	#fields;

	/**
	 * @param {Site} site 
	 * @param {Word} name 
	 * @param {DataField[]} fields 
	 */
	constructor(site, name, fields) {
		this.#site = site;
		this.#name = name;
		this.#fields = fields;
	}

	/**
	 * @type {Site}
	 */
	get site() {
		return this.#site;
	}

	/**
	 * @type {Word}
	 */
	get name() {
		return this.#name;
	}

	/**
	 * @type {DataField[]}
	 */
	get fields() {
		return this.#fields.slice();
	}

	hasTags() {
		return this.#fields.some(f => f.hasTag());
	}

	/**
	 * Returns index of a field.
	 * Returns -1 if not found.
	 * @param {Word} name 
	 * @returns {number}
	 */
	findField(name) {
		let found = -1;
		let i = 0;
		for (let f of this.#fields) {
			if (f.name.toString() == name.toString()) {
				found = i;
				break;
			}
			i++;
		}

		return found;
	}

	/**
	 * @type {string[]}
	 */
	get fieldNames() {
		return this.#fields.map(f => f.name.value);
	}

	/**
	 * @param {Word} name 
	 * @returns {boolean}
	 */
	hasField(name) {
		return this.findField(name) != -1;
	}

	/**
	 * @param {Word} name 
	 * @returns {boolean}
	 */
	hasMember(name) {
		return this.hasField(name) || name.value == "copy";
	}

	/**
	 * @returns {string}
	 */
	toStringFields() {
		return `{${this.#fields.map(f => f.toString()).join(", ")}}`;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `${this.name.toString()} ${this.toStringFields()}`;
	}

	/**
	 * @param {Scope} scope 
	 * @returns {InstanceMembers}
	 */
	evalFieldTypes(scope) {
		/**
		 * @type {InstanceMembers}
		 */
		const fields = {};

		for (let f of this.#fields) {
			const f_ = f.eval(scope);

			if (f_) {
				fields[f.name.value] = f_;
			}
		}

		return fields;
	}

	/**
	 * @param {Type} self
	 * @returns {Type}
	 */
	genCopyType(self) {
		return new FuncType(this.#fields.map(f => new ArgType(f.name, f.type, true)), self);
	}

	/**
	 * @type {number}
	 */
	get nFields() {
		return this.#fields.length;
	}

	/**
	 * @param {number} i 
	 * @returns {DataType}
	 */
	getFieldType(i) {
		return this.#fields[i].type;
	}

	/**
	 * @param {string} name 
	 * @returns {number}
	 */
	getFieldIndex(name) {
		const i = this.findField(new Word(Site.dummy(), name));

		if (i == -1) {
			throw new Error(`field ${name} not find in ${this.toString()}`);
		} else {
			return i;
		}
	}

	/**
	 * @param {number} i
	 * @returns {string}
	 */
	getFieldName(i) {
		return this.#fields[i].name.toString();
	}

	/**
	 * Gets insance member value.
	 * @param {Type} self
	 * @returns {InstanceMembers}
	 */
	genInstanceMembers(self) {
		const members = {
			...genCommonInstanceMembers(self),
			copy: new FuncType(this.#fields.map(f => new ArgType(f.name, f.type, true)), self),
		};

		for (let f of this.fields) {
			members[f.name.value] = f.type;
		}

		return members;
	}

	/**
	 * @param {Type} self
	 * @returns {TypeMembers}
	 */
	genTypeMembers(self) {
		return {
			...genCommonTypeMembers(self)
		};
	}

	/**
	 * @returns {[string, string, NamedTypeSchema[]]}
	 */
	genTypeDetails() {
		const inputTypeParts = [];
		const outputTypeParts = [];
		const internalTypeParts = [];

		this.fieldNames.forEach((fn, i) => {
			const ftd = assertDefined(this.getFieldType(i).typeDetails);
			inputTypeParts.push(`${fn}: ${ftd.inputType}`);
			outputTypeParts.push(`${fn}: ${ftd.outputType}`);
			internalTypeParts.push({
				...ftd.internalType,
				name: fn
			});
		})

		return [
			`{${inputTypeParts.join(", ")}}`,
			`{${outputTypeParts.join(", ")}}`,
			internalTypeParts	
		];
	}

	/**
	 * @param {any} obj
	 * @param {JsToUplcHelpers} helpers
	 * @return {Promise<UplcData[]>}
	 */
	async jsFieldsToUplc(obj, helpers) {
		/**
		 * @type {Promise<UplcData>[]}
		 */
		const fields = [];

		if (Object.keys(obj).length == this.nFields && Object.keys(obj).every(k => this.hasField(new Word(Site.dummy(), k)))) {
			this.fieldNames.forEach((fieldName, i) => {
				const arg = assertDefined(obj[fieldName]);

				const fieldType = this.getFieldType(i);

				if (!fieldType.typeDetails) {
					throw new Error(`typeDetails for ${fieldType.name} not yet implemented`);
				}

				fields.push(fieldType.jsToUplc(arg, helpers));
			});
		} else {
			throw new Error(`expected ${this.nFields} args, got ${Object.keys(obj).length}`);
		}

		return Promise.all(fields);
	}

	/**
	 * Uses field names as keys, not tags
	 * @param {any} obj
	 * @param {JsToUplcHelpers} helpers
	 * @return {Promise<[UplcData, UplcData][]>}
	 */
	async jsMapToUplc(obj, helpers) {
		/**
		 * @type {[UplcData, UplcData][]}
		 */
		const fields = [];

		if (Object.keys(obj).length == this.nFields && Object.keys(obj).every(k => this.hasField(new Word(Site.dummy(), k)))) {
			for (let i = 0; i < this.nFields; i++) {
				const fieldName = this.fieldNames[i];

				const arg = assertDefined(obj[fieldName]);

				const fieldType = this.getFieldType(i);

				if (!fieldType.typeDetails) {
					throw new Error(`typeDetails for ${fieldType.name} not yet implemented`);
				}

				fields.push([
					new ByteArrayData(textToBytes(this.#fields[i].tag)),
					await fieldType.jsToUplc(arg, helpers)
				]);
			};
		} else {
			throw new Error(`expected ${this.nFields} args, got ${Object.keys(obj).length}`);
		}

		return fields;
	}

	/**
	 * @param {UplcData[]} fields 
	 * @param {UplcToJsHelpers} helpers
	 * @returns {Promise<any>}
	 */
	async uplcFieldsToJs(fields, helpers) {
		const obj = {};

		for (let i = 0; i < fields.length; i++) {
			const f = fields[i];

			const fn = this.getFieldName(i);

			obj[fn] = await this.getFieldType(i).uplcToJs(f, helpers);
		};

		return obj;
	}

	/**
	 * For Cip68-tagged structs
	 * @param {[UplcData, UplcData][]} fields 
	 * @param {UplcToJsHelpers} helpers
	 * @returns {Promise<any>}
	 */
	async uplcMapToJs(fields, helpers) {
		const obj = {};

		for (let i = 0; i < this.#fields.length; i++) {
			const f = this.#fields[i];

			const fn = this.getFieldName(i);

			const j = fields.findIndex(([key, value]) => {
				return ByteArrayData.comp(key.bytes, textToBytes(f.tag))
			})

			if (j == -1) {
				throw new Error(`couldn't find field ${f.tag}`)
			}

			obj[fn] = await this.getFieldType(i).uplcToJs(fields[j][1], helpers);
		};

		return obj;
	}

	/**
	 * @param {ToIRContext} ctx
	 * @param {string} path
	 * @param {IRDefinitions} map 
	 * @param {number} constrIndex
	 */
	toIR_new(ctx, path, map, constrIndex) {
		const isConstr = constrIndex != -1;

		/**
		 * @type {IR}
		 */
		let ir;

		if (this.hasTags()) {
			ir = IR.new`__core__mkNilPairData(())`;

			for (let i = this.nFields - 1; i >= 0; i--) {
				const f = this.#fields[i];

				ir = IR.new`__core__mkCons(
					__core__mkPairData(
						__core__bData(#${bytesToHex(textToBytes(f.tag))}),
						${f.type.path}____to_data(${f.name.value})
					),
					${ir}
				)`;
			}

			// TODO: according to https://cips.cardano.org/cips/cip68/#metadata an additional 'extra' (which can be unit)  should be added. Is that really necessary?
			ir = IR.new`__core__constrData(
				0,
				__core__mkCons(
					__core__mapData(${ir}),
					__core__mkCons(
						__core__iData(1),
						__core__mkNilData(())
					)
				)
			)`;

			ir = IR.new`(${new IR(this.#fields.map(f => new IR(f.name.value))).join(", ")}) -> {${ir}}`;
		} else if (this.nFields == 1) {
			if (isConstr) {
				ir = IR.new`(self) -> {
					__core__constrData(${constrIndex}, __helios__common__list_1(${this.getFieldType(0).path}____to_data(self)))
				}${this.site}`;
			} else {
				ir = IR.new`__helios__common__identity`;
		}
		} else {
			ir = IR.new`__core__mkNilData(())`;

			for (let i = this.nFields - 1; i >= 0; i--) {
				const f = this.#fields[i];

				ir = IR.new`__core__mkCons(${f.type.path}____to_data(${f.name.value}), ${ir})`;
			}

			if (isConstr) {
				ir =  IR.new`__core__constrData(${constrIndex}, ${ir})`;
			}

			ir = IR.new`(${new IR(this.#fields.map(f => new IR(f.name.value))).join(", ")}) -> {${ir}}`;
		}

		const key = `${path}____new`;

		map.set(key, ir);
	}

	/**
	 * @internal
	 * @param {ToIRContext} ctx
	 * @param {string} path
	 * @param {IRDefinitions} map 
	 * @param {string[]} getterNames
	 * @param {number} constrIndex
	 */
	toIR_copy(ctx, path, map, getterNames, constrIndex = -1) {
		const key = `${path}__copy`;

		let ir = StructLiteralExpr.toIRInternal(ctx, this.site, path, this.#fields.map(df => new IR(df.name.value)));

		// wrap with defaults

		for (let i = getterNames.length - 1; i >= 0; i--) {
			const fieldName = this.#fields[i].name.toString();

			ir = FuncArg.wrapWithDefaultInternal(ir, fieldName, IR.new`${getterNames[i]}(self)`);
		}

		const args = new IR(this.#fields.map(f => new IR([
			new IR(`__useopt__${f.name.toString()}`),
			new IR(", "),
			new IR(`${f.name.toString()}`)
		]))).join(", ")

		ir = IR.new`(self) -> {
			(${args}) -> {
				${ir}
			}
		}`;

		map.set(key, ir);
	}

	/**
	 * @internal
	 * @param {string} baseName
	 * @param {boolean} isEnumMember
	 * @returns {IR}
	 */
	toIR_show(baseName, isEnumMember = false) {
		if (this.hasTags()) {
			assert(!isEnumMember);
			let ir = IR.new`""`;

			for (let i = 0; i < this.nFields; i++) {
				const f = this.#fields[i];
				const p = f.type.path;

				ir = IR.new`__core__appendString(
					${ir},
					__core__appendString(
						${i > 0 ? `", ${f.name}: "`: `"${f.name}: "`},
						(opt) -> {
							opt(
								(valid, value) -> {
									__core__ifThenElse(
										valid,
										() -> {
											(opt) -> {
												opt(
													(valid, value) -> {
														__core__ifThenElse(
															valid,
															() -> {
																${p}__show(value)()
															},
															() -> {
																"<n/a>"
															}
														)()
													}
												)
											}(${p}__from_data_safe(value))
										},
										() -> {
											"<n/a>"
										}
									)()
								}
							)
						}(__helios__common__cip68_field_safe(self, #${bytesToHex(textToBytes(f.tag))}))
					)
				)`;
			}

			return IR.new`(data) -> {
				__core__chooseData(
					data,
					() -> {
						(fields) -> {
							__core__chooseList(
								fields,
								() -> {"${baseName}{<n/a>}"},
								() -> {
									(data) -> {
										__core__chooseData(
											data,
											() -> {"${baseName}{<n/a>}"},
											() -> {
												(self) -> {
													__core__appendString(
														"${baseName}{",
														__core__appendString(
															${ir},
															"}"
														)
													)
												}(__core__unMapData(data))
											},
											() -> {"${baseName}{<n/a>}"},
											() -> {"${baseName}{<n/a>}"},
											() -> {"${baseName}{<n/a>}"}
										)()
									}(__core__headList__safe(fields))
								}
							)()
						}(__core__sndPair(__core__unConstrData__safe(data)))
					},
					() -> {"${baseName}{<n/a>}"},
					() -> {"${baseName}{<n/a>}"},
					() -> {"${baseName}{<n/a>}"},
					() -> {"${baseName}{<n/a>}"}
				)
			}`;
		} else if (this.nFields == 1 && !isEnumMember) {
			return IR.new`${this.#fields[0].type.path}__show`;
		} else {
			let ir = IR.new`(fields) -> {""}`;

			for (let i = this.nFields - 1; i >= 0; i--) {
				const f = this.#fields[i];
				const p = f.type.path;

				ir = IR.new`(fields) -> {
					__core__chooseList(
						fields,
						() -> {""},
						() -> {
							__core__appendString(
								${i > 0 ? `", ${f.name}: "` : `"${f.name}: "`},
								__core__appendString(
									(opt) -> {
										opt(
											(valid, value) -> {
												__core__ifThenElse(
													valid,
													() -> {
														${p}__show(value)()
													},
													() -> {
														"<n/a>"
													}
												)()
											}
										)
									}(${p}__from_data_safe(__core__headList__safe(fields))),
									${ir}(__core__tailList__safe(fields))
								)
							)
						}
					)()
				}`;
			}

			return IR.new`(self) -> {
				() -> {
					__core__appendString(
						"${baseName}{",
						__core__appendString(
							${ir}(self),
							"}"
						)
					)
				}
			}`;
		}
	}

	/**
	 * @internal
	 * @returns {IR}
	 */
	toIR_is_valid_data() {
		if (this.hasTags()) {
			const fields = this.#fields;

			let ir = IR.new``;

			fields.forEach((f, i) => {
				if (i == 0) {
					ir = IR.new`__helios__common__test_cip68_field(
						data,
						__core__bData(#${bytesToHex(textToBytes(f.tag))}),
						${f.type.path}__is_valid_data	
					)`;
				} else {
					ir = IR.new`__core__ifThenElse(
						__helios__common__test_cip68_field(
							data,
							__core__bData(#${bytesToHex(textToBytes(f.tag))}),
							${f.type.path}__is_valid_data	
						),
						() -> {
							${ir}
						},
						() -> {
							false
						}
					)()`;
				}
			});

			return IR.new`(data) -> {
				${ir}
			}`;
		} else if (this.nFields == 1) {
			return IR.new`${this.#fields[0].type.path}__is_valid_data`;
		} else {
			const reversedFields = this.#fields.slice().reverse();

			let ir = IR.new`(fields) -> {
				__core__chooseList(
					fields,
					true,
					false
				)
			}`;

			reversedFields.forEach(f => {
				ir = IR.new`(fields) -> {
					__core__chooseList(
						fields,
						() -> {
							false
						},
						() -> {
							(head) -> {
								__core__ifThenElse(
									${f.type.path}__is_valid_data(head),
									() -> {${ir}(__core__tailList__safe(fields))},
									() -> {false}
								)()
							}(__core__headList__safe(fields))
						}
					)()
				}`;
			});

			return IR.new`(data) -> {
				__core__chooseData(
					data,
					() -> {false},
					() -> {false},
					() -> {
						${ir}(__core__unListData__safe(data))
					},
					() -> {false},
					() -> {false}
				)()
			}`;
		}
	}

	/**
	 * @internal
	 * @param {string} path
	 * @returns {IR}
	 */
	toIR_from_data_fields(path) {
		if (this.hasTags()) {

			//let ir = IR.new`(data) -> {__core__mkNilPairData(())}`;

			let ir = IR.new(`(data) -> {
				(ignore) -> {
					data
				}(
					__core__ifThenElse(
						${path}__is_valid_data(data),
						() -> {
							()
						},
						() -> {
							__core__trace("Warning: invalid ${this.name.toString()} data", ())
						}
					)()
				)
			}`);
			/*for (let i = this.nFields - 1; i >= 0; i--) {
				const f = this.#fields[i]
				const ftPath = f.type.path;

				ir = IR.new`(data) -> {
					__core__mkCons(
						__core__mkPairData(
							__core__bData(#${bytesToHex(textToBytes(f.tag))}),
							${ftPath}____to_data(
								${ftPath}__from_data(
									__helios__common__cip68_field(
										data, 
										#${bytesToHex(textToBytes(f.tag))}
									)
								)
							)
						),
						${ir}(data)
					)
				}`;
			}

			ir = IR.new`(data) -> {
				__core__constrData(
					0, 
					__core__mkCons(
						__core__mapData(${ir}(data)),
						__core__mkCons(
							__core__iData(1),
							__core__mkNilData(())
						)
					)
				)
			}`;*/

			return ir;
		} else {
			let ir = IR.new(`(fields) -> {
				(ignore) -> {
					fields
				}(
					__core__ifThenElse(
						${path}__is_valid_data(__core__listData(fields)),
						() -> {
							()
						},
						() -> {
							__core__trace("Warning: invalid ${this.name.toString()} data", ())
						}
					)()
				)
			}`)

			return ir;

			/*let ir = IR.new`(fields) -> {__core__mkNilData(())}`;

			for (let i = this.nFields - 1; i >= 0; i--) {
				const ftPath = this.getFieldType(i).path;

				ir = IR.new`(fields) -> {
					__core__mkCons(
						${ftPath}____to_data(
							${ftPath}__from_data(
								__core__headList(fields)
							)
						), 
						${ir}(__core__tailList(fields))
					)
				}`;
			}

			return ir;*/
		}
	}

	/**
	 * Doesn't return anything, but sets its IRdef in the map
	 * @param {ToIRContext} ctx
	 * @param {string} path
	 * @param {IRDefinitions} map
	 * @param {number} constrIndex
	 */
	toIR(ctx, path, map, constrIndex) {
		/**
		 * @type {string[]}
		 */
		const getterNames = [];

		if (this.hasTags()) {
			for (let i = 0; i < this.#fields.length; i++) {
				const f = this.#fields[i];
				const key = `${path}__${f.name.value}`;

				// equalsData is much more efficient than first converting to byteArray
				const getter = IR.new`(self) -> {${f.type.path}__from_data(__helios__common__cip68_field(self, #${bytesToHex(textToBytes(f.tag))}))}`;

				map.set(key, getter);
				getterNames.push(key);
			}
		} else {
			const isConstr = constrIndex != -1;

			const getterBaseName = isConstr ? "__helios__common__enum_field" : "__helios__common__struct_field";

			if (this.fields.length == 1 && !isConstr) {
				const f = this.fields[0];
				const key = `${path}__${f.name.value}`;

				const getter =  IR.new`__helios__common__identity${f.site}`;
				
				map.set(key, getter);

				getterNames.push(key);
			} else {
				// add a getter for each field
				for (let i = 0; i < this.#fields.length; i++) {
					let f = this.#fields[i];
					let key = `${path}__${f.name.value}`;
					getterNames.push(key);

					/**
					 * @type {IR}
					 */
					let getter;

					if (i < 20) {
						getter = IR.new`(self) ${null}->${f.site} {
							${f.type.path}__from_data(${getterBaseName}_${i}(self))
						}`
					} else {
						let inner = new IR("self");

						if (isConstr) {
							inner = IR.new`__core__sndPair(__core__unConstrData(${inner}))`;
						}

						for (let j = 0; j < i; j++) {
							inner = IR.new`__core__tailList(${inner})`;
						}

						inner = IR.new`${f.type.path}__from_data(__core__headList(${inner}))`

						getter = IR.new`(self) ${null}->${f.site} {${inner}}`;
					}

					map.set(key, getter)
				}
			}
		}

		this.toIR_new(ctx, path, map, constrIndex);
		this.toIR_copy(ctx, path, map, getterNames);
	}
}

/**
 * Struct statement
 * @internal
 */
export class StructStatement extends Statement {
	#parameters;
	#dataDef;
	#impl;

	/**
	 * @param {Site} site
	 * @param {Word} name
	 * @param {TypeParameters} parameters
	 * @param {DataField[]} fields 
	 * @param {ImplDefinition} impl
	 */
	constructor(site, name, parameters, fields, impl) {
		super(site, name);

		this.#parameters = parameters;
		this.#dataDef = new DataDefinition(this.site, name, fields);
		this.#impl = impl;
	}

	get path() {
		return this.#parameters.genTypePath(super.path);
	}

	/**
	 * @param {string} basePath 
	 */
	setBasePath(basePath) {
		super.setBasePath(basePath);

		this.#impl.setBasePath(this.path);
	}

	/**
	 * @returns {HeliosDataClass<HeliosData>}
	 */
	genOffChainType() {
		const statement = this;

		class Struct extends HeliosData {
			/**
			 * So we can access fields by index
			 * @type {HeliosData[]}
			 */
			#fields;

			/**
			 * @param  {...any} args
			 */
			constructor(...args) {
				super();

				this.#fields = [];

				if (args.length == 1 && (typeof args[0] == "object") && Object.keys(args[0]).length == statement.#dataDef.nFields && Object.keys(args[0]).every(k => statement.#dataDef.hasField(new Word(Site.dummy(), k)))) {
					statement.#dataDef.fieldNames.forEach((fieldName, i) => {
						const arg = assertDefined(args[0][fieldName]);

						const fieldType = statement.#dataDef.getFieldType(i);

						if (!fieldType.offChainType) {
							throw new Error(`offChainType for ${fieldType.name} not yet implemented`);
						}

						const FieldClass = fieldType.offChainType;

						const instance = arg instanceof FieldClass ? arg : new FieldClass(arg);

						this.#fields.push(instance);
						this[fieldName] = instance;
					});
				} else if (args.length != statement.#dataDef.nFields) {
					throw new Error(`expected ${statement.#dataDef.nFields} args, got ${args.length}`);
				} else {
					args.forEach((arg, i) => {
						const fieldName = statement.#dataDef.getFieldName(i);
						const fieldType = statement.#dataDef.getFieldType(i);

						if (!fieldType.offChainType) {
							throw new Error(`offChainType for ${fieldType.name} not yet implemented`);
						}

						const FieldClass = fieldType.offChainType;

						const instance = arg instanceof FieldClass ? arg : new FieldClass(arg);

						this.#fields.push(instance);
						this[fieldName] = instance;
					});
				}
			}

			/**
			 * Overload 'instanceof' operator
			 * @param {any} other 
			 * @returns {boolean}
			 */
			static [Symbol.hasInstance](other) {
				return (other._structStatement === statement) && (other instanceof HeliosData);
			}

			/**
			 * @type {StructStatement}
			 */
			get _structStatement() {
				return statement;
			}

			/**
			 * @type {HeliosData[]}
			 */
			get _fields() {
				return this.#fields;
			}

			/**
			 * @returns {UplcData}
			 */
			_toUplcData() {
				if (this.#fields.length == 1) {
					return this.#fields[0]._toUplcData();
				} else {
					return new ListData(this.#fields.map(f => f._toUplcData()));
				}
			}

			/**
			 * @param {string | number[]} bytes 
			 * @returns {Struct}
			 */
			static fromUplcCbor(bytes) {
				return Struct.fromUplcData(UplcData.fromCbor(bytes));
			}

			/**
			 * @param {UplcData} data 
			 * @returns {Struct}
			 */
			static fromUplcData(data) {
				const dataItems = data.list;

				if (dataItems.length != statement.#dataDef.nFields) {
					throw new Error("unexpected number of fields");
				}

				const args = dataItems.map((item, i) => {
					return assertDefined(statement.#dataDef.getFieldType(i).offChainType).fromUplcData(item);
				});

				return new Struct(...args);
			}

			/**
			 * @returns {boolean}
			 */
			static isBuiltin() {
				return false;
			}
		}

		Object.defineProperty(Struct, "name", {value: this.name, writable: false});		

		return Struct;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `struct ${this.name.toString()}${this.#parameters.toString()} ${this.#dataDef.toStringFields()}`;
	}

	/**
	 * Evaluates own type and adds to scope
	 * @param {TopScope} scope 
	 */
	eval(scope) {
		const [type, typeScope] = this.#parameters.createParametricType(scope, this.site, (typeScope) => {
			const props = {
				fieldNames: this.#dataDef.fieldNames,
				name: this.name.value,
				path: this.path, // includes template parameters
				genTypeDetails: (self) => {
					const [inputType, outputType, internalTypeFields] = this.#dataDef.genTypeDetails();

					return {
						inputType: inputType,
						outputType: outputType,
						internalType: {
							type: "Struct",
							fieldTypes: internalTypeFields
						}
					};
				},
				jsToUplc: async (obj, helpers) => {
					if (this.#dataDef.hasTags()) {
						const pairs = await this.#dataDef.jsMapToUplc(obj, helpers);

						return new ConstrData(0, [new MapData(pairs), new IntData(1n)]);
					} else {
						/**
						 * @type {UplcData[]}
						 */
						const fields = await this.#dataDef.jsFieldsToUplc(obj, helpers);

						if (fields.length == 1) {
							return fields[0];
						} else {
							return new ListData(fields);
						}
					}
				},
				uplcToJs: async (data, helpers) => {
					if (this.#dataDef.hasTags()) {
						return this.#dataDef.uplcMapToJs(data.fields[0].map, helpers);
					} else {
						if (this.#dataDef.nFields == 1) {
							return this.#dataDef.getFieldType(0).uplcToJs(data, helpers);
						} else {
							return this.#dataDef.uplcFieldsToJs(data.list, helpers);
						}
					}
				},
				genOffChainType: () => this.genOffChainType(),
				genInstanceMembers: (self) => ({
					...genCommonInstanceMembers(self),
					...this.#dataDef.evalFieldTypes(typeScope),
					...this.#impl.genInstanceMembers(typeScope),
					copy: this.#dataDef.genCopyType(self)
				}),
				genTypeMembers: (self) => ({
					...genCommonTypeMembers(self),
					...this.#impl.genTypeMembers(typeScope)
				})
			};

			if (this.#parameters.hasParameters()) {
				return new GenericParametricType(props);
			} else {
				return new GenericType(props);
			}
		});

		const path = this.#parameters.hasParameters() ? super.path : this.path;
		
		scope.set(this.name, new NamedEntity(this.name.value, path, type));

		void this.#dataDef.evalFieldTypes(typeScope);

		typeScope.assertAllUsed();

		this.#impl.eval(typeScope);
	}

	/**
	 * @param {string} namespace 
	 * @param {(name: string, cs: ConstStatement) => void} callback 
	 */
	loopConstStatements(namespace, callback) {
		this.#impl.loopConstStatements(`${namespace}${this.name.value}::`, callback);
	}

	/**
	 * @param {ToIRContext} ctx
	 * @param {IRDefinitions} map
	 */
	toIR(ctx, map) {
		map.set(`${this.path}__is_valid_data`, this.#dataDef.toIR_is_valid_data());

		if (this.#dataDef.hasTags()) {
			map.set(`${this.path}____eq`, IR.new`__helios__common____eq${this.site}`);
			map.set(`${this.path}____neq`, IR.new`__helios__common____neq${this.site}`);
			map.set(`${this.path}__serialize`, IR.new`__helios__common__serialize${this.site}`);
			map.set(`${this.path}____to_data`, IR.new`__helios__common__identity${this.site}`);

			if (config.CHECK_CASTS && !ctx.simplify) {
				map.set(`${this.path}__from_data`, IR.new`(data) -> {
					(ignore) -> {
						data
					}(
						__core__ifThenElse(
							${this.path}__is_valid_data(data),
							() -> {
								()
							},
							() -> {
								__core__trace("Warning: invalid ${this.name.toString()} data", ())
							}
						)()
					)
				}${this.site}`);
			} else {
				map.set(`${this.path}__from_data`, IR.new`__helios__common__identity${this.site}`);
			}

			map.set(`${this.path}__from_data_safe`, IR.new`__helios__option__SOME_FUNC${this.site}`);
		} else {
			const implPath = this.#dataDef.nFields == 1 ? this.#dataDef.getFieldType(0).path : "__helios__struct";

			map.set(`${this.path}____eq`, IR.new`${implPath}____eq${this.site}`);
			map.set(`${this.path}____neq`, IR.new`${implPath}____neq${this.site}`);
			map.set(`${this.path}__serialize`, IR.new`${implPath}__serialize${this.site}`);

			// the from_data method can include field checks
			if (this.#dataDef.fieldNames.length == 1 || (!(config.CHECK_CASTS && !ctx.simplify))) {
				map.set(`${this.path}__from_data`, IR.new`${implPath}__from_data${this.site}`);
			} else {
				map.set(`${this.path}__from_data`, IR.new`(data) -> {
					(ignore) -> {
						__core__unListData(data)
					}(
						__core__ifThenElse(
							${this.path}__is_valid_data(data),
							() -> {
								()
							},
							() -> {
								__core__trace("Warning: invalid ${this.name.toString()} data", ())
							}
						)()
					)
				}${this.site}`);
			}
			if (this.#dataDef.fieldNames.length == 1) {
				map.set(`${this.path}__from_data_safe`, IR.new`${this.#dataDef.getFieldType(0).path}__from_data_safe${this.site}`);
			} else {
				map.set(`${this.path}__from_data_safe`, IR.new`(data) -> {
					__core__chooseData(
						data,
						() -> {__helios__option__NONE_FUNC},
						() -> {__helios__option__NONE_FUNC},
						() -> {
							__helios__option__SOME_FUNC(__core__unListData__safe(data))
						},
						() -> {__helios__option__NONE_FUNC},
						() -> {__helios__option__NONE_FUNC}
					)()
				}`);
			}

			map.set(`${this.path}____to_data`, new IR(`${implPath}____to_data`, this.site));
		}

		// super.toIR adds __new and copy, which might depend on __to_data, so must come after
		this.#dataDef.toIR(ctx, this.path, map, -1);
		map.set(`${this.path}__show`, this.#dataDef.toIR_show(this.name.value));

		this.#impl.toIR(ctx, map);
	}
}

/**
 * Function statement
 * (basically just a named FuncLiteralExpr)
 * @internal
 */
export class FuncStatement extends Statement {
	#parameters;
	#funcExpr;

	/**
	 * @param {Site} site 
	 * @param {Word} name 
	 * @param {TypeParameters} parameters
	 * @param {FuncLiteralExpr} funcExpr 
	 */
	constructor(site, name, parameters, funcExpr) {
		super(site, name);
		this.#parameters = parameters;
		this.#funcExpr = funcExpr;
	}

	/**
	 * @type {string}
	 */
	get path() {
		return this.#parameters.genFuncPath(super.path,);
	}

	/**
	 * @type {number}
	 */
	get nArgs() {
		return this.#funcExpr.nArgs;
	}

	/**
	 * @type {string[]}
	 */
	get argNames() {
		return this.#funcExpr.argNames;
	}
	
	/**
	 * @type {Type[]}
	 */
	get argTypes() {
		return this.#funcExpr.argTypes;
	}

	/**
	 * @type {string[]}
	 */
	get argTypeNames() {
		return this.#funcExpr.argTypeNames;
	}

	/**
	 * @type {Type}
	 */
	get retType() {
		return this.#funcExpr.retType;
	}

	/**
	 * @type {Site}
	 */
	get retSite() {
		return this.#funcExpr.retExpr.site;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `func ${this.name.toString()}${this.#parameters.toString()}${this.#funcExpr.toString()}`;
	}

	/**
	 * Evaluates a function and returns a func value
	 * @param {Scope} scope 
	 * @param {boolean} isMember functions that are members of structs or enums aren't added to their own internal scope as they are always accessed through member access
	 * @returns {null | EvalEntity}
	 */
	evalInternal(scope, isMember = false) {
		const typed = this.#parameters.evalParametricFunc(scope, (subScope) => {
			const type = this.#funcExpr.evalType(subScope);

			if (!type) {
				return null;
			}

			if (isMember) {
				void this.#funcExpr.evalInternal(subScope);	
			} else {
				const implScope = new Scope(subScope);

				// recursive calls expect func value, not func type
				implScope.set(this.name, new NamedEntity(this.name.value, super.path, type.toTyped()));

				void this.#funcExpr.evalInternal(implScope);
			}

			return type;
		});

		return typed;
	}

	/**
	 * Evaluates type of a funtion.
	 * Separate from evalInternal so we can use this function recursively inside evalInternal
	 * @param {Scope} scope 
	 * @returns {null | ParametricFunc | FuncType}
	 */
	evalType(scope) {
		return this.#parameters.evalParametricFuncType(scope, (subScope) => {
			return this.#funcExpr.evalType(subScope);
		});
	}

	/**
	 * @param {Scope} scope 
	 */
	eval(scope) {
		const typed = this.evalInternal(scope);

		if (typed) {
			assert(!typed.asType);

			scope.set(this.name, new NamedEntity(this.name.value, super.path, typed));
		}
	}

	/**
	 * Do nothing
	 * @param {string} namespace 
	 * @param {(name: string, cs: ConstStatement) => void} callback 
	 */
	loopConstStatements(namespace, callback) {
	}

	/**
	 * Returns IR of function
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIRInternal(ctx) {
		return this.#funcExpr.toIR(ctx);
	}

	/**
	 * @param {ToIRContext} ctx
	 * @param {IRDefinitions} map 
	 */
	toIR(ctx, map) {
		map.set(this.path, this.toIRInternal(ctx));
	}

	/**
	 * @param {Statement} s 
	 * @returns {boolean}
	 */
	static isMethod(s) {
		if (s instanceof FuncStatement) {
			return s.#funcExpr.isMethod();
		} else {
			return false;
		}
	}
}

/**
 * EnumMember defintion is similar to a struct definition
 * @internal
 */
export class EnumMember {
	/** @type {null | EnumStatement} */
	#parent;

	/** @type {null | number} */
	#constrIndex;

	#dataDef;

	/**
	 * @param {Word} name
	 * @param {DataField[]} fields
	 */
	constructor(name, fields) {
		this.#parent = null; // registered later
		this.#constrIndex = null;
		this.#dataDef = new DataDefinition(name.site, name, fields);
	}

	/**
	 * @returns {number}
	 */
	get constrIndex() {
		if (this.#constrIndex === null) {
			throw new Error("constrIndex not set");
		} else {
			return this.#constrIndex;
		}
	}

	/**
	 * @type {Word}
	 */
	get name() {
		return this.#dataDef.name;
	}

	/** 
	 * @param {EnumStatement} parent
	 * @param {number} i
	*/
	registerParent(parent, i) {
		this.#parent = parent;
		this.#constrIndex = i;
	}
	
	/**
	 * @type {EnumStatement}
	 */
	get parent() {
		if (this.#parent === null) {
			throw new Error("parent not yet registered");
		} else {
			return this.#parent;
		}
	}

	/**
	 * @returns {HeliosDataClass<HeliosData>}
	 */
	genOffChainType() {
		const statement = this;

		const enumStatement = statement.parent;

		const index = statement.constrIndex;

		const nFields = statement.#dataDef.nFields;

		/**
		 * @type {[string, DataType][]} - [name, type]
		 */
		const fields = [];

		for (let i = 0; i < nFields; i++) {
			fields.push([statement.#dataDef.getFieldName(i), statement.#dataDef.getFieldType(i)]);
		}

		// similar to Struct
		class EnumVariant extends HeliosData {
			/**
			 * So we can access fields by index
			 * @type {HeliosData[]}
			 */
			#fields;

			/**
			 * @param  {...any} args
			 */
			constructor(...args) {
				super();
				if (args.length != nFields) {
					throw new Error(`expected ${nFields} args, got ${args.length}`);
				}

				this.#fields = [];

				args.forEach((arg, i) => {
					const [fieldName, fieldType] = fields[i];
					const FieldClass = assertDefined(fieldType.offChainType);

					const instance = arg instanceof FieldClass ? arg : new FieldClass(arg);

					this.#fields.push(instance);
					this[fieldName] = instance;

				});
			}

			/**
			 * Overload 'instanceof' operator
			 * @param {any} other 
			 * @returns {boolean}
			 */
			static [Symbol.hasInstance](other) {
				return (other._enumVariantStatement === statement) && (other instanceof HeliosData);
			}

			/**
			 * @type {EnumStatement}
			 */
			get _enumStatement() {
				return enumStatement;
			}

			/**
			 * @type {EnumMember}
			 */
			get _enumVariantStatement() {
				return statement;
			}

			/**
			 * @returns {UplcData}
			 */
			_toUplcData() {
				return new ConstrData(index, this.#fields.map(f => f._toUplcData()));
			}

			/**
			 * @returns {boolean}
			 */
			static isBuiltin() {
				return false;
			}

			/**
			 * @param {string | number[]} bytes 
			 * @returns {EnumVariant}
			 */
			static fromUplcCbor(bytes) {
				return EnumVariant.fromUplcData(UplcData.fromCbor(bytes));
			}

			/**
			 * @param {UplcData} data 
			 * @returns {EnumVariant}
			 */
			static fromUplcData(data) {
				assert(data.index == index, "wrong index");

				const dataItems = data.list;

				if (dataItems.length != nFields) {
					throw new Error("unexpected number of fields");
				}

				const args = dataItems.map((item, i) => {
					return assertDefined(fields[i][1].offChainType).fromUplcData(item);
				});

				return new EnumVariant(...args);
			}
		}

		Object.defineProperty(EnumVariant, "name", {value: this.#dataDef.name, writable: false});

		return EnumVariant;

	}

	/**
	 * @type {DataDefinition}
	 */
	get dataDefinition() {
		return this.#dataDef;
	}

	/**
	 * @param {Scope} scope 
	 */
	evalDataFields(scope) {
		this.#dataDef.evalFieldTypes(scope);
	}

	/**
	 * @param {Scope} scope 
	 * @returns {(parent: DataType) => EnumMemberType}
	 */
	evalType(scope) {
		if (this.#parent === null) {
			throw new Error("parent should've been registered");
		}

		return (parent) => {
			const path = `${parent.path}__${this.#dataDef.name.value}`; 

			const props = {
				name: this.#dataDef.name.value,
				path: path, 
				constrIndex: this.constrIndex,
				genOffChainType: () => this.genOffChainType(),
				parentType: parent,
				fieldNames: this.#dataDef.fieldNames,
				genInstanceMembers: (self) => {
					const res = {
						...genCommonInstanceMembers(self),
						...this.#dataDef.evalFieldTypes(scope),
						copy: this.#dataDef.genCopyType(self)
					}

					return res;
				},
				genTypeMembers: (self) => ({
					...genCommonEnumTypeMembers(self, parent),
				})
			};

			if (this.parent.hasParameters()) {
				return new GenericParametricEnumMemberType(props);
			} else {
				return new GenericEnumMemberType(props);
			}
		};
	}

	get path() {
		return `${this.parent.path}__${this.#dataDef.name.toString()}`;
	}

	/**
	 * @param {ToIRContext} ctx
	 * @param {IRDefinitions} map 
	 */
	toIR(ctx, map) {
		map.set(`${this.path}____eq`, IR.new`__helios__common____eq${this.#dataDef.site}`);
		map.set(`${this.path}____neq`, IR.new`__helios__common____neq${this.#dataDef.site}`);
		map.set(`${this.path}__serialize`, IR.new`__helios__common__serialize${this.#dataDef.site}`);

		map.set(`${this.path}__is_valid_data`, IR.new`(data) -> {
			__core__chooseData(
				data,
				() -> {
					(pair) -> {
						__core__ifThenElse(
							__core__equalsInteger(__core__fstPair(pair), ${this.#constrIndex}),
							() -> {
								${this.#dataDef.toIR_is_valid_data()}(__core__listData(__core__sndPair(pair)))
							},
							() -> {
								false
							}
						)()
					}(__core__unConstrData__safe(data))
				},
				() -> {false},
				() -> {false},
				() -> {false},
				() -> {false}
			)()
		}`);

		if (config.CHECK_CASTS && !ctx.simplify) {
			map.set(`${this.path}__from_data`, IR.new`(data) -> {
				(ignore) -> {
					data
				}(
					__core__ifThenElse(
						${this.path}__is_valid_data(data),
						() -> {
							()
						},
						() -> {
							__core__trace("Warning: invalid ${this.name.toString()} data", ())
						}
					)()
				)
			}`);
		} else {
			map.set(`${this.path}__from_data`, new IR(`(data) -> {
				__helios__common__assert_constr_index(data, ${this.constrIndex})
			}`, this.#dataDef.site));
		}

		map.set(`${this.path}__from_data_safe`, IR.new`(data) -> {
			__core__chooseData(
				data,
				() -> {
					(index) -> {
						__core__ifThenElse(
							__core__equalsInteger(index, ${this.constrIndex}),
							() -> {
								__helios__option__SOME_FUNC(data)
							},
							() -> {
								__helios__option__NONE_FUNC
							}
						)()
					}(__core__fstPair(__core__unConstrData__safe(data)))
				},
				() -> {__helios__option__NONE_FUNC},
				() -> {__helios__option__NONE_FUNC},
				() -> {__helios__option__NONE_FUNC},
				() -> {__helios__option__NONE_FUNC}
			)()
		}`);

		map.set(`${this.path}____to_data`, new IR("__helios__common__identity", this.#dataDef.site));

		// super.toIR adds __new and copy, which might depend on __to_data, so must come after
		this.#dataDef.toIR(ctx, this.path, map, this.constrIndex);

		const longName = (this.#parent?.name?.value ?? "") + "::" + this.name.value;
		map.set(`${this.path}__show`, IR.new`(data) -> {
			__core__chooseData(
				data,
				() -> {
					(fields) -> {
						${this.#dataDef.toIR_show(longName, true)}(fields)()
					}(__core__sndPair(__core__unConstrData__safe(data)))
				},
				() -> {"${longName}{<n/a>}"},
				() -> {"${longName}{<n/a>}"},
				() -> {"${longName}{<n/a>}"},
				() -> {"${longName}{<n/a>}"}
			)
		}`);
	}
}

/**
 * Enum statement, containing at least one member
 * @internal
 */
export class EnumStatement extends Statement {
	#parameters;
	#members;
	#impl;

	/**
	 * @param {Site} site 
	 * @param {Word} name 
	 * @param {TypeParameters} parameters
	 * @param {EnumMember[]} members 
	 * @param {ImplDefinition} impl
	 */
	constructor(site, name, parameters, members, impl) {
		super(site, name);
		this.#parameters = parameters;
		this.#members = members;
		this.#impl = impl;
		
		for (let i = 0; i < this.#members.length; i++) {
			this.#members[i].registerParent(this, i);
		}
	}

	/**
	 * @type {string}
	 */
	get path() {
		return this.#parameters.genTypePath(super.path);
	}

	/**
	 * @returns {boolean}
	 */
	hasParameters() {
		return this.#parameters.hasParameters();
	}

	/**
	 * @param {string} basePath 
	 */
	setBasePath(basePath) {
		super.setBasePath(basePath);

		this.#impl.setBasePath(this.path);
	}

	/**
	 * @internal
	 * @returns {HeliosDataClass<HeliosData>}
	 */
	genOffChainType() {
		const statement = this;

		const nVariants = statement.nEnumMembers;

		/**
		 * @type {HeliosDataClass<HeliosData>[]}
		 */
		const variants = [];

		for (let i = 0; i < nVariants; i++) {
			variants.push(this.#members[i].genOffChainType());
		}

		class Enum extends HeliosData {
			constructor() {
				super();
				throw new Error("can't be constructed (hint: construct an enum)");
			}

			/**
			 * Overload 'instanceof' operator
			 * @param {any} other 
			 * @returns {boolean}
			 */
			static [Symbol.hasInstance](other) {
				return (other._enumStatement === statement) && (other instanceof HeliosData);
			}

			/**
			 * @type {EnumStatement}
			 */
			get _enumStatement() {
				return statement;
			}

			/**
			 * @param {number[] | string} bytes Array of bytes, or hexadecimal representation.
			 * @returns {HeliosData}
			 */
			static fromUplcCbor(bytes) {
				return Enum.fromUplcData(UplcData.fromCbor(bytes));
			}

			/**
			 * @param {UplcData} data 
			 * @returns {HeliosData}
			 */
			static fromUplcData(data) {
				const variant = assertDefined(variants[data.index], "index out of range");

				return variant.fromUplcData(data);
			}

			/**
			 * @returns {boolean}
			 */
			static isBuiltin() {
				return false;
			}
		}

		Object.defineProperty(Enum, "name", {value: this.name, writable: false});

		for (let v of variants) {
			Object.defineProperty(Enum, v.name, {value: v, writable: false});
		}

		return Enum;
	}

	/**
	 * Returns index of enum member.
	 * Returns -1 if not found
	 * @param {Word} name 
	 * @returns {number}
	 */
	// returns an index
	findEnumMember(name) {
		let found = -1;
		let i = 0;
		for (let member of this.#members) {
			if (member.name.toString() == name.toString()) {
				found = i;
				break;
			}
			i++;
		}

		return found;
	}

	/**
	 * @param {number} i
	 * @returns {EnumMember}
	 */
	getEnumMember(i) {
		return assertDefined(this.#members[i]);
	}

	/**
	 * @param {Word} name
	 * @returns {boolean}
	 */
	hasEnumMember(name) {
		return this.findEnumMember(name) != -1;
	}

	/**
	 * @returns {number}
	 */
	get nEnumMembers() {
		return this.#members.length;
	}

	/**
	 * @param {Scope} scope 
	 */
	eval(scope) {
		const [type, typeScope] = this.#parameters.createParametricType(scope, this.site, (typeScope) => {
			/**
			 * @type {{[name: string]: (parent: DataType) => EnumMemberType}}
			 */
			const genFullMembers = {};

			this.#members.forEach(m => {
				genFullMembers[m.name.value] = m.evalType(typeScope);
			});

			const props = {
				name: this.name.value,
				path: this.path,
				genTypeDetails: (self) => {
					const inputEnumTypeParts = [];
					const outputEnumTypeParts = [];
					const internalEnumTypeParts = [];

					this.#members.forEach(member => {
						const [inputType, outputType, internalTypeFields] = member.dataDefinition.genTypeDetails();
						
						inputEnumTypeParts.push(`{"${member.name.value}": ${inputType}}`);
						outputEnumTypeParts.push(`{"${member.name.value}": ${outputType}}`);
						internalEnumTypeParts.push({name: member.name.value, fieldTypes: internalTypeFields});
					});

					return {
						inputType: inputEnumTypeParts.join(" | "),
						outputType: outputEnumTypeParts.join(" | "),
						internalType: {
							type: "Enum",
							variantTypes: internalEnumTypeParts
						}
					};
				},
				jsToUplc: async (obj, helpers) => {
					const memberName = assertDefined(Object.keys(obj)[0]);

					const i = this.#members.findIndex(m => m.name.value == memberName);

					if (i == -1) {
						throw new Error(`invalid ${memberName} of ${this.name.value}`);
					}

					const member = this.#members[i];

					const fields = await member.dataDefinition.jsFieldsToUplc(assertDefined(obj[memberName]), helpers);

					return new ConstrData(i, fields);
				},
				uplcToJs: async (data, helpers) => {
					const i = data.index;

					if (i < 0 || i >= this.#members.length) {
						throw new Error(`enum variant index ${i} out of range`);
					}

					const member = this.#members[i];

					return {
						[member.name.value]: await member.dataDefinition.uplcFieldsToJs(data.fields, helpers)
					};
				},
				genOffChainType: () => this.genOffChainType(),
				genInstanceMembers: (self) => ({
					...genCommonInstanceMembers(self),
					...this.#impl.genInstanceMembers(typeScope),
				}),
				genTypeMembers: (self) => {
					const typeMembers_ = {
						...genCommonTypeMembers(self),
						...this.#impl.genTypeMembers(typeScope)
					};
					
					// TODO: detect duplicates
					for (let memberName in genFullMembers) {
						typeMembers_[memberName] = genFullMembers[memberName](assertDefined(self.asDataType))
					}

					return typeMembers_
				}
			};

			if (this.#parameters.hasParameters()) {
				return new GenericParametricType(props)
			} else {
				return new GenericType(props);
			}
		});

		// don't include type parameters in path (except empty), these are added by application statement
		const path = this.#parameters.hasParameters() ? super.path : this.path;
		
		scope.set(this.name, new NamedEntity(this.name.value, path, type));

		this.#members.forEach(m => {
			m.evalDataFields(typeScope);
		});

		typeScope.assertAllUsed();
		
		this.#impl.eval(typeScope);
	}

	/**
	 * @param {string} namespace 
	 * @param {(name: string, cs: ConstStatement) => void} callback 
	 */
	loopConstStatements(namespace, callback) {
		this.#impl.loopConstStatements(`${namespace}${this.name.value}::`, callback);
	}

	/**
	 * @returns {IR}
	 */
	toIR_is_valid_data() {
		let ir = IR.new`false`;

		this.#members.forEach(m => {
			ir = IR.new`__core__ifThenElse(
				${m.path}__is_valid_data(data),
				() -> {
					true
				},
				() -> {
					${ir}
				}
			)()`;
		});

		return IR.new`(data) -> {
			${ir}
		}`;
	}

	/**
	 * @internal
	 * @returns {IR}
	 */
	toIR_show() {
		const name = this.name.value;

		const last = this.#members[this.#members.length-1];

		let ir = IR.new`${last.path}__show(data)()`;

		for (let i = this.#members.length - 2; i >= 0; i--) {
			const m = this.#members[i];

			ir = IR.new`__core__ifThenElse(
				__core__equalsInteger(index, ${m.constrIndex}),
				() -> {
					${m.path}__show(data)()
				},
				() -> {
					${ir}
				}
			)()`;
		}

		return IR.new`(data) -> {
			__core__chooseData(
				data,
				() -> {
					(index) -> {
						${ir}
					}(__core__fstPair(__core__unConstrData__safe(data)))
				},
				() -> {"${name}{<n/a>}"},
				() -> {"${name}{<n/a>}"},
				() -> {"${name}{<n/a>}"},
				() -> {"${name}{<n/a>}"}
			)
		}`;
	}

	/**
	 * @param {ToIRContext} ctx
	 * @param {IRDefinitions} map 
	 */
	toIR(ctx, map) {
		map.set(`${this.path}____eq`, IR.new`__helios__common____eq${this.site}`);
		map.set(`${this.path}____neq`, IR.new`__helios__common____neq${this.site}`);
		map.set(`${this.path}__serialize`, IR.new`__helios__common__serialize${this.site}`);
		map.set(`${this.path}____to_data`, IR.new`__helios__common__identity${this.site}`);

		map.set(`${this.path}__is_valid_data`, this.toIR_is_valid_data());
		map.set(`${this.path}__show`, this.toIR_show());

		// there could be circular dependencies here, which is ok
		if (config.CHECK_CASTS && !ctx.simplify) {
			map.set(`${this.path}__from_data`, IR.new`(data) -> {
				(ignore) -> {
					data
				}(
					__core__ifThenElse(
						${this.path}__is_valid_data(data),
						() -> {
							()
						},
						() -> {
							__core__trace("Warning: invalid ${this.name.toString()} data", ())
						}
					)()
				)
			}${this.site}`);
		} else {
			map.set(`${this.path}__from_data`, IR.new`__helios__common__identity${this.site}`);
		}

		map.set(`${this.path}__from_data_safe`, IR.new`__helios__option__SOME_FUNC${this.site}`);

		// member __new and copy methods might depend on __to_data, so must be added after
		for (let member of this.#members) {
			member.toIR(ctx, map);
		}

		this.#impl.toIR(ctx, map);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `enum ${this.name.toString()}${this.#parameters.toString()} {${this.#members.map(m => m.toString()).join(", ")}}`;
	}
}

/**
 * Impl statements, which add functions and constants to registry of user types (Struct, Enum Member and Enums)
 * @internal
 */
export class ImplDefinition {
	#selfTypeExpr;
	#statements;

	/**
	 * @param {Expr} selfTypeExpr;
	 * @param {(FuncStatement | ConstStatement)[]} statements 
	 */
	constructor(selfTypeExpr, statements) {
		this.#selfTypeExpr = selfTypeExpr;
		this.#statements = statements;
	}

	/**
	 * @type {Site}
	 */
	get site() {
		return this.#selfTypeExpr.site;
	}

	/**
	 * @param {string} basePath 
	 */
	setBasePath(basePath) {
		for (let s of this.#statements) {
			s.setBasePath(basePath);
		}
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `${this.#statements.map(s => s.toString()).join("\n")}`;
	}

	/**
	 * @param {Scope} scope 
	 * @returns {TypeMembers}
	 */
	genTypeMembers(scope) {
		/**
		 * @type {TypeMembers}
		 */
		const typeMembers = {};

		for (let s of this.#statements) {
			if (s instanceof ConstStatement) {
				const s_ = s.evalType(scope);
				if (s_) {
					typeMembers[s.name.value] = s_.toTyped();
				}
			} else if (!FuncStatement.isMethod(s)) {
				const s_ = s.evalType(scope);

				if (s_) {
					typeMembers[s.name.value] = s_;
				}
			}
		}

		return typeMembers;
	}

	/**
	 * Doesn't add the common types
	 * @param {Scope} scope 
	 * @returns {InstanceMembers}
	 */
	genInstanceMembers(scope) {
		/**
		 * @type {InstanceMembers}
		 */
		const instanceMembers = {};

		for (let s of this.#statements) {
			if (FuncStatement.isMethod(s)) {
				const s_ = s.evalType(scope);

				if (s_) {
					instanceMembers[s.name.value] = s_;
				}
			}
		}

		return instanceMembers;
	}

	/**
	 * @param {Scope} scope 
	 */
	eval(scope) {
		void this.#selfTypeExpr.eval(scope);

		for (let s of this.#statements) {
			if (s instanceof FuncStatement) {
				void s.evalInternal(scope, true);
			} else {
				void s.evalInternal(scope);
			}
		}
	}

	/**
	 * @param {string} namespace 
	 * @param {(name: string, cs: ConstStatement) => void} callback 
	 */
	loopConstStatements(namespace, callback) {
		for (let s of this.#statements) {
			s.loopConstStatements(namespace, callback);
		}
	}
	
	/**
	 * Returns IR of all impl members
	 * @param {ToIRContext} ctx
	 * @param {IRDefinitions} map 
	 */
	toIR(ctx, map) {
		for (let s of this.#statements) {
			s.toIR(ctx, map);
		}
	}
}


//////////////////////////////////
// Section 26: Helios AST building
//////////////////////////////////

const AUTOMATIC_METHODS = [
	"__eq",
	"__neq",
	"copy",
	"show",
	"from_data",
	"from_data_safe",
	"is_valid_data",
	"serialize"
];

/**
 * @type {null | ((path: StringLiteral) => (string | null))}
 */
let importPathTranslator = null

/**
 * Used by VSCode plugin and CLI
 * The sources can't be modified directly because that messes up the codemapping
 * @internal
 * @param {(path: StringLiteral) => (string | null)} fn 
 */
export function setImportPathTranslator(fn) {
	importPathTranslator = fn
}

/**
 * @internal
 * @param {Token[]} ts
 * @returns {Statement[]}
 */
export function buildProgramStatements(ts) {
	/**
	 * @type {Statement[]}
	 */
	let statements = [];

	while (ts.length != 0) {
		const t = ts.shift()?.assertWord();

		if (!t) {
			continue;
		}

		const kw = t.value;

		/**
		 * @type {Statement | (Statement | null)[] | null}
		 */
		let s = null;

		if (kw == "const") {
			s = buildConstStatement(t.site, ts);
		} else if (kw == "struct") {
			s = buildStructStatement(t.site, ts);
		} else if (kw == "func") {
			s = buildFuncStatement(t.site, ts);
		} else if (kw == "enum") {
			s = buildEnumStatement(t.site, ts);
		} else if (kw == "import") {
			s = buildImportStatements(t.site, ts);
		} else {
			t.syntaxError(`invalid top-level keyword '${kw}'`);
		}

		if (s) {
			if (Array.isArray(s)) {
				for (let s_ of s) {
					if (s_) {
						statements.push(s_);
					}
				}
			} else {
				statements.push(s);
			}
		}
	}

	return statements;
}

/**
 * @internal
 * @param {Token[]} ts
 * @param {null | ScriptPurpose} expectedPurpose
 * @returns {[ScriptPurpose, Word] | null} - [purpose, name] (ScriptPurpose is an integer)
 * @internal
 */
export function buildScriptPurpose(ts, expectedPurpose = null) {
	// need at least 2 tokens for the script purpose
	if (ts.length < 2) {

		if (ts.length == 0) {
			Site.dummy().syntaxError("invalid script purpose syntax");
		} else {
			ts[0].syntaxError("invalid script purpose syntax");
			ts.splice(0);
		}

		return null;
	}

	const purposeWord = ts.shift()?.assertWord();

	if (!purposeWord) {
		return null;
	}

	/**
	 * @type {ScriptPurpose | null}
	 */
	let purpose = null;

	if (purposeWord.isWord("spending")) {
		purpose = "spending";
	} else if (purposeWord.isWord("minting")) {
		purpose = "minting";
	} else if (purposeWord.isWord("staking")) {
		purpose = "staking";
	} else if (purposeWord.isWord("testing")) { // 'test' is not reserved as a keyword though
		purpose = "testing";
	} else if (purposeWord.isWord("endpoint")) {
		purpose = "endpoint";
	} else if (purposeWord.isWord("module")) {
		purpose = "module";
	} else if (purposeWord.isKeyword()) {
		purposeWord.syntaxError(`script purpose missing`);

		ts.unshift(purposeWord);

		return null;
	} else {
		purposeWord.syntaxError(`unrecognized script purpose '${purposeWord.value}' (expected 'testing', 'spending', 'staking', 'minting', 'endpoint' or 'module')`);
		purpose = "unknown";
	}

	if (expectedPurpose !== null && purpose !== null) {
		if (expectedPurpose != purpose) {
			purposeWord.syntaxError(`expected '${expectedPurpose}' script purpose`);
		}
	}

	const name = assertToken(ts.shift(), purposeWord.site)?.assertWord()?.assertNotKeyword();

	if (!name) {
		return null;
	}

	if (name.value === "main") {
		name.syntaxError(`${purposeWord.value} script can't be named 'main'`);
	}

	return [purpose, name];
}

/**
 * Also used by VSCode plugin
 * @internal
 * @param {Token[]} ts 
 * @param {null | ScriptPurpose} expectedPurpose 
 * @returns {[null | ScriptPurpose, Word | null, Statement[], number]}
 */
export function buildScript(ts, expectedPurpose = null) {
	const first = ts[0];

	const purposeName = buildScriptPurpose(ts, expectedPurpose);

	const statements = buildProgramStatements(ts);

	let mainIdx = -1;

	const [purpose, name] = purposeName !== null ? purposeName : [null, null];

	if (purpose != "module") {
		mainIdx = statements.findIndex(s => s.name.value === "main");

		if (mainIdx == -1) {
			if (name !== null) {
				first.site.merge(name.site).syntaxError("entrypoint 'main' not found");
			} else {
				first.site.syntaxError("entrypoint 'main' not found");
			}
		}
	}

	return [purpose, name, statements, mainIdx];
}

/**
 * Quickly extract the script purpose header of a script source, by parsing only the minimally necessary characters.
 * @param {string} rawSrc 
 * @returns {null | [ScriptPurpose, string]} Returns `null` if the script header is missing or syntactically incorrect. The first string returned is the script purpose, the second value returned is the script name.
 */
export function extractScriptPurposeAndName(rawSrc) {
	try {
		let src = new Source(rawSrc, "");

		let tokenizer = new Tokenizer(src);

		let gen = tokenizer.streamTokens();

		// Don't parse the whole script, just 'eat' 2 tokens: `<purpose> <name>`
		let ts = [];
		for (let i = 0; i < 2; i++) {
			let yielded = gen.next();
			if (yielded.done) {
				return null;
			}

			ts.push(yielded.value);
		}

		const purposeName = buildScriptPurpose(ts);

		src.throwErrors();

		if (purposeName !== null) {
			const [purpose, name] = purposeName;

			return [purpose, name.value];
		} else {
			throw new Error("unexpected"); // should've been caught above by calling src.throwErrors()
		}
	} catch (e) {
		if (!(e instanceof UserError)) {
			throw e;
		} else {
			return null;
		}
	}
}

/**
 * @internal
 * @param {Site} site 
 * @param {Token[]} ts 
 * @returns {ConstStatement | null}
 */
function buildConstStatement(site, ts) {
	if (ts.length == 0) {
		site.syntaxError("invalid syntax (expected name after 'const')");
		return null;
	}

	const name = assertToken(ts.shift(), site)?.assertWord()?.assertNotKeyword();

	if (!name) {
		return null;
	}

	if (!(ts.length > 0 && ts[0].isSymbol(":"))) {
		site.merge(name.site).syntaxError(`expected type annotation after 'const ${name.value}'`);
		ts.splice(0);
		return null;
	}

	const colon = assertDefined(ts.shift());

	let equalsPos = SymbolToken.find(ts, "=");
	const statementEndPos = Word.find(ts, ["const", "func", "struct", "enum", "import"]);

	let typeEndPos = equalsPos;

	let hasRhs = false;

	if (equalsPos == -1 && statementEndPos == -1) {
		typeEndPos = ts.length;
	} else if (statementEndPos != -1 && (equalsPos == -1 || (equalsPos > statementEndPos))) {
		typeEndPos = statementEndPos;
	} else if (equalsPos == 0) {
		colon.site.merge(ts[0].site).syntaxError("expected type expression between ':' and '='");
		ts.shift();
		return null;
	} else {
		hasRhs = true;
	}

	let endSite = ts[typeEndPos-1].site;

	const typeExpr = buildTypeExpr(colon.site, ts.splice(0, typeEndPos));
	if (!typeExpr) {
		return null;
	}

	/**
	 * @type {null | Expr}
	 */
	let valueExpr = null;

	if (hasRhs) {
		const maybeEquals = ts.shift();

		if (maybeEquals === undefined) {
			site.merge(name.site).syntaxError("expected '=' after 'const'");
			ts.splice(0);
			return null;
		} else if (!maybeEquals.isSymbol("=")) {
			site.merge(maybeEquals.site).syntaxError("expected '=' after 'const'");
			return null;
		} else {
			const equals = maybeEquals.assertSymbol("=");

			if (!equals) {
				return null;
			}

			const nextStatementPos = Word.find(ts, ["const", "func", "struct", "enum", "import"]);

			const tsValue = nextStatementPos == -1 ? ts.splice(0) : ts.splice(0, nextStatementPos);

			if (tsValue.length == 0) {
				equals.syntaxError("expected expression after '='");
				return null;
			} else {
				endSite = tsValue[tsValue.length-1].site;

				valueExpr = buildValueExpr(tsValue);
			}
		}
	}

	return new ConstStatement(site.merge(endSite), name, typeExpr, valueExpr);
}

/**
 * @param {Site} site 
 * @param {Token[]} ts 
 * @returns {RefExpr | null}
 */
function buildTypeClassExpr(site, ts) {
	const name = assertToken(ts.shift(), site, "expected word")?.assertWord()?.assertNotKeyword();
	if (!name) {
		return null;
	}

	return new RefExpr(name);
}

/**
 * @param {Site} site 
 * @param {Token[]} ts 
 * @returns {null | TypeParameter}
 */
function buildTypeParameter(site, ts) {
	const name = assertToken(ts.shift(), site, "expected type parameter name")?.assertWord()?.assertNotKeyword() ?? null;
	if (!name) {
		return null;
	}

	const maybeColon = ts.shift();
	if (!maybeColon) {
		return new TypeParameter(name, null);
	}

	const colon = maybeColon.assertSymbol(":");
	if (!colon) {
		return null;
	}

	const typeClassExpr = buildTypeClassExpr(site, ts);
	if (!typeClassExpr) {
		return null;
	}

	if (ts.length > 0) {
		ts[0].syntaxError("unexpected token");
		return null;
	}

	return new TypeParameter(name, typeClassExpr);
}

/**
 * @param {Token[]} ts 
 * @param {boolean} isForFunc
 * @returns {TypeParameters}
 */
function buildTypeParameters(ts, isForFunc) {
	if (ts.length > 0 && ts[0].isGroup("[")) {
		const brackets = assertDefined(ts.shift()).assertGroup("[");

		if (brackets) {
			/**
			 * @type {TypeParameter[] | null}
			 */
			const params = reduceNull(brackets.fields.map(fts => {
				return buildTypeParameter(brackets.site, fts);
			}));

			if (params) {
				return new TypeParameters(params, isForFunc);
			}			
		}
	}

	return new TypeParameters([], isForFunc);
}

/**
 * @internal
 * @param {Token[]} ts
 * @returns {[Token[], Token[]]}
 */
function splitDataImpl(ts) {
	const implPos = Word.find(ts, ["const", "func"]);

	if (implPos == -1) {
		return [ts, []];
	} else {
		return [ts.slice(0, implPos), ts.slice(implPos)];
	}
}


/**
 * @internal
 * @param {Site} site 
 * @param {Token[]} ts 
 * @returns {StructStatement | null}
 */
function buildStructStatement(site, ts) {
	const maybeName = assertToken(ts.shift(), site, "expected name after 'struct'");
	if (!maybeName) {
		return null;
	}

	const name = maybeName.assertWord()?.assertNotKeyword();
	if (!name) {
		return null;
	}

	const parameters = buildTypeParameters(ts, false);

	const maybeBraces = assertToken(ts.shift(), name.site, `expected '{...}' after 'struct ${name.toString()}'`);
	if (!maybeBraces) {
		return null;
	}

	if (!maybeBraces.isGroup("{", 1)) {
		maybeBraces.syntaxError("expected non-empty '{..}' without separators");
		return null;
	}

	const braces = maybeBraces.assertGroup("{", 1);

	if (!braces) {
		return null;
	}

	const [tsFields, tsImpl] = splitDataImpl(braces.fields[0]);

	const fields = buildDataFields(tsFields, true);

	/**
	 * @type {Expr}
	 */
	let selfTypeExpr = new RefExpr(name);

	if (parameters.hasParameters()) {
		selfTypeExpr = new ParametricExpr(
			selfTypeExpr.site, 
			selfTypeExpr,
			parameters.parameterNames.map(n => new RefExpr(new Word(selfTypeExpr.site, n)))
		)
	}

	const impl = buildImplDefinition(tsImpl, selfTypeExpr, fields.map(f => f.name), braces.site.endSite);

	if (impl === null) {
		return null;
	} else {
		return new StructStatement(site.merge(braces.site), name, parameters, fields, impl);
	}
}

/**
 * @internal
 * @param {Token[]} ts 
 * @param {boolean} allowTags
 * @returns {DataField[]}
 */
function buildDataFields(ts, allowTags = false) {
	/** @type {DataField[]} */
	const fields = []

	/**
	 * @param {Word} fieldName
	 */
	function assertUnique(fieldName) {
		if (fields.findIndex(f => f.name.toString() == fieldName.toString()) != -1) {
			fieldName.typeError(`duplicate field \'${fieldName.toString()}\'`);
		}
	}

	/**
	 * @type {Map<string, StringLiteral>}
	 */
	const tags = new Map()

	while (ts.length > 0) {
		const colonPos = SymbolToken.find(ts, ":");

		if (colonPos == -1) {
			ts[0].site.merge(ts[ts.length-1].site).syntaxError("expected ':' in data field");
			return fields;
		}

		const colon = ts[colonPos];
		const tsBef = ts.slice(0, colonPos);
		const tsAft = ts.slice(colonPos+1);
		const maybeFieldName = tsBef.shift();
		if (maybeFieldName === undefined) {
			colon.syntaxError("expected word before ':'");
			continue;
		} else {
			const fieldName = maybeFieldName?.assertWord()?.assertNotKeyword();

			if (!fieldName) {
				return fields;
			}

			assertUnique(fieldName);

			if (tsAft.length == 0) {
				colon.syntaxError("expected type expression after ':'");
				return fields;
			}

			const nextColonPos = SymbolToken.find(tsAft, ":");

			if (nextColonPos != -1) {
				if (nextColonPos == 0) {
					tsAft[nextColonPos].syntaxError("expected word before ':'");
					return fields;
				}

				void tsAft[nextColonPos-1].assertWord();

				ts = tsAft.splice(nextColonPos-1);
			} else {
				ts = [];
			}

			/**
			 * @type {null | StringLiteral}
			 */
			let tag = null;

			if (allowTags && tsAft.length > 0 && tsAft[tsAft.length - 1] instanceof StringLiteral) {
				tag = assertClass(tsAft.pop(), StringLiteral);

				if (tags.has(tag.value)) {
					tag.syntaxError(`duplicate field tag "${tag.value}"`);
					return fields;
				}

				tags.set(tag.value, tag)
			} else {
				if (tags.has(fieldName.value)) {
					fieldName.syntaxError(`duplicate field tag "${fieldName.value}" (default taken from field name)`);
					return fields;
				}
				tags.set(fieldName.value, new StringLiteral(fieldName.site, fieldName.value));

			}

			const typeExpr = buildTypeExpr(colon.site, tsAft);

			if (!typeExpr) {
				return fields;
			}

			fields.push(new DataField(fieldName, typeExpr, tag));
		}
	}

	return fields;
}

/**
 * @internal
 * @param {Site} site 
 * @param {Token[]} ts 
 * @param {null | Expr} methodOf - methodOf !== null then first arg can be named 'self'
 * @returns {FuncStatement | null}
 */
function buildFuncStatement(site, ts, methodOf = null) {
	const name = assertToken(ts.shift(), site)?.assertWord()?.assertNotKeyword();

	if (!name) {
		return null;
	}

	if (ts.length == 0) {
		name.site.syntaxError("invalid syntax");
		return null;
	}

	const parameters = buildTypeParameters(ts, true);

	const fnExpr = buildFuncLiteralExpr(ts, methodOf, false);

	if (!fnExpr) {
		return null;
	}

	return new FuncStatement(site.merge(fnExpr.site), name, parameters, fnExpr);
}

/**
 * @internal
 * @param {Token[]} ts 
 * @param {null | Expr} methodOf - methodOf !== null then first arg can be named 'self'
 * @param {boolean} allowInferredRetType
 * @returns {FuncLiteralExpr | null}
 */
function buildFuncLiteralExpr(ts, methodOf = null, allowInferredRetType = false) {
	const parens = assertDefined(ts.shift()).assertGroup("(");
	if (!parens) {
		return null;
	}

	const site = parens.site;
	const args = buildFuncArgs(parens, methodOf);

	const arrow = assertToken(ts.shift(), site)?.assertSymbol("->");
	if (!arrow) {
		return null;
	}

	const bodyPos = Group.find(ts, "{");

	if (bodyPos == -1) {
		site.syntaxError("no function body");
		return null;
	} else if (bodyPos == 0 && !allowInferredRetType) {
		site.syntaxError("no return type specified");
	}

	const retTypeExpr = buildFuncRetTypeExpr(arrow.site, ts.splice(0, bodyPos), allowInferredRetType);

	const bodyGroup = assertToken(ts.shift(), site)?.assertGroup("{", 1)

	if (!bodyGroup) {
		return null;
	}

	const bodyExpr = buildValueExpr(bodyGroup.fields[0]);

	if (!bodyExpr) {
		return null;
	}

	return new FuncLiteralExpr(arrow.site, args, retTypeExpr, bodyExpr);
}

/**
 * @internal
 * @param {Group} parens 
 * @param {null | Expr} methodOf - methodOf !== nul then first arg can be named 'self'
 * @returns {FuncArg[]}
 */
function buildFuncArgs(parens, methodOf = null) {
	/** @type {FuncArg[]} */
	const args = [];

	let hasDefaultArgs = false;

	for (let i = 0; i < parens.fields.length; i++) {
		const f = parens.fields[i];
		const ts = f.slice();

		const name = assertToken(ts.shift(), parens.site)?.assertWord();

		if (!name) {
			continue;
		}

		if (name.toString() == "self") {
			if (i != 0 || methodOf === null) {
				name.syntaxError("'self' is reserved");
			} else {
				if (ts.length > 0) {
					if (ts[0].isSymbol(":")) {
						ts[0].syntaxError("unexpected type expression after 'self'");
					} else {
						ts[0].syntaxError("unexpected token");
					}
				} else {
					args.push(new FuncArg(name, methodOf));
				}
			}
		} else if (name.toString() == "_") {
			if (ts.length > 0) {
				if (ts[0].isSymbol(":")) {
					ts[0].syntaxError("unexpected type expression after '_'");
				} else {
					ts[0].syntaxError("unexpected token");
				}
			} else {
				args.push(new FuncArg(name, methodOf));
			}
		} else {
			if (name.isKeyword()) {
				name.syntaxError("unexpected keyword");
			}

			for (let prev of args) {
				if (prev.name.toString() == name.toString()) {
					name.syntaxError(`duplicate argument '${name.toString()}'`);
				}
			}

			const maybeColon = ts.shift();
			if (maybeColon === undefined) {
				name.syntaxError(`expected ':' after '${name.toString()}'`);
			} else {
				const colon = maybeColon.assertSymbol(":");

				if (!colon) {
					continue;
				}

				const equalsPos = SymbolToken.find(ts, "=");

				/**
				 * @type {null | Expr}
				 */
				let defaultValueExpr = null;

				if (equalsPos != -1) {
					if (equalsPos == ts.length-1) {
						ts[equalsPos].syntaxError("expected expression after '='");
					} else {
						const vts = ts.splice(equalsPos);

						vts.shift()?.assertSymbol("=");
						
						defaultValueExpr = buildValueExpr(vts);

						hasDefaultArgs = true;
					}
				} else {
					if (hasDefaultArgs) {
						name.syntaxError("positional args must come before default args");
					}
				}

				/**
				 * @type {null | Expr}
				 */
				let typeExpr = null;

				if (ts.length == 0) {
					colon.syntaxError("expected type expression after ':'");
				} else {
					typeExpr = buildTypeExpr(colon.site, ts);
				}

				args.push(new FuncArg(name, typeExpr, defaultValueExpr));
			}
		}
	}

	return args;
}

/**
 * @internal
 * @param {Site} site 
 * @param {Token[]} ts 
 * @returns {EnumStatement | null}
 */
function buildEnumStatement(site, ts) {
	const name = assertToken(ts.shift(), site, "expected word after 'enum'")?.assertWord()?.assertNotKeyword();
	if (!name) {
		return null;
	}

	const parameters = buildTypeParameters(ts, false);

	const braces = assertToken(ts.shift(), name.site, `expected '{...}' after 'enum ${name.toString()}'`)?.assertGroup("{", 1);

	if (!braces) {
		return null;
	}

	const [tsMembers, tsImpl] = splitDataImpl(braces.fields[0]);

	if (tsMembers.length == 0) {
		braces.syntaxError("expected at least one enum member");
	}

	/** @type {EnumMember[]} */
	const members = [];

	while (tsMembers.length > 0) {
		const member = buildEnumMember(tsMembers);

		if (!member) {
			continue;
		}

		members.push(member);
	}

	/**
	 * @type {Expr}
	 */
	let selfTypeExpr = new RefExpr(name);

	if (parameters.hasParameters()) {
		selfTypeExpr = new ParametricExpr(
			selfTypeExpr.site, 
			selfTypeExpr,
			parameters.parameterNames.map(n => new RefExpr(new Word(selfTypeExpr.site, n)))
		)
	}

	const impl = buildImplDefinition(tsImpl, selfTypeExpr, members.map(m => m.name), braces.site.endSite);

	if (!impl) {
		return null;
	}

	return new EnumStatement(site.merge(braces.site), name, parameters, members, impl);
}

/**
 * @internal
 * @param {Site} site 
 * @param {Token[]} ts 
 * @returns {(ImportFromStatement | ImportModuleStatement | null)[] | null}
 */
function buildImportStatements(site, ts) {
	const t = assertToken(ts.shift(), site, "expected '{...}' or Word after 'import'");
	if (!t) {
		return null;
	}

	if (t.isWord()) {
		const statement = buildImportModuleStatement(site, t);

		if (!statement) {
			return null;
		}

		return [statement];
	} else {
		return buildImportFromStatements(site, t, ts);
	}
}

/**
 * @param {Site} site 
 * @param {Token} maybeName 
 * @returns {ImportModuleStatement | null}
 */
function buildImportModuleStatement(site, maybeName) {
	/**
	 * @type {Word | null}
	 */
	let moduleName = null;

	if (maybeName instanceof StringLiteral && importPathTranslator) {
		const translated = importPathTranslator(maybeName);
		if (!translated) {
			return null;
		}

		moduleName = new Word(maybeName.site, translated);
	} else {
		moduleName = maybeName.assertWord()?.assertNotKeyword() ?? null;
	}

	if (!moduleName) {
		return null;
	}

	return new ImportModuleStatement(site, moduleName);
}

/**
 * 
 * @param {Site} site 
 * @param {Token} maybeBraces 
 * @param {Token[]} ts 
 * @returns {(ImportFromStatement | null)[] | null}
 */
function buildImportFromStatements(site, maybeBraces, ts) {
	const braces = maybeBraces.assertGroup("{");
	if (!braces) {
		return null;
	}

	const maybeFrom = assertToken(ts.shift(), maybeBraces.site, "expected 'from' after 'import {...}'")?.assertWord("from");
	if (!maybeFrom) {
		return null;
	}

	const maybeModuleName = assertToken(ts.shift(), maybeFrom.site, "expected module name after 'import {...} from'");
	if (!maybeModuleName) {
		return null;
	}

	/**
	 * @type {null | undefined | Word}
	 */
	let moduleName = null;

	if (maybeModuleName instanceof StringLiteral && importPathTranslator) {
		const translated = importPathTranslator(maybeModuleName);

		if (!translated) {
			return null;
		}

		moduleName = new Word(maybeModuleName.site, translated);
	} else {
		moduleName = maybeModuleName.assertWord()?.assertNotKeyword();
	}

	if (!moduleName) {
		return null;
	}

	const mName = moduleName;

	if (braces.fields.length === 0) {
		braces.syntaxError("expected at least 1 import field");
	}

	return braces.fields.map(fts => {
		const ts = fts.slice();
		const maybeOrigName = ts.shift();

		if (maybeOrigName === undefined) {
			braces.syntaxError("empty import field");
			return null;
		} else {
			const origName = maybeOrigName.assertWord();

			if (!origName) {
				return null;
			} else if (ts.length === 0) {
				return new ImportFromStatement(site, origName, origName, mName);
			} else {
				const maybeAs = ts.shift();

				if (maybeAs === undefined) {
					maybeOrigName.syntaxError(`expected 'as' or nothing after '${origName.value}'`);
					return null;
				} else {
					maybeAs.assertWord("as");

					const maybeNewName = ts.shift();

					if (maybeNewName === undefined) {
						maybeAs.syntaxError("expected word after 'as'");
						return null;
					} else {
						const newName = maybeNewName.assertWord();

						if (!newName) {
							return null;
						}

						const rem = ts.shift();
						if (rem !== undefined) {
							rem.syntaxError("unexpected token");
							return null;
						} else {
							return new ImportFromStatement(site, newName, origName, mName);
						}
					}
				}
			}
		}
	}).filter(f => f !== null)
}

/**
 * @internal
 * @param {Token[]} ts 
 * @returns {EnumMember | null}
 */
function buildEnumMember(ts) {
	const name = assertDefined(ts.shift()).assertWord()?.assertNotKeyword();

	if (!name) {
		return null;
	} else if (ts.length == 0 || ts[0].isWord()) {
		return new EnumMember(name, []);
	} else {
		const braces = assertToken(ts.shift(), name.site)?.assertGroup("{", 1);

		if (!braces) {
			return null;
		} else {
			const fields = buildDataFields(braces.fields[0]);

			return new EnumMember(name, fields);
		}
	}
}

/** 
 * @internal
 * @param {Token[]} ts 
 * @param {Expr} selfTypeExpr - reference to parent type
 * @param {Word[]} fieldNames - to check if impl statements have a unique name
 * @param {?Site} endSite
 * @returns {ImplDefinition | null}
 */
function buildImplDefinition(ts, selfTypeExpr, fieldNames, endSite) {
	/**
	 * @param {Word} name
	 * @returns {boolean}
	 */
	function isNonAuto(name) {
		if (AUTOMATIC_METHODS.findIndex(n => n == name.toString()) != -1) {
			name.syntaxError(`'${name.toString()}' is a reserved member`);
			return false;
		} else {
			return true;
		}
	}

	for (let fieldName of fieldNames) {
		if (!isNonAuto(fieldName)) {
			return null;
		}
	}

	const statements = buildImplMembers(ts, selfTypeExpr);

	/** 
	 * @param {number} i
	 * @returns {boolean} - ok
	 */
	function isUnique(i) {
		let s = statements[i];

		isNonAuto(s.name);

		for (let fieldName of fieldNames) {
			if (fieldName.toString() == s.name.toString()) {
				s.name.syntaxError(`'${s.name.toString()}' is duplicate`);
				return false;
			}
		}

		for (let j = i+1; j < statements.length; j++) {
			if (statements[j].name.toString() == s.name.toString()) {
				statements[j].name.syntaxError(`'${s.name.toString()}' is duplicate`);
				return false;
			}
		}

		return true;
	}

	const n = statements.length;

	for (let i = 0; i < n; i++) {
		if (!isUnique(i)) {
			return null;
		}
	}

	if (n > 0 && endSite !== null) {
		statements[n-1].site.setEndSite(endSite);
	}

	return new ImplDefinition(selfTypeExpr, statements);
}

/**
 * @internal
 * @param {Token[]} ts 
 * @param {Expr} methodOf
 * @returns {(ConstStatement | FuncStatement)[]}
 */
function buildImplMembers(ts, methodOf) {
	/** @type {(ConstStatement | FuncStatement)[]} */
	const statements = [];

	while (ts.length != 0) {
		const t = assertDefined(ts.shift()).assertWord();

		if (!t) {
			continue;
		}

		const kw = t.value;

		/**
		 * @type {null | ConstStatement | FuncStatement}
		 */
		let s = null;

		if (kw == "const") {
			s = buildConstStatement(t.site, ts);
		} else if (kw == "func") {
			s = buildFuncStatement(t.site, ts, methodOf);
		} else {
			t.syntaxError("invalid impl syntax");
		}

		if (s) {
			statements.push(s);
		}
	}

	return statements
}

/**
 * TODO: chain like value
 * @internal
 * @param {Site} site
 * @param {Token[]} ts 
 * @returns {Expr | null}
 */
function buildTypeExpr(site, ts) {
	if (ts.length == 0) {
		site.syntaxError("expected token");
		return null;
	}

	if (ts[0].isGroup("[")) {
		return buildListTypeExpr(site, ts);
	} else if (ts[0].isWord("Map")) {
		return buildMapTypeExpr(site, ts);
	} else if (ts[0].isWord("Option")) {
		return buildOptionTypeExpr(site, ts);
	} else if (ts[0].isWord("Iterator")) {
		return buildIteratorTypeExpr(site, ts);
	} else if (ts.length > 1 && ts[0].isGroup("(") && ts[1].isSymbol("->")) {
		return buildFuncTypeExpr(site, ts);
	} else if (ts.length > 2 && ts[0].isGroup("[") && ts[1].isGroup("(") && ts[2].isSymbol("->")) {
		return buildFuncTypeExpr(site, ts);
	} else if (SymbolToken.find(ts, "::") > Group.find(ts, "[")) {
		return buildTypePathExpr(site, ts);
	} else if (Group.find(ts, "[") > SymbolToken.find(ts, "::")) {
		return buildParametricTypeExpr(site, ts);
	} else if (ts.length == 1 && ts[0].isWord()) {
		return buildTypeRefExpr(site, ts);
	} else if (ts.length == 1 && ts[0].isGroup("(")) {
		return buildTupleTypeExpr(ts[0]);
	} else {
		ts[0].syntaxError("invalid type syntax");
		return null;
	}
}

/**
 * @param {Site} site
 * @param {Token[]} ts 
 * @returns {ParametricExpr | null}
 */
function buildParametricTypeExpr(site, ts) {
	const brackets = assertToken(ts.pop(), site)?.assertGroup("[");
	if (!brackets) {
		return null;
	}

	const baseExpr = buildTypeExpr(brackets.site, ts);
	if (!baseExpr) {
		return null;
	}

	const typeExprs = reduceNull(brackets.fields.map(fts => {
		return buildTypeExpr(brackets.site, fts);
	}));

	if (!typeExprs) {
		return null;
	}

	return new ParametricExpr(brackets.site, baseExpr, typeExprs);
}

/**
 * @internal
 * @param {Site} site
 * @param {Token[]} ts 
 * @returns {ListTypeExpr | null}
 */
function buildListTypeExpr(site, ts) {
	const brackets = assertToken(ts.shift(), site)?.assertGroup("[", 0);

	if (!brackets) {
		return null
	}

	const itemTypeExpr = buildTypeExpr(brackets.site, ts);

	if (!itemTypeExpr) {
		return null;
	}

	return new ListTypeExpr(brackets.site, itemTypeExpr);
}

/**
 * @internal
 * @param {Site} site
 * @param {Token[]} ts 
 * @returns {MapTypeExpr | null}
 */
function buildMapTypeExpr(site, ts) {
	const kw = assertToken(ts.shift(), site)?.assertWord("Map");

	if (!kw) {
		return null;
	}

	const maybeKeyTypeExpr = assertToken(ts.shift(), kw.site, "missing Map key-type");

	if (!maybeKeyTypeExpr) {
		return null;
	}

	const keyTypeTs = maybeKeyTypeExpr.assertGroup("[", 1)?.fields[0];
	if (keyTypeTs === null || keyTypeTs === undefined) {
		return null;
	} else if (keyTypeTs.length == 0) {
		kw.syntaxError("missing Map key-type (brackets can't be empty)");
		return null;
	} 

	const keyTypeExpr = buildTypeExpr(kw.site, keyTypeTs);
	if (!keyTypeExpr) {
		return null;
	}

	if (ts.length == 0) {
		kw.syntaxError("missing Map value-type");
		return null;
	} 

	const valueTypeExpr = buildTypeExpr(kw.site, ts);

	if (!valueTypeExpr) {
		return null;
	}

	return new MapTypeExpr(kw.site, keyTypeExpr, valueTypeExpr);
}

/**
 * @internal
 * @param {Site} site
 * @param {Token[]} ts 
 * @returns {Expr | null}
 */
function buildOptionTypeExpr(site, ts) {
	const kw = assertToken(ts.shift(), site)?.assertWord("Option");

	if (!kw) {
		return null;
	}

	const typeTs = assertToken(ts.shift(), kw.site)?.assertGroup("[", 1)?.fields[0];

	if (!typeTs) {
		return null;
	}

	const someTypeExpr = buildTypeExpr(kw.site, typeTs);
	if (!someTypeExpr) {
		return null;
	}

	const typeExpr = new OptionTypeExpr(kw.site, someTypeExpr);
	if (ts.length > 0) {
		if (ts[0].isSymbol("::") && ts[1].isWord(["Some", "None"])) {
			if (ts.length > 2) {
				ts[2].syntaxError("unexpected token");
				return null;
			} else {
				const memberName = ts[1].assertWord()

				if (!memberName) {
					return null;
				}

				return new PathExpr(ts[0].site, typeExpr, memberName);
			}
		} else {
			ts[0].syntaxError("invalid option type syntax");
			return null;
		}
	} else {
		return typeExpr;
	}
}

/**
 * @internal
 * @param {Site} site
 * @param {Token[]} ts
 * @returns {IteratorTypeExpr | null}
 */
function buildIteratorTypeExpr(site, ts) {
	const kw = assertToken(ts.shift(), site)?.assertWord("Iterator");

	if (!kw) {
		return null;
	}

	const maybeGroup = assertToken(ts.shift(), kw.site, "missing Map key-type");

	if (!maybeGroup) {
		return null;
	}

	const group = maybeGroup.assertGroup("[");
	if (!group) {
		return null;
	}
	
	const typeArgs = reduceNull(group.fields.map(gts => {
		return buildTypeExpr(group.site, gts);
	}));

	if (!typeArgs) {
		return null;
	}

	if (typeArgs.length < 1) {
		throw group.site.typeError("expected at least one type arg for Iterator[...]");
	}

	return new IteratorTypeExpr(kw.site, typeArgs);
}

/**
 * @internal
 * @param {Site} site
 * @param {Token[]} ts 
 * @returns {FuncTypeExpr | null}
 */
function buildFuncTypeExpr(site, ts) {
	const parens = assertToken(ts.shift(), site)?.assertGroup("(");
	if (!parens) {
		return null;
	}

	let hasOptArgs = false;

	const argTypes = reduceNull(parens.fields.map(f => {
		const fts = f.slice();

		if (fts.length == 0) {
			parens.syntaxError("expected func arg type");
			return null;
		}

		const funcArgTypeExpr = buildFuncArgTypeExpr(fts[0].site, fts);

		if (!funcArgTypeExpr) {
			return null;
		}

		if (hasOptArgs) {
			if (!funcArgTypeExpr.isOptional()) {
				funcArgTypeExpr.syntaxError("optional arguments must come last");
				return null;
			}
		} else {
			if (funcArgTypeExpr.isOptional()) {
				hasOptArgs = true;
			}
		}

		return funcArgTypeExpr;
	}));

	if (!argTypes) {
		return null;
	} 

	if (argTypes.some(at => at.isNamed()) && argTypes.some(at => !at.isNamed())) {
		argTypes[0].syntaxError("can't mix named and unnamed args in func type");
		return null;
	}

	const arrow = assertToken(ts.shift(), parens.site)?.assertSymbol("->");

	if (!arrow) {
		return null;
	}

	const retType = buildFuncRetTypeExpr(arrow.site, ts, false);

	if (!retType) {
		return null;
	}

	return new FuncTypeExpr(parens.site, argTypes, retType);
}

/**
 * @param {Site} site
 * @param {Token[]} ts 
 * @returns {FuncArgTypeExpr | null}
 */
function buildFuncArgTypeExpr(site, ts) {
	const colonPos = SymbolToken.find(ts, ":");

	if (colonPos != -1 && colonPos != 1) {
		ts[0].syntaxError("invalid syntax");
		return null;
	}

	/**
	 * @type {Word | null}
	 */
	let name = null;

	if (colonPos != -1) {
		name = assertToken(ts.shift(), site)?.assertWord()?.assertNotKeyword() ?? null;

		if (!name) {
			return null;
		}

		const colon = assertToken(ts.shift(), site)?.assertSymbol(":");

		if (!colon) {
			return null;
		}
		
		if (ts.length == 0) {
			colon.syntaxError("expected type expression after ':'");
			return null;
		}
	}

	const next = assertToken(ts[0], site);

	if (!next) {
		return null;
	}

	const hasDefault = next.isSymbol("?");
	if (hasDefault) {
		const opt = assertToken(ts.shift(), site);

		if (!opt) {
			return null;
		}

		if (ts.length == 0) {
			opt.syntaxError("invalid type expression after '?'");
			return null;
		}
	}

	const typeExpr = buildTypeExpr(next.site, ts);
	if (!typeExpr) {
		return null;
	}

	return new FuncArgTypeExpr(name !== null ? name.site : typeExpr.site, name, typeExpr, hasDefault);
}

/**
 * @internal
 * @param {Site} site 
 * @param {Token[]} ts 
 * @param {boolean} allowInferredRetType
 * @returns {null | Expr}
 */
function buildFuncRetTypeExpr(site, ts, allowInferredRetType = false) {
	if (ts.length === 0) {
		if (allowInferredRetType) {
			return null;
		} else {
			site.syntaxError("expected type expression after '->'");
			return null;
		}
	} else {
		if (ts[0].isGroup("(") && (ts.length == 1 || !ts[1].isSymbol("->"))) {
			const group = assertToken(ts.shift(), site)?.assertGroup("(");

			if (!group) {
				return null;
			} else if (group.fields.length == 0) {
				return new VoidTypeExpr(group.site);
			} else if (group.fields.length == 1) {
				group.syntaxError("expected 0 or 2 or more types in multi return type");
				return null;
			} else {
				const itemTypeExprs_ = group.fields.map(fts => {
					fts = fts.slice();

					return buildTypeExpr(group.site, fts);
				});

				const itemTypeExprs = reduceNull(itemTypeExprs_);

				if (!itemTypeExprs) {
					return null;
				}

				return new TupleTypeExpr(group.site, itemTypeExprs);
			}
		} else {
			return buildTypeExpr(site, ts);
		}
	}
}

/**
 * @internal
 * @param {Site} site
 * @param {Token[]} ts 
 * @returns {null | PathExpr}
 */
function buildTypePathExpr(site, ts) {
	const i = SymbolToken.findLast(ts, "::");

	assert(i != -1);

	const baseExpr = buildTypeExpr(ts[0].site, ts.splice(0, i));
	if (!baseExpr) {
		return null;
	}

	const dcolon = assertToken(ts.shift(), site)?.assertSymbol("::");
	if (!dcolon) {
		return null;
	}

	const memberName = assertToken(ts.shift(), dcolon.site)?.assertWord()?.assertNotKeyword();
	if (!memberName) {
		return null;
	}
	
	return new PathExpr(dcolon.site, baseExpr, memberName);
}

/**
 * @internal
 * @param {Site} site
 * @param {Token[]} ts 
 * @returns {RefExpr | null}
 */
function buildTypeRefExpr(site, ts) {
	const name = assertToken(ts.shift(), site)?.assertWord()?.assertNotKeyword();

	if (!name) {
		return null;
	}

	if (ts.length > 0) {
		ts[0].syntaxError("invalid type syntax");
		return null;
	}

	return new RefExpr(name);
}

/**
 * @param {Token} t
 * @returns {TupleTypeExpr | null}
 */
function buildTupleTypeExpr(t) {
	const group = t.assertGroup("(");

	if (!group) {
		return null;
	}

	if (group.fields.length < 2) {
		group.syntaxError("expected at least two items for tuple type");
		return null;
	}

	const itemTypeExprs = reduceNull(group.fields.map(fts => {
		return buildTypeExpr(group.site, fts);
	}));

	if (!itemTypeExprs) {
		return null;
	}

	return new TupleTypeExpr(group.site, itemTypeExprs);
}

/**
 * @internal
 * @param {Token[]} ts 
 * @param {number} prec 
 * @returns {Expr | null}
 */
function buildValueExpr(ts, prec = 0) {
	assert(ts.length > 0);

	// lower index in exprBuilders is lower precedence
	/** @type {((ts: Token[], prev: number) => (Expr | null))[]} */
	const exprBuilders = [
		/**
		 * 0: lowest precedence is assignment
		 * @param {Token[]} ts_ 
		 * @param {number} prec_ 
		 * @returns 
		 */
		function (ts_, prec_) {
			return buildMaybeAssignOrChainExpr(ts_, prec_);
		},
		/**
		 * 1: piped expression
		 * @param {Token[]} ts_ 
		 * @param {number} prec_ 
		 * @returns 
		 */
		function (ts_, prec_) {
			return buildPipedExpr(ts_, prec_);
		},
		makeBinaryExprBuilder('||'), // 2: logical or operator
		makeBinaryExprBuilder('&&'), // 3: logical and operator
		makeBinaryExprBuilder(['==', '!=']), // 4: eq or neq
		makeBinaryExprBuilder(['<', '<=', '>', '>=']), // 5: comparison
		makeBinaryExprBuilder(['+', '-']), // 6: addition subtraction
		makeBinaryExprBuilder(['*', '/', '%']), // 7: multiplication division remainder
		makeUnaryExprBuilder(['!', '+', '-']), // 8: logical not, negate
		/**
		 * 9: variables or literal values chained with: (enum)member access, indexing and calling
		 * @param {Token[]} ts_ 
		 * @param {number} prec_ 
		 * @returns 
		 */
		function (ts_, prec_) {
			return buildChainedValueExpr(ts_, prec_);
		}
	];

	return exprBuilders[prec](ts, prec);
}

/**
 * @internal
 * @param {Token[]} ts
 * @param {number} prec
 * @returns {Expr | null}
 */
function buildMaybeAssignOrChainExpr(ts, prec) {
	let semicolonPos = SymbolToken.find(ts, ";");
	const equalsPos = SymbolToken.find(ts, "=");

	if (semicolonPos == -1) {
		if (equalsPos != -1) {
			ts[equalsPos].syntaxError("invalid assignment syntax, expected ';' after '...=...'");
			return null;
		} else {
			return buildValueExpr(ts, prec + 1);
		}
	} else {
		if (equalsPos == -1 || equalsPos > semicolonPos) {
			const upstreamExpr = buildValueExpr(ts.splice(0, semicolonPos), prec+1);
			const site = assertDefined(ts.shift()).site;

			if (ts.length == 0) {
				site.syntaxError("expected expression after ';'");
				return null;
			} else if (upstreamExpr === null) {
				// error will already have been created
				return null;
			} else {
				const downstreamExpr = buildValueExpr(ts, prec);

				if (downstreamExpr === null) {
					// error will already have been created
					return null;
				} else {
					return new ChainExpr(site, upstreamExpr, downstreamExpr);
				}
			}
		} else if (equalsPos != -1 && equalsPos < semicolonPos) {
			const equals = ts[equalsPos].assertSymbol("=");

			if (!equals) {
				return null;
			}

			const equalsSite = equals.site;

			const lts = ts.splice(0, equalsPos);

			const lhs = buildAssignLhs(equalsSite, lts);
			
			assertDefined(ts.shift()).assertSymbol("=");

			semicolonPos = SymbolToken.find(ts, ";");
			assert(semicolonPos != -1);

			let upstreamTs = ts.splice(0, semicolonPos);
			if (upstreamTs.length == 0) {
				equalsSite.syntaxError("expected expression between '=' and ';'");
				return null;
			}

			const upstreamExpr = buildValueExpr(upstreamTs, prec + 1);

			const semicolon  = assertToken(ts.shift(), equalsSite)?.assertSymbol(";");

			if (!semicolon) {
				return null;
			}

			const semicolonSite = semicolon.site;

			if (ts.length == 0) {
				semicolonSite.syntaxError("expected expression after ';'");
				return null;
			}

			const downstreamExpr = buildValueExpr(ts, prec);

			if (downstreamExpr === null || upstreamExpr === null || lhs === null) {
				// error will already have been thrown internally
				return null;
			} else {
				return new AssignExpr(equalsSite, lhs, upstreamExpr, downstreamExpr);
			}
		} else {
			ts[0].syntaxError("unhandled");
			return null;
		}
	}
}

/**
 * @internal
 * @param {Site} site
 * @param {Token[]} ts 
 * @param {boolean} isSwitchCase
 * @returns {DestructExpr | null}
 */
function buildDestructExpr(site, ts, isSwitchCase = false) {
	if (ts.length == 0) {
		site.syntaxError("expected token inside destructuring braces");
		return null;
	}

	let maybeName = assertToken(ts.shift(), site);

	if (!maybeName) {
		return null;
	}

	if (maybeName.isWord("_")) {
		if (ts.length != 0) {
			maybeName.syntaxError("unexpected tokens after '_'");
			return null;
		} else {
			return new DestructExpr(new Word(maybeName.site, "_"), null);
		}
	} else {
		let name = new Word(maybeName.site, "_");

		if (ts.length >= 1 && ts[0].isSymbol(":")) {
			// name + ':' + type + optional braces
			// or name + ':' + tuple-type
			// or name + ':' + tuple-parens-destruct

			let name_ = maybeName.assertWord()?.assertNotKeyword();

			if (!name_) {
				return null;
			}

			name = name_;

			const colon = assertToken(ts.shift(), name.site)?.assertSymbol(":");

			if (!colon) {
				return null;
			}

			if (ts.length == 0) {
				colon.syntaxError("expected type expression after ':'");
				return null;
			}

			// the next group token might be a tuple type instead of a tuple destruct expression (if it doesn't contain a colon it is a type)
			if (ts[0].isGroup("(") && !ts[0].assertGroup("(")?.fields.some(fs => fs.some(t => t.isSymbol(":")))) {
				const typeExpr = buildTypeExpr(colon.site, ts);
				return new DestructExpr(name, typeExpr);
			}

			const destructExprsIsTuple = buildDestructExprs(ts);

			if (!destructExprsIsTuple) {
				return null
			}

			const [destructExprs, isTuple] = destructExprsIsTuple;

			/**
			 * @type {Expr | null}
			 */
			let typeExpr = null;
			
			if (!isTuple) {
				typeExpr = buildTypeExpr(colon.site, ts);

				if (!typeExpr) {
					return null;
				}
			} else if (ts.length > 0) {
				ts[0].syntaxError("unexpected tokens");
			}

			return new DestructExpr(name, typeExpr, destructExprs, isTuple);
		} else if (ts.length == 0) {
			// only name in case of regular destruct (rhs of assign or nested in switch)

			if (isSwitchCase) {
				const typeName = maybeName.assertWord()?.assertNotKeyword();

				if (!typeName) {
					return null;
				}

				const typeExpr = new RefExpr(typeName);

				if (!typeExpr) {
					return null;
				} 

				return new DestructExpr(name, typeExpr);
			} else if(maybeName.isGroup("(")) {
				const destructExprsIsTuple = buildDestructExprs([maybeName]);

				if (!destructExprsIsTuple) {
					return null;
				}

				const [destructExprs, isTuple] = destructExprsIsTuple;

				return new DestructExpr(new Word(maybeName.site, "_"), null, destructExprs, isTuple);
			} else {
				const name = maybeName.assertWord()?.assertNotKeyword();

				if (!name) {
					return null;
				}

				return new DestructExpr(name, null);
			}
		} else {
			// type + braces or parenthesis

			ts.unshift(maybeName);

			const destructExprsIsTuple = buildDestructExprs(ts);

			if (!destructExprsIsTuple) {
				return null;
			}

			const [destructExprs, isTuple] = destructExprsIsTuple;
	
			/**
			 * @type {Expr | null}
			 */
			let typeExpr = null;

			if (!isTuple) {
				typeExpr = buildTypeExpr(site, ts);

				if (!typeExpr) {
					return null;
				}
			} else if (ts.length != 0) {
				ts[0].syntaxError("unexpected tokens");
			}		

			// name is '_' (so ignored)
			return new DestructExpr(name, typeExpr, destructExprs, isTuple);
		}
	}
}

/**
 * Pops the last element of ts if it is a braces group
 * @param {Token[]} ts
 * @returns {null | [DestructExpr[], boolean]}
 */
function buildDestructExprs(ts) {
	if (ts.length == 0) {
		return [[], false];
	} else if (ts[ts.length -1].isGroup("{")) {
		const group = assertDefined(ts.pop()).assertGroup("{");

		if (!group) {
			return null;
		}

		const destructExprs = group.fields.map(fts => {
			return buildDestructExpr(group.site, fts);
		});
	
		if (destructExprs.every(le => le !== null && le.isIgnored() && !le.hasDestructExprs())) {
			group.syntaxError("expected at least one used field while destructuring")
			return null;
		}

		const destructExprs_ = reduceNull(destructExprs);

		if (!destructExprs_) {
			return null;
		}

		return [destructExprs_, false];
	} else if (ts[ts.length - 1].isGroup("(")) {
		const group = assertDefined(ts.pop()).assertGroup("(");

		if (!group) {
			return null;
		}

		const destructExprs = group.fields.map(fts => {
			return buildDestructExpr(group.site, fts);
		});
	
		if (destructExprs.every(le => le !== null && le.isIgnored() && !le.hasDestructExprs())) {
			group.syntaxError("expected at least one used field while destructuring a tuple")
			return null;
		}

		const destructExprs_ = reduceNull(destructExprs);

		if (!destructExprs_) {
			return null;
		}

		return [destructExprs_, true];
	} else {
		return [[], false];
	}	
}

/**
 * @internal
 * @param {Site} site 
 * @param {Token[]} ts 
 * @returns {null | DestructExpr}
 */
function buildAssignLhs(site, ts) {
	const maybeName = ts.shift();
	if (maybeName === undefined) {
		site.syntaxError("expected a name or destruct expression before '='");
		return null;
	} else {
		if (maybeName.isWord()) {
			ts.unshift(maybeName);

			const lhs = buildDestructExpr(maybeName.site, ts);

			if (lhs === null) {
				return null;
			} else if (lhs.isIgnored() && !lhs.hasDestructExprs()) {
				maybeName.syntaxError(`unused assignment ${maybeName.toString()}`);
				return null;
			}

			return lhs;
		} else if (maybeName.isGroup("(")) {
			/**
			 * @type {DestructExpr[]}
			 */
			const inner = [];
			
			const group = maybeName.assertGroup("(");

			if (!group) {
				return null;
			}

			if (group.fields.length < 2) {
				group.syntaxError("expected at least 2 lhs' for multi-assign");
				return null;
			}

			let someNoneUnderscore = false;
			for (let fts of group.fields) {
				if (fts.length == 0) {
					group.syntaxError("unexpected empty field for multi-assign");
					return null;
				}

				fts = fts.slice();

				const lhs = buildDestructExpr(group.site, fts);

				if (!lhs) {
					return null;
				}
				
				if (!lhs.isIgnored() || lhs.hasDestructExprs()) {
					someNoneUnderscore = true;
				}

				// check that name is unique
				inner.forEach(p => {
					if (!lhs.isIgnored() && p.name.value === lhs.name.value) {
						lhs.name.syntaxError(`duplicate name '${lhs.name.value}' in tuple destruct expr`);
					}
				});

				inner.push(lhs);
			}

			if (!someNoneUnderscore) {
				group.syntaxError("expected at least one non-underscore in lhs of multi-assign");
				return null;
			}

			return new DestructExpr(new Word(group.site, "_"), null, inner, true);
		} else {
			maybeName.syntaxError("unexpected syntax for lhs of =");
			return null;
		}
	}
}

/**
 * @param {Token[]} ts 
 * @param {number} prec
 * @returns {Expr | null} 
 */
function buildPipedExpr(ts, prec) {
	const iOp = SymbolToken.findLast(ts, ["|"]);

	if (iOp == ts.length - 1) {
		ts[iOp].syntaxError(`invalid syntax, '${ts[iOp].toString()}' can't be used as a post-unary operator`);
		return null;
	} else if (iOp == 0) {
		ts[iOp].syntaxError(`invalid syntax, '${ts[iOp].toString()}' can't be used as a pre-unary operator`);
		return null;
	} else if (iOp > 0) {
		if (ts[iOp+1].isSymbol()) {
			const next = assertDefined(ts[iOp+1].assertSymbol());

			if (!(next.value == "." || next.value in BINARY_SYMBOLS_MAP)) {
				next.syntaxError("invalid pipe syntax");
				return null;
			}

			const innerTs= ts.splice(0, iOp);
			
			const op = ts.shift()?.assertSymbol("|");

			if (!op) {
				return null;
			}

			/**
			 * @type {Token[]}
			 */
			let newTs = [
				new Group(op.site, "(", [
					innerTs
				])
			];
			
			newTs = newTs.concat(ts);

			return buildValueExpr(newTs, prec + 1);
		} else {
			const op = ts[iOp].assertSymbol();
			
			const a = buildValueExpr(ts.slice(0, iOp), prec);
			
			if (!a || !op) {
				return null;
			}

			const b = buildValueExpr(ts.slice(iOp + 1), prec + 1);

			if (!b) {
				return null;
			}

			return new CallExpr(op.site, b, [new CallArgExpr(a.site, null, a)]);
		}
	} else {
		return buildValueExpr(ts, prec + 1);
	}
}

/**
 * @internal
 * @param {string | string[]} symbol 
 * @returns {(ts: Token[], prec: number) => (Expr | null)}
 */
function makeBinaryExprBuilder(symbol) {
	// default behaviour is left-to-right associative
	return function (ts, prec) {
		const iOp = SymbolToken.findLast(ts, symbol);

		if (iOp == ts.length - 1) {
			// post-unary operator, which is invalid
			ts[iOp].syntaxError(`invalid syntax, '${ts[iOp].toString()}' can't be used as a post-unary operator`);
			return null;
		} else if (iOp > 0) { // iOp == 0 means maybe a (pre)unary op, which is handled by a higher precedence
			const a = buildValueExpr(ts.slice(0, iOp), prec);
			const b = buildValueExpr(ts.slice(iOp + 1), prec + 1);
			const op = ts[iOp].assertSymbol();

			if (!a || !b || !op) {
				return null;
			}

			return new BinaryExpr(op, a, b);
		} else {
			return buildValueExpr(ts, prec + 1);
		}
	};
}

/**
 * @internal
 * @param {string | string[]} symbol 
 * @returns {(ts: Token[], prec: number) => (Expr | null)}
 */
function makeUnaryExprBuilder(symbol) {
	// default behaviour is right-to-left associative
	return function (ts, prec) {
		if (ts[0].isSymbol(symbol)) {
			const rhs = buildValueExpr(ts.slice(1), prec);
			const op = ts[0].assertSymbol();

			if (!rhs || !op) {
				return null;
			}

			return new UnaryExpr(op, rhs);
		} else {
			return buildValueExpr(ts, prec + 1);
		}
	}
}

/**
 * @internal
 * @param {Token[]} ts 
 * @param {number} prec 
 * @returns {Expr | null}
 */
function buildChainedValueExpr(ts, prec) {
	/** 
	 * @type {Expr | null} 
	 */
	const expr = buildChainStartValueExpr(ts);

	return buildRemainingChainedValueExpr(expr, ts, prec);
}


/**
 * @internal
 * @param {Expr | null} expr
 * @param {Token[]} ts
 * @param {number} prec
 * @returns {Expr | null}
 */
function buildRemainingChainedValueExpr(expr, ts, prec) {
	// now we can parse the rest of the chaining
	while (ts.length > 0) {
		if (expr === null) {
			return null;
		}

		const t = assertDefined(ts.shift());

		if (t.isGroup("(")) {
			expr = buildCallExpr(t.site, expr, assertDefined(t.assertGroup()));
		} else if (t.isGroup("[")) {
			expr = buildParametricValueExpr(expr, assertDefined(t.assertGroup("[")));
		} else if (t.isSymbol(".") && ts.length > 0 && ts[0].isWord("switch")) {
			expr = buildSwitchExpr(expr, ts);
		} else if (t.isSymbol(".")) {
			const name = assertToken(ts.shift(), t.site)?.assertWord()?.assertNotKeyword();

			if (!name) {
				return null;
			}

			expr = new MemberExpr(t.site, expr, name);
		} else if (t.isGroup("{")) {
			t.syntaxError("invalid syntax");
			return null;
		} else if (t.isSymbol("::")) {
			t.syntaxError("invalid syntax");
			return null;
		} else {
			t.syntaxError(`invalid syntax '${t.toString()}'`);
			return null;
		}
	}

	return expr;
}

/**
 * @param {Expr} expr 
 * @param {Group} brackets 
 * @returns {ParametricExpr | null}
 */
function buildParametricValueExpr(expr, brackets) {
	const typeExprs = reduceNull(brackets.fields.map(fts => {
		if (fts.length == 0) {
			brackets.site.syntaxError("unexpected empty field");
			return null;
		} else {
			const typeExpr = buildTypeExpr(brackets.site, fts);

			if (fts.length != 0) {
				fts[0].syntaxError("unexpected token");
				return null;
			} else {
				return typeExpr;
			}
		}
	}));

	if (!typeExprs) {
		return null;
	}

	return new ParametricExpr(brackets.site, expr, typeExprs);
}

/**
 * @param {Site} site 
 * @param {Expr} fnExpr 
 * @param {Group} parens
 * @returns {CallExpr | null}
 */
function buildCallExpr(site, fnExpr, parens) {
	const callArgs = buildCallArgs(parens);

	if (callArgs === null) {
		return null;
	} else {
		return new CallExpr(site, fnExpr, callArgs);
	}
}

/**
 * @internal
 * @param {Token[]} ts 
 * @returns {Expr | null}
 */
function buildChainStartValueExpr(ts) {
	if (ts.length > 1 && ts[0].isGroup("(") && ts[1].isSymbol("->")) {
		return buildFuncLiteralExpr(ts, null, true);
	} else if (ts[0].isWord("if")) {
		return buildIfElseExpr(ts);
	} else if (ts[0].isWord("switch")) {
		ts[0].syntaxError("expected '... .switch' instead of 'switch'");
		return null;
	} else if (ts[0].isLiteral()) {
		return new PrimitiveLiteralExpr(assertDefined(ts.shift())); // can simply be reused
	} else if (ts[0].isGroup("(")) {
		return buildParensExpr(ts);
	} else if (Group.find(ts, "{") != -1) {
		if (ts[0].isGroup("[")) {
			return buildListLiteralExpr(ts);
		} else if (ts[0].isWord("Map") && ts[1].isGroup("[")) {
			return buildMapLiteralExpr(ts); 
		} else if (ts[0].isWord("Option") && ts[1].isGroup("[") && ts[2].isSymbol("::") && ts[3].isWord("Some")) {
			return buildOptionSomeLiteralExpr(ts);
		} else {
			// could be switch or literal struct construction
			const iBraces = Group.find(ts, "{");
			const iSwitch = Word.find(ts, "switch");
			const iPeriod = SymbolToken.find(ts, ".");

			if (iSwitch != -1 && iPeriod != -1 && iSwitch < iBraces && iPeriod < iBraces && iSwitch > iPeriod) {
				return buildValueExpr(ts.splice(0, iPeriod));
			} else {
				return buildStructLiteralExpr(ts);
			}
		}
	} else if (SymbolToken.find(ts, "::") != -1) {
		return buildValuePathExpr(ts);
	} else if (ts[0].isWord()) {
		if (ts[0].isWord("const") || ts[0].isWord("struct") || ts[0].isWord("enum") || ts[0].isWord("func") || ts[0].isWord("import")) {
			ts[0].syntaxError(`invalid use of '${assertDefined(ts[0].assertWord()).value}', can only be used as top-level statement`);
			return null;
		} else {
			const name = assertDefined(ts.shift()?.assertWord());

			if (name.value == "self") {
				return new RefExpr(name);
			} else {
				const n = name.assertNotKeyword();

				if (!n) {
					return null;
				}

				return new RefExpr(n);
			}
		}
	} else {
		ts[0].syntaxError("invalid syntax");
		return null;
	}
}

/**
 * @internal
 * @param {Token[]} ts
 * @returns {Expr | null}
 */
function buildParensExpr(ts) {
	const group = assertDefined(ts.shift()).assertGroup("(");

	if (!group) {
		return null;
	}

	const site = group.site;

	if (group.fields.length === 0) {
		group.syntaxError("expected at least one expr in parens");
		return null;
	} else {
		const fields = group.fields.map(fts => buildValueExpr(fts));

		/**
		 * @type {Expr[]}
		 */
		const nonNullFields = [];

		fields.forEach(f => {
			if (f !== null) {
				nonNullFields.push(f);
			}
		});

		if (nonNullFields.length == 0) {
			// error will already have been thrown internally
			return null;
		} else {
			return new ParensExpr(site, nonNullFields);
		}
	}
}

/**
 * @internal
 * @param {Group} parens 
 * @returns {CallArgExpr[] | null}
 */
function buildCallArgs(parens) {
	/**
	 * @type {Set<string>}
	 */
	const names = new Set();

	const callArgs = reduceNull(parens.fields.map(fts => {
		const callArg = buildCallArgExpr(parens.site, fts);

		if (callArg !== null && callArg.isNamed()) {
			if (names.has(callArg.name)) {
				callArg.syntaxError(`duplicate named call arg ${callArg.name}`);
			}

			names.add(callArg.name);
		}

		return callArg;
	}));

	if (callArgs === null) {
		return null;
	} else {
		if (callArgs.some(ca => ca.isNamed()) && callArgs.some(ca => !ca.isNamed())) {
			callArgs[0].syntaxError("can't mix positional and named args");
			return null;
		}

		return callArgs;
	}
}

/**
 * @param {Site} site 
 * @param {Token[]} ts 
 * @returns {CallArgExpr | null}
 */
function buildCallArgExpr(site, ts) {
	if (ts.length == 0) {
		site.syntaxError("invalid syntax");
		return null;
	}

	/**
	 * @type {null | undefined | Word}
	 */
	let name = null;

	if (ts.length >= 2 && ts[0].isWord() && ts[1].isSymbol(":")) {
		name = assertDefined(ts.shift()).assertWord()?.assertNotKeyword();

		if (!name) {
			return null;
		}

		const colon = assertDefined(ts.shift());

		if (ts.length == 0) {
			colon.syntaxError("expected value expressions after ':'");
			return null;
		}
	}

	const value = buildValueExpr(ts);

	if (!value) {
		return null;
	}

	return new CallArgExpr(name != null ? name.site : value.site, name, value);
}

/**
 * @internal
 * @param {Token[]} ts 
 * @returns {IfElseExpr | null}
 */
function buildIfElseExpr(ts) {
	const ifWord = assertDefined(ts.shift()).assertWord("if");

	if (!ifWord) {
		return null;
	}

	const site = ifWord.site;

	/** @type {Expr[]} */
	const conditions = [];

	/** @type {Expr[]} */
	const branches = [];
	while (true) {
		const parens = assertToken(ts.shift(), site)?.assertGroup("(");

		if (!parens) {
			return null;
		}

		const braces = assertToken(ts.shift(), site)?.assertGroup("{");

		if (!braces) {
			return null;
		}

		if (parens.fields.length != 1) {
			parens.syntaxError("expected single if-else condition");
			return null;
		}

		if (braces.fields.length == 0) {
			braces.syntaxError("branch body can't be empty");
			return null;
		} else if (braces.fields.length != 1) {
			braces.syntaxError("expected single if-else branch expession");
			return null;
		}

		const cond = buildValueExpr(parens.fields[0]);
		const branch = buildValueExpr(braces.fields[0]);

		if (cond === null || branch === null) {
			continue;
		}

		conditions.push(cond);
		branches.push(branch);

		const maybeElse = ts.shift();

		if (maybeElse === undefined ) {
			// add a void else branch
			branches.push(new VoidExpr(braces.site));
			break;
		} else {
			const elseWord = maybeElse.assertWord("else");

			if (!elseWord) {
				return null;
			}

			const next = assertToken(ts.shift(), elseWord.site);

			if (!next) {
				return null;
			} else if (next.isGroup("{")) {
				// last group
				const braces = next.assertGroup();

				if (!braces) {
					return null;
				}

				if (braces.fields.length != 1) {
					braces.syntaxError("expected single expession for if-else branch");
					return null;
				}

				const elseBranch = buildValueExpr(braces.fields[0]);

				if (!elseBranch) {
					return null;
				}

				branches.push(elseBranch);
				break;
			} else if (next.isWord("if")) {
				continue;
			} else {
				next.syntaxError("unexpected token");
				return null;
			}
		}
	}

	return new IfElseExpr(site, conditions, branches);
}

/**
 * @internal
 * @param {Expr} controlExpr
 * @param {Token[]} ts 
 * @returns {Expr | null} - EnumSwitchExpr or DataSwitchExpr
 */
function buildSwitchExpr(controlExpr, ts) {
	const switchWord = assertDefined(ts.shift()).assertWord("switch");

	if (!switchWord) {
		return null;
	}

	const site = switchWord.site;

	const braces = assertToken(ts.shift(), site)?.assertGroup("{");

	if (!braces) {
		return null;
	}

	/** @type {SwitchCase[]} */
	const cases = [];

	/** @type {null | SwitchDefault} */
	let def = null;

	for (let tsInner of braces.fields) {
		if (tsInner[0].isWord("else") || tsInner[0].isWord("_")) {
			if (def !== null) {
				def.syntaxError("duplicate default case in switch");
				return null;
			}

			def = buildSwitchDefault(tsInner);
		} else {
			if (def !== null) {
				def.syntaxError("switch default case must come last");
				return null;
			}

			const c = buildSwitchCase(tsInner);

			if (c === null) {
				return null;
			} else {
				cases.push(c);
			}
		}
	}

	// check the uniqueness of each case here
	/** @type {Set<string>} */
	const set = new Set()
	for (let c of cases) {
		let t = c.memberName.toString();
		if (set.has(t)) {
			c.memberName.syntaxError(`duplicate switch case '${t}')`);
			return null;
		}

		set.add(t);
	}

	if (cases.length < 1) {
		site.syntaxError("expected at least one switch case");
		return null;
	}

	if (cases.some(c => c.isDataMember())) {
		if (cases.length + (def === null ? 0 : 1) > 5) {
			site.syntaxError(`too many cases for data switch, expected 5 or less, got ${cases.length.toString()}`);
			return null;
		} else {
			let count = 0;
			cases.forEach(c => {if (!c.isDataMember()){count++}});

			if (count > 1) {
				site.syntaxError(`expected at most 1 enum case in data switch, got ${count}`);
				return null;
			} else {
				if (count === 1 && cases.some(c => c instanceof UnconstrDataSwitchCase)) {
					site.syntaxError(`can't have both enum and (Int, []Data) in data switch`);
					return null;
				} else {
					return new DataSwitchExpr(site, controlExpr, cases, def);
				}
			}
		}
	} else {
		return new EnumSwitchExpr(site, controlExpr, cases, def);
	}
}

/**
 * @internal
 * @param {Site} site
 * @param {Token[]} ts
 * @param {boolean} isAfterColon
 * @returns {Word | null} 
 */
function buildSwitchCaseName(site, ts, isAfterColon) {
	const first = ts.shift();

	if (first === undefined) {
		if (isAfterColon) {
			site.syntaxError("invalid switch case syntax, expected member name after ':'");
			return null;
		} else {
			site.syntaxError("invalid switch case syntax");
			return null;
		}
	}
		
	if (first.isWord("Map")) {
		const second = ts.shift();

		if (!second) {
			site.syntaxError("expected token after 'Map'");
			return null;
		}

		const keyTs = second.assertGroup("[]", 1)?.fields[0];

		if (keyTs === undefined || keyTs === null) {
			return null;
		}

		const key = keyTs.shift();

		if (key === undefined) {
			second.syntaxError("expected 'Map[Data]Data'");
			return null;
		}

		key.assertWord("Data");

		if (keyTs.length > 0) {
			keyTs[0].syntaxError("unexpected token after 'Data'");
			return null;
		}

		const third = ts.shift();

		if (third === undefined) {
			site.syntaxError("expected token after 'Map[Data]");
			return null;
		}

		third.assertWord("Data");

		if (ts.length > 0) {
			ts[0].syntaxError("unexpected token after 'Map[Data]Data'");
			return null;
		}

		return new Word(first.site, "Map[Data]Data");
	} else if (first.isWord()) {
		if (ts.length > 0) {
			ts[0].syntaxError("unexpected token");
			return null;
		}

		return first?.assertWord()?.assertNotKeyword() ?? null;
	} else if (first.isGroup("[")) {
		// list 
		first.assertGroup("[", 0);

		const second = ts.shift();

		if (second === undefined) {
			site.syntaxError("expected token after '[]'");
			return null;
		} else if (ts.length > 0) {
			ts[0].syntaxError("unexpected token");
			return null;
		}

		second.assertWord("Data");

		return new Word(first.site, "[]Data");
	} else {
		first.syntaxError("invalid switch case name syntax");
		return null;
	}
}

/**
 * @internal
 * @param {Token[]} ts 
 * @returns {SwitchCase | null}
 */
function buildSwitchCase(ts) {
	const arrowPos = SymbolToken.find(ts, "=>");

	if (arrowPos == -1) {
		ts[0].syntaxError("expected '=>' in switch case");
		return null;
	} else if (arrowPos == 0) {
		ts[0].syntaxError("expected '<word>' or '<word>: <word>' to the left of '=>'");
		return null;
	}

	const tsLeft = ts.splice(0, arrowPos);

	if (tsLeft.length === 1 && tsLeft[0].isGroup("(")) {
		return buildMultiArgSwitchCase(tsLeft, ts);
	} else {
		return buildSingleArgSwitchCase(tsLeft, ts);
	}
}

/**
 * @internal
 * @param {Token[]} ts 
 * @returns {null | [?Word, Word]} - varName is optional
 */
function buildSwitchCaseNameType(ts) {
	const colonPos = SymbolToken.find(ts, ":");

	/** @type {null | Word} */
	let varName = null;

	/** @type {null | Word} */
	let memberName = null;

	if (colonPos != -1) {
		const maybeVarName = assertDefined(ts.shift()).assertWord()?.assertNotKeyword();

		if (!maybeVarName) {
			return null;
		}

		varName = maybeVarName;
		
		const maybeColon = ts.shift();

		if (maybeColon === undefined) {
			varName.syntaxError("invalid switch case syntax, expected '(<name>: <enum-member>)', got '(<name>)'");
			return null;
		} else {
			void maybeColon.assertSymbol(":");

			memberName = buildSwitchCaseName(maybeColon.site, ts, true);
		}
	} else {
		memberName = buildSwitchCaseName(ts[0].site, ts, false);
	}

	if (ts.length !== 0) {
		ts[0].syntaxError("unexpected token");
		return null;
	}

	if (memberName === null) {
		// error will already have been thrown internally
		return null;
	} else {
		return [varName, memberName];
	}
}

/**
 * @internal
 * @param {Token[]} tsLeft
 * @param {Token[]} ts
 * @returns {SwitchCase | null}
 */
function buildMultiArgSwitchCase(tsLeft, ts) {
	const parens = assertDefined(tsLeft.shift()).assertGroup("(");

	if (!parens) {
		return null;
	}

	const pairs = reduceNull(parens.fields.map(fts => buildSwitchCaseNameType(fts)));

	if (pairs === null) {
		return null;
	}

	assert(tsLeft.length === 0);

	if (pairs.length !== 2) {
		parens.syntaxError(`expected (Int, []Data) case, got (${pairs.map(p => p[1].value).join(", ")}`);
		return null;
	} else if (pairs[0][1].value != "Int" || pairs[1][1].value != "[]Data") {
		parens.syntaxError(`expected (Int, []Data) case, got (${pairs[0][1].value}, ${pairs[1][1].value})`);
		return null;
	} else {
		const maybeArrow = ts.shift();

		if (maybeArrow === undefined) {
			parens.syntaxError("expected '=>'");
			return null;
		} else {
			const arrow = maybeArrow.assertSymbol("=>");

			if (!arrow) {
				return null;
			}

			const bodyExpr = buildSwitchCaseBody(arrow.site, ts);

			if (bodyExpr === null) {
				return null;
			} else {
				return new UnconstrDataSwitchCase(arrow.site, pairs[0][0], pairs[1][0], bodyExpr);
			}
		}
	}
}

/**
 * @internal
 * @param {Token[]} tsLeft 
 * @param {Token[]} ts 
 * @returns {SwitchCase | null}
 */
function buildSingleArgSwitchCase(tsLeft, ts) {
	const site = tsLeft[tsLeft.length-1].site;

	const destructExpr = buildDestructExpr(site, tsLeft, true);

	if (destructExpr === null) {
		return null;
	} else if (!destructExpr.hasType()) {
		destructExpr.site.syntaxError("invalid switch case syntax");
		return null;
	}
	
	const maybeArrow = ts.shift();

	if (maybeArrow === undefined) {
		site.syntaxError("expected '=>'");
		return null;
	} else {
		const arrow = maybeArrow.assertSymbol("=>");

		if (!arrow) {
			return null;
		}

		const bodyExpr = buildSwitchCaseBody(arrow.site, ts);

		if (bodyExpr === null) {
			return null;
		} else {
			return new SwitchCase(arrow.site, destructExpr, bodyExpr);
		}
	}
}

/**
 * @internal
 * @param {Site} site 
 * @param {Token[]} ts 
 * @returns {Expr | null}
 */
function buildSwitchCaseBody(site, ts) {
	/** @type {null | Expr} */
	let bodyExpr = null;

	if (ts.length == 0) {
		site.syntaxError("expected expression after '=>'");
		return null;
	} else if (ts[0].isGroup("{")) {
		if (ts.length > 1) {
			ts[1].syntaxError("unexpected token");
			return null;
		}

		const tsBody = ts[0].assertGroup("{", 1)?.fields[0];

		if (tsBody === undefined || tsBody === null) {
			return null;
		}

		bodyExpr = buildValueExpr(tsBody);
	} else {
		bodyExpr = buildValueExpr(ts);
	}

	return bodyExpr;
}

/**
 * @internal
 * @param {Token[]} ts 
 * @returns {SwitchDefault | null}
 */
function buildSwitchDefault(ts) {
	const elseWord = assertDefined(ts.shift()).assertWord();

	if (!elseWord) {
		return null;
	} else if (!(elseWord.isWord("else") || elseWord.isWord("_"))) {
		elseWord.syntaxError("expected 'else' or '_'");
		return null;
	}

	const site = elseWord.site;

	const maybeArrow = ts.shift();
	if (maybeArrow === undefined) {
		site.syntaxError(`expected '=>' after '${elseWord.value}'`);
		return null;
	} else {
		const arrow = maybeArrow.assertSymbol("=>");

		if (!arrow) {
			return null;
		}

		/** @type {null | Expr} */
		let bodyExpr = null;

		if (ts.length == 0) {
			arrow.syntaxError("expected expression after '=>'");
			return null;
		} else if (ts[0].isGroup("{")) {
			if (ts.length > 1) {
				ts[1].syntaxError("unexpected token");
				return null;
			} else {
				const bodyTs = ts[0].assertGroup("{", 1)?.fields[0];

				if (bodyTs === undefined || bodyTs === null) {
					return null;
				}

				bodyExpr = buildValueExpr(bodyTs);
			}
		} else {
			bodyExpr = buildValueExpr(ts);
		}

		if (!bodyExpr) {
			arrow.syntaxError("empty switch default case body");
			return null;
		}

		return new SwitchDefault(arrow.site, bodyExpr);
	}
}

/**
 * @internal
 * @param {Token[]} ts 
 * @returns {ListLiteralExpr | null}
 */
function buildListLiteralExpr(ts) {
	const group = assertDefined(ts.shift()).assertGroup("[", 0);

	if (!group) {
		return null;
	}

	const site = group.site;

	const bracesPos = Group.find(ts, "{");

	if (bracesPos == -1) {
		site.syntaxError("invalid list literal expression syntax");
		return null;
	}

	const itemTypeTs = ts.splice(0, bracesPos);

	const itemTypeExpr = buildTypeExpr(site, itemTypeTs.slice());

	if (!itemTypeExpr) {
		return null;
	}

	const braces = assertToken(ts.shift(), site)?.assertGroup("{");

	if (!braces) {
		return null;
	}

	const itemExprs = reduceNull(braces.fields.map(fts => buildValueExpr(fts[0]?.isGroup("{") ? itemTypeTs.concat(fts) : fts)));

	if (itemExprs === null) {
		// error will have already been thrown internally
		return null;
	}

	return new ListLiteralExpr(site, itemTypeExpr, itemExprs);
}

/**
 * @internal
 * @param {Token[]} ts 
 * @returns {StructLiteralExpr | null}
 */
function buildOptionSomeLiteralExpr(ts) {
	const site = ts[0].site;

	const bracesPos = Group.find(ts, "{");

	if (bracesPos == -1) {
		site.syntaxError("invalid Option[]::Some literal expression syntax");
		return null;
	}

	const mainTypeTs = ts.splice(0, bracesPos);

	const brackets = assertToken(mainTypeTs[1], site)?.assertGroup("[", 1);

	if (!brackets) {
		return null;
	}

	const someTypeTs = brackets.fields[0].slice();

	const braces = assertToken(ts.shift(), site)?.assertGroup("{", 1);

	if (!braces) {
		return null;
	}

	if (braces.fields[0][0]?.isGroup("{")) {
		const inner = new Group(
			braces.site,
			"{",
			[
				someTypeTs.concat(braces.fields[0])
			]
		);

		return buildStructLiteralExpr(mainTypeTs.concat(inner).concat(ts));
	} else {
		return buildStructLiteralExpr(mainTypeTs.concat([braces]).concat(ts));
	}

}

/**
 * @internal
 * @param {Token[]} ts
 * @returns {MapLiteralExpr | null}
 */
function buildMapLiteralExpr(ts) {
	const mapWord = assertDefined(ts.shift()).assertWord("Map");

	if (!mapWord) {
		return null;
	}

	const site = mapWord.site;

	const bracket = assertDefined(ts.shift()).assertGroup("[", 1);

	if (!bracket) {
		return null;
	}

	const keyTypeTs = bracket.fields[0];

	const keyTypeExpr = buildTypeExpr(site, keyTypeTs.slice());

	if (!keyTypeExpr) {
		return null;
	}

	const bracesPos = Group.find(ts, "{");

	if (bracesPos == -1) {
		site.syntaxError("invalid map literal expression syntax");
		return null;
	}

	const valueTypeTs = ts.splice(0, bracesPos);

	const valueTypeExpr = buildTypeExpr(site, valueTypeTs.slice());

	if (!valueTypeExpr) {
		return null;
	}

	const braces = assertDefined(ts.shift()).assertGroup("{");

	if (!braces) {
		return null;
	}

	/**
	 * @type {null | [Expr, Expr][]}
	 */
	const pairs = reduceNullPairs(braces.fields.map(fts => {
		const colonPos = SymbolToken.find(fts, ":");

		if (colonPos == -1) {
			if (fts.length == 0) {
				braces.syntaxError("unexpected empty field");
			} else {
				fts[0].syntaxError("expected ':' in map literal field");
			}
		} else if (colonPos == 0) {
			fts[colonPos].syntaxError("expected expression before ':' in map literal field");
		} else if (colonPos == fts.length - 1) {
			fts[colonPos].syntaxError("expected expression after ':' in map literal field");
		} else {
			const keyTs = fts.slice(0, colonPos);
			const keyExpr = buildValueExpr(keyTs[0]?.isGroup("{") ? keyTypeTs.concat(keyTs) : keyTs);

			const valueTs = fts.slice(colonPos+1)
			const valueExpr = buildValueExpr(valueTs[0]?.isGroup("{") ? valueTypeTs.concat(valueTs) : valueTs);

			/**
			 * @type {[Expr | null, Expr | null]}
			 */
			return [keyExpr, valueExpr];
		}

		return [null, null];
	}));

	if (pairs === null) {
		return null;
	}

	return new MapLiteralExpr(site, keyTypeExpr, valueTypeExpr, pairs);
}

/**
 * @internal
 * @param {Token[]} ts 
 * @returns {StructLiteralExpr | null}
 */
function buildStructLiteralExpr(ts) {
	const bracesPos = Group.find(ts, "{");

	assert(bracesPos != -1);

	const site = ts[bracesPos].site;

	if (bracesPos == 0) {
		site.syntaxError("expected struct type before braces");
		return null;
	}
	
	const typeExpr = buildTypeExpr(site, ts.splice(0, bracesPos));

	if (!typeExpr) {
		return null;
	}

	const braces = assertDefined(ts.shift()).assertGroup("{");

	if (!braces) {
		return null;
	}

	const fields = reduceNull(braces.fields.map(fts => buildStructLiteralField(braces.site, fts)));

	if (fields === null) {
		return null;
	} 
	
	if (fields.every(f => f.isNamed()) || fields.every(f => !f.isNamed())) {
		return new StructLiteralExpr(typeExpr, fields);
	} else {
		braces.site.syntaxError("mangled literal struct (hint: specify all fields positionally or all with keys)");
		return null;
	}
}

/**
 * @internal
 * @param {Site} site - site of the braces
 * @param {Token[]} ts
 * @returns {StructLiteralField | null}
 */
function buildStructLiteralField(site, ts) {
	if (ts.length > 2 && ts[0].isWord() && ts[1].isSymbol(":")) {
		return buildStructLiteralNamedField(site, ts);
	} else {
		return buildStructLiteralUnnamedField(site, ts);
	}
}

/**
 * @internal
 * @param {Site} site
 * @param {Token[]} ts
 * @returns {StructLiteralField | null}
 */
function buildStructLiteralNamedField(site, ts) {
	const name = assertToken(ts.shift(), site, "empty struct literal field")?.assertWord()?.assertNotKeyword();

	if (!name) {
		return null;
	}

	const colon = assertToken(ts.shift(), name.site, "expected ':' after struct field name")?.assertSymbol(":");

	if (!colon) {
		return null;
	}

	if (ts.length == 0) {
		colon.syntaxError("expected expression after ':'");
		return null;
	}
	const valueExpr = buildValueExpr(ts);

	if (!valueExpr) {
		return null;
	}

	return new StructLiteralField(name, valueExpr);
}

/**
 * @internal
 * @param {Site} site
 * @param {Token[]} ts
 * @returns {StructLiteralField | null}
 */
function buildStructLiteralUnnamedField(site, ts) {
	const valueExpr = buildValueExpr(ts);

	if (!valueExpr) {
		return null;
	}

	return new StructLiteralField(null, valueExpr);
}

/**
 * @internal
 * @param {Token[]} ts 
 * @returns {Expr | null}
 */
function buildValuePathExpr(ts) {
	const dcolonPos = SymbolToken.findLast(ts, "::");

	assert(dcolonPos != -1);

	const typeExpr = buildTypeExpr(ts[dcolonPos].site, ts.splice(0, dcolonPos));

	if (!typeExpr) {
		return null;
	}

	const dcolon = assertDefined(ts.shift()?.assertSymbol("::"));

	const memberName = assertToken(ts.shift(), dcolon.site)?.assertWord()?.assertNotKeyword();

	if (!memberName) {
		return null;
	}
	
	return new ValuePathExpr(typeExpr.site, typeExpr, memberName);
}


/////////////////////////////////
// Section 27: IR Context objects
/////////////////////////////////

/**
 * Scope for IR names.
 * Works like a stack of named values from which a Debruijn index can be derived
 * @internal
 */
export class IRScope {
	#parent;
	/** variable name (can be empty if no usable variable defined at this level) */
	#variable;

	/**
	 * @param {null | IRScope} parent 
	 * @param {null | IRVariable} variable
	 */
	constructor(parent, variable) {
		this.#parent = parent;
		this.#variable = variable;
	}

	/**
	 * Calculates the Debruijn index of a named value. Internal method
	 * @param {Word | IRVariable} name 
	 * @param {number} index 
	 * @returns {[number, IRVariable]}
	 */
	getInternal(name, index) {
		if (this.#variable !== null && (name instanceof Word && this.#variable.toString() == name.toString()) || (name instanceof IRVariable && this.#variable == name)) {
			return [index, this.#variable];
		} else if (this.#parent === null) {
			throw name.referenceError(`variable ${name.toString()} not found`);
		} else {
			return this.#parent.getInternal(name, index + 1);
		}
	}

	/**
	 * Calculates the Debruijn index.
	 * @param {Word | IRVariable} name 
	 * @returns {[number, IRVariable]}
	 */
	get(name) {
		// one-based
		return this.getInternal(name, 1);
	}

	/**
	 * Checks if a named builtin exists
	 * @param {string} name 
	 * @param {boolean} strict - if true then throws an error if builtin doesn't exist
	 * @returns {boolean}
	 */
	static isBuiltin(name, strict = false) {
		if (name.startsWith(BUILTIN_PREFIX)) {
			if (strict) {
				void this.findBuiltin(name); // assert that builtin exists
			}
			return true;
		} else {
			return false;
		}
	}

	/**
	 * Returns index of a named builtin
	 * Throws an error if builtin doesn't exist
	 * @param {string} name 
	 * @returns {number}
	 */
	static findBuiltin(name) {
		let i = UPLC_BUILTINS.findIndex(info => { return BUILTIN_PREFIX + info.name == name });
		assert(i != -1, `${name} is not a real builtin`);
		return i;
	}
}

/**
 * IR class that represents function arguments
 * @internal
 */
export class IRVariable extends Token {
	#name;

	/**
	 * @param {Word} name
	 */
	constructor(name) {
		super(name.site);
		this.#name = name;
	}

	/**
	 * @type {string}
	 */
	get name() {
		return this.#name.toString();
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return this.name;
	}

	/**
	 * @param {Map<IRVariable, IRVariable>} newVars 
	 * @returns {IRVariable}
	 */
	copy(newVars) {
		const newVar = new IRVariable(this.#name);

		newVars.set(this, newVar);

		return newVar;
	}
}


/////////////////////////////
// Section 28: IR AST objects
/////////////////////////////


/**
 * The optimizer maps expressions to expected values, calling notifyCopy assures that that mapping isn't lost for copies (copying is necessary when inlining)
 * @internal
 * @typedef {(oldExpr: IRExpr, newExpr: IRExpr) => void} NotifyCopy
 */

/**
 * Interface for:
 *   * `IRErrorExpr`
 *   * `IRCallExpr`
 *   * `IRFuncExpr`
 *   * `IRNameExpr`
 *   * `IRLiteralExpr`
 * 
 * The `copy()` method is needed because inlining can't use the same IRNameExpr twice, 
 *   so any inlineable expression is copied upon inlining to assure each nested IRNameExpr is unique.
 *   This is important to do even the the inlined expression is only called once, because it might still be inlined into multiple other locations that are eliminated in the next iteration.
 * 
 * `flatSize` returns the number of bits occupied by the equivalent UplcTerm in the final serialized UPLC program
 *   This is used to detect small IRFuncExprs and inline them
 * @internal
 * @typedef {{
 *   site: Site,
 *   flatSize: number
 *   resolveNames(scope: IRScope): void,
 *   toString(indent?: string): string,
 *   copy(notifyCopy: NotifyCopy, varMap: Map<IRVariable, IRVariable>): IRExpr,
 *   toUplc(): UplcTerm
 * }} IRExpr
 */

/**
 * Intermediate Representation variable reference expression
 * @internal
 * @implements {IRExpr}
 */
export class IRNameExpr {
	/**
	 * @readonly
	 * @type {Site}
	 */
	site;

	#name;

	/**
	 * @type {null | number} - cached debruijn index 
	 */
	#index;

	/**
	 * @type {null | IRVariable} - cached variable
	 */
	#variable;

	/**
	 * @param {Word} name 
	 * @param {null | IRVariable} variable
	 */
	constructor(name, variable = null) {
		this.site = name.site;
		assert(name.toString() != "_");
		assert(!name.toString().startsWith("undefined"));
		this.#name = name;
		this.#index = null;
		this.#variable = variable;
	}

	/**
	 * @type {string}
	 */
	get name() {
		return this.#name.toString();
	}

	/**
	 * isVariable() should be used to check if a IRNameExpr.variable is equal to a IRVariable (includes special handling of "__core*")
	 * @type {IRVariable}
	 */
	get variable() {
		if (this.#variable === null) {
			throw new Error(`variable should be set (name: ${this.name})`);
		} else {
			return this.#variable;
		}
	}

	/**
	 * @type {number}
	 */
	get flatSize() {
		if (this.isCore()) {
			let nForce = 0;

			let name = this.name;
			if (!name.startsWith(MACRO_BUILTIN_PREFIX) && name.startsWith(BUILTIN_PREFIX)) {
				if (name.endsWith(SAFE_BUILTIN_SUFFIX)) {
					name = name.slice(0, name.length - SAFE_BUILTIN_SUFFIX.length);
				}

				nForce = UPLC_BUILTINS[IRScope.findBuiltin(name)].forceCount;
			}

			return 13 + 4*nForce; // 4 for header, 7 for builtin index, 4 per force
		} else {
			return 13; // 4 for term header, and assume DeBruijn index fits in 7 bits
		}
	}

	/**
	 * Used when inlining
	 * @param {(oldExpr: IRExpr, newExpr: IRExpr) => void} notifyCopy
	 * @param {Map<IRVariable, IRVariable>} varMap
	 * @returns {IRNameExpr}
	 */
	copy(notifyCopy, varMap) {
		const variable = (this.#variable ? (varMap.get(this.#variable) ?? this.#variable) : this.#variable);

		const newExpr = new IRNameExpr(this.#name, variable);

		notifyCopy(this, newExpr);

		return newExpr;
	}

	/**
	 * @internal
	 * @returns {boolean}
	 */
	isCore() {
		const name = this.name;

		return name.startsWith(BUILTIN_PREFIX);
	}

	/**
	 * @internal
	 * @returns {boolean}
	 */
	isParam() {
		return this.name.startsWith("__PARAM")
	}

	/**
	 * @param {IRVariable} ref 
	 * @returns {boolean}
	 */
	isVariable(ref) {
		if (this.isCore()) {
			return false;
		} else {
			return this.variable === ref;
		}
	}

	/**
	 * @param {string} indent 
	 * @returns {string}
	 */
	toString(indent = "") {
		return this.#name.toString();
	}

	/**
	 * @param {IRScope} scope
	 */
	resolveNames(scope) {
		if (!this.isCore()) {
			if (this.#variable == null || this.isParam()) {
				[this.#index, this.#variable] = scope.get(this.#name);
			} else {
				[this.#index, this.#variable] = scope.get(this.#variable);
			}
		}
	}

	/**
	 * @returns {UplcTerm}
	 */
	toUplc() {
		if (this.isCore()) {
			return IRCallExpr.newUplcBuiltin(this.site, this.name);
		} else if (this.#index === null) {
			// use a dummy index (for size calculation)
			return new UplcVariable(
				this.site,
				new UplcInt(this.site, BigInt(0), false),
			);
		} else {
			return new UplcVariable(
				this.site,
				new UplcInt(this.site, BigInt(this.#index), false),
			);
		}
	}
}

/**
 * IR wrapper for UplcValues, representing literals
 * @internal
 * @implements {IRExpr}
 */
export class IRLiteralExpr {
	/**
	 * @readonly
	 * @type {Site}
	 */
	site;

	/**
	 * @type {UplcValue}
	 */
	#value;

	/**
	 * @param {UplcValue} value 
	 */
	constructor(value) {
		this.site = value.site;

		this.#value = value;
	}

	/**
	 * @type {UplcValue}
	 */
	get value() {
		return this.#value;
	}

	/**
	 * @type {number}
	 */
	get flatSize() {
		return (new UplcConst(this.#value)).flatSize;
	}

	/**
	 * @param {string} indent 
	 * @returns {string}
	 */
	toString(indent = "") {
		return this.#value.toString();
	}

	/**
	 * @param {NotifyCopy} notifyCopy
	 * @param {Map<IRVariable, IRVariable>} varMap
	 * @returns {IRExpr}
	 */
	copy(notifyCopy, varMap) {
		return this;
	}

	/**
	 * Linking doesn't do anything for literals
	 * @param {IRScope} scope 
	 */
	resolveNames(scope) {
	}

	/**
	 * @returns {UplcConst}
	 */
	toUplc() {
		return new UplcConst(this.#value);
	}
}

/**
 * IR function expression with some args, that act as the header, and a body expression
 * @internal
 * @implements {IRExpr}
 */
export class IRFuncExpr {
	/**
	 * @readonly
	 * @type {Site}
	 */
	site;

	/**
	 * Mutation is more convenient and much faster when applying some optimizations.
	 * @readwrite
	 * @type {IRVariable[]}
	 */
	args;

	/**
	 * Mutation is more convenient and much faster when applying some optimizations.
	 * @readwrite
	 * @type {IRExpr}
	 */
	body;

	/**
	 * A unique tag, that distinguishes each IRFuncExpr from each other IRFuncExpr (used for hashing)
	 * @readonly
	 * @type {number}
	 */
	tag;

	/**
	 * @param {Site} site 
	 * @param {IRVariable[]} args 
	 * @param {IRExpr} body 
	 * @param {number} tag
	 */
	constructor(site, args, body, tag) {
		this.site = site;
		this.args = args;
		this.body = assertDefined(body);
		this.tag = tag;
	}

	/**
	 * @type {number}
	 */
	get flatSize() {
		const nArgs = this.args.length;
		return 4 + (nArgs > 0 ? (nArgs - 1)*4 : 0) + this.body.flatSize;
	}

	/**
	 * @returns {boolean}
	 */
	hasOptArgs() {
		const b = this.args.some(a => a.name.startsWith("__useopt__"));

		if (b) {
			return b;
		}

		if (this.body instanceof IRFuncExpr) {
			return this.body.hasOptArgs();
		} else {
			return false;
		}
	}

	/**
	 * @param {string} indent 
	 * @returns {string}
	 */
	toString(indent = "") {
		let innerIndent = (this.body instanceof IRCallExpr && this.body.func instanceof IRFuncExpr && this.body.args.length == 1 && this.body.func instanceof IRFuncExpr && this.body.func.args[0].name.startsWith("__")) ? indent : indent + TAB;

		let s = "(" + this.args.map(n => n.toString()).join(", ") + ") -> {\n" + innerIndent;
		s += this.body.toString(innerIndent);
		s += "\n" + indent + "}";

		return s;
	}

	/**
	 * @param {IRScope} scope 
	 */
	resolveNames(scope) {
		// in the zero-arg case no Debruijn indices need to be added because we use Delay/Force

		for (let arg of this.args) {
			scope = new IRScope(scope, arg);
		}

		this.body.resolveNames(scope);
	}

	/**
	 * @param {NotifyCopy} notifyCopy
	 * @param {Map<IRVariable, IRVariable>} varMap
	 * @returns {IRExpr}
	 */
	copy(notifyCopy, varMap) {
		const args = this.args.map(a => a.copy(varMap));
		const newExpr = new IRFuncExpr(this.site, args, this.body.copy(notifyCopy, varMap), this.tag);

		notifyCopy(this, newExpr);

		return newExpr;
	}

	/** 
	 * @returns {UplcTerm}
	 */
	toUplc() {
		let term = this.body.toUplc();

		if (this.args.length == 0) {
			// a zero-arg func is turned into a UplcDelay term
			term = new UplcDelay(this.site, term);
		} else {
			for (let i = this.args.length - 1; i >= 0; i--) {
				term = new UplcLambda(this.site, term, this.args[i].toString());
			}
		}

		return term;
	}
}

/**
 * Base class of IRUserCallExpr and IRCoreCallExpr
 * @internal
 * @implements {IRExpr}
 */
export class IRCallExpr {
	/**
	 * @readonly
	 * @type {Site}
	 */
	site;

	/**
	 * Mutation is more convenient and much faster when applying some optimizations.
	 * @readwrite
	 * @type {IRExpr}
	 */
	func;

	/**
	 * Mutation is more convenient and much faster when applying some optimizations.
	 * @readwrite
	 * @type {IRExpr[]}
	 */
	args;

	/**
	 * @param {Site} site
	 * @param {IRExpr} func
	 * @param {IRExpr[]} args
	 */
	constructor(site, func, args) {
		this.site = site;
		this.func = func;
		this.args = args;
	}

	/**
	 * @returns {boolean}
	 */
	isSafeBuiltin() {
		if (this.func instanceof IRNameExpr && this.func.isCore()) {
			return this.func.name.endsWith(SAFE_BUILTIN_SUFFIX);
		} else {
			return false;
		}
	}

	/**
	 * Returns an empty string this isn't a builtin
	 * @type {string}
	 */
	get builtinName() {
		if (this.func instanceof IRNameExpr && this.func.isCore()) {
			let name = this.func.name.toString().slice(BUILTIN_PREFIX.length);

			if (name.endsWith(SAFE_BUILTIN_SUFFIX)) {
				name = name.slice(0, name.length - SAFE_BUILTIN_SUFFIX.length);
			}

			return name;
		} else {
			return "";
		}
	}

	get flatSize() {
		return 4 + this.args.reduce((prev, arg) => arg.flatSize + prev, 0) + this.func.flatSize;
	}

	/**
	 * @param {string} indent 
	 * @returns {string}
	 */
	argsToString(indent = "") {
		return this.args.map(argExpr => argExpr.toString(indent)).join(", ")
	}

	/**
	 * @param {string} indent
	 * @returns {string}
	 */
	toString(indent = "") {
		if (this.builtinName == "ifThenElse") {
			return `${BUILTIN_PREFIX}${this.builtinName}(\n${indent}${TAB}${this.args[0].toString(indent + TAB)},\n${indent}${TAB}${this.args[1].toString(indent + TAB)},\n${indent}${TAB}${this.args[2].toString(indent+TAB)}\n${indent})`;
		} else if (this.builtinName != "") {
			return `${BUILTIN_PREFIX}${this.builtinName}(${this.argsToString(indent)})`;
		} else if (this.func instanceof IRFuncExpr && this.func.args.length == 1) {
			return `${this.func.args[0].name} = ${this.args[0].toString(indent)};\n${indent}${this.func.body.toString(indent)}`;
		} else {
			let comment = (this.func instanceof IRFuncExpr && this.func.args.length == 1 && this.func.args[0].name.startsWith("__")) ? `/*${this.func.args[0].name}*/` : "";

			return `${this.func.toString(indent)}(${comment}${this.argsToString(indent)})`;
		}
	}

	/**
	 * @param {IRScope} scope 
	 */
	resolveNamesInArgs(scope) {
		for (let argExpr of this.args) {
			argExpr.resolveNames(scope);
		}
	}

	/**
	 * @param {IRScope} scope 
	 */
	resolveNames(scope) {
		if (this.func instanceof IRNameExpr && this.func.isCore()) {
			this.resolveNamesInArgs(scope);
		} else {
			this.func.resolveNames(scope);
			this.resolveNamesInArgs(scope);
		}
	}

	/**
	 * @param {NotifyCopy} notifyCopy
	 * @param {Map<IRVariable, IRVariable>} varMap
	 * @returns {IRExpr}
	 */
	copy(notifyCopy, varMap) {
		const newExpr = new IRCallExpr(this.site, this.func.copy(notifyCopy, varMap), this.args.map(a => a.copy(notifyCopy, varMap)));

		notifyCopy(this, newExpr);

		return newExpr;
	}

	/**
	 * @param {UplcTerm} term
	 * @returns {UplcTerm}
	 */
	toUplcCall(term) {
		if (this.args.length == 0) {
			// assuming underlying zero-arg function has been converted into a UplcDelay term
			term = new UplcForce(this.site, term);
		} else {
			for (let argExpr of this.args) {
				
				term = new UplcCall(this.site, term, argExpr.toUplc());
			}
		}

		return term;
	}

	/**
	 * @param {Site} site
	 * @param {string} name - full name of builtin, including prefix
	 * @returns {UplcTerm}
	 */
	static newUplcBuiltin(site, name) {
		assert(name.startsWith(BUILTIN_PREFIX));

		if (name.endsWith(SAFE_BUILTIN_SUFFIX)) {
			name = name.slice(0, name.length - SAFE_BUILTIN_SUFFIX.length);
		}

		const builtinName = name.slice(BUILTIN_PREFIX.length);

		/**
		 * @type {UplcTerm}
		 */
		let term = new UplcBuiltin(site, builtinName);

		if (!builtinName.startsWith("macro__")) {
			const nForce = UPLC_BUILTINS[IRScope.findBuiltin(name)].forceCount;
	
			for (let i = 0; i < nForce; i++) {
				term = new UplcForce(site, term);
			}
		}
 
		return term;
	}

	/**
	 * @returns {UplcTerm}
	 */
	toUplc() {
		if (this.func instanceof IRNameExpr && this.func.name.startsWith(BUILTIN_PREFIX)) {
			let term = IRCallExpr.newUplcBuiltin(this.site, this.func.name);

			return this.toUplcCall(term);
		} else {
			return this.toUplcCall(this.func.toUplc());
		}
	}
}

/**
 * Intermediate Representation error call (with optional literal error message)
 * @internal
 * @implements {IRExpr}
 */
export class IRErrorExpr {
	/**
	 * @readonly
	 * @type {Site}
	 */
	site;

	#msg;

	/**
	 * @param {Site} site 
	 * @param {string} msg 
	 */
	constructor(site, msg = "") {
		this.site = site;
		this.#msg = msg;
	}

	/**
	 * @type {number}
	 */
	get flatSize() {
		return 4;
	}

	/**
	 * @param {string} indent 
	 * @returns {string}
	 */
	toString(indent = "") {
		return "error()";
	}

	/**
	 * @param {IRScope} scope 
	 */
	resolveNames(scope) {
	}

	/**
	 * @param {NotifyCopy} notifyCopy
	 * @returns {IRExpr}
	 */
	copy(notifyCopy) {
		const newExpr = new IRErrorExpr(this.site, this.#msg);

		notifyCopy(this, newExpr);

		return newExpr;
	}

	/**
	 * @returns {UplcTerm}
	 */
	toUplc() {
		return new UplcError(this.site, this.#msg);
	}
}

/**
 * @internal
 * @param {IRExpr} root 
 * @param {{
 *   nameExpr?: (expr: IRNameExpr) => void
 *   errorExpr?: (expr: IRErrorExpr) => void
 *   literalExpr?: (expr: IRLiteralExpr) => void
 *   callExpr?: (expr: IRCallExpr) => void
 *   funcExpr?: (expr: IRFuncExpr) => void
 *   exit?: () => boolean
 * }} callbacks 
 * @returns 
 */
export function loopIRExprs(root, callbacks) {
	const stack = [root];

	let head = stack.pop();

	while (head) {
		if (head instanceof IRNameExpr) {
			if (callbacks.nameExpr) {
				callbacks.nameExpr(head);
			}
		} else if (head instanceof IRErrorExpr) {
			if (callbacks.errorExpr) {
				callbacks.errorExpr(head);
			}
		} else if (head instanceof IRLiteralExpr) {
			if (callbacks.literalExpr) {
				callbacks.literalExpr(head);
			}
		} else if (head instanceof IRCallExpr) {
			stack.push(head.func);

			for (let a of head.args) {
				stack.push(a);
			}

			if (callbacks.callExpr) {
				callbacks.callExpr(head);
			}
		} else if (head instanceof IRFuncExpr) {
			if (callbacks.funcExpr) {
				callbacks.funcExpr(head);
			}

			stack.push(head.body);
		}

		if (callbacks.exit && callbacks.exit()) {
			return;
		}

		head = stack.pop();
	}
}


/////////////////////////////////////
// Section 29: IR AST build functions
/////////////////////////////////////

/**
 * @internal
 */
class IRExprTagger {
	#tag;

	constructor() {
		this.#tag = 0;
	}

	genTag() {
		this.#tag += 1;

		return this.#tag;
	}
}

/**
 * Build an Intermediate Representation expression
 * @param {Token[]} ts 
 * @param {IRExprTagger | null} funcTagger // each IRFuncExpr needs a unique tag, so that hashing different IRFuncExprs with the same args and bodies leads to a different hash
 * @returns {IRExpr}
 * @internal
 */
export function buildIRExpr(ts, funcTagger = null) {
	/** @type {null | IRExpr} */
	let expr = null;

	if (funcTagger === null) {
		funcTagger = new IRExprTagger();
	}

	while (ts.length > 0) {
		let t = ts.shift();

		if (t === undefined) {
			throw new Error("unexpected: no tokens");
		} else {
			if (t.isWord() && ts.length > 1 && ts[0].isSymbol("=")) {
				const equalsSite = assertDefined(assertDefined(ts.shift()).assertSymbol("=")).site;

				const semiColonPos = SymbolToken.find(ts, ";");

				assert(semiColonPos != -1, "expected semicolon after '='");
				
				const upstreamTokens = ts.splice(0, semiColonPos);

				const upstreamExpr = buildIRExpr(upstreamTokens, funcTagger);

				const semicolonSite = assertDefined(assertDefined(ts.shift()).assertSymbol(";")).site;

				const downstreamExpr = buildIRExpr(ts, funcTagger);

				const argWord = assertDefined(t.assertWord());

				expr = new IRCallExpr(equalsSite, new IRFuncExpr(semicolonSite, [new IRVariable(argWord)], downstreamExpr, funcTagger.genTag()), [upstreamExpr])
			} else if (t.isGroup("(") && ts.length > 0 && ts[0].isSymbol("->")) {
				assert(expr === null, "shouldn't be preceded by an expr");

				ts.unshift(t);

				expr = buildIRFuncExpr(ts, funcTagger);
			} else if (t.isGroup("(")) {
				let group = assertDefined(t.assertGroup(), "should be a group");

				if (expr === null) {
					if (group.fields.length == 1) {
						expr = buildIRExpr(group.fields[0], funcTagger)
					} else if (group.fields.length == 0) {
						expr = new IRLiteralExpr(new UplcUnit(t.site));
					} else {
						group.syntaxError("unexpected parentheses with multiple fields");
					}
				} else {
					let args = [];
					for (let f of group.fields) {
						args.push(buildIRExpr(f, funcTagger));
					}

					expr = new IRCallExpr(t.site, expr, args);
				}
			} else if (t.isSymbol("-")) {
				// only makes sense next to IntegerLiterals
				let int = assertDefined(ts.shift(), "expected digit after '-'");
				if (int instanceof IntLiteral) {
					expr = new IRLiteralExpr(new UplcInt(int.site, int.value * (-1n)));
				} else {
					throw int.site.typeError(`expected literal int, got ${int}`);
				}
			} else if (t instanceof BoolLiteral) {
				assert(expr === null);
				expr = new IRLiteralExpr(new UplcBool(t.site, t.value));
			} else if (t instanceof IntLiteral) {
				assert(expr === null);
				expr = new IRLiteralExpr(new UplcInt(t.site, t.value));
			} else if (t instanceof ByteArrayLiteral) {
				assert(expr === null);
				if (t.bytes.length == 0 && ts[0] != undefined && ts[0] instanceof ByteArrayLiteral) {
					// literal data is ##<...>
					const next = assertDefined(ts.shift(), "expected hexadecimal bytestring after '##'");

					if (next instanceof ByteArrayLiteral) {
						expr = new IRLiteralExpr(new UplcDataValue(next.site, UplcData.fromCbor(next.bytes)));
					} else {
						throw new Error("unexpected token after '##'");
					}
				} else {
					expr = new IRLiteralExpr(new UplcByteArray(t.site, t.bytes));
				}
			} else if (t instanceof StringLiteral) {
				assert(expr === null);
				expr = new IRLiteralExpr(new UplcString(t.site, t.value));
			} else if (t.isWord("error")) {
				assert(expr === null, "unexpected expr before 'error'");

				let maybeGroup = ts.shift();
				if (maybeGroup === undefined) {
					throw t.site.syntaxError("expected parens after error");
				} else {
					assertDefined(maybeGroup.assertGroup("(", 0), "expected empty parens after 'error'");
					
					expr = new IRErrorExpr(t.site, "");
				}
			} else if (t.isWord()) {
				const w = assertDefined(t.assertWord(), "expected word");

				if (expr !== null) {
					throw new Error(`unexpected expr '${expr.toString()}' before word '${w.value}'`);
				}

				expr = new IRNameExpr(w);
			} else {
				throw new Error("unhandled untyped token " + t.toString());
			}
		}
	}

	if (expr === null) {
		throw new Error("expr is null");
	} else {
		return expr;
	}
}

/**
 * Build an IR function expression
 * @param {Token[]} ts 
 * @param {IRExprTagger} funcTagger
 * @returns {IRFuncExpr}
 */
function buildIRFuncExpr(ts, funcTagger) {
	let maybeParens = ts.shift();
	if (maybeParens === undefined) {
		throw new Error("empty func expr");
	} else {
		let parens = assertDefined(maybeParens.assertGroup("("));

		assertDefined(ts.shift()).assertSymbol("->");
		let braces = assertDefined(assertDefined(ts.shift()).assertGroup("{"));

		/**
		 * @type {Word[]}
		 */
		let argNames = [];

		for (let f of parens.fields) {
			assert(f.length == 1, "expected single word per arg");
			argNames.push(assertDefined(f[0].assertWord()));
		}

		if (braces.fields.length > 1) {
			throw braces.syntaxError("unexpected comma in function body")
		} else if (braces.fields.length == 0) {
			throw braces.syntaxError("empty function body")
		}

		let bodyExpr = buildIRExpr(braces.fields[0], funcTagger);

		return new IRFuncExpr(parens.site, argNames.map(a => new IRVariable(a)), bodyExpr, funcTagger.genTag());
	}
}


///////////////////////////////////
// Section 30: IR pseudo evaluation
///////////////////////////////////


/**
 * @internal
 * @typedef {{
 *   toString(): string
 *   isLiteral(): boolean
 *   hasError(maybe: boolean): boolean
 *   withoutLiterals(): IRValue
 *   withoutErrors(): IRValue
 *   dump(codeMapper: IRValueCodeMapper, depth?: number): any
 * }} IRValue
 */


/**
 * @internal
 */
class IRStack {
    /**
     * @readonly
     * @type {[IRVariable, IRValue][]}
     */
    values;

    #isLiteral;

    /**
     * @param {[IRVariable, IRValue][]} values 
     * @param {boolean} isLiteral
     */
    constructor(values, isLiteral) {
        this.values = values;
        this.#isLiteral = isLiteral || values.length == 0;
    }

    /**
     * @param {IRVariable} variable 
     * @returns {boolean}
     */
    static isGlobal(variable) {
        return variable.name.match(/^__(helios|const|module)__/) !== null;
    }

    /**
     * @param {IRValueCodeMapper} codeMapper 
     * @param {number} depth 
     * @returns {any}
     */
    dump(codeMapper, depth = 0) {
        return {
            isLiteral: this.#isLiteral,
            codes: codeMapper.getCodes(this),
            ...(depth > 0 ? {values: this.values.map(([_, arg]) => {
                return arg.dump(codeMapper, depth - 1)
            })} : {})
        }
    }

    /**
     * @returns {boolean}
     */
    isLiteral() {
        return this.#isLiteral;
    }

    /**
     * @return {IRStack}
     */
    withoutLiterals() {
        /**
         * @type {[IRVariable, IRValue][]}
         */
        const varVals = this.values.map(([vr, vl]) => {
            if (IRStack.isGlobal(vr)) {
                return [vr, vl];
            } else {
                return [vr, vl.withoutLiterals()]
            }
        });

        return new IRStack(
            varVals, 
            varVals.every(([_, v]) => v.isLiteral())
        );
    }

    /**
     * @param {IRVariable} v 
     * @returns {IRValue}
     */
    getValue(v) {
        const j = this.values.findIndex(([va]) => va == v)

        if (j != -1) {
            return this.values[j][1];
        }

        throw new Error(`${v.name} not found in IRStack`);
    }

    /**
     * @param {[IRVariable, IRValue][]} args 
     * @returns {IRStack}
     */
    extend(args) {
        assert(args.every(([_, v]) => !(v instanceof IRErrorValue)));

        return new IRStack(
            this.values.concat(args),
            this.#isLiteral && args.every(([_, v]) => v.isLiteral())
        );
    }

    /**
     * @param {Set<IRVariable>} irVars 
     * @returns {IRStack}
     */
    filter(irVars) {
        const varVals = this.values.filter(([v]) => irVars.has(v));
        return new IRStack(varVals, varVals.every(([_, v]) => v.isLiteral()));
    }

    /**
     * @returns {IRStack}
     */
    static empty() {
        return new IRStack([], true);
    }

    /**
     * Both stack are expected to have the same shape
     * TODO: get rid of this
     * @param {IRStack} other
     * @returns {IRStack}
     */
    merge(other) {
        const n = this.values.length;

        assert(n == other.values.length);

        let stack = IRStack.empty();

        for (let i = 0; i < n; i++) {
            const a = this.values[i];
            const b = other.values[i];

            if (a == b) {
                stack = stack.extend([a]);
            } else {
                stack = stack.extend([[a[0], IRMultiValue.flatten([a[1], b[1]])]]);
            }
        }

        return stack;
    }
}

/**
 * @internal
 * @implements {IRValue}
 */
export class IRLiteralValue {
	/**
	 * @readonly
	 */
	value;

	/**
	 * @param {UplcValue} value 
	 */
	constructor(value) {
		this.value = value;
	}

    /**
     * @returns {string}
     */
    toString() {
        return this.value.toString();
    }

    /**
     * @returns {boolean}
     */
    isLiteral() {
        return true;
    }

    /**
     * @param {boolean} maybe
     * @returns {boolean}
     */
    hasError(maybe = true) {
        return false;
    }

    /**
     * @returns {IRValue}
     */
    withoutLiterals() {
        if (this.value instanceof UplcUnit) {
            return new IRAnyValue();
        } else {
            return new IRDataValue();
        }
    }

    /**
     * @returns {IRValue}
     */
    withoutErrors() {
        return this;
    }

    /**
     * @param {IRValueCodeMapper} codeMapper 
     * @param {number} depth 
     * @returns {any}
     */
    dump(codeMapper, depth = 0) {
        return {
            type: "Literal",
            code: codeMapper.getCode(this),
            value: this.toString()
        }
    }
}

/**
 * @internal
 * @implements {IRValue}
 */
export class IRDataValue {
    /**
     * @returns {boolean}
     */
    isLiteral() {
        return false;
    }

    /**
     * @returns {IRValue}
     */
    withoutLiterals() {
        return this;
    }

    /**
     * @param {boolean} maybe
     * @returns {boolean}
     */
    hasError(maybe = true) {
        return false;
    }

    /**
     * @returns {IRValue}
     */
    withoutErrors() {
        return this;
    }

    /**
     * @returns {string}
     */
    toString() {
        return `Data`;
    }

    /**
     * @param {IRValueCodeMapper} codeMapper 
     * @param {number} depth 
     * @returns 
     */
    dump(codeMapper, depth = 0) {
        return {
            code: codeMapper.getCode(this),
            type: "Data"
        }
    }
}

/**
 * @param {IRExpr} expr 
 * @returns {Set<IRVariable>}
 */
function collectIRVariables(expr) {
    /**
     * @type {Set<IRVariable>}
     */
    const s = new Set();

    loopIRExprs(expr, {
        nameExpr: (nameExpr) => {
            if (!nameExpr.isCore()) {
                s.add(nameExpr.variable);
            }
        }
    });

    return s;
}

/**
 * @internal
 * @implements {IRValue}
 */
export class IRBuiltinValue {
    /**
     * @readonly
     * @type {IRNameExpr}
     */
    builtin;

    /**
     * @param {IRNameExpr} builtin 
     */
    constructor(builtin) {
		assert(builtin.isCore());

        this.builtin = builtin;
    }

    /**
     * @type {string}
     */
    get builtinName() {
        let name = this.builtin.name.slice(BUILTIN_PREFIX.length);

        if (name.endsWith(SAFE_BUILTIN_SUFFIX)) {
            name = name.slice(0, name.length - SAFE_BUILTIN_SUFFIX.length);
        }

        return name;
    }

    /**
     * @returns {string}
     */
    toString() {
        return `Builtin__${this.builtinName}`;
    }

    /**
     * @returns {boolean}
     */
    isLiteral() {
        return true;
    }

    /**
     * @returns {IRValue}
     */
    withoutLiterals() {
        return this;
    }

    /**
     * @param {boolean} maybe
     * @returns {boolean}
     */
    hasError(maybe = true) {
        return false;
    }

    /**
     * @returns {IRValue}
     */
    withoutErrors() {
        return this;
    }

    /**
     * @param {IRValueCodeMapper} codeMapper 
     * @param {number} depth 
     * @returns {any}
     */
    dump(codeMapper, depth = 0) {
        return {
            type: "Builtin",
            code: codeMapper.getCode(this),
            name: this.builtin.name
        }
    }
}

/**
 * @internal
 * @implements {IRValue}
 */
export class IRFuncValue {
    /**
     * @readonly
     * @type {IRStack}
     */
    stack;

	/**
	 * @readonly
	 * @type {IRFuncExpr}
	 */
	definition;

	/**
     * @param {IRFuncExpr} definition
     * @param {IRStack} stack
	 */
	constructor(definition, stack) {
        this.definition = definition;
        const irVars = collectIRVariables(definition);
        this.stack = stack.filter(irVars);
	}

    /**
     * @param {IRFuncExpr} definition 
     * @param {IRStack} stack 
     * @returns {IRFuncValue}
     */
    static new(definition, stack) {
        return new IRFuncValue(definition, stack);
    }

    /**
     * @returns {boolean}
     */
    isLiteral() {
        return this.stack.isLiteral();
    }

    /**
     * @returns {IRValue}
     */
    withoutLiterals() {
        return new IRFuncValue(this.definition, this.stack.withoutLiterals());
    }

    /**
     * @param {boolean} maybe
     * @returns {boolean}
     */
    hasError(maybe = true) {
        return false;
    }

    /**
     * @returns {IRValue}
     */
    withoutErrors() {
        return this;
    }

    /**
     * @param {IRValueCodeMapper} codeMapper 
     * @param {number} depth 
     * @returns {any}
     */
    dump(codeMapper, depth = 0) {
        return {
            type: "Fn",
            tag: this.definition.tag,
            codes: codeMapper.getCodes(this),
            definition: this.definition.toString(),
            stack: this.stack.dump(codeMapper, depth)
        }
    }

    /**
     * @returns {string}
     */
    toString() {
        return `Fn${this.definition.tag}`;
    }
}

/**
 * @internal
 * @implements {IRValue}
 */
export class IRErrorValue {
    /**
     * @returns {boolean}
     */
    isLiteral() {
        return false;
    }

    /**
     * @returns {IRValue}
     */
    withoutLiterals() {
        return this;
    }

    /**
     * @param {boolean} maybe
     * @returns {boolean}
     */
    hasError(maybe = true) {
        return true;
    }

    /**
     * @returns {IRValue}
     */
    withoutErrors() {
        throw new Error("can't remove IRErrorValue from IRErrorValue");
    }

    /**
     * @returns {string}
     */
    toString() {
        return `Error`;
    }

    /**
     * @param {IRValueCodeMapper} codeMapper 
     * @param {number} depth 
     * @returns {any}
     */
    dump(codeMapper, depth = 0) {
        return {
            type: "Error",
            code: codeMapper.getCode(this)
        }
    }
}

/**
 * Can be Data of any function
 * Simply eliminated when encountered in an IRMultiValue
 * @internal
 * @implements {IRValue}
 */
export class IRAnyValue {
    /**
     * @returns {boolean}
     */
    isLiteral() {
        return false;
    }

    /**
     * @returns {IRValue}
     */
    withoutLiterals() {
        return this;
    }

    /**
     * Maybe this IRAnyValue instance represents an Error, we can't know for sure.
     * @param {boolean} maybe
     * @returns {boolean}
     */
    hasError(maybe = true) {
        return maybe;
    }

    /**
     * @returns {IRValue}
     */
    withoutErrors() {
        return this;
    }

    /**
     * @returns {string}
     */
    toString() {
        return `Any`;
    }

    /**
     * @param {IRValueCodeMapper} codeMapper 
     * @param {number} depth 
     * @returns {any}
     */
    dump(codeMapper, depth = 0) {
        return {
            type: "Any",
            code: codeMapper.getCode(this)
        }
    }
}

/**
 * @internal
 * @implements {IRValue}
 */
export class IRMultiValue {
	/**
	 * @readonly
	 */
	values;

	/**
	 * @param {IRValue[]} values 
	 */
	constructor(values) {
        assert(values.every(v => !(v instanceof IRLiteralValue)));
		this.values = values;
	}

    /**
     * @param {boolean} maybe
     * @returns {boolean}
     */
    hasError(maybe = true) {
        return this.values.some(v => v.hasError(maybe));
    }

    /**
     * @returns {boolean}
     */
    isLiteral() {
        return false;
    }

    /**
     * @returns {IRValue}
     */
    withoutLiterals() {
        return IRMultiValue.flatten(this.values.map(v => v.withoutLiterals()));
    }

    /**
     * @returns {boolean}
     */
    hasData() {
        return this.values.some(v => v instanceof IRDataValue);
    }

    /**
     * @returns {boolean}
     */
    hasLiteral() {
        return this.values.some(v => v instanceof IRLiteralValue);
    }

    /**
     * @param {IRValueCodeMapper} codeMapper 
     * @param {number} depth 
     * @returns {any}
     */
    dump(codeMapper, depth = 0) {
        return {
            type: "Multi",
            code: codeMapper.getCode(this),
            values: this.values.slice().map(v => v.dump(codeMapper, depth))
        }
    }

    toString() {
        /**
         * @type {string[]}
         */
        const parts = [];

        if (this.hasError()) {
            parts.push(`Error`);
        }

        if (this.hasData()) {
            parts.push(`Data`);
        }

        this.values.forEach(v => {
            if (v instanceof IRFuncValue) {
                parts.push(`Fn${v.definition.tag}`);
            } else if (v instanceof IRBuiltinValue) {
                parts.push(`Builtin_${v.builtin.name.slice(("__core__").length)}`);
            }
        });

        this.values.forEach(v => {
            if (v instanceof IRLiteralValue) {
                parts.push(v.toString());
            }
        });

        if (parts.length == 1 && parts[0] == `Error` && this.values.some(v => v instanceof IRAnyValue)) {
            parts.push("Any");
        }

        return `(${parts.join(" | ")})`;
    }

	/**
	 * @param {IRValue[]} values 
	 * @returns {IRValue}
	 */
	static flatten(values) {
        if (values.length == 1) {
            return values[0];
        }

		// flatten nested IRMultiValues
		values = values.map(v => {
			if (v instanceof IRMultiValue) {
				return v.values
			} else {
				return [v]
			}
		}).flat();

        if (values.length == 1) {
            return values[0];
        } else if (values.every((v, i) => v instanceof IRLiteralValue && ((i == 0) || (v.toString() == values[0].toString())))) {
            return values[0];
        }
        
        const hasError = values.some(v => v instanceof IRErrorValue);
		const hasData = values.some(v => v instanceof IRDataValue || (v instanceof IRLiteralValue && !(v.value instanceof UplcUnit)));
        const hasAny = values.some(v => v instanceof IRAnyValue || (v instanceof IRLiteralValue && v.value instanceof UplcUnit));

		/**
		 * @type {IRValue[]}
		 */
		let flattened = [];

		if (values.some(v => v instanceof IRFuncValue || v instanceof IRBuiltinValue)) {
			/**
			 * @type {Map<IRExpr, IRFuncValue | IRBuiltinValue>}
			 */
			const s = new Map();

			values.forEach(v => {
				if (v instanceof IRFuncValue) {
                    const prev = s.get(v.definition);

                    if (prev instanceof IRFuncValue) {
                        s.set(v.definition, IRFuncValue.new(v.definition, prev.stack.merge(v.stack)));
                    } else {
                        s.set(v.definition, v);
                    }
                } else if (v instanceof IRBuiltinValue) {
                    s.set(v.builtin, v)
                }
			});

            flattened = flattened.concat(Array.from(s.values()));
		} else if (hasData) {
            flattened.push(new IRDataValue());
        } else if (hasAny) {
            flattened.push(new IRAnyValue());
        }

        if (hasError) {
            flattened.push(new IRErrorValue());
        }

        if (flattened.length == 1) {
			return flattened[0];
		} else {
			return new IRMultiValue(flattened);
		}
	}

	/**
	 * @param {IRValue[]} values 
	 * @returns {IRValue[][]}
	 */
	static allPermutations(values) {
		if (!values.some(v => v instanceof IRMultiValue)) {
			return [values];
		} else {
			/**
			 * @type {IRValue[][]}
			 */
			const permutations = [];

			let ns = values.map(v => {
				if (v instanceof IRMultiValue) {
					return v.values.length;
				} else {
					return 1;
				}
			});

			let N = ns.reduce((prev, n) => {
				return prev * n
			}, 1);

			for (let i = 0; i < N; i++) {
				let j = i;

				/**
				 * @type {number[]}
				 */
				const is = [];

				ns.forEach(n => {
					is.push(j % n);

					j = Math.floor(j / n);
				});

				permutations.push(is.map((j, i) => {
					const v = assertDefined(values[i]);

					if (v instanceof IRMultiValue) {
						return v.values[j];
					} else {
						assert(j == 0);
						return v;
					}
				}));
			}

			return permutations;
		}
	}

    /**
     * @returns {IRValue}
     */
    withoutErrors() {
        return IRMultiValue.flatten(this.values.filter(v => !(v instanceof IRErrorValue)));
    }
}

/**
 * Codes are used to combine multiple IRValues (including nested IRStacks that are part of IRFuncValues) into a single number.
 * 
 * We can't have however use the full depth of IRStack values because there could be callback-recursion.
 * @internal
 */
class IRValueCodeMapper {
    /**
     * @type {number}
     */
    #nextUnused;

    /**
     * @type {Map<string, number>}
     */
    #usedCodes;

    /**
     * @type {Map<IRValue | IRStack, number[]>}
     */
    #valueCodes;

    constructor() {
        this.#usedCodes = new Map([
            ["Any", 0],
            ["Data", 1],
            ["Error", 2]
        ]);
        this.#nextUnused = 3;
        this.#valueCodes = new Map();
    }

    static get maxDepth() {
        return 10;
    }

    /**
     * @private
     * @param {string} key 
     * @returns {number}
     */
    genCode(key) {
        let code = this.#usedCodes.get(key);

        if (code !== undefined) {
            return code;
        }

        code = this.#nextUnused;
        this.#nextUnused += 1;

        this.#usedCodes.set(key, code);

        return code;
    }

    /**
     * @private
     * @param {IRValue | IRStack} v 
     * @returns {number[]}
     */
    genCodes(v) {
        if (v instanceof IRBuiltinValue) {
            return (new Array(IRValueCodeMapper.maxDepth)).fill(this.genCode(v.builtinName));
        } else if (v instanceof IRDataValue) {
            return (new Array(IRValueCodeMapper.maxDepth)).fill(this.genCode("Data"));
        } else if (v instanceof IRErrorValue) {
            return (new Array(IRValueCodeMapper.maxDepth)).fill(this.genCode("Error"));
        } else if (v instanceof IRAnyValue) {
            return (new Array(IRValueCodeMapper.maxDepth)).fill(this.genCode("Any"));
        } else if (v instanceof IRLiteralValue) {
            return (new Array(IRValueCodeMapper.maxDepth)).fill(this.genCode(v.value.toString()));
        } else if (v instanceof IRFuncValue) {
            const tag = `Fn${assertDefined(v.definition.tag)}`;
            const stackCodes = this.getCodes(v.stack);

            /**
             * @type {number[]}
             */
            const codes = [];

            for (let i = 0; i < IRValueCodeMapper.maxDepth; i++) {
                const key = i == 0 ? tag : `${tag}(${stackCodes[i-1]})`;

                codes.push(this.genCode(key));
            }

            return codes;
        } else if (v instanceof IRStack) {
            const valueCodes = v.values.map(([_, v]) => this.getCodes(v));

            /**
             * @type {number[]}
             */
            const codes = [];

            for (let i = 0; i < IRValueCodeMapper.maxDepth - 1; i++) {
                const key = `[${valueCodes.map(vc => vc[i]).join(",")}]`;
                codes.push(this.genCode(key));
            }

            return codes;
        } else if (v instanceof IRMultiValue) {
            const valueCodes = v.values.map(v => this.getCodes(v));

            /**
             * @type {number[]}
             */
            const codes = [];

            for (let i = 0; i < IRValueCodeMapper.maxDepth; i++) {
                const key = `{${valueCodes.map(vc => vc[i]).sort().join(",")}}`;
                codes.push(this.genCode(key));
            }

            return codes;
        } else {
            throw new Error("unhandled");
        }
    }

    /**
     * @param {IRValue | IRStack} v 
     * @returns {number[]}
     */
    getCodes(v) {
        const cached = this.#valueCodes.get(v);

        if (cached) {
            return cached;
        }

        const codes = this.genCodes(v);

        this.#valueCodes.set(v, codes);

        return codes;
    }

    /**
     * @param {IRValue} v
     * @returns {number}
     */
    getCode(v) {
        const codes = this.getCodes(v);

        return codes[IRValueCodeMapper.maxDepth-1];
    }

    /**
     * @param {IRValue} fn 
     * @param {IRValue[]} args 
     */
    getCallCode(fn, args) {
        const key = `${assertDefined(this.getCode(fn))}(${args.map(a => this.getCode(a)).join(",")})`;
        return this.genCode(key);
    }

    /**
     * @param {IRValue} a 
     * @param {IRValue} b 
     * @returns {boolean}
     */
    eq(a, b) {
        const ca = this.getCode(a);
        const cb = this.getCode(b);

        return ca == cb;
    }
}

/**
 * @internal
 * @type {{[name: string]: (args: IRValue[]) => IRValue}}
 */
const IR_BUILTIN_CALLBACKS = {
    addInteger: ([a, b]) => {
        return new IRDataValue();
    },
    subtractInteger: ([a, b]) => {
        return new IRDataValue();
    },
    multiplyInteger: ([a, b]) => {
        if (a instanceof IRLiteralValue && a.value.int == 0n) {
            return a;
        } else if (b instanceof IRLiteralValue && b.value.int == 0n) {
            return b;
        } else {
            return new IRDataValue();
        }
    },
    divideInteger: ([a, b]) => {
        if (a instanceof IRLiteralValue && a.value.int == 0n) {
            return IRMultiValue.flatten([a, new IRErrorValue()]);
        } else if (b instanceof IRLiteralValue) {
            if (b.value.int == 0n) {
                return new IRErrorValue();
            } else if (b.value.int == 1n) {
                return a;
            } else {
                return new IRDataValue();
            }
        } else {
            return IRMultiValue.flatten([new IRDataValue(), new IRErrorValue()]);
        }
    },
    modInteger: ([a, b]) => {
        if (b instanceof IRLiteralValue) {
            if (b.value.int == 1n) {
                return new IRLiteralValue(new UplcInt(b.value.site, 0n, true));
            } else if (b.value.int == 0n) {
                return new IRErrorValue();
            } else {
                return new IRDataValue();
            }
        } else {
            return IRMultiValue.flatten([
                new IRDataValue(),
                new IRErrorValue()
            ]);
        }
    },
    quotientInteger: ([a, b]) => {
        if (a instanceof IRLiteralValue && a.value.int == 0n) {
            return IRMultiValue.flatten([a, new IRErrorValue()]);
        } else if (b instanceof IRLiteralValue) {
            if (b.value.int == 0n) {
                return new IRErrorValue();
            } else if (b.value.int == 1n) {
                return a;
            } else {
                return new IRDataValue();
            }
        } else {
            return IRMultiValue.flatten([new IRDataValue(), new IRErrorValue()]);
        }
    },
    remainderInteger: ([a, b]) => {
        if (b instanceof IRLiteralValue) {
            if (b.value.int == 1n) {
                return new IRLiteralValue(new UplcInt(b.value.site, 0n, true));
            } else if (b.value.int == 0n) {
                return new IRErrorValue();
            } else {
                return new IRDataValue();
            }
        } else {
            return IRMultiValue.flatten([
                new IRDataValue(),
                new IRErrorValue()
            ]);
        }
    },
    equalsInteger: ([a, b]) => {
        return new IRDataValue();
    },
    lessThanInteger: ([a, b]) => {
        return new IRDataValue();
    },
    lessThanEqualsInteger: ([a, b]) => {
        return new IRDataValue();
    },
    appendByteString: ([a, b]) => {
        return new IRDataValue();
    },
    consByteString: ([a, b]) => {
        return new IRDataValue();
    },
    sliceByteString: ([a, b, c]) => {
        if (b instanceof IRLiteralValue && b.value.int <= 0n) {
            return new IRLiteralValue(new UplcByteArray(b.value.site, []));
        } else {
            return new IRDataValue();
        }
    },
    lengthOfByteString: ([a]) => {
        return new IRDataValue();
    },
    indexByteString: ([a, b]) => {
        if (b instanceof IRLiteralValue && b.value.int < 0n) {
            return new IRErrorValue();
        } else if (a instanceof IRLiteralValue && a.value.bytes.length == 0) {
            return new IRErrorValue();
        } else {
            return IRMultiValue.flatten([
                new IRDataValue(),
                new IRErrorValue()
            ]);
        }
    },
    equalsByteString: ([a, b]) => {
        return new IRDataValue();
    },
    lessThanByteString: ([a, b]) => {
        return new IRDataValue();
    },
    lessThanEqualsByteString: ([a, b]) => {
        return new IRDataValue();
    },
    appendString: ([a, b]) => {
        return new IRDataValue();
    },
    equalsString: ([a, b]) => {
        return new IRDataValue();
    },
    encodeUtf8: ([a]) => {
        return new IRDataValue();
    },
    decodeUtf8: ([a]) => {
        return IRMultiValue.flatten([
            new IRDataValue(),
            new IRErrorValue()
        ]);
    },
    sha2_256: ([a]) => {
        return new IRDataValue();
    },
    sha3_256: ([a]) => {
        return new IRDataValue();
    },
    blake2b_256: ([a]) => {
        return new IRDataValue();
    },
    verifyEd25519Signature: ([a, b, c]) => {
        if (a instanceof IRLiteralValue && a.value.bytes.length != 32) {
            return new IRErrorValue();
        } else if (c instanceof IRLiteralValue && c.value.bytes.length != 64) {
            return new IRErrorValue();
        } else {
            return IRMultiValue.flatten([
                new IRDataValue(),
                new IRErrorValue()
            ]);
        }
    },
    ifThenElse: ([a, b, c]) => {
        if (a instanceof IRLiteralValue) {
            if (a.value.bool) {
                return b;
            } else {
                return c;
            }
        } else {
            return IRMultiValue.flatten([b, c]);
        }
    },
    chooseUnit: ([a, b]) => {
        return b;
    },
    trace: ([a, b]) => {
        return b;
    },
    fstPair: ([a]) => {
        return new IRDataValue();
    },
    sndPair: ([a]) => {
        return new IRDataValue();
    },
    chooseList: ([a, b, c]) => {
        if (a instanceof IRLiteralValue) {
            if (a.value.list.length == 0) {
                return b;
            } else {
                return c;
            }
        } else {
            return IRMultiValue.flatten([b, c]);
        }
    },
    mkCons: ([a, b]) => {
        return new IRDataValue();
    },
    headList: ([a]) => {
        return IRMultiValue.flatten([
            new IRDataValue(),
            new IRErrorValue()
        ]);
    },
    tailList: ([a]) => {
        return IRMultiValue.flatten([
            new IRDataValue(),
            new IRErrorValue()
        ]);
    },
    nullList: ([a]) => {
        return new IRDataValue();
    },
    chooseData: ([a, b, c, d, e, f]) => {
        if (a instanceof IRLiteralValue) {
            const data = a.value.data;

            if (data instanceof ConstrData) {
                return b;
            } else if (data instanceof MapData) {
                return c;
            } else if (data instanceof ListData) {
                return d;
            } else if (data instanceof IntData) {
                return e;
            } else if (data instanceof ByteArrayData) {
                return f;
            } else {
                throw new Error("unhandled UplcData type");
            }
        } else {
            return IRMultiValue.flatten([b, c, d, e, f]);
        }
    },
    constrData: ([a, b]) => {
        return new IRDataValue();
    },
    mapData: ([a])  => {
        return new IRDataValue();
    },
    listData: ([a]) => {
        return new IRDataValue();
    },
    iData: ([a]) => {
        return new IRDataValue();
    },
    bData: ([a]) => {
        return new IRDataValue();
    },
    unConstrData: ([a]) => {
        return IRMultiValue.flatten([
            new IRDataValue(),
            new IRErrorValue()
        ]);
    },
    unMapData: ([a]) => {
        return IRMultiValue.flatten([
            new IRDataValue(),
            new IRErrorValue()
        ]);
    },
    unListData: ([a]) => {
        return IRMultiValue.flatten([
            new IRDataValue(),
            new IRErrorValue()
        ]);
    },
    unIData: ([a]) => {
        return IRMultiValue.flatten([
            new IRDataValue(),
            new IRErrorValue()
        ]);
    },
    unBData: ([a]) => {
        return IRMultiValue.flatten([
            new IRDataValue(),
            new IRErrorValue()
        ]);
    },
    equalsData: ([a, b]) => {
        return new IRDataValue();
    },
    mkPairData: ([a, b]) => {
        return new IRDataValue();
    },
    mkNilData: ([a]) => {
        return new IRDataValue();
    },
    mkNilPairData: ([a]) => {
        return new IRDataValue();
    },
    serialiseData: ([a]) => {
        return new IRDataValue();
    }
};

/**
 * @internal
 */
export class IREvaluator {
    /**
     * Unwraps an IR AST
     * @type {(
     *   {
     *     stack: IRStack,
     *     expr: IRErrorExpr | IRLiteralExpr | IRNameExpr | IRFuncExpr | IRCallExpr
     *   } |
     *   {calling: IRCallExpr, code: number, args: IRValue[]} |
     *   {fn: IRFuncExpr, owner: null | IRExpr, stack: IRStack} | 
     *   {multi: number, owner: null | IRExpr} | 
     *   {value: IRValue, owner: null | IRExpr} |
     *   {ignore: number, owner: null | IRExpr} | 
     *   {cacheExpr: IRCallExpr, code: number, value: IRValue}
     * )[]}
     */
    #compute;

    /**
     * @type {IRValue[]}
     */
    #reduce;

    /**
	 * Keep track of the eval result of each expression
	 * @type {Map<IRExpr, IRValue>}
	 */
	#exprValues;

    /**
     * Keep track of all values passed through IRVariables
     * @type {Map<IRVariable, IRValue>}
     */
    #variableValues;

    /**
     * IRFuncExpr tag as key
     * @type {Map<number, number>}
     */
    #callCount;

    /**
     * @type {Map<IRFuncExpr, Set<IRCallExpr>>}
     */
    #funcCallExprs;

    /**
     * @type {Map<IRVariable, Set<IRNameExpr>>}
     */
    #variableReferences;

    /**
     * @type {Map<IRCallExpr, Map<number, IRValue>>}
     */
    #cachedCalls;

    #evalLiterals;

    /**
     * @param {boolean} evalLiterals 
     */
	constructor(evalLiterals = true) {
        this.#compute = [];
        this.#reduce = [];

        // data structures used by optimization
        this.#exprValues = new Map();
        this.#variableValues = new Map();
        this.#callCount = new Map();
        this.#funcCallExprs = new Map();
        this.#variableReferences = new Map();
        this.#cachedCalls = new Map();
        this.#evalLiterals = evalLiterals;
	}

    /**
     * @type {IRFuncExpr[]}
     */
    get funcExprs() {
        return Array.from(this.#funcCallExprs.keys());
    }

    /**
     * @param {IRExpr} expr 
     * @returns {undefined | IRValue}
     */
    getExprValue(expr) {
        return this.#exprValues.get(expr);
    }

    /**
     * @param {IRVariable} v
     * @returns {undefined | IRValue}
     */
    getVariableValue(v) {
        return this.#variableValues.get(v);
    }

    /**
     * @param {IRVariable} v 
     * @returns {number}
     */
    countVariableReferences(v) {
        return this.#variableReferences.get(v)?.size ?? 0;
    }

    /**
     * @param {IRVariable} v 
     * @returns {IRNameExpr[]}
     */
    getVariableReferences(v) {
        return Array.from(this.#variableReferences.get(v) ?? [])
    }

    /**
     * @param {IRFuncExpr} fn
     * @returns {number}
     */
    countFuncCalls(fn) {
        return this.#callCount.get(fn.tag) ?? 0;
    }

    /**
     * @param {IRExpr} expr 
     * @returns {boolean}
     */
    expectsError(expr) {
        const v = this.getExprValue(expr);

        if (v) {
            if (v instanceof IRErrorValue) {
                return true;
            } else if (v instanceof IRMultiValue && v.hasError()) {
                return true;
            } else if (v instanceof IRAnyValue) {
                return true;
            } else {
                return false;
            }
        } else {
            // the expression might be recently formed, so if not found, better be on the safe side
            return true;
        }
    }

    /**
     * @param {IRFuncExpr} fn
     * @returns {number[]} indices
     */
    getUnusedFuncVariables(fn) {
        /**
         * @type {number[]}
         */
        const indices = [];

        fn.args.forEach((a, i) => {
            const s = this.#variableReferences.get(a);
            if (!s || s.size == 0) {
                indices.push(i);
            }
        })

        return indices;
    }

    /**
     * @param {IRFuncExpr} fn 
     * @returns {IRCallExpr[]}
     */
    getFuncCallExprs(fn) {
        return Array.from(this.#funcCallExprs.get(fn) ?? []);
    }

    /**
     * @param {IRFuncExpr} fn 
     * @returns {boolean}
     */
    onlyDedicatedCallExprs(fn) {
        const callExprs = this.getFuncCallExprs(fn);

        // if there are no callExprs then we don't know exactly how the function is called (eg. main), and we can't flatten
        if (callExprs.length == 0) {
            return false;
        }

        return callExprs.every(ce => {
            if (ce.func == fn) {
                // literally calling fn directly
                return true;
            }

            const v = this.getExprValue(ce.func);

            if (!v) {
                return false;
            } else if (v instanceof IRMultiValue) {
                return v.values.every(vv => !(vv instanceof IRFuncValue) || (vv.definition == fn))
            } else if (v instanceof IRFuncValue) {
                //assert(v.definition == fn, `expected ${fn.toString()}, not ${v.definition.toString()}`);
                return true;
            } else {
                throw new Error(`unexpected ${v.toString()}`);
            }
        });
    }

    /**
     * @param {IRFuncExpr} fn
     * @param {number[]} unused
     * @returns {boolean}
     */
    noUnusedArgErrors(fn, unused) {
        const callExprs = this.getFuncCallExprs(fn);

        return callExprs.every(ce => {
            return unused.every(i => {
                return !this.expectsError(ce.args[i]);
            });
        });
    }

    /**
     * @param {IRFuncExpr} first 
     * @param {IRFuncExpr} second
     */
    onlyNestedCalls(first, second) {
        const callExprs = this.getFuncCallExprs(second);

        // if there are no callExprs then we don't know exactly how the function is called (eg. main), and we can't flatten
        if (callExprs.length == 0) {
            return false;
        }

        return callExprs.every(ce => {
            if (ce.func instanceof IRCallExpr) {
                const v = this.getExprValue(ce.func.func);

                if (!v) {
                    return false;
                } else if (v instanceof IRMultiValue) {
                    return v.values.every(vv => !(vv instanceof IRFuncValue) || (vv.definition == first))
                } else if (v instanceof IRFuncValue) {
                    return v.definition == first;
                } else {
                    throw new Error(`unexpected ${v.toString()}`);
                }
            } else {
                return false;
            }
        });
    }

    /**
     * The newExpr should evaluate to exactly the same values etc. as the oldExpr
     * @param {IRExpr} oldExpr 
     * @param {IRExpr} newExpr 
     */
    notifyCopyExpr(oldExpr, newExpr) {
        const oldValue = this.#exprValues.get(oldExpr);
        if (oldValue) {
            this.#exprValues.set(newExpr, oldValue);
        }
    }

    /**
     * Push onto the computeStack, unwrapping IRCallExprs
     * @private
     * @param {IRStack} stack
     * @param {IRExpr} expr
     */
    pushExpr(stack, expr) {
        if (expr instanceof IRErrorExpr) {
            this.#compute.push({stack: stack, expr: expr});
        } else if (expr instanceof IRLiteralExpr) {
            this.#compute.push({stack: stack, expr: expr});
        } else if (expr instanceof IRNameExpr) {
            this.#compute.push({stack: stack, expr: expr});
        } else if (expr instanceof IRFuncExpr) {
            this.#compute.push({stack: stack, expr: expr});
        } else if (expr instanceof IRCallExpr) {
            this.#compute.push({stack: stack, expr: expr});

            this.pushExpr(stack, expr.func);

            expr.args.forEach(a => this.pushExpr(stack, a));
        } else {
            throw new Error("unexpected expression type");
        }
    }

    /**
     * @private
     * @param {IRExpr} expr 
     * @param {IRValue} value 
     * @returns {IRValue} combined value
     */
    setExprValue(expr, value) {
        const outputs = this.#exprValues.get(expr);

        if (outputs) {
            const combined = IRMultiValue.flatten([outputs, value]);
            this.#exprValues.set(expr, combined);
            return combined;
        } else {
            this.#exprValues.set(expr, value);
            return value;
        }
    }

    /**
     * @private
     * @param {null | IRExpr} owner 
     * @param {IRValue} value 
     */
    pushReductionValue(owner, value) {
        if (owner) {
            const combined = this.setExprValue(owner, value);

            if (value instanceof IRAnyValue || (value instanceof IRMultiValue && value.values.some(v => v instanceof IRAnyValue))) {
                value = combined;
            }
        }

        this.#reduce.push(value);
    }

    /**
     * @private
     * @param {IRStack} stack
     * @param {IRNameExpr} nameExpr
     * @returns {IRValue}
     */
    getValue(stack, nameExpr) {
        const variable = nameExpr.variable;

        const s = this.#variableReferences.get(variable);

        if (s) {
            s.add(nameExpr);
        } else {
            this.#variableReferences.set(variable, new Set([nameExpr]));
        }

        return stack.getValue(variable);
    }

    /**
     * @private
     * @param {IRExpr} owner
     * @param {IRNameExpr} nameExpr 
     * @param {IRValue[]} args
     */
    callBuiltin(owner, nameExpr, args) {
        let builtin = nameExpr.name.slice(BUILTIN_PREFIX.length);
        const isSafe = builtin.endsWith(SAFE_BUILTIN_SUFFIX);
        if (isSafe) {
            builtin = builtin.slice(0, builtin.length - SAFE_BUILTIN_SUFFIX.length);
        }

        // collect results for each permutation of multivalued args

        /**
         * @type {IRValue[][]}
         */
        const permutations = IRMultiValue.allPermutations(args);

        const resValues = permutations.map(args => {
            if (args.every(a => a instanceof IRLiteralValue)) {
                try {
                    const res = UplcBuiltin.evalStatic(new Word(Site.dummy(), builtin), args.map(a => assertClass(a, IRLiteralValue).value));
                    return new IRLiteralValue(res);
                } catch (e) {
                    if (e instanceof RuntimeError) {
                        return new IRErrorValue();
                    } else {
                        throw e;
                    }
                }
            } else if (args.some(a => a instanceof IRErrorValue)) {
                return new IRErrorValue();
            } else {
                const res = assertDefined(IR_BUILTIN_CALLBACKS[builtin], `builtin ${builtin} not defined in IR_BUILTIN_CALLBACKS`)(args);
                
                if (isSafe && res instanceof IRMultiValue && res.hasError()) {
                    return res.withoutErrors();
                } else {
                    return res;
                }
            }
        });

        this.pushReductionValue(owner, IRMultiValue.flatten(resValues));
    }

    /**
     * @private
     * @param {IRFuncExpr} fn 
     */
    incrCallCount(fn) {
        const prev = this.#callCount.get(fn.tag);

        if (prev) {
            this.#callCount.set(fn.tag, Math.min(prev + 1, Number.MAX_SAFE_INTEGER));
        } else {
            this.#callCount.set(fn.tag, 1);
        }
    }

    /**
     * @private
     * @param {IRVariable[]} variables 
     * @param {IRValue[]} values 
     * @returns {[IRVariable, IRValue][]}
     */
    mapVarsToValues(variables, values) {
        assert(variables.length == values.length, "variables and values don't have the same length([" + variables.map(v => v.name).join(",") + "] vs [" + values.map(v => v.toString()).join(",") + "]");

        /**
         * @type {[IRVariable, IRValue][]}
         */
        const m = [];

        variables.forEach((variable, i) => {
            const value = values[i];

            const allValues = this.#variableValues.get(variable);

            if (allValues) {
                this.#variableValues.set(variable, IRMultiValue.flatten([allValues, value]));
            } else {
                this.#variableValues.set(variable, value);
            }

            m.push([variable, value]);
        });

        return m;
    }

    /**
     * @private
     * @param {IRStack} stack
     * @param {null | IRExpr} owner
     * @param {IRFuncExpr} fn
     * @param {IRValue[]} args
     */
    pushFuncCall(stack, owner, fn, args) {
        if (args.some(a => a instanceof IRErrorValue)) {
            this.pushReductionValue(owner, new IRErrorValue());
        } else {
            if (args.some(a => a.hasError(true))) {
                this.#compute.push({multi: 2, owner: owner});
                this.#compute.push({value: new IRErrorValue(), owner: owner});
                args = args.map(a => a.withoutErrors())
            }
            
            const varsToValues = this.mapVarsToValues(fn.args, args);
            stack = stack.extend(varsToValues);
    
            this.incrCallCount(fn);
            this.#compute.push({fn: fn, owner: owner, stack: stack});
            this.pushExpr(stack, fn.body); 
        }
    }

    /**
     * @private
     * @param {IRExpr} owner for entry point ths is the entry point IRFuncExpr, for all other calls this is the IRCallExpr
     * @param {IRFuncValue} v
     * @param {IRValue[]} args 
     */
    callFunc(owner, v, args) {
        const fn = v.definition;
        const stack = v.stack;

        if (owner instanceof IRCallExpr) {
            const s = this.#funcCallExprs.get(fn);

            if (!s) {
                this.#funcCallExprs.set(fn, new Set([owner]));
            } else {
                s.add(owner);
            }
        }

        this.pushFuncCall(stack, owner, fn, args);
    }

    /**
     * Call an unknown function (eg. returned at the deepest point of recursion)
     * Make sure any arguments that are functions are also called so that all possible execution paths are touched (TODO: should we also called function values returned by those calls etc.?)
     * Absorb the return values of these functions
     * @private
     * @param {IRExpr} owner
     * @param {IRAnyValue} fn
     * @param {IRValue[]} args
     */
    callAnyFunc(owner, fn, args) {
        if (args.some(a => a instanceof IRErrorValue)) {
            this.pushReductionValue(owner, new IRErrorValue());
        } else {
            if (args.some(a => a.hasError(false))) {
                this.#compute.push({multi: 2, owner: owner});
                this.#compute.push({value: new IRErrorValue(), owner: owner});
                args = args.map(a => a.withoutErrors())
            }

            /**
             * Only user-defined functions!
             * @type {IRFuncValue[]}
             */
            const fnsInArgs = [];

            args.forEach(a => {
                if (a instanceof IRMultiValue) {
                    a.values.forEach(aa => {
                        if (aa instanceof IRFuncValue) {
                            fnsInArgs.push(aa);
                        }
                    });
                } else if (a instanceof IRFuncValue) {
                    fnsInArgs.push(a);
                }
            });

            this.#compute.push({ignore: fnsInArgs.length, owner: owner});

            fnsInArgs.forEach(fn => {
                const def = assertClass(fn.definition, IRFuncExpr);

                this.pushFuncCall(fn.stack, null, def, def.args.map(a => new IRAnyValue()));
            });
        }
    }

    /**
     * @private
     * @param {IRCallExpr} expr
     * @param {number} code
     * @param {IRValue} value
     */
    cacheValue(expr, code, value) {
        const prev = this.#cachedCalls.get(expr);

        if (prev) {
            const prevPrev = prev.get(code);

            if (prevPrev && !(prevPrev instanceof IRAnyValue)) {
                const newValue = IRMultiValue.flatten([prevPrev, value]);
                prev.set(code, newValue);
            } else {
                prev.set(code, value);
            }
        } else {
            this.#cachedCalls.set(expr, new Map([[code, value]]));
        }
    }

    /**
     * @private
     * @param {IRCallExpr} expr 
     * @param {number} code 
     */
    prepareCacheValue(expr, code) {
        this.#compute.push({value: new IRAnyValue(), cacheExpr: expr, code: code});
    }

    /**
     * @private
     */
    evalInternal() {
        const codeMapper = new IRValueCodeMapper();

        let head = this.#compute.pop();

		while (head) {
            if ("cacheExpr" in head) {
                this.cacheValue(head.cacheExpr, head.code, head.value);
            } else if ("expr" in head) {
                const expr = head.expr;

                if (expr instanceof IRCallExpr) {
                    let fn = assertDefined(this.#reduce.pop());

                    /**
                     * @type {IRValue[]}
                     */
                    let args = [];

                    for (let i = 0; i < expr.args.length; i++) {
                        args.push(assertDefined(this.#reduce.pop()))
                    }

                    // don't allow partial literal args (could lead to infinite recursion where the partial literal keeps updating)
                    //  except when calling builtins (partial literals are important: eg. in divideInteger(<data>, 10) we know that the callExpr doesn't return an error)
                    const allLiteral = fn.isLiteral() && args.every(a => a.isLiteral());

                    if (!allLiteral && !(fn instanceof IRBuiltinValue) && !(fn instanceof IRFuncValue && fn.definition.args.length == 1 && IRStack.isGlobal(fn.definition.args[0]))) {
                        fn = fn.withoutLiterals();
                        args = args.map(a => a.withoutLiterals());
                    }

                    const fns = fn instanceof IRMultiValue ? fn.values : [fn];

                    if (fns.length > 1) {
                        this.#compute.push({multi: fns.length, owner: expr});
                    }

                    for (let fn of fns) {
                        const code = codeMapper.getCallCode(fn, args);
                        const cached = this.#cachedCalls.get(expr)?.get(code);

                        if (cached) {
                            this.pushReductionValue(expr, cached);
                            
                            // increment the call count even though we are using a cached value
                            for (let fn of fns) {
                                if (fn instanceof IRFuncValue) {
                                    this.incrCallCount(fn.definition);
                                }
                            }
                        } else {
                            this.#compute.push({calling: expr, code: code, args: args});
                            //this.cacheValue(expr, code, new IRAnyValue());

                            if (fn instanceof IRAnyValue) {///} || fn instanceof IRDataValue) {
                                this.callAnyFunc(expr, fn, args);
                            } else if (fn instanceof IRErrorValue) {
                                this.pushReductionValue(expr, new IRErrorValue());
                            } else if (fn instanceof IRFuncValue) {
                                this.callFunc(expr, fn, args);
                                this.prepareCacheValue(expr, code);
                            } else if (fn instanceof IRBuiltinValue) {
                                this.callBuiltin(expr, fn.builtin, args);
                                this.prepareCacheValue(expr, code);
                            } else {
                                console.log(expr.toString());
                                throw expr.site.typeError("unable to call " + fn.toString());
                            }
                        }
                    }
                } else if (expr instanceof IRErrorExpr) {
                    this.pushReductionValue(expr, new IRErrorValue());
                } else if (expr instanceof IRNameExpr) {
                    if (expr.isParam()) {
                        this.pushReductionValue(expr, new IRDataValue());
                    } else if (expr.isCore()) {
                        this.pushReductionValue(expr, new IRBuiltinValue(expr));
                    } else {
                        this.pushReductionValue(expr, this.getValue(head.stack, expr));
                    }
                } else if (expr instanceof IRLiteralExpr) {
                    if (this.#evalLiterals ) {
                        this.pushReductionValue(expr, new IRLiteralValue(expr.value));
                    } else {
                        if (expr.value instanceof UplcUnit) {
                            this.pushReductionValue(expr, new IRAnyValue());
                        } else {
                            this.pushReductionValue(expr, new IRDataValue());
                        }
                    }
                } else if (expr instanceof IRFuncExpr) {
                    // don't set owner because it is confusing wrt. return value type
                    this.#reduce.push(IRFuncValue.new(expr, head.stack));
                } else {
                    throw new Error("unexpected expr type");
                }
            } else if ("calling" in head) {
                // keep track of recursive calls
                
                const last = assertDefined(this.#reduce.pop());
                this.cacheValue(head.calling, head.code, last);
                this.pushReductionValue(head.calling, last);
			} else if ("fn" in head && head.fn instanceof IRFuncExpr) {
                // track the owner
                const owner = head.owner;
                const last = assertDefined(this.#reduce.pop());
                
                this.setExprValue(head.fn, last);
                this.pushReductionValue(owner, last);
			} else if ("multi" in head) {
                // collect multiple IRValues from the reductionStack and put it back as a single IRMultiValue

				/**
				 * @type {IRValue[]}
				 */
				const values = [];

				for (let i = 0; i < head.multi; i++) {
					values.push(assertDefined(this.#reduce.pop()));
				}

                this.pushReductionValue(head.owner, IRMultiValue.flatten(values));
            } else if ("value" in head) {
                this.pushReductionValue(head.owner, head.value);
            } else if ("ignore" in head) {
                const vs = [new IRAnyValue()];
                for (let i = 0; i < head.ignore; i++) {
                    if (assertDefined(this.#reduce.pop()) instanceof IRErrorValue) {
                        vs.push(new IRErrorValue());
                    }
                }

                this.pushReductionValue(head.owner, IRMultiValue.flatten(vs));
			} else {
				throw new Error("unexpected term");
			}

            head = this.#compute.pop();
		}
    }

    /**
     * @private
     * @param {IRExpr} expr entry point
     * @returns {IRValue}
     */
    evalFirstPass(expr) {
        this.pushExpr(IRStack.empty(), expr);

        this.evalInternal();

        const res = assertDefined(this.#reduce.pop());

        assert(this.#reduce.length == 0, "expected a single reduction value in first phase [" + this.#reduce.map(v => v.toString()).join(", ") + "]");

        if (res instanceof IRFuncValue) {
            return res;
        } else if (res instanceof IRLiteralValue) {
            return res; // used by const
        } else if (res instanceof IRMultiValue && res.values.some(v => v instanceof IRAnyValue)) {
            return res;
        } else {
            console.log(annotateIR(this, expr));
            throw new Error(`expected entry point function, got ${res.toString()}`);
        }
    }

    /**
     * @private
     * @param {IRFuncValue} main
     * @returns {IRValue}
     */
    evalSecondPass(main) {
        const definition = assertClass(main.definition, IRFuncExpr);
        const args = definition.args.map(a => new IRDataValue());
        this.callFunc(definition, main, args);

        this.evalInternal();

        const res = assertDefined(this.#reduce.pop());

        assert(this.#reduce.length == 0, "expected a single reduction value in second phase [" + res.toString() + ", " + this.#reduce.map(v => v.toString()).join(", ") + "]");

        const finalValues = (res instanceof IRMultiValue) ? res.values : [res];

        for (let v of finalValues) {
            if (v instanceof IRErrorValue) {

                // ok
                /*if (finalValues.length == 1) {
                    console.error("Warning: script always fails");
                }*/
            } else if (v instanceof IRLiteralValue) {
                // ok
            } else if (v instanceof IRDataValue) {
                // ok
            } else {
                // ok, (could be a literal UplcUnit, which is treated as Any because in other contexts it could be a function)
            }
        }

        return IRMultiValue.flatten(finalValues);
    }

	/**
	 * @param {IRExpr} expr entry point
     * @returns {IRValue}
	 */
	eval(expr) {
        const res = this.evalFirstPass(expr);

        if (res instanceof IRFuncValue) {
            return this.evalSecondPass(res);
        } else {
            return res;
        }
	}

    /**
     * @param {IRExpr} expr 
     * @returns {UplcData}
     */
    evalConst(expr) {
        const res = this.evalFirstPass(expr);

        if (res instanceof IRLiteralValue) {
            let v = res.value;

            if (v instanceof UplcDataValue) {
                return v.data;
            } else if (v instanceof UplcInt) {
                return new IntData(v.int);
            } else if (v instanceof UplcBool) {
                return new ConstrData(v.bool ? 1 : 0, []);
            } else if (v instanceof UplcList) {
                if (v.isDataList()) {
                    return new ListData(v.list.map(item => item.data));
                } else if (v.isDataMap()) {
                    return new MapData(v.list.map(item => {
                        const pair = assertClass(item, UplcPair);

                        return [pair.key, pair.value];
                    }));
                }
            } else if (v instanceof UplcString) {
                return new ByteArrayData(textToBytes(v.string));
            } else if (v instanceof UplcByteArray) {
                return new ByteArrayData(v.bytes);
            }

            throw new Error(`unable to turn '${v.toString()}' into data`);
        } else {
            throw new Error("expected IRLiteralValue");
        }
    }
}


/**
 * Used to debug the result of IREvalation
 * @internal
 * @param {IREvaluator} evaluation 
 * @param {IRExpr} expr 
 * @returns {string}
 */
export function annotateIR(evaluation, expr) {
    /**
     * @param {IRExpr} expr 
     * @param {string} indent
     * @returns {string}
     */
    const annotate = (expr, indent) => {
        if (expr instanceof IRLiteralExpr) {
            return expr.value.toString();
        } else if (expr instanceof IRErrorExpr) {
            return `error()`;
        } else if (expr instanceof IRNameExpr) {
            const output = evaluation.getExprValue(expr);

            if (output) {
                return `${expr.name}: ${output.toString()}`
            } else {
                return expr.name;
            }
        } else if (expr instanceof IRFuncExpr) {
            const output = evaluation.getExprValue(expr);

            const isGlobalDef = expr.args.length == 1 && expr.args[0].name.startsWith("__");
            const innerIndent = indent + (isGlobalDef ? "" : TAB);

            let countStr = "";
            const count = evaluation.countFuncCalls(expr);
            if (count == Number.MAX_SAFE_INTEGER) {
                countStr = "\u221e";
            } else {
                countStr = count.toString();
            }

            return `Fn${expr.tag}(${expr.args.map(a => {
                const v = evaluation.getVariableValue(a);

                if (v) {
                    return `${a.name}: ${v.toString()}`;
                } else {
                    return a.name;
                }
            }).join(", ")})${countStr} -> ${output ? output.toString() + " " : ""}{\n${innerIndent}${annotate(expr.body, innerIndent)}\n${indent}}`;
        } else if (expr instanceof IRCallExpr) {
            if (expr.func instanceof IRFuncExpr && expr.args.length == 1) {
                return `${expr.func.args[0].name} = ${annotate(expr.args[0], indent)};\n${indent}${annotate(expr.func.body, indent)}`;
            } else {
                const output = evaluation.getExprValue(expr);

                const isGlobalDef = expr.func instanceof IRFuncExpr && expr.func.args.length == 1 && expr.func.args[0].name.startsWith("__");
                const globalDef = expr.func instanceof IRFuncExpr && expr.func.args.length == 1 ? expr.func.args[0].name : "";

                const parens = `(${isGlobalDef ? `\n${indent}${TAB}/* ${globalDef} */` : ""}${expr.args.map(a => `\n${indent}${TAB}${annotate(a, indent + TAB)}`).join(",")}${(expr.args.length > 0) || isGlobalDef ? `\n${indent}` : ""})${output ? `: ${output.toString()}` : ""}`;

                if (expr.func instanceof IRNameExpr) {
                    return `${expr.func.toString()}${parens}`;
                } else {
                    return `${annotate(expr.func, indent)}${parens}`;
                }
            }
        } else {
            throw new Error("unhandled IRExpr");
        }
    }

    return annotate(expr, "");
}


//////////////////////////////
// Section 31: IR optimization
//////////////////////////////

/**
 * Any IRFuncExpr that is smaller or equal to this number will be inlined.
 * 
 * Examples of helios builtin functions that should be inlined:
 *   * __helios__bool__and 
 *   * __helios__common__enum_field_0
 * 
 * This is a number of bits/
 */
const INLINE_MAX_SIZE = 128;

/**
 * @param {IRExpr} func 
 * @returns {boolean}
 */
function isIdentityFunc(func) {
    if (func instanceof IRFuncExpr && func.args.length == 1 && func.body instanceof IRNameExpr && func.body.isVariable(func.args[0])) {
        return true;
    } else {
        return false;
    }
}

/**
 * State that must be maintained over optimization iterations
 */
export class IROptimizerState{
    #commonExprCount;

    constructor() {
        this.#commonExprCount = 0
    }

    incrCommonExprCount() {
        this.#commonExprCount += 1
    }

    get commonExprCount() {
        return this.#commonExprCount
    }
}

/**
 * Recursive algorithm that performs the following optimizations.
 * 
 * Optimizations performed in both `aggressive == false` and `aggressive == true` cases:
 *   * replace `IRNameExpr` by `IRLiteralExpr` if the expected value is IRLiteralValue
 *   * replace `IRCallExpr` by `IRLiteralExpr` if the expected value is IRLiteralValue
 * 
 * Optimizations only performed in the `aggressive == true` case:
 *   * replace `IRNameExpr` by `IRErrorExpr` if the expected value is IRErrorValue
 *   * replace `IRCallExpr` by `IRErrorExpr` if the expected value is IRErrorValue
 *   * replace `__core__addInteger(<expr>, 0)` or `__core__addInteger(0, <expr>)` by `<expr>`
 *   * replace `__core__subtractInteger(<expr>, 0)` by `<expr>`
 *   * replace `__core__multiplyInteger(<expr>, 1)` or `__core__multiplyInteger(1, <expr>)` by `<expr>`
 *   * replace `__core__divideInteger(<expr>, 1)` by `<expr>`
 *   * replace `__core__quotientInteger(<expr>, 1)` by `<expr>`
 *   * replace `__core__appendByteString(<expr>, #)` or `__core__appendByteString(#, <expr>)` by `<expr>`
 *   * replace `__core__appendString(<expr>, "")` or `__core__appendString("", <expr>)` by `<expr>`
 *   * replace `__core__decodeUtf8(__core__encodeUtf8(<expr>))` by `<expr>`
 *   * replace `__core__ifThenElse(true, <expr-a>, <expr-b>)` by `<expr-a>` if `<expr-b>` doesn't expect IRErrorValue
 *   * replace `__core__ifThenElse(false, <expr-a>, <expr-b>)` by `<expr-b>` if `<expr-a>` doesn't expect IRErrorValue
 *   * replace `__core__ifThenElse(__core__nullList(<lst-expr>), <expr-a>, <expr-b>)` by `__core__chooseList(<lst-expr>, <expr-a>, <expr-b>)`
 *   * replace `__core__ifThenElse(<cond-expr>, <expr-a>, <expr_a>)` by `<expr-a>` if `<cond-expr>` doesn't expect IRErrorValue
 *   * replace `__core__chooseUnit(<expr>, ())` by `<expr>` (because `<expr>` is expected to return unit as well)
 *   * replace `__core__chooseUnit((), <expr>)` by `<expr>`
 *   * replace `__core__trace(<msg-expr>, <ret-expr>)` by `<ret_expr>` if `<msg-expr>` doesn't expect IRErrorValue
 *   * replace `__core__chooseList([], <expr-a>, <expr-b>)` by `<expr-a>` if `<expr-b>` doesn't expect IRErrorValue
 *   * replace `__core__chooseList([...], <expr-a>, <expr-b>)` by `<expr-b>` if `<expr-a>` doesn't expect IRErrorValue
 *   * replace `__core__chooseData(ConstrData, <C-expr>, <M-expr>, <L-expr>, <I-expr>, <B-expr>)` by `<C-expr>` if none of the other expressions expect IRErrorValue
 *   * replace `__core__chooseData(__core__constrData(<index-expr>, <fields-expr>), <C-expr>, <M-expr>, <L-expr>, <I-expr>, <B-expr>)` by `<C-expr>` if none of the other expressions expect IRErrorValue
 *   * replace `__core__chooseData(MapData, <C-expr>, <M-expr>, <L-expr>, <I-expr>, <B-expr>)` by `<M-expr>` if none of the other expression expect IRErrorValue
 *   * replace `__core__chooseData(__core__mapData(<data-expr>), <C-expr>, <M-expr>, <L-expr>, <I-expr>, <B-expr>)` by `<M-expr>` if none of the other expression expect IRErrorValue
 *   * replace `__core__chooseData(ListData, <C-expr>, <M-expr>, <L-expr>, <I-expr>, <B-expr>)` by `<L-expr>` if none of the other expression expect IRErrorValue
 *   * replace `__core__chooseData(__core__listData(<data-expr>), <C-expr>, <M-expr>, <L-expr>, <I-expr>, <B-expr>)` by `<L-expr>` if none of the other expression expect IRErrorValue
 *   * replace `__core__chooseData(IntData, <C-expr>, <M-expr>, <L-expr>, <I-expr>, <B-expr>)` by `<I-expr>` if none of the other expression expect IRErrorValue
 *   * replace `__core__chooseData(__core__iData(<data-expr>), <C-expr>, <M-expr>, <L-expr>, <I-expr>, <B-expr>)` by `<I-expr>` if none of the other expression expect IRErrorValue
 *   * replace `__core__chooseData(ByteArrayData, <C-expr>, <M-expr>, <L-expr>, <I-expr>, <B-expr>)` by `<B-expr>` if none of the other expression expect IRErrorValue
 *   * replace `__core__chooseData(__core__bData(<data-expr>), <C-expr>, <M-expr>, <L-expr>, <I-expr>, <B-expr>)` by `<B-expr>` if none of the other expression expect IRErrorValue
 *   * replace `__core__unMapData(__core__mapData(<expr>))` by `<expr>`
 *   * replace `__core__unListData(__core__listData(<expr>))` by `<expr>`
 *   * replace `__core__unIData(__core__iData(<expr>))` by `<expr>`
 *   * replace `__core__unBData(__core__bData(<expr>))` by `<expr>`
 *   * replace `__core__equalsData(__core__iData(<expr-a>), __core__iData(<expr-b>))` by `__core__equalsInteger(<expr-a>, <expr-b>)`
 *   * replace `__core__equalsData(__core__bData(<expr-a>), __core__bData(<expr-b>))` by `__core__equalsByteString(<expr-a>, <expr-b>)`
 *   * remove unused IRFuncExpr arg variables if none if the corresponding IRCallExpr args expect errors and if all the the IRCallExprs expect only this IRFuncExpr
 *   * replace IRCallExpr args that are uncalled IRFuncExprs with `()`
 *   * flatten nested IRFuncExprs if the correspondng IRCallExprs always call them in succession
 *   * replace `(<vars>) -> {<name-expr>(<vars>)}` by `<name-expr>` if each var is only referenced once (i.e. only referenced in the call)
 *   * replace `(<var>) -> {<var>}(<arg-expr>)` by `<arg-expr>`
 *   * replace `<name-expr>(<arg-expr>)` by `<arg-expr>` if the expected value of `<name-expr>` is the identity function
 *   * replace `(<vars>) -> {<func-expr>(<vars>)}` by `<func-expr>` if each var is only referenced once (i.e. only referenced in the call)
 *   * inline (copies) of `<name-expr>` in `(<vars>) -> {...}(<name-expr>, ...)`
 *   * inline `<fn-expr>` in `(<vars>) -> {...}(<fn-expr>, ...)` if the corresponding var is only referenced once
 *   * inline `<fn-expr>` in `(<vars>) -> {...}(<fn-expr>, ...)` if `<fn-expr>` has a Uplc flat size smaller than INLINE_MAX_SIZE
 *   * inline `<call-expr>` in `(<vars>) -> {...}(<call-expr>, ...)` if the corresponding var is only referenced once and if all the nested IRFuncExprs are only evaluated once and if the IRCallExpr doesn't expect an error
 *   * replace `() -> {<expr>}()` by `<expr>`
 * 
 * Optimizations that we have considered, but are NOT performed:
 *   * replace `__core__subtractInteger(0, <expr>)` by `__core__multiplyInteger(<expr>, -1)`
 *       reason: it is unclear if either method is cheaper for the majority of cases
 *   * replace `__core__multiplyInteger(<expr>, -1)` by `__core__subtractInteger(0, <expr>)`
 *       reason: it is unclear if either method is cheaper for the majority of cases
 * 
 * @internal
 * @param {IREvaluator} evaluation 
 * @param {IRExpr} expr
 * @param {boolean} aggressive
 * @returns {IRExpr}
 */
export class IROptimizer {
    #evaluator;
    #root;
    #state;

    /**
     * @type {Map<IRVariable, IRExpr>}
     */
    #inlining;

    /**
     * @param {IRExpr} root
     * @param {IROptimizerState} state
     */
    constructor(root, state) {
        this.#evaluator = new IREvaluator();
        this.#root = root;
        IROptimizer.assertNoDuplicateExprs(root);

        this.#state = state;
        this.#inlining = new Map();

        this.init();
    }

    /**
     * @param {IRExpr} expr 
     * @returns {boolean}
     */
    expectsError(expr) {
        return this.#evaluator.expectsError(expr);
    }

    /**
     * @private
     * @param {IRFuncExpr} fn 
     */
    countFuncCalls(fn) {
        return this.#evaluator.countFuncCalls(fn);
    }

    /**
     * Makes sure the callCount is copied from IREvaluator
     * @private
     * @param {IRFuncExpr} old 
     * @param {IRVariable[]} args
     * @param {IRExpr} body
     * @returns {IRFuncExpr}
     */
    newFuncExpr(old, args, body) {
        const funcExpr = new IRFuncExpr(
            old.site,
            args,
            body,
            old.tag
        );

        this.#evaluator.notifyCopyExpr(old, funcExpr);

        return funcExpr;
    }

    /**
     * Apply optimizations that require access to the root:
     *   * flatten nested IRFuncExpr where possible
     *   * remove unused IRFuncExpr variables
     * @private
     */
    init() {
        this.#evaluator.eval(this.#root);

        if (config.DEBUG) {
            console.log(annotateIR(this.#evaluator, this.#root));
        }

        this.removeUnusedArgs();

        this.replaceUncalledArgsWithUnit();

        // rerun evaluation
        this.#evaluator = new IREvaluator();

        this.#evaluator.eval(this.#root);

        if (config.DEBUG) {
            console.log(annotateIR(this.#evaluator, this.#root));
        }

        this.flattenNestedFuncExprs();
    }

    /**
     * Mutates
     * @private
     */
    removeUnusedArgs() {
        const funcExprs = this.#evaluator.funcExprs.filter(expr => {
            const unusedIndices = this.#evaluator.getUnusedFuncVariables(expr);

            return unusedIndices.length > 0 && this.#evaluator.onlyDedicatedCallExprs(expr) && this.#evaluator.noUnusedArgErrors(expr, unusedIndices);
        });

        funcExprs.forEach(expr => {
            const unusedIndices = this.#evaluator.getUnusedFuncVariables(expr);
            const unused = new Set(unusedIndices);

            const callExprs = this.#evaluator.getFuncCallExprs(expr);

            callExprs.forEach(callExpr => {
                callExpr.args = callExpr.args.filter((a, i) => !unused.has(i));
            });
            
            expr.args = expr.args.filter((a, i) => !unused.has(i));
        });
    }

    /**
     * TODO: improve IREvaluator to make sure all possible IRFuncExpr calls are evaluated
     * @private
     */
    replaceUncalledArgsWithUnit() {
        loopIRExprs(this.#root, {
            callExpr: (callExpr) => {
                callExpr.args = callExpr.args.map(a => {
                    if (a instanceof IRFuncExpr && this.#evaluator.countFuncCalls(a) == 0) {
                        return new IRLiteralExpr(new UplcUnit(a.site));
                    } else {
                        return a;
                    }
                });
            }
        });
    }

    /**
     * In scope order, call func before call args
     * @private
     */
    collectFuncExprs() {
        /**
         * @type {IRFuncExpr[]}
         */
        const funcExprs = [];

        loopIRExprs(this.#root, {
            funcExpr: (funcExpr) => {
                funcExprs.push(funcExpr);
            }
        });

        return funcExprs;
    }

    /**
     * @private
     */
    flattenNestedFuncExprs() {
        const funcExprs = this.collectFuncExprs();

        /**
         * @type {Set<IRFuncExpr>}
         */
        const done = new Set();

        funcExprs.forEach(expr => {
            if (done.has(expr)) {
                return;
            }

            let last = expr;
            let args = expr.args.slice();
            let depth = 1;

            while (last.body instanceof IRFuncExpr && this.#evaluator.onlyDedicatedCallExprs(last.body) && this.#evaluator.onlyNestedCalls(last, last.body)) {
                depth += 1;
                last = last.body;
                args = args.concat(last.args.slice());
                done.add(last);
            }

            if (depth == 1) {
                // don't do anything
                return;
            }

            const callExprs = this.#evaluator.getFuncCallExprs(last);

            assert(callExprs.length > 0);

            callExprs.forEach(callExpr => {
                let inner = callExpr;

                /**
                 * @type {IRExpr[][]}
                 */
                let allArgs = [];

                for (let i = 0; i < depth; i++) {
                    allArgs.push(inner.args.slice());

                    if (i < depth - 1) {
                        inner = assertClass(inner.func, IRCallExpr);
                    }
                }

                callExpr.func = inner.func;
                callExpr.args = allArgs.reverse().flat();
            });
                
            expr.args = args;
            expr.body = last.body;
        })
    }

    /**
     * @param {IRFuncExpr} start
     * @param {IRNameExpr} nameExpr
     * @returns {boolean}
     */
    isEvaluatedMoreThanOnce(start, nameExpr) {
        /**
         * @type {Map<IRExpr, IRCallExpr | IRFuncExpr>}
         */
        const parents = new Map();

        let foundNameExpr = false;

        loopIRExprs(start, {
            funcExpr: (funcExpr) => {
                parents.set(funcExpr.body, funcExpr);
            },
            callExpr: (callExpr) => {
                parents.set(callExpr.func, callExpr);
                callExpr.args.forEach(a => {parents.set(a, callExpr)});
            },
            nameExpr: (ne) => {
                foundNameExpr = ne == nameExpr;
            },
            exit: () => {
                return foundNameExpr;
            }
        });

        let parent = parents.get(nameExpr);

        while (parent && parent != start) {
            if (parent instanceof IRFuncExpr && this.countFuncCalls(parent) > 1) {
                return true;
            }
            
            parent = parents.get(parent);
        }

        return false;
    }

    /**
     * @param {IRVariable} v 
     * @param {IRExpr} expr 
     */
    inline(v, expr) {
        this.#inlining.set(v, expr);
    }
    
    /**
     * @private
     * @param {IRNameExpr} expr 
     * @returns {IRExpr}
     */
    optimizeNameExpr(expr) {
        const v = this.#evaluator.getExprValue(expr);
    
        if (v) {
            if (v instanceof IRLiteralValue) {
                return new IRLiteralExpr(v.value);
            } else if (v instanceof IRErrorValue) {
                return new IRErrorExpr(expr.site);
            }
        }

        if (!expr.isCore()) {
            const newExpr = this.#inlining.get(expr.variable);

            if (newExpr) {
                // always copy to make sure any (nested) IRNameExpr is unique (=> unique DeBruijn index)
                //  also so that functions that are inlined multiple times each get unique variables
                return newExpr.copy((oldExpr, newExpr) => {
                    this.#evaluator.notifyCopyExpr(oldExpr, newExpr);
                }, new Map());
            }
        }

        return expr;
    }

    /**
     * The optimizations are only performed in aggressive mode
     * @private
     * @param {IRCallExpr} expr
     * @returns {IRExpr}
     */
    optimizeBuiltinCallExpr(expr) {
        const builtinName = expr.builtinName;

        const args = expr.args;

        switch (builtinName) {
            case "addInteger": {
                const [a, b] = args;

                if (a instanceof IRLiteralExpr && a.value.int == 0n) {
                    return b;
                } else if (b instanceof IRLiteralExpr && b.value.int == 0n) {
                    return a;
                }

                break;
            };
            case "subtractInteger": {
                const [a, b] = args;

                if (b instanceof IRLiteralExpr && b.value.int == 0n) {
                    return a;
                }

                break;
            };
            case "multiplyInteger": {
                const [a, b] = args;

                if (a instanceof IRLiteralExpr && a.value.int == 1n) {
                    return b;
                } else if (b instanceof IRLiteralExpr && b.value.int == 1n) {
                    return a;
                }

                break;
            };
            case "divideInteger": {
                const [a, b] = args;

                if (b instanceof IRLiteralExpr && b.value.int == 1n) {
                    return a;
                }

                break;
            };
            case "quotientInteger": {
                const [a, b] = args;

                if (b instanceof IRLiteralExpr && b.value.int == 1n) {
                    return a;
                }

                break;
            };
            case "appendByteString": {
                const [a, b] = args;

                if (a instanceof IRLiteralExpr && a.value.bytes.length == 0) {
                    return b;
                } else if (b instanceof IRLiteralExpr && b.value.bytes.length == 0) {
                    return a;
                }

                break;
            };
            case "appendString": {
                const [a, b] = args;

                if (a instanceof IRLiteralExpr && a.value.string == "") {
                    return b;
                } else if (b instanceof IRLiteralExpr && b.value.string == "") {
                    return a;
                }

                break;
            };
            case "decodeUtf8": {
                const [arg] = args;

                if (arg instanceof IRCallExpr && arg.func instanceof IRNameExpr && arg.builtinName == "encodeUtf8") {
                    return arg.args[0];
                }

                break;
            };
            case "ifThenElse": {
                const [cond, a, b] = args;

                if (cond instanceof IRLiteralExpr) {
                    if (cond.value.bool && !this.expectsError(b)) {
                        return a;
                    } else if (!cond.value.bool && !this.expectsError(a)) {
                        return b;
                    }
                } else if (!this.expectsError(cond) && a.toString() == b.toString()) {
                    return a;
                } else if (cond instanceof IRCallExpr && cond.func instanceof IRNameExpr && cond.builtinName == "nullList") {
                    const newExpr = new IRCallExpr(
                        expr.site,
                        new IRNameExpr(new Word(expr.site, `${BUILTIN_PREFIX}chooseList`)),
                        [cond.args[0], a, b]
                    );

                    this.#evaluator.notifyCopyExpr(expr, newExpr);

                    return newExpr;
                }

                break;
            };
            case "chooseUnit": {
                const [a, b] = args;

                if (a instanceof IRLiteralExpr && a.value instanceof UplcUnit) {
                    return b;
                } else if (b instanceof IRLiteralExpr && b.value instanceof UplcUnit) {
                    return a;
                }

                break;
            };
            case "trace": {
                const [a, b] = args;
                    
                if (!this.expectsError(a)) {
                    return b;
                }

                break;
            };
            case "chooseList": {
                const [lst, a, b] = args;

                if (lst instanceof IRLiteralExpr) {
                    if (lst.value.list.length == 0 && !this.expectsError(b)) {
                        return a;
                    } else if (lst.value.list.length > 0 && !this.expectsError(a)) {
                        return b;
                    }
                }

                break;
            };
            case "chooseData": {
                const [cond, C, M, L, I, B] = args;

                if (cond instanceof IRLiteralExpr) {
                    if (cond.value.data instanceof ConstrData && !this.expectsError(M) && !this.expectsError(L) && !this.expectsError(I) && !this.expectsError(B)) {
                        return C;
                    } else if (cond.value.data instanceof MapData && !this.expectsError(C) && !this.expectsError(L) && !this.expectsError(I) && !this.expectsError(B)) {
                        return M;
                    } else if (cond.value.data instanceof ListData && !this.expectsError(C) && !this.expectsError(M) && !this.expectsError(I) && !this.expectsError(B)) {
                        return L;
                    } else if (cond.value.data instanceof IntData && !this.expectsError(C) && !this.expectsError(M) && !this.expectsError(L) && !this.expectsError(B)) {
                        return I;
                    } else if (cond.value.data instanceof ByteArrayData && !this.expectsError(C) && !this.expectsError(M) && !this.expectsError(L) && !this.expectsError(I)) {
                        return B;
                    }
                } else if (cond instanceof IRCallExpr && cond.func instanceof IRNameExpr && !this.expectsError(cond)) {
                    if (cond.builtinName == "constrData" && !this.expectsError(M) && !this.expectsError(L) && !this.expectsError(I) && !this.expectsError(B)) {
                        return C;
                    } else if (cond.builtinName == "mapData" && !this.expectsError(C) && !this.expectsError(L) && !this.expectsError(I) && !this.expectsError(B)) {
                        return M;
                    } else if (cond.builtinName == "listData" && !this.expectsError(C) && !this.expectsError(M) && !this.expectsError(I) && !this.expectsError(B)) {
                        return L;
                    } else if (cond.builtinName == "iData" && !this.expectsError(C) && !this.expectsError(M) && !this.expectsError(L) && !this.expectsError(B)) {
                        return I;
                    } else if (cond.builtinName == "bData" && !this.expectsError(C) && !this.expectsError(M) && !this.expectsError(L) && !this.expectsError(I)) {
                        return B;
                    }
                }

                break;
            };
            case "unMapData": {
                const [arg] = args;

                if (arg instanceof IRCallExpr && arg.func instanceof IRNameExpr && arg.builtinName == "mapData") {
                    return arg.args[0];
                }

                break;
            };
            case "unListData": {
                const [arg] = args;

                if (arg instanceof IRCallExpr && arg.func instanceof IRNameExpr && arg.builtinName == "listData") {
                    return arg.args[0];
                }

                break;
            };
            case "unIData": {
                const [arg] = args;

                if (arg instanceof IRCallExpr && arg.func instanceof IRNameExpr && arg.builtinName == "iData") {
                    return arg.args[0];
                }

                break;
            };
            case "unBData": {
                const [arg] = args;

                if (arg instanceof IRCallExpr && arg.func instanceof IRNameExpr && arg.builtinName == "bData") {
                    return arg.args[0];
                }

                break;
            };
            case "equalsData": {
                const [a, b] = args;

                if (
                    a instanceof IRCallExpr && a.func instanceof IRNameExpr && a.builtinName == "iData" &&
                    b instanceof IRCallExpr && b.func instanceof IRNameExpr && b.builtinName == "iData"
                ) {
                    const newExpr = new IRCallExpr(expr.site, new IRNameExpr(new Word(expr.site, `${BUILTIN_PREFIX}equalsInteger`)), [a.args[0], b.args[0]]);

                    this.#evaluator.notifyCopyExpr(expr, newExpr);

                    return newExpr;
                } else if (
                    a instanceof IRCallExpr && a.func instanceof IRNameExpr && a.builtinName == "bData" &&
                    b instanceof IRCallExpr && b.func instanceof IRNameExpr && b.builtinName == "bData"
                ) {
                    const newExpr = new IRCallExpr(expr.site, new IRNameExpr(new Word(expr.site, `${BUILTIN_PREFIX}equalsByteString`)), [a.args[0], b.args[0]]);

                    this.#evaluator.notifyCopyExpr(expr, newExpr);

                    return newExpr;
                }

                break;
            };
        }

        return expr;
    }

    /**
     * @private
     * @param {IRCallExpr} expr 
     * @returns {IRExpr}
     */
    optimizeCallExpr(expr) {
        const v = this.#evaluator.getExprValue(expr);
    
        if (v) {
            if (v instanceof IRLiteralValue) {
                return new IRLiteralExpr(v.value);
            } else if (v instanceof IRErrorValue) {
                return new IRErrorExpr(expr.site);
            }
        }

        let func = expr.func;
        
        let args = expr.args.map(a => this.optimizeInternal(a));

        if (isIdentityFunc(func)) {
            assert(args.length == 1);

            return args[0];
        } else if (func instanceof IRNameExpr) {
            const v = this.#evaluator.getExprValue(func);

            if (v instanceof IRFuncValue && isIdentityFunc(v.definition)) {
                assert(args.length == 1);

                return args[0];
            }
        }

        // see if any arguments can be inlined
        if (func instanceof IRFuncExpr) {
            let unused = new Set();

            const funcExpr = func;
            const variables = func.args;

            args.forEach((a, i) => {
                const v = variables[i];

                if (a instanceof IRNameExpr) {
                    // inline all IRNameExprs
                    unused.add(i);
                    this.inline(v, a);
                } else if (a instanceof IRFuncExpr && (this.#evaluator.countVariableReferences(v) == 1 || a.flatSize <= INLINE_MAX_SIZE)) {
                    // inline IRFuncExpr if it is only reference once
                    unused.add(i);
                    this.inline(v, a);
                } else if (a instanceof IRCallExpr && this.#evaluator.countVariableReferences(v) == 1 && !this.expectsError(a)) {
                    const nameExpr = this.#evaluator.getVariableReferences(v)[0];

                    if (!this.isEvaluatedMoreThanOnce(funcExpr, nameExpr)) {
                        unused.add(i);
                        this.inline(v, a);
                    }
                }
            });

            if (unused.size > 0) {
                args = args.filter((a, i) => !unused.has(i));

                const newFuncExpr = this.newFuncExpr(
                    func,
                    func.args.filter((a, i) => !unused.has(i)),
                    func.body
                );

                func = newFuncExpr;
            }
        }

        if (args.length == 0 && func instanceof IRFuncExpr && func.args.length == 0) {
            return this.optimizeInternal(func.body);
        }

        const newExpr = new IRCallExpr(
            expr.site, 
            this.optimizeInternal(func),
            args
        );

        this.#evaluator.notifyCopyExpr(expr, newExpr);

        const builtinName = newExpr.builtinName;

        if (builtinName != "") {
            return this.optimizeBuiltinCallExpr(newExpr);
        }

        return newExpr;
    }

    /**
     * @private
     * @param {IRFuncExpr} expr 
     * @returns {IRExpr}
     */
    optimizeFuncExpr(expr) {
        expr = this.newFuncExpr(
            expr, 
            expr.args,
            this.optimizeInternal(expr.body)
        );

        if (
            expr.body instanceof IRCallExpr && 
            (expr.body.func instanceof IRNameExpr || expr.body.func instanceof IRFuncExpr) && 
            expr.body.args.length == expr.args.length && 
            expr.body.args.every((a, i) => {
                return a instanceof IRNameExpr && a.isVariable(expr.args[i]) && this.#evaluator.countVariableReferences(expr.args[i]) == 1;
            })
        ) {
            return expr.body.func;
        }

        return expr;
    }

    /**
     * @private
     * @param {IRExpr} expr 
     */
    optimizeInternal(expr) {
        const newExpr = (() => {
            if (expr instanceof IRLiteralExpr) {
                // already optimal
                return expr;
            } else if (expr instanceof IRErrorExpr) {
                // already optimal
                return expr;
            } else if (expr instanceof IRNameExpr) {
                return this.optimizeNameExpr(expr);
            } else if (expr instanceof IRCallExpr) {
                return this.optimizeCallExpr(expr);
            } else if (expr instanceof IRFuncExpr) {
                return this.optimizeFuncExpr(expr);
            } else {
                throw new Error("unhandled IRExpr");
            }
        })();

        return newExpr;
    }

    /**
     * @param {IRExpr} expr 
     */
    static assertNoDuplicateExprs(expr) {
        /**
         * @type {Set<IRExpr>}
         */
        const s = new Set();

        loopIRExprs(expr, {
            nameExpr: (nameExpr) => {
                if (s.has(nameExpr)) {
                    console.log(expr.toString());
                    throw new Error("duplicate IRNameExpr " + nameExpr.name);
                }

                s.add(nameExpr);
            }
        });
    }

    /**
     * @returns {IRExpr}
     */
    optimize() {
        const expr = this.optimizeInternal(this.#root);

        // TODO: factorize common subexprs
        IROptimizer.assertNoDuplicateExprs(expr);

        return expr;
    }
}


/////////////////////////
// Section 32: IR Program
/////////////////////////


/**
 * Wrapper for IRFuncExpr, IRCallExpr or IRLiteralExpr
 * @internal
 */
export class IRProgram {
	#expr;
	#properties;

	/**
	 * @param {IRFuncExpr | IRCallExpr | IRLiteralExpr} expr
	 * @param {ProgramProperties} properties
	 */
	constructor(expr, properties) {
		this.#expr = expr;
		this.#properties = properties;
	}

	/**
	 * @param {IRExpr} expr 
	 * @returns {IRFuncExpr | IRCallExpr | IRLiteralExpr}
	 */
	static assertValidRoot(expr) {
		if (expr instanceof IRFuncExpr || expr instanceof IRCallExpr || expr instanceof IRLiteralExpr) {
			return expr;
		} else {
			throw new Error("invalid IRExpr type for IRProgram");
		}
	}

	/**
	 * @internal
	 * @param {IR} ir 
	 * @param {null | ScriptPurpose} purpose
	 * @param {boolean} simplify
	 * @param {IRScope} scope
	 * @returns {IRProgram}
	 */
	static new(ir, purpose, simplify = false, scope = new IRScope(null, null)) {
		let [irSrc, codeMap] = ir.generateSource();
		
		const callsTxTimeRange = irSrc.match(/\b__helios__tx__time_range\b/) !== null;

		let irTokens = tokenizeIR(irSrc, codeMap);

		let expr = buildIRExpr(irTokens);
		
		try {
			expr.resolveNames(scope);
		
			if (simplify) {
				// inline literals and evaluate core expressions with only literal args (some can be evaluated with only partial literal args)
				expr = IRProgram.simplify(expr);
			}

			// make sure the debruijn indices are correct (doesn't matter for simplication because names are converted into unique IRVariables, but is very important before converting to UPLC)
			expr.resolveNames(scope);

			const program = new IRProgram(IRProgram.assertValidRoot(expr), {
				purpose: purpose,
				callsTxTimeRange: callsTxTimeRange
			});

			return program;
		} catch (e) {
			console.log((new Source(irSrc, "")).pretty());

			throw e;
		}
	}

	/**
	 * @returns {string}
	 */
	annotate() {
		const evaluator = new IREvaluator();

		evaluator.eval(this.#expr);

		return annotateIR(evaluator, this.#expr);
	}

	/**
	 * @param {IRExpr} expr
	 * @returns {IRExpr}
	 */
	static simplify(expr) {
		let dirty = true;
		let oldState = expr.toString();
		let optState = new IROptimizerState();

		while (dirty) {
			dirty = false;

			const optimizer = new IROptimizer(expr, optState);

			expr = optimizer.optimize();

			const newState = expr.toString();

			if (newState != oldState) {
				dirty = true;
				oldState = newState;
			}
		}

		return expr;
	}

	/**
	 * @internal
	 * @type {IRFuncExpr | IRCallExpr | IRLiteralExpr}
	 */
	get expr() {
		return this.#expr;
	}

	/**
	 * @internal
	 * @type {ProgramProperties}
	 */
	get properties() {
		return this.#properties;
	}

	/**
	 * @internal
	 * @type {Site}
	 */
	get site() {
		return this.#expr.site;
	}

	/**
	 * @type {UplcData}
	 */
	get data() {
		if (this.#expr instanceof IRLiteralExpr) {
			let v = this.#expr.value;

			if (v instanceof UplcDataValue) {
				return v.data;
			} else if (v instanceof UplcInt) {
				return new IntData(v.int);
			} else if (v instanceof UplcBool) {
				return new ConstrData(v.bool ? 1 : 0, []);
			} else if (v instanceof UplcList) {
				if (v.isDataList()) {
					return new ListData(v.list.map(item => item.data));
				} else if (v.isDataMap()) {
					return new MapData(v.list.map(item => {
						const pair = assertClass(item, UplcPair);

						return [pair.key, pair.value];
					}));
				}
			} else if (v instanceof UplcString) {
				return new ByteArrayData(textToBytes(v.string));
			} else if (v instanceof UplcByteArray) {
				return new ByteArrayData(v.bytes);
			}
		} 

		throw new Error(`unable to turn '${this.toString()}' into data`);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return this.#expr.toString();
	}

	/**
	 * @returns {UplcProgram}
	 */
	toUplc() {
		return new UplcProgram(this.#expr.toUplc(), this.#properties);
	}

	/**
	 * @returns {number}
	 */
	calcSize() {
		return this.toUplc().calcSize();
	}
}

/**
 * @internal
 */
export class IRParametricProgram {
	/**
	 * @type {IRProgram}
	 */
	#irProgram;

	/**
	 * @type {number}
	 */
	#nParams;

	/**
	 * @param {IRProgram} irProgram
	 * @param {number} nParams
	 */
	constructor(irProgram, nParams) {
		this.#irProgram = irProgram;
		this.#nParams = nParams;
	}

	/**
	 * @internal
	 * @param {IR} ir 
	 * @param {null | ScriptPurpose} purpose
	 * @param {number} nParams
	 * @param {boolean} simplify
	 * @returns {IRParametricProgram}
	 */
	static new(ir, purpose, nParams, simplify = false) {
		let scope = new IRScope(null, null);

		for (let i = 0; i < nParams; i++) {
			const internalName = `__PARAM_${i}`;

			scope = new IRScope(scope, new IRVariable(new Word(Site.dummy(), internalName)));
		}

		const irProgram = IRProgram.new(ir, purpose, simplify, scope);

		return new IRParametricProgram(irProgram, nParams);
	}

	/**
	 * @type {IRProgram}
	 */
	get program() {
		return this.#irProgram;
	}

	/**
	 * @returns {UplcProgram}
	 */
	toUplc() {
		let exprUplc = this.#irProgram.expr.toUplc();

		for (let i = 0; i < this.#nParams; i++) {
			exprUplc = new UplcLambda(Site.dummy(), exprUplc);
		}

		return new UplcProgram(exprUplc, this.#irProgram.properties);
	}
}



/////////////////////////////
// Section 33: Helios program
/////////////////////////////


/**
 * A Module is a collection of statements
 */
class Module {
	#name;
	#statements;

	/**
	 * @param {Word} name 
	 * @param {Statement[]} statements
	 */
	constructor(name, statements) {
		this.#name = name;
		this.#statements = statements;

		this.#statements.forEach(s => s.setBasePath(`__module__${this.#name.toString()}`));
	}

	/**
	 * @param {string} rawSrc
	 * @returns {Module}
	 */
	static new(rawSrc) {
		let rawName = "";
		const purposeName = extractScriptPurposeAndName(rawSrc);
		if (purposeName) {
			rawName = purposeName[1];
		}

		const src = new Source(rawSrc, rawName);

		const ts = tokenize(src);

		src.throwErrors();

		if (ts === null) {
			throw new Error("should've been thrown above");
		}

		if (ts.length == 0) {
			throw UserError.syntaxError(src, 0, 1, "empty script");
		}

		const [purpose, name, statements, mainIdx] = buildScript(ts, "module");

		src.throwErrors();

		if (name !== null) {
			return new Module(name, statements);
		} else {
			throw new Error("unexpected"); // should've been caught by calling src.throwErrors() above
		}
	}

	throwErrors() {
		this.#name.site.src.throwErrors();
	}

	/**
	 * @type {Word}
	 */
	get name() {
		return this.#name;
	}

	/**
	 * @type {Statement[]}
	 */
	get statements() {
		return this.#statements.slice();
	}

	/**
	 * @param {string} namespace 
	 * @param {(name: string, cs: ConstStatement) => void} callback 
	 */
	loopConstStatements(namespace, callback) {
		for (let s of this.#statements) {
			s.loopConstStatements(namespace, callback);
		}
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return this.#statements.map(s => s.toString()).join("\n");
	}

	/**
	 * @param {ModuleScope} scope 
	 */
	evalTypes(scope) {
		for (let s of this.statements) {
			s.eval(scope);
		}
	}

	/**
	 * This module can depend on other modules
	 * TODO: detect circular dependencies
	 * @param {Module[]} modules 
	 * @param {Module[]} stack
	 * @returns {Module[]}
	 */
	filterDependencies(modules, stack = []) {
		/**
		 * @type {Module[]}
		 */
		let deps = [];

		/** @type {Module[]} */
		let newStack = [this];
		newStack = newStack.concat(stack);

		for (let s of this.#statements) {
			if (s instanceof ImportFromStatement || s instanceof ImportModuleStatement) {
				let mn = s.moduleName.value;

				if (mn == this.name.value) {
					throw s.syntaxError("can't import self");
				} else if (stack.some(d => d.name.value == mn)) {
					throw s.syntaxError("circular import detected");
				}

				// if already in deps, then don't add (because it will have been added before along with all its dependencies)
				if (!deps.some(d => d.name.value == mn)) {
					let m = modules.find(m => m.name.value == mn);

					if (m === undefined) {
						throw s.referenceError(`module '${mn}' not found`);
					} else {
						// only add deps that weren't added before
						let newDeps = m.filterDependencies(modules, newStack).concat([m]).filter(d => !deps.some(d_ => d_.name.value == d.name.value));

						deps = deps.concat(newDeps);
					}
				}
			}
		}

		return deps;
	}
}

/**
 * The entrypoint module
 */
class MainModule extends Module {
	/**
	 * @param {Word} name 
	 * @param {Statement[]} statements 
	 */
	constructor(name, statements) {
		super(name, statements);
	}

	/**
	 * @type {FuncStatement}
	 */
	get mainFunc() {
		for (let s of this.statements) {
			if (s.name.value == "main") {
				if (!(s instanceof FuncStatement)) {	
					throw s.typeError("'main' isn't a function statement");
				} else {
					return s;
				}
			}
		}

		throw new Error("'main' not found (is a module being used as an entrypoint?)");
	}
}

/**
 * @typedef {{[name: string]: any}} UserTypes
 */

/**
 * @typedef {{
 *   allowPosParams: boolean
 *   invertEntryPoint: boolean
 * }} ProgramConfig
 */

/**
 * @type {ProgramConfig}
 */
const DEFAULT_PROGRAM_CONFIG = {
	allowPosParams: false,
	invertEntryPoint: false
}

/**
 * Helios root object
 */
 export class Program {
	/**
	 * @type {ScriptPurpose}
	 */
	#purpose;

	/**
	 * @type {Module[]}
	 */
	#modules;

	/**
	 * @type {ProgramConfig}
	 */
	#config;

	/** 
	 * @type {UserTypes} 
	 */
	#types;

	/**
	 * Cache of const values
	 * @type {Object.<string, HeliosData>}
	 */
	#parameters;
	
	/**
	 * @internal
	 * @param {ScriptPurpose} purpose
	 * @param {Module[]} modules
	 * @param {ProgramConfig} config
	 */
	constructor(purpose, modules, config) {
		this.#purpose = purpose;
		this.#modules = modules;
		this.#config = config;
		this.#types = {};
		this.#parameters = {};
	}

	/**
	 * @internal
	 */
	throwErrors() {
		this.#modules.forEach(m => m.throwErrors());
	}

	/**
	 * @internal
	 * @param {string} rawSrc 
	 * @returns {[purpose, Module[]]}
	 */
	static parseMainInternal(rawSrc) {
		let rawName = "";
		const purposeName = extractScriptPurposeAndName(rawSrc);
		if (purposeName) {
			rawName = purposeName[1];
		}

		const src = new Source(rawSrc, rawName);

		const ts = tokenize(src);

		src.throwErrors();

		if (ts === null) {
			throw new Error("should've been thrown above");
		}

		if (ts.length == 0) {
			throw UserError.syntaxError(src, 0, 1, "empty script");
		}

		const [purpose, name, statements, mainIdx] = buildScript(ts);

		src.throwErrors();

		if (purpose !== null && name !== null) {
			/**
			 * @type {Module[]}
			 */
			const modules = [new MainModule(name, statements.slice(0, mainIdx+1))];

			if (mainIdx < statements.length - 1) {
				modules.push(new Module(name, statements.slice(mainIdx+1)));
			}

			return [purpose, modules];
		} else {
			throw new Error("unexpected"); // should've been caught by calling src.throwErrors() above
		}
	}

	/**
	 * @internal
	 * @param {string} mainName 
	 * @param {string[]} moduleSrcs
	 * @returns {Module[]}
	 */
	static parseImports(mainName, moduleSrcs = []) {
		let imports = moduleSrcs.map(src => {
			return Module.new(src);
		});

		/**
		 * @type {Set<string>}
		 */
		let names = new Set();

		names.add(mainName);

		for (let m of imports) {
			if (names.has(m.name.value)) {
				throw m.name.syntaxError(`non-unique module name '${m.name.value}'`);
			}

			names.add(m.name.value);
		}

		return imports;
	}

	/**
	 * @internal
	 * @param {string} mainSrc 
	 * @param {string[]} moduleSrcs
	 * @returns {[null | ScriptPurpose, Module[]]}
	 */
	static parseMain(mainSrc, moduleSrcs) {
		let [purpose, modules] = Program.parseMainInternal(mainSrc);

		const site = modules[0].name.site;

		const imports = Program.parseImports(modules[0].name.value, moduleSrcs);
		
		const mainImports = modules[0].filterDependencies(imports);

		/** @type {Module[]} */
		let postImports = [];

		if (modules.length > 1) {
			postImports = modules[modules.length - 1].filterDependencies(imports).filter(m => !mainImports.some(d => d.name.value == m.name.value));
		}

		// create the final order of all the modules (this is the order in which statements will be added to the IR)
		modules = mainImports.concat([modules[0]]).concat(postImports).concat(modules.slice(1));

		if (purpose == "module") {
			throw site.syntaxError("can't use module for main");
		}

		return [purpose, modules];
	}

	/**
	 * Creates  a new program.
	 * @param {string} mainSrc 
	 * @param {string[]} moduleSrcs - optional sources of modules, which can be used for imports
	 * @param {ProgramConfig} config
	 * @returns {Program}
	 */
	static new(mainSrc, moduleSrcs = [], validatorTypes = {}, config = DEFAULT_PROGRAM_CONFIG) {
		return Program.newInternal(mainSrc, moduleSrcs, {}, config);
	}

	/**
	 * Creates  a new program.
	 * @internal
	 * @param {string} mainSrc 
	 * @param {string[]} moduleSrcs - optional sources of modules, which can be used for imports
	 * @param {{[name: string]: Type}} validatorTypes
	 * @param {ProgramConfig} config
	 * @returns {Program}
	 */
	static newInternal(mainSrc, moduleSrcs = [], validatorTypes = {}, config = DEFAULT_PROGRAM_CONFIG) {
		const [purpose, modules] = Program.parseMain(mainSrc, moduleSrcs);
	
		/**
		 * @type {Program}
		 */
		let program;

		switch (purpose) {
			case "testing":
				program = new TestingProgram(modules);
				break;
			case "spending":
				program = new SpendingProgram(modules, config);
				break;
			case "minting":
				program = new MintingProgram(modules, config);
				break;
			case "staking":
				program = new StakingProgram(modules, config);
				break;
			case "endpoint":
				program = new EndpointProgram(modules);
				break;
			default:
				throw new Error("unhandled script purpose");
		}

		const topScope = program.evalTypes(validatorTypes);

		program.throwErrors();

		if (purpose != "endpoint") {
			program.fillTypes(topScope);
		}

		return program;
	}

	/**
	 * @type {ProgramConfig}
	 */
	get config() {
		return this.#config;
	}

	/**
	 * @internal
	 * @type {number}
	 */
	get nPosParams() {
		return 0;
	}

	/**
	 * @internal
	 * @type {Type[]}
	 */
	get posParams() {
		return this.mainArgTypes.slice(0, this.nPosParams);
	}

	/** 
	 * @internal
	 * @type {Module[]} 
	 */
	get mainImportedModules() {
		/** @type {Module[]} */
		let ms = [];

		for (let m of this.#modules) {
			if (m instanceof MainModule) {
				break;
			} else {
				ms.push(m);
			}
		}

		return ms;
	}

	/**
	 * @internal
	 * @type {MainModule}
	 */
	get mainModule() {
		for (let m of this.#modules) {
			if (m instanceof MainModule) {
				return m;
			}
		}

		throw new Error("MainModule not found");
	}

	/**
	 * @internal
	 * @type {null | Module}
	 */
	get postModule() {
		let m = this.#modules[this.#modules.length - 1];

		if (m instanceof MainModule) {
			return null;
		} else {
			return m;
		}
	}

	/**
	 * @type {ScriptPurpose}
	 */
	get purpose() {
		return this.#purpose;
	}

	/**
	 * @type {string}
	 */
	get name() {
		return this.mainModule.name.value;
	}

	/**
	 * @internal
	 * @type {FuncStatement}
	 */
	get mainFunc() {
		return this.mainModule.mainFunc;
	}

	/**
	 * @internal
	 * @type {Site}
	 */
	get mainRetExprSite() {
		return this.mainFunc.retSite;
	}

	/**
	 * @internal
	 * @type {string[]}
	 */
	get mainArgNames() {
		return this.mainFunc.argNames;
	}

	/**
	 * @internal
	 * @type {DataType[]}
	 */
	get mainArgTypes() {
		return this.mainFunc.argTypes.map(at => assertDefined(at.asDataType));
	}

	/**
	 * @internal
	 * @type {string}
	 */
	get mainPath() {
		return this.mainFunc.path;
	}

	/**
	 * @internal
	 * @type {Statement[]}
	 */
	get mainStatements() {
		return this.mainModule.statements;
	}

	/**
	 * Needed to list the paramTypes, and to call changeParam
	 * @internal
	 * @type {Statement[]}
	 */
	get mainAndPostStatements() {
		let statements = this.mainModule.statements;

		if (this.postModule != null) {
			statements = statements.concat(this.postModule.statements);
		}

		return statements;
	}

	/**
	 * @internal
	 * @type {[Statement, boolean][]} - boolean value marks if statement is import or not
	 */
	get allStatements() {
		/**
		 * @type {[Statement, boolean][]}
		 */
		let statements = [];

		for (let i = 0; i < this.#modules.length; i++) {
			let m = this.#modules[i];

			// MainModule or PostModule => isImport == false
			let isImport = !(m instanceof MainModule || (i == this.#modules.length - 1));

			statements = statements.concat(m.statements.map(s => [s, isImport]));
		}

		return statements;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return this.#modules.map(m => m.toString()).join("\n");
	}

	/**
	 * @internal
	 * @param {GlobalScope} globalScope
	 * @returns {TopScope}
	 */
	evalTypesInternal(globalScope) {
		const topScope = new TopScope(globalScope);

		// loop through the modules

		for (let i = 0; i < this.#modules.length; i++) {
			const m = this.#modules[i];

			// reuse main ModuleScope for post module
			const moduleScope = (m ===  this.postModule) ? topScope.getModuleScope(this.mainModule.name) : new ModuleScope(topScope);

			m.evalTypes(moduleScope);

			if (m instanceof MainModule) {
				topScope.setStrict(false);
			}

			if (m !== this.postModule) {
				topScope.setScope(m.name, moduleScope);
			}
		}
		
		return topScope;
	}

	/**
	 * @internal
	 * @param {{[name: string]: Type}} validatorTypes
	 * @returns {TopScope}
	 */
	evalTypes(validatorTypes = {}) {
		throw new Error("not yet implemented");
	}

	/**
	 * @type {UserTypes}
	 */
	get types() {
		return this.#types;
	}

	/**
	 * Fill #types with convenient javascript equivalents of Int, ByteArray etc.
	 * @internal
	 * @param {TopScope} topScope
	 */
	fillTypes(topScope) {
		const mainModuleScope = topScope.getModuleScope(this.mainModule.name);

		mainModuleScope.loopTypes((name, type) => {
			if (type?.asDataType?.offChainType) {
				this.#types[name] = type.asDataType.offChainType;
			}
		});
	}

	/**
	 * @internal
	 * @param {(name: string, cs: ConstStatement) => void} callback 
	 */
	loopConstStatements(callback) {
		const postModule = this.postModule;

		for (let m of this.#modules) {
			const namespace = (m instanceof MainModule || m === postModule) ? "" : `${m.name.value}::`;

			m.loopConstStatements(namespace, callback);
		}
	}

	/**
	 * @internal
	 * @type {{[name: string]: DataType}}
	 */
	get paramTypes() {
		/**
		 * @type {{[name: string]: DataType}}
		 */
		let res = {};

		this.loopConstStatements((name, constStatement) => {
			res[name] = constStatement.type
		});

		return res;
	}

	/**
	 * Change the literal value of a const statements  
	 * @internal
	 * @param {string} name
	 * @param {UplcData} data
	 */
	changeParamSafe(name, data) {
		let found = false;

		this.loopConstStatements((constName, constStatement) => {
			if (!found) {
				if (constName == name) {
					constStatement.changeValueSafe(data);
					found = true;
				}
			}
		})

		if (!found) {
			throw this.mainFunc.referenceError(`param '${name}' not found`);
		}
	}

	/**
	 * @internal
	 * @param {string} name 
	 * @returns {ConstStatement | null}
	 */
	findConstStatement(name) {
		/**
		 * @type {ConstStatement | null}
		 */
		let cs = null;

		this.loopConstStatements((constName, constStatement) => {
			if (cs === null) {
				if (name == constName)  {
					cs = constStatement;
				}
			}
		});

		return cs;
	}

	/**
	 * @internal
	 * @param {ConstStatement} constStatement
	 * @returns {UplcValue}
	 */
	evalConst(constStatement) {
		const ctx = new ToIRContext(false)

		const map = this.fetchDefinitions(ctx, new IR(""), (s, isImport) => {
			let found = false;
			s.loopConstStatements("", (_, cs) => {
				if (!found) {
					if (cs === constStatement) {
						found = true;
					}
				}
			})

			return found;
		});

		const path = constStatement.path;

		const inner = new IR(path);

		const ir = this.wrapInner(ctx, inner, map);

		const [irSrc, codeMap] = ir.generateSource();

		const irTokens = tokenizeIR(irSrc, codeMap);

		const expr = buildIRExpr(irTokens);

		const scope = new IRScope(null, null);

		expr.resolveNames(scope);

		const evaluation = new IREvaluator();
		
		try {
			const data = evaluation.evalConst(expr);

			return new UplcDataValue(expr.site, data);
		} catch (e) {
			console.log(irSrc);

			throw e;
		}
	}

	/**
	 * Doesn't use wrapEntryPoint
	 * @param {string} name - can be namespace: "Type::ConstName" or "Module::ConstName" or "Module::Type::ConstName"
	 * @returns {UplcValue}
	 */
	evalParam(name) {
		/** 
		 * @type {ConstStatement | null} 
		 */
		let constStatement = this.findConstStatement(name);

		if (!constStatement) {
			throw new Error(`param '${name}' not found`);
		}

		return this.evalConst(constStatement);
	}
	
	/**
	 * Alternative way to get the parameters as HeliosData instances
	 * @returns {{[name: string]: HeliosData | any}}
	 */
	get parameters() {
		const that = this;

		// not expensive, so doesn't need to be evaluated on-demand
		const types = this.paramTypes;

		const handler = {
			/**
			 * Return from this.#parameters if available, or calculate
			 * @param {{[name: string]: HeliosData}} target 
			 * @param {string} name
			 * @returns {HeliosData}
			 */
			get(target, name) {
				if (name in target) {
					return target[name];
				} else {
					const type = assertDefined(types[name], `invalid param name '${name}'`);
					
					const uplcValue = that.evalParam(name);

					const value = assertDefined(type.offChainType).fromUplcData(uplcValue.data);
						
					target[name] = value;

					return value;
				}
			},
			
			/**
			 * @param {{[name: string]: HeliosData}} target
			 * @param {string} name
			 * @param {HeliosData | any} rawValue
			 * @returns {boolean}
			 */
			set(target, name, rawValue) {
				let permissive = false;
				if (name.startsWith("?")) {
					name = name.slice(1);
					permissive = true;
				}

				if (!types[name]) {
					if (!permissive) {
						throw new Error(`invalid parameter name '${name}'`);
					}
				} else {
					const UserType = assertDefined(types[name].offChainType, `invalid param name '${name}'`);

					const value = rawValue instanceof UserType ? rawValue : new UserType(rawValue);

					target[name] = value;

					that.changeParamSafe(name, value._toUplcData());
				}

				return true;
			}
		};

		return new Proxy(this.#parameters, handler);
	}

	/**
	 * Use proxy for setting
	 * @param {{[name: string]: HeliosData | any}} values
	 */
	set parameters(values) {
		const proxy = this.parameters;

		for (let name in values) {
			proxy[name] = values[name];
		}
	}

	/**
	 * @internal
	 * @param {ToIRContext} ctx
	 * @param {(s: Statement, isImport: boolean) => boolean} endCond
	 * @returns {IRDefinitions} 
	 */
	statementsToIR(ctx, endCond) {		
		/**
		 * @type {IRDefinitions}
		 */
		const map = new Map();

		for (let [statement, isImport] of this.allStatements) {
			statement.toIR(ctx, map);

			if (endCond(statement, isImport)) {
				break;
			}
		}

		return map;
	}

	/**
	 * For top-level statements
	 * @internal
	 * @param {IR} mainIR
	 * @param {IRDefinitions} map
	 * @returns {IR}
	 */
	static injectMutualRecursions(mainIR, map) {
		const keys = Array.from(map.keys());

		/**
		 * @param {string} name
		 * @param {string[]} potentialDependencies 
		 * @returns {string[]}
		 */
		const filterMutualDependencies = (name, potentialDependencies) => {
			// names to be treated
			const stack = [name];

			/**
			 * @type {Set<string>}
			 */
			let set = new Set();

			while (stack.length > 0) {
				const name = assertDefined(stack.shift());

				const ir = assertDefined(map.get(name));

				const localDependencies = keys.slice(keys.findIndex(name.includes("[") ? ((prefix) => {return (n => n.startsWith(prefix))})(name.split("[")[0]): (n => n == name))).filter(dep => !set.has(dep));

				for (let i = 0; i < localDependencies.length; i++) {
					const dep = localDependencies[i];
					if (ir.includes(dep)) {
						set.add(dep)

						if (dep != name) {
							stack.push(dep);
						}
					}
				}
			}

			return potentialDependencies.filter(d => set.has(d));
		}

		for (let i = keys.length - 1; i >= 0; i--) {
			const k = keys[i];

			// don't make a final const statement self-recursive (makes evalParam easier)
			// don't make __helios builtins mutually recursive
			// don't make __from_data and ____<op> methods mutually recursive (used frequently inside the entrypoint)
			if ((k.startsWith("__const") && i == keys.length - 1) || k.startsWith("__helios") || (k.endsWith("__from_data") && !config.CHECK_CASTS) || k.includes("____")) {
				continue;
			}

			let prefix = assertDefined(k.match(/(__const)?([^[]+)(\[|$)/))[0];

			// get all following definitions including self, excluding constants
			// also don't mutual recurse helios functions
			const potentialDependencies = keys.slice(i).filter(k => (k.startsWith(prefix) || k.startsWith(`__const${prefix}`)) && !(k.endsWith("__from_data") && !config.CHECK_CASTS) && !k.includes("____"));

			const dependencies = filterMutualDependencies(k, potentialDependencies);

			if (dependencies.length > 0) {
				const escaped = k.replace(/\[/g, "\\[").replace(/]/g, "\\]");

				const re = new RegExp(`\\b${escaped}(\\b|$)`, "gm");
				const newStr = `${k}(${dependencies.join(", ")})`;
				// do the actual replacing
				for (let k_ of keys) {
					map.set(k_, assertDefined(map.get(k_)).replace(re, newStr));
				}

				mainIR = mainIR.replace(re, newStr);

				const wrapped = new IR([
					new IR(`(${dependencies.join(", ")}) -> {`),
					assertDefined(map.get(k)),
					new IR("}")
				]);

				// wrap own definition
				map.set(k, wrapped);
			}
		}

		return mainIR;
	}

	/**
	 * Also merges builtins and map
	 * @internal
	 * @param {ToIRContext} ctx
	 * @param {IR} mainIR
	 * @param {IRDefinitions} map 
	 * @returns {IRDefinitions}
	 */
	static applyTypeParameters(ctx, mainIR, map) {
		const builtinGenerics = fetchRawGenerics(ctx);

		/**
		 * @type {Map<string, [string, IR]>}
		 */
		const added = new Map();

		/**
		 * @param {string} name 
		 * @param {string} location
		 */
		const add = (name, location) => {
			if (map.has(name) || added.has(name)) {
				return;
			}

			const pName = IRParametricName.parse(name);

			const genericName = pName.toTemplate();
			const genericFuncName = pName.toTemplate(true);

			let ir = builtinGenerics.get(name) ?? builtinGenerics.get(genericName) ?? builtinGenerics.get(genericFuncName) ?? map.get(genericName);

			if (!ir) {
				throw new Error(`${genericName} undefined in ir`);
			} else if (ir instanceof IR) {
				ir = pName.replaceTemplateNames(ir);

				added.set(name, [location, ir]);

				ir.search(RE_IR_PARAMETRIC_NAME, (name_) => add(name_, name));
			} else {
				const ir_ = ir(pName.ttp, pName.ftp);

				added.set(name, [location, ir_]);

				ir_.search(RE_IR_PARAMETRIC_NAME, (name_) => add(name_, name));
			}
		};

		for (let [k, v] of map) {
			v.search(RE_IR_PARAMETRIC_NAME, (name) => add(name, k));
		}

		mainIR.search(RE_IR_PARAMETRIC_NAME, (name) => add(name, "main"))

		// we need to keep templates, otherwise find() might fail to inject the applied definitions in the right location
		let entries = Array.from(map.entries());

		/**
		 * @param {string} name
		 * @returns {number}
		 */
		const find = (name) => {
			for (let i = entries.length - 1; i >= 0; i--) {
				if (entries[i][0] == name) {
					return i;
				}
			}

			if (name == "main") {
				return entries.length;
			} else {
				throw new Error(`${name} not found`);
			}
		};

		const addedEntries = Array.from(added.entries());

		for (let i = 0; i < addedEntries.length; i++) {
			const [name, [location, ir]] = addedEntries[i];

			const j = find(location);

			// inject right before location

			entries = entries.slice(0, j).concat([[name, ir]]).concat(entries.slice(j));
		}

		/**
		 * Remove template because they don't make any sense in the final output
		 */
		entries = entries.filter(([key, _]) => !IRParametricName.isTemplate(key));

		return new Map(entries);
	}

	/**
	 * @internal
	 * @param {IR} ir 
	 * @param {IRDefinitions} definitions 
	 * @returns {Set<string>}
	 */
	collectAllUsed(ir, definitions) {
		/**
		 * Set of global paths
		 * @type {Set<string>}
		 */
		const used = new Set();

		/**
		 * @type {IR[]}
		 */
		const stack = [ir];

		const RE = /__[a-zA-Z0-9_[\]@]+/g;

		while (stack.length > 0) {
			const ir = assertDefined(stack.pop());

			ir.search(RE, (match) => {
				if (!used.has(match)) {
					used.add(match);

					const def = definitions.get(match);

					if (def) {
						stack.push(def);
					}
				}
			})
		}

		return used;
	}

	/**
	 * @internal
	 * @param {IR} ir 
	 * @param {IRDefinitions} definitions 
	 * @returns {IRDefinitions}
	 */
	eliminateUnused(ir, definitions) {
		const used = this.collectAllUsed(ir, definitions);

		// eliminate all definitions that are not in set

		/**
		 * @type {IRDefinitions}
		 */
		const result = new Map();

		for (let [k, ir] of definitions) {
			if (used.has(k)) {
				result.set(k, ir);
			}
		}

		// Loop internal const statemtsn
		this.loopConstStatements((name, cs) => {
			const path = cs.path;

			if (used.has(path) && !definitions.has(cs.path)) {
				throw cs.site.referenceError(`used unset const '${name}' (hint: use program.parameters['${name}'] = ...)`);
			}
		});

		return result;
	}

	/**
	 * Loops over all statements, until endCond == true (includes the matches statement)
	 * Then applies type parameters
	 * @internal
	 * @param {ToIRContext} ctx
	 * @param {IR} ir
	 * @param {(s: Statement) => boolean} endCond
	 * @returns {IRDefinitions}
	 */
	fetchDefinitions(ctx, ir, endCond) {
		let map = this.statementsToIR(ctx, endCond);

		return Program.applyTypeParameters(ctx, ir, map);
	}

	/**
	 * @internal
	 * @param {ToIRContext} ctx
	 * @param {IR} ir
	 * @param {IRDefinitions} definitions
	 * @returns {IR}
	 */
	wrapInner(ctx, ir, definitions) {
		ir = Program.injectMutualRecursions(ir, definitions);

		definitions = this.eliminateUnused(ir, definitions);

		ir = IR.wrapWithDefinitions(ir, definitions);

		// add builtins as late as possible, to make sure we catch as many dependencies as possible
		const builtins = fetchRawFunctions(ctx, assertClass(ir, IR), definitions);

		ir = IR.wrapWithDefinitions(ir, builtins);

		return ir;
	}

	/**
	 * @internal
	 * @param {ToIRContext} ctx
	 * @param {IR} ir
	 * @param {null | IRDefinitions} extra
	 * @returns {IR}
	 */
	wrapEntryPoint(ctx, ir, extra = null) {
		let map = this.fetchDefinitions(ctx, ir, (s) => s.name.value == "main");

		if (extra) {
			map = new Map(Array.from(extra.entries()).concat(Array.from(map.entries())));
		}

		return this.wrapInner(ctx, ir, map);
	}

	/**
	 * @internal
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIRInternal(ctx) {
		throw new Error("not yet implemented");
	}

	/**
	 * @internal
	 * @param {ToIRContext} ctx
	 * @param {null | IRDefinitions} extra
	 * @returns {IR}
	 */
	toIR(ctx, extra = null) {
		const ir = this.toIRInternal(ctx)

		return this.wrapEntryPoint(ctx, ir, extra);
	}

	/**
	 * Non-positional named parameters
	 * @internal
	 * @type {[string, Type][]}
	 */
	get requiredParameters() {
		const ir = this.toIRInternal(new ToIRContext(false));
		const definitions = this.fetchDefinitions(new ToIRContext(false), ir, (s) => s.name.value == "main");
		const used = this.collectAllUsed(ir, definitions);
		
		/**
		 * @type {[string, Type][]}
		 */
		const lst = [];

		this.loopConstStatements((name, cs) => {
			if (!cs.isSet() && used.has(cs.path)) {
				lst.push([name, cs.type]);
			}
		});

		return lst;
	}

	/**
	 * Returns the Intermediate Representation AST of the program.
	 * @param {boolean} optimized if `true`, returns the IR of the optimized program
	 * @param {boolean} annotate add internal type information annotations to the returned AST 
	 * @returns {string}
	 */
	dumpIR(optimized = false, annotate = false) {
		const ir = this.toIR(new ToIRContext(optimized));

		const irProgram = IRProgram.new(ir, this.#purpose, optimized);

		if (!annotate) {
			return new Source(irProgram.toString(), this.name).pretty();
		} else {
			return new Source(irProgram.annotate(), this.name).pretty();
		}
	}

	/**
	 * @param {boolean} simplify 
	 * @returns {UplcProgram}
	 */
	compile(simplify = false) {
		const ir = this.toIR(new ToIRContext(simplify));

		if (this.nPosParams > 0) {
			const irProgram = IRParametricProgram.new(ir, this.#purpose, this.nPosParams, simplify);

			// TODO: UplcParametricProgram
			return irProgram.toUplc();
		} else {
			const irProgram = IRProgram.new(ir, this.#purpose, simplify);
			
			//console.log(new Source(ir.generateSource()[0], "main").pretty());
			
			return irProgram.toUplc();
		}
	}
}

class RedeemerProgram extends Program {
	/**
	 * @param {ScriptPurpose} purpose
	 * @param {Module[]} modules 
	 * @param {ProgramConfig} config
	 */
	constructor(purpose, modules, config = DEFAULT_PROGRAM_CONFIG) {
		super(purpose, modules, config);
	}

	/**
	 * @type {number}
	 */
	get nPosParams() {
		return this.mainFunc.nArgs - (this.config.invertEntryPoint ? 0 : 2);
	}

	/**
	 * @internal
	 * @param {GlobalScope} scope
	 * @returns {TopScope}
	 */
	evalTypesInternal(scope) {
		const topScope = super.evalTypesInternal(scope);

		// check the 'main' function

		const main = this.mainFunc;
		const argTypeNames = main.argTypeNames;
		const argTypes = main.argTypes;
		const retType = main.retType;
		const nArgs = argTypes.length;

		if (this.config.allowPosParams) {
			if (nArgs < 2) {
				main.typeError("expected at least 2 args for main");
				return topScope;
			}
		} else {
			if (nArgs != 2) {
				main.typeError("expected 2 args for main");
				return topScope;
			}
		}
		
		for (let i = 0; i < nArgs; i++) {
			if (i == nArgs - 1) {
				if (argTypeNames[i] != "" && !(new ScriptContextType()).isBaseOf(argTypes[i])) {
					main.typeError(`illegal type for arg ${nArgs} in main, expected 'ScriptContext', got ${argTypes[i].toString()}`);
				}
			} else {
				if (argTypeNames[i] != "" && !(new DefaultTypeClass()).isImplementedBy(argTypes[i])) {
					main.typeError(`illegal ${i == nArgs - 2 ? "redeemer " : ""}argument type in main: '${argTypes[i].toString()}`);
				}
			}
		}

		if (!(BoolType.isBaseOf(retType))) {
			main.typeError(`illegal return type for main, expected 'Bool', got '${retType.toString()}'`);
		}

		return topScope;
	}

	/**
	 * @internal
	 * @param {ScriptTypes} validatorTypes
	 * @returns {TopScope}
	 */
	evalTypes(validatorTypes = {}) {
		const scope = GlobalScope.new(validatorTypes);

		return this.evalTypesInternal(scope);	
	}

	/**
	 * @internal
	 * @param {ToIRContext} ctx
	 * @returns {IR} 
	 */
	toIRInternal(ctx) {
		const outerArgNames = this.config.invertEntryPoint ? [] : ["redeemer", "ctx"];
		const nOuterArgs = outerArgNames.length;

		const nArgs = this.mainFunc.nArgs;
		const argTypeNames = this.mainFunc.argTypeNames;
		const argTypes = this.mainArgTypes;

		const innerArgs = argTypes.map((t, i) => {
			const name = (i >= (nArgs-nOuterArgs)) ? outerArgNames[i-(nArgs-nOuterArgs)] : `__PARAM_${i.toString()}`;

			// empty path
			if (argTypeNames[i] != "") {
				return new IR([
					new IR(`${assertNonEmpty(t.path)}__from_data`),
					new IR("("),
					new IR(name),
					new IR(")")
				]);
			} else {
				// unused arg, 0 is easier to optimize
				return new IR("0");
			}
		});

		let ir = new IR([
			new IR(`${TAB}${TAB}__core__ifThenElse`),
			new IR("(", this.mainRetExprSite),
			new IR(`\n${TAB}${TAB}${TAB}${this.mainPath}(`),
			new IR(innerArgs).join(", "),
			new IR(`),\n${TAB}${TAB}${TAB}() -> {()},\n${TAB}${TAB}${TAB}() -> {__helios__error("validation returned false")}\n${TAB}${TAB})`),
			new IR("(", this.mainRetExprSite),
			new IR(")")
		]);

		if (nOuterArgs > 0) {
			const outerArgs = outerArgNames.map((n) => new IR(n));

			ir = new IR([
				new IR(`${TAB}/*entry point*/\n${TAB}(`),
				new IR(outerArgs).join(", "),
				new IR(`) -> {\n`),
				ir,
				new IR(`\n${TAB}}`)
			])
		}

		return ir;
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `${this.purpose} ${this.name}\n${super.toString()}`;
	}
}

/**
 * Used by CLI
 * @internal
 */
export class DatumRedeemerProgram extends Program {
	/**
	 * @param {ScriptPurpose} purpose
	 * @param {Module[]} modules
	 * @param {ProgramConfig} config
	 */
	constructor(purpose, modules, config) {
		super(purpose, modules, config);
	}

	/**
	 * @type {number}
	 */
	get nPosParams() {
		return this.mainFunc.nArgs - (this.config.invertEntryPoint ? 0 : 3);
	}

	/**
	 * @type {DataType}
	 */
	get datumType() {
		return this.mainArgTypes[0]
	}

	/**
	 * @type {string}
	 */
	get datumTypeName() {
		return this.mainFunc.argTypeNames[0];
	}

	/**
	 * @internal
	 * @param {GlobalScope} scope 
	 * @returns {TopScope}
	 */
	evalTypesInternal(scope) {
		const topScope = super.evalTypesInternal(scope);

		// check the 'main' function

		const main = this.mainFunc;
		const argTypeNames = main.argTypeNames;
		const argTypes = main.argTypes;
		const retType = main.retType;
		const nArgs = main.nArgs;

		if (this.config.allowPosParams) {
			if (argTypes.length < 3) {
				main.typeError("expected at least 3 args for main");	
				return topScope;
			}
		} else {
			if (argTypes.length != 3) {
				main.typeError("expected 3 args for main");	
				return topScope;
			}
		}

		for (let i = 0; i < nArgs; i++) {
			if (i == nArgs - 1) {
				if (argTypeNames[i] != "" && !(new ScriptContextType()).isBaseOf(argTypes[i])) {
					main.typeError(`illegal type for arg ${nArgs} in main: expected 'ScriptContext', got '${argTypes[i].toString()}'`);
				}
			} else {
				if (argTypeNames[i] != "" && !(new DefaultTypeClass()).isImplementedBy(argTypes[i])) {
					main.typeError(`illegal type for arg ${i+1} in main ${i == nArgs - 2 ? "(datum) " : (i == nArgs - 3 ? "(redeemer) " : "")}: '${argTypes[i].toString()}`);
				}
			}
		}

		if (!(BoolType.isBaseOf(retType))) {
			main.typeError(`illegal return type for main, expected 'Bool', got '${retType.toString()}'`);
		}

		return topScope;
	}

	/**
	 * @internal
	 * @param {ScriptTypes} scriptTypes
	 * @returns {TopScope}
	 */
	evalTypes(scriptTypes) {
		const scope = GlobalScope.new(scriptTypes);

		return this.evalTypesInternal(scope);	
	}

	/**
	 * @internal
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIRInternal(ctx) {
		const outerArgNames = this.config.invertEntryPoint ? [] : ["datum", "redeemer", "ctx"];
		const nOuterArgs = outerArgNames.length;

		const nArgs = this.mainFunc.nArgs;
		const argTypeNames = this.mainFunc.argTypeNames;

		const innerArgs = this.mainArgTypes.map((t, i) => {
			const name = (i >= (nArgs-nOuterArgs)) ? outerArgNames[i-(nArgs-nOuterArgs)] : `__PARAM_${i.toString()}`;

			// empty path
			if (argTypeNames[i] != "") {
				return new IR([
					new IR(`${assertNonEmpty(t.path)}__from_data`),
					new IR("("),
					new IR(name),
					new IR(")")
				]);
			} else {
				// unused arg, 0 is easier to optimize
				return new IR("0");
			}
		});

		let ir = new IR([
			new IR(`${TAB}${TAB}__core__ifThenElse`),
			new IR("(", this.mainRetExprSite),
			new IR(`\n${TAB}${TAB}${TAB}${this.mainPath}(`),
			new IR(innerArgs).join(", "),
			new IR(`),\n${TAB}${TAB}${TAB}() -> {()},\n${TAB}${TAB}${TAB}() -> {__helios__error("validation returned false")}\n${TAB}${TAB})`),
			new IR("(", this.mainRetExprSite),
			new IR(")")
		]);

		if (nOuterArgs > 0) {
			const outerArgs = outerArgNames.map((n) => new IR(n));

			ir = new IR([
				new IR(`${TAB}/*entry point*/\n${TAB}(`),
				new IR(outerArgs).join(", "),
				new IR(`) -> {\n`),
				ir,
				new IR(`\n${TAB}}`)
			]);
		}

		return ir;
	}

	/**
	 * @internal
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	datumCheckToIR(ctx) {
		if (this.datumTypeName == "") {
			return new IR(`(data) -> {data}`);
		} else {
			const datumPath = this.datumType.path;

			const ir = new IR(`(data) -> {${datumPath}____to_data(${datumPath}__from_data(data))}`);

			return this.wrapEntryPoint(ctx, ir);
		}
	}

	/**
	 * Used by cli
	 * @internal
	 * @returns {UplcProgram}
	 */
	compileDatumCheck() {
		const ir = this.datumCheckToIR(new ToIRContext(false));

		const irProgram = IRProgram.new(ir, null, false);
			
		return irProgram.toUplc();
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `${this.purpose} ${this.name}\n${super.toString()}`;
	}
}

class GenericProgram extends Program {
	/**
	 * @param {ScriptPurpose} purpose 
	 * @param {Module[]} modules 
	 * @param {ProgramConfig} config
	 */
	constructor(purpose, modules, config) {
		super(purpose, modules, config);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return `${this.purpose} ${this.name}\n${super.toString()}`;
	}

	/**
	 * @internal
	 * @param {ScriptTypes} scriptTypes
	 * @returns {TopScope}
	 */
	evalTypes(scriptTypes) {
		const scope = GlobalScope.new(scriptTypes);

		const topScope = super.evalTypesInternal(scope);

		// check the 'main' function

		const main = this.mainFunc;
		const argTypeNames = main.argTypeNames;
		const argTypes = main.argTypes;
		const retType = main.retType;


		argTypeNames.forEach((argTypeName, i) => {
			if (argTypeName != "" && !(new DefaultTypeClass()).isImplementedBy(argTypes[i])) {
				main.typeError(`illegal argument type in main: '${argTypes[i].toString()}`);
			}
		});

		// TODO: support tuple return values ?
		if (!((new DefaultTypeClass()).isImplementedBy(retType))) {
			main.typeError(`illegal return type for main: '${retType.toString()}'`);
		}

		return topScope;
	}

	/**
	 * @internal
	 * @param {ToIRContext} ctx
	 * @returns {IR}
	 */
	toIRInternal(ctx) {
		const argTypeNames = this.mainFunc.argTypeNames;

		const innerArgs = this.mainArgTypes.map((t, i) => {
			// empty path
			if (argTypeNames[i] != "") {
				return new IR([
					new IR(`${assertNonEmpty(t.path)}__from_data`),
					new IR("("),
					new IR(`arg${i}`),
					new IR(")")
				]);
			} else {
				// unused arg, 0 is easier to optimize
				return new IR("0")
			}
		});

		let ir = new IR([
			new IR(`${this.mainPath}(`),
			new IR(innerArgs).join(", "),
			new IR(")"),
		]);

		const retType = assertDefined(this.mainFunc.retType.asDataType);

		ir = new IR([
			new IR(`${retType.path}____to_data`),
			new IR("("),
			ir,
			new IR(")")
		]);

		const outerArgs = this.mainFunc.argTypes.map((_, i) => new IR(`arg${i}`));

		ir = new IR([
			new IR(`${TAB}/*entry point*/\n${TAB}(`),
			new IR(outerArgs).join(", "),
			new IR(`) -> {\n${TAB}${TAB}`),
			ir,
			new IR(`\n${TAB}}`),
		]);

		return ir;
	}
}

class TestingProgram extends GenericProgram {
	/**
	 * @param {Module[]} modules 
	 */
	constructor(modules) {
		super("testing", modules, DEFAULT_PROGRAM_CONFIG);
	}
}

class SpendingProgram extends DatumRedeemerProgram {
	/**
	 * @param {Module[]} modules
	 * @param {ProgramConfig} config
	 */
	constructor(modules, config) {
		super("spending", modules, config);
	}
}

class MintingProgram extends RedeemerProgram {
	/**
	 * @param {Module[]} modules 
	 * @param {ProgramConfig} config
	 */
	constructor(modules, config = DEFAULT_PROGRAM_CONFIG) {
		super("minting", modules, config);
	}
}

class StakingProgram extends RedeemerProgram {
	/**
	 * @param {Module[]} modules 
	 * @param {ProgramConfig} config
	 */
	constructor(modules, config = DEFAULT_PROGRAM_CONFIG) {
		super("staking", modules, config);
	}
}

/**
 * @internal
 */
class EndpointProgram extends GenericProgram {
	/**
	 * @param {Module[]} modules 
	 */
	constructor(modules) {
		super("endpoint", modules, DEFAULT_PROGRAM_CONFIG);
	}

	/**
	 * @internal
	 * @param {ScriptTypes} scriptTypes
	 * @returns {TopScope}
	 */
	evalTypes(scriptTypes = {}) {
		const scope = GlobalScope.new(scriptTypes);

		const topScope = super.evalTypesInternal(scope);
		
		const main = this.mainFunc;
		const argTypes = main.argTypes;
		const argTypeNames = main.argTypeNames;
		const retType = main.retType;

		if (argTypeNames.length == 0) {
			main.typeError("expected at least argument 'ContractContext'");
			return topScope;
		}

		argTypeNames.forEach((argTypeName, i) => {
			if (i != argTypeNames.length -1 && argTypeName != "" && !(new DefaultTypeClass()).isImplementedBy(argTypes[i])) {
				main.typeError(`illegal argument type in main: '${argTypes[i].toString()}`);
			}
		});

		if (argTypeNames[argTypeNames.length-1] != "") {
			const lastArgType = argTypes[argTypes.length-1];
			if (!(lastArgType instanceof ContractContextType)) {
				main.typeError(`expected 'ContractContext' for arg ${argTypes.length}, got '${lastArgType.toString()}'`);
			}
		}
		
		// TODO: support tuple return values ?
		if (!((new DefaultTypeClass()).isImplementedBy(retType))) {
			main.typeError(`illegal return type for main: '${retType.toString()}'`);
		}
		
		return topScope;
	}
}


/////////////////////////////
// Section 34: Native scripts
/////////////////////////////

/**
 * @internal
 */
export class NativeContext {
    #firstValidSlot;
    #lastValidSlot;
    #keys;

    /**
     * 
     * @param {bigint | null} firstValidSlot 
     * @param {bigint | null} lastValidSlot 
     * @param {PubKeyHash[]} keys
     */
    constructor(firstValidSlot, lastValidSlot, keys) {
        this.#firstValidSlot = firstValidSlot;
        this.#lastValidSlot = lastValidSlot;
        this.#keys = keys;
    }

    /**
     * Used by NativeAfter
     * @param {bigint} slot 
     * @returns {boolean}
     */
    isAfter(slot) {
        if (this.#firstValidSlot !== null) {
            return this.#firstValidSlot >= slot;
        } else {
            console.error("Warning: tx validity time range start not set but checked in native script");
            return false;
        }
    }

    /**
     * 
     * @param {bigint} slot 
     * @returns {boolean}
     */
    isBefore(slot) {
        if (this.#lastValidSlot !== null) {
            return this.#lastValidSlot < slot;
        } else {
            console.error("Warning: tx validity time range end not set but checked in native script");
            return false;
        }
    }

    /**
     * 
     * @param {PubKeyHash} key
     * @returns {boolean}
     */
    isSignedBy(key) {
        return this.#keys.some((k => k.eq(key)));
    }
}

/**
 * Helios supports Cardano [native scripts](https://cips.cardano.org/cips/cip29/). 
 * See `Tx.attachScript()` for how `NativeScript` can be used when building a transaction.
 * 
 * NativeScript allows creating basic multi-signature and time-based validators.
 * This is a legacy technology, but can be cheaper than using Plutus.
 */
export class NativeScript extends CborData {
    #type;

    /**
     * @param {number} type 
     */
    constructor(type) {
        super();
        this.#type = type;
    }

    /**
     * @returns {number[]}
     */
    typeToCbor() {
        return Cbor.encodeInteger(BigInt(this.#type));
    }

    /**
     * @param {string | number[]} raw 
     * @returns {NativeScript}
     */
    static fromCbor(raw) {
        const bytes = (typeof raw == "string") ? hexToBytes(raw) : raw;

        if (bytes[0] == 0) {
            bytes.shift();
        }

        let type = -1;

        /**
         * @type {bigint}
         */
        let nOrSlot = -1n;

        /**
         * @type {NativeScript | null}
         */
        let script = null;

        Cbor.decodeTuple(bytes, (i, fieldBytes) => {
            if (i == 0) {
                type = Number(Cbor.decodeInteger(fieldBytes))
            } else {
                switch(type) {
                    case 0:
                        assert(i == 1);

                        script = new NativeSig(PubKeyHash.fromCbor(fieldBytes));
                        
                        break;
                    case 1:
                    case 2: {
                            assert(i == 1);

                            /**
                             * @type {NativeScript[]}
                             */
                            const children = [];

                            Cbor.decodeList(fieldBytes, (_, listBytes) => {
                                children.push(NativeScript.fromCbor(listBytes))
                            });

                            switch (type) {
                                case 1:
                                    script = new NativeAll(children);
                                    break;
                                case 2:
                                    script = new NativeAny(children);
                                    break;
                                default:
                                    throw new Error("unexpected");
                            }
                        }

                        break;
                    case 3:
                        if (i == 1) {
                            nOrSlot = Cbor.decodeInteger(fieldBytes);
                        } else {
                            assert(i == 2);

                            /**
                             * @type {NativeScript[]}
                             */
                            const children = [];

                            Cbor.decodeList(fieldBytes, (_, listBytes) => {
                                children.push(NativeScript.fromCbor(listBytes))
                            });

                            script = new NativeAtLeast(Number(nOrSlot), children);
                        }

                        break;
                    case 4:
                    case 5:
                        assert(i == 1);

                        nOrSlot = Cbor.decodeInteger(fieldBytes);

                        switch(type) {
                            case 4:
                                script = new NativeAfter(nOrSlot);
                                break;
                            case 5:
                                script = new NativeBefore(nOrSlot);
                                break;
                            default:
                                throw new Error("unexpected");
                        }

                        break;
                    default:
                        throw new Error("unexpected");
                }
            }
        });

        if (!script) {
            throw new Error("unable to deserialize native script");
        } else {
            return script;
        }
    }

    /**
     * @param {string | Object} json 
     * @returns {NativeScript}
     */
    static fromJson(json) {
        const obj = (typeof json == "string") ? JSON.parse(json) : json;

        const type = obj.type;

        if (!type) {
            throw new Error("invalid Native script");
        }

        switch (type) {
            case "sig": {
                const keyHash = obj.keyHash;

                if (!keyHash) {
                    throw new Error("invalid NativeKey script");
                }

                return new NativeSig(PubKeyHash.fromHex(keyHash));
            }
            case "all": {
                /**
                 * @type {Object[]}
                 */
                const scripts = obj.scripts;

                if (!scripts) {
                    throw new Error("invalid NativeAll script");
                }

                return new NativeAll(scripts.map(s => NativeScript.fromJson(s)));
            }
            case "any": {
                /**
                 * @type {Object[]}
                 */
                const scripts = obj.scripts;

                if (!scripts) {
                    throw new Error("invalid NativeAny script");
                }

                return new NativeAny(scripts.map(s => NativeScript.fromJson(s)));
            }
            case "atLeast": {
                const n = obj.required;

                if (typeof n != "number") {
                    throw new Error("invalid NativeAtLeast script");
                }

                /**
                 * @type {Object[]}
                 */
                const scripts = obj.scripts;

                if (!scripts) {
                    throw new Error("invalid NativeAtLeast script");
                }
    

                return new NativeAtLeast(n, scripts.map(s => NativeScript.fromJson(s)));
            }
            case "after": {
                const slot = obj.slot;

                if (typeof slot != "number") {
                    throw new Error("invalid NativeAfter script");
                }

                return new NativeAfter(BigInt(slot));
            }
            case "before": {
                const slot = obj.slot;

                if (typeof slot != "number") {
                    throw new Error("invalid NativeAfter script");
                }

                return new NativeBefore(BigInt(slot));
            }
            default:
                throw new Error(`unrecognized NativeScript type '${type}'`);
        }
    }

    /**
     * @returns {Object}
     */
    toJson() {
        throw new Error("not implemented");
    }

    /**
     * @internal
     * @param {NativeContext} context 
     * @returns {boolean}
     */
    eval(context) {
       throw new Error("not implemented");
    }

    /**
     * Calculates the blake2b-224 (28 bytes) hash of the NativeScript.
     * 
     * **Note**: a 0 byte is prepended before to the serialized CBOR representation, before calculating the hash.
     * @returns {number[]}
     */
    hash() {
        let innerBytes = this.toCbor();

		innerBytes.unshift(0);

		// used for both script addresses and minting policy hashes
		return Crypto.blake2b(innerBytes, 28);
    }

    /**
     * A `NativeScript` can be used both as a Validator and as a MintingPolicy
     * @type {ValidatorHash}
     */
    get validatorHash() {
        return new ValidatorHash(this.hash());
    }

    /**
     * A `NativeScript` can be used both as a Validator and as a MintingPolicy
     * @type {MintingPolicyHash}
     */
    get mintingPolicyHash() {
        return new MintingPolicyHash(this.hash());
    }
}

class NativeSig extends NativeScript {
    #pkh;

    /**
     * @param {PubKeyHash} pkh 
     */
    constructor(pkh) {
        super(0);
        this.#pkh = pkh;
    }

    /**
     * @returns {number[]}
     */
    toCbor() {
        return Cbor.encodeTuple([
            this.typeToCbor(),
            this.#pkh.toCbor()
        ]);
    }

    /**
     * @returns {Object}
     */
    toJson() {
        return {
            type: "sig",
            keyHash: this.#pkh.hex
        }
    }

    /**
     * @internal
     * @param {NativeContext} context 
     * @returns {boolean}
     */
    eval(context) {
        return context.isSignedBy(this.#pkh);
    }
}

class NativeAll extends NativeScript {
    #scripts;

    /**
     * @param {NativeScript[]} scripts 
     */
    constructor(scripts) {
        super(1);
        assert(scripts.length > 0);
        this.#scripts = scripts;
    }

    /**
     * @returns {number[]}
     */
    toCbor() {
        return Cbor.encodeTuple([
            this.typeToCbor(),
            Cbor.encodeDefList(this.#scripts)
        ]);
    }

    /**
     * @returns {Object}
     */
    toJson() {
        return {
            type: "all",
            scripts: this.#scripts.map(s => s.toJson())
        }
    }

    /**
     * @internal
     * @param {NativeContext} context 
     * @returns {boolean}
     */
    eval(context) {
        return this.#scripts.every(s => s.eval(context));
    }
}

class NativeAny extends NativeScript {
    #scripts;

    /**
     * @param {NativeScript[]} scripts
     */
    constructor(scripts) {
        super(2);
        assert(scripts.length > 0);
        this.#scripts = scripts;
    }

    /**
     * @returns {number[]}
     */
    toCbor() {
        return Cbor.encodeTuple([
            this.typeToCbor(),
            Cbor.encodeDefList(this.#scripts)
        ]);
    }

    /**
     * @returns {Object}
     */
    toJson() {
        return {
            type: "any",
            scripts: this.#scripts.map(s => s.toJson())
        }
    }

    /**
     * @internal
     * @param {NativeContext} context
     * @returns {boolean}
     */
    eval(context) {
        return this.#scripts.some(s => s.eval(context));
    }
}

class NativeAtLeast extends NativeScript {
    #required;
    #scripts;

    /**
     * @param {number} required
     * @param {NativeScript[]} scripts
     */
    constructor(required, scripts) {
        super(3);
        assert(scripts.length >= required);
        this.#required = required;
        this.#scripts = scripts;
    }

    /**
     * @returns {number[]}
     */
    toCbor() {
        return Cbor.encodeTuple([
            this.typeToCbor(),
            Cbor.encodeInteger(BigInt(this.#required)),
            Cbor.encodeDefList(this.#scripts)
        ]);
    }

    /**
     * @returns {Object}
     */
    toJson() {
        return {
            type: "atLeast",
            required: this.#required,
            scripts: this.#scripts.map(s => s.toJson())
        };
    }

    /**
     * @internal
     * @param {NativeContext} context
     * @returns {boolean}
     */
    eval(context) {
        const count = this.#scripts.reduce((prev, s) => prev + (s.eval(context) ? 1 : 0), 0);

        return count >= this.#required;
    }
}

class NativeAfter extends NativeScript {
    #slot;

    /**
     * @param {bigint} slot
     */
    constructor(slot) {
        super(4);
        this.#slot = slot;
    }

    /**
     * @returns {number[]}
     */
    toCbor() {
        return Cbor.encodeTuple([
            this.typeToCbor(),
            Cbor.encodeInteger(this.#slot)
        ])
    }

    /**
     * @returns {Object}
     */
    toJson() {
        const slot = Number(this.#slot);

        if (BigInt(slot) != this.#slot) {
            console.error("Warning: slot overflow (not representable by Number in Native script Json)");
        }

        return {
            type: "after",
            slot: slot
        };
    }

    /**
     * @internal
     * @param {NativeContext} context
     * @returns {boolean}
     */
    eval(context) {
        return context.isAfter(this.#slot);
    }
}

class NativeBefore extends NativeScript {
    #slot;

    /**
     * @param {bigint} slot
     */
    constructor(slot) {
        super(5);
        this.#slot = slot;
    }

    /**
     * @returns {number[]}
     */
    toCbor() {
        return Cbor.encodeTuple([
            this.typeToCbor(),
            Cbor.encodeInteger(this.#slot)
        ])
    }

    /**
     * @returns {Object}
     */
    toJson() {
        const slot = Number(this.#slot);

        if (BigInt(slot) != this.#slot) {
            console.error("Warning: slot overflow (not representable by Number in Native script Json)");
        }

        return {
            type: "before",
            slot: slot
        };
    }

    /**
     * @internal
     * @param {NativeContext} context
     * @returns {boolean}
     */
    eval(context) {
        return context.isBefore(this.#slot);
    }
}


///////////////////////
// Section 35: Tx types
///////////////////////

/**
 * Represents a Cardano transaction. Can also be used as a transaction builder.
 */
export class Tx extends CborData {
	/**
	 * @type {TxBody}
	 */
	#body;

	/**
	 * @type {TxWitnesses}
	 */
	#witnesses;

	/**
	 * @type {boolean}
	 */
	#valid;

	/** 
	 * @type {null | TxMetadata} 
	 */
	#metadata;

	// the following field(s) aren't used by the serialization (only for building)
	/**
	 * Upon finalization the slot is calculated and stored in the body
	 * @type {null | bigint | Date} 
	 */
	#validTo;

	/**
	 * Upon finalization the slot is calculated and stored in the body 
	 *  @type {null | bigint | Date} 
	 */
	#validFrom;

	/**
	 * Use `Tx.new()` instead of this constructor for creating a new Tx builder.
	 * @param {TxBody} body
	 * @param {TxWitnesses} witnesses
	 * @param {boolean} valid
	 * @param {null | TxMetadata} metadata
	 * @param {null | bigint | Date} validTo
	 * @param {null | bigint | Date} validFrom
	 */
	constructor(
		body = new TxBody(), 
		witnesses = new TxWitnesses(), 
		valid = false, 
		metadata = null, 
		validTo = null,
		validFrom = null
	) {
		super();
		this.#body = body;
		this.#witnesses = witnesses;
		this.#valid = valid; // building is only possible if valid==false
		this.#metadata = metadata;
		this.#validTo = validTo;
		this.#validFrom = validFrom;
	}

	/**
	 * Create a new Tx builder.
	 * @returns {Tx}
	 */
	static new() {
		return new Tx();
	}

	/**
	 * @type {TxBody}
	 */
	get body() {
		return this.#body;
	}

	/**
	 * @type {number[]}
	 */
	get bodyHash() {
		return this.#body.hash();
	}

	/**
	 * @type {TxWitnesses}
	 */
	get witnesses() {
		return this.#witnesses;
	}

	/**
	 * Used by emulator to check if tx is valid.
	 * @param {bigint} slot
	 * @returns {boolean}
	 */
	isValid(slot) {
		if (!this.#valid) {
			return false;
		} else {
			return this.#body.isValid(slot);
		}
	}

	/** 
	 * Serialize a transaction.
	 * @returns {number[]}
	 */
	toCbor() {
		return Cbor.encodeTuple([
			this.#body.toCbor(),
			this.#witnesses.toCbor(),
			Cbor.encodeBool(this.#valid),
			this.#metadata === null ? Cbor.encodeNull() : this.#metadata.toCbor(),
		]);
	}

	/**
	 * Creates a new Tx without the metadata for client-side signing where the client can't know the metadata before tx-submission.
	 * @returns {Tx}
	 */
	withoutMetadata() {
		return new Tx(
			this.#body,
			this.#witnesses,
			this.#valid,
			null, // TODO: try null first, other wise try an empty TxMetadata instance
			this.#validTo,
			this.#validFrom
		)
	}

	/**
	 * Deserialize a CBOR encoded Cardano transaction (input is either an array of bytes, or a hex string).
	 * @param {number[] | string} raw
	 * @returns {Tx}
	 */
	static fromCbor(raw) {
		let bytes = (typeof raw == "string") ? hexToBytes(raw) : raw;

		bytes = bytes.slice();

		let tx = new Tx();

		let n = Cbor.decodeTuple(bytes, (i, fieldBytes) => {
			switch(i) {
				case 0:
					tx.#body = TxBody.fromCbor(fieldBytes);
					break;
				case 1:
					tx.#witnesses = TxWitnesses.fromCbor(fieldBytes);
					break;
				case 2:
					tx.#valid = Cbor.decodeBool(fieldBytes);
					break;
				case 3:
					if (Cbor.isNull(fieldBytes)) {
						Cbor.decodeNull(fieldBytes);

						tx.#metadata = null;
					} else {
						tx.#metadata = TxMetadata.fromCbor(fieldBytes);
					}
					break;
				default:
					throw new Error("bad tuple size");
			}
		});

		assert(n == 4);
		assert(bytes.length == 0);

		return tx;
	}

	/**
	 * Used by bundler for macro finalization
	 * @param {UplcData} data
	 * @param {NetworkParams} networkParams
	 * @param {Address} changeAddress
	 * @param {TxInput[]} spareUtxos
	 * @param {{[name: string]: (UplcProgram | (() => UplcProgram))}} scripts UplcPrograms can be lazy
	 * @returns {Promise<Tx>}
	 */
	static async finalizeUplcData(data, networkParams, changeAddress, spareUtxos, scripts) {
		const fields = data.fields;

		assert(fields.length == 12);

		const inputs = fields[0].list.map(d => TxInput.fromUplcData(d));
		const refInputs = fields[1].list.map(d => TxInput.fromUplcData(d));
		const outputs = fields[2].list.map(d => TxOutput.fromUplcData(d));
		//txBody.#fee = Value.fromUplcData(fields[3]).lovelace.value;
		const minted = Value.fromUplcData(fields[4]).assets;
		//txBody.#dcerts = fields[5].list.map(d => DCert.fromUplcData(d));
		//txBody.#withdrawals = new Map(fields[6].map.map(([key, value]) => {
		//	return [Address.fromUplcData(key), value.int];
		//}));
		// validity
		const signers = fields[8].list.map(d => PubKeyHash.fromUplcData(d));
		const redeemers = fields[9].map.map(([key, value]) => {
			if (key.index == 1) {
				assert(key.fields.length == 1);
				const outputId = TxOutputId.fromUplcData(key.fields[0]);
				const i = inputs.findIndex(input => input.txId.eq(outputId.txId) && input.utxoIdx == outputId.utxoIdx);
				assert(i != -1);
				return new SpendingRedeemer(inputs[i], i, value);
			} else if (key.index == 0) {
				assert(key.fields.length == 1);
				const mph = MintingPolicyHash.fromUplcData(key.fields[0]);
				const i = minted.mintingPolicies.findIndex(m => m.eq(mph));
				assert(i != -1);
				return new MintingRedeemer(mph, i, value);
			} else {
				throw new Error("unhandled redeemer constr index");
			}
		});

		// build the tx from scratch
		const tx = new Tx();

		// TODO: automatically added any available scripts
		inputs.forEach((input, i) => {
			const redeemer = redeemers.find(r => (r instanceof SpendingRedeemer) && r.inputIndex == i) ?? null;

			if (redeemer instanceof SpendingRedeemer) {
				tx.addInput(input, redeemer.data);

				if (input.address.validatorHash) {
					if  (input.address.validatorHash.hex in scripts) {
						const uplcProgram = scripts[input.address.validatorHash.hex];

						if (uplcProgram instanceof UplcProgram) {
							tx.attachScript(uplcProgram);
						} else {
							tx.attachScript(uplcProgram());
						}
					} else {
						throw new Error(`script for SpendingRedeemer (vh:${input.address.validatorHash.hex}) not found in [${Object.keys(scripts).join(", ")}]`);
					}
				} else {
					throw new Error("unexpected (expected a validator address");
				}
			} else {
				assert(redeemer === null);
				tx.addInput(input);
			}
		});

		refInputs.forEach(refInput => {
			tx.addRefInput(refInput);
		});

		// filter out spareUtxos that are already used as inputs
		spareUtxos = spareUtxos.filter(utxo => {
			return inputs.every(input => !input.eq(utxo)) && 
				refInputs.every(input => !input.eq(utxo));
		});

		outputs.forEach(output => {
			tx.addOutput(output);
		});

		minted.mintingPolicies.forEach((mph, i) => {
			const redeemer = redeemers.find(r => (r instanceof MintingRedeemer) && r.mphIndex == i) ?? null;

			if (redeemer instanceof MintingRedeemer) {
				tx.mintTokens(mph, minted.getTokens(mph), redeemer.data);

				if (mph.hex in scripts) {
					const uplcProgram = scripts[mph.hex];

					if (uplcProgram instanceof UplcProgram) {
						tx.attachScript(uplcProgram);
					} else {
						tx.attachScript(uplcProgram());
					}
				} else {
					throw new Error(`policy for mph ${mph.hex} not found in ${Object.keys(scripts)}`);
				}
			} else {
				throw new Error("missing MintingRedeemer");
			}
		});

		signers.forEach(pk => {
			tx.addSigner(pk);
		});

		return await tx.finalize(networkParams, changeAddress, spareUtxos);
	}

	/**
	 * @param {NetworkParams} networkParams
	 * @returns {UplcData}
	 */
	toTxData(networkParams) {
		return this.#body.toTxData(networkParams, this.witnesses.redeemers, this.witnesses.datums, this.id());
	}

	/**
	 * A serialized tx throws away input information
	 * This must be refetched from the network if the tx needs to be analyzed
	 * @param {(id: TxOutputId) => Promise<TxOutput>} fn
	 */
	async completeInputData(fn) {
		await this.#body.completeInputData(fn, this.#witnesses);
	}

	/**
	 * @param {null | NetworkParams} params If specified: dump all the runtime details of each redeemer (datum, redeemer, scriptContext)
	 * @returns {Object}
	 */
	dump(params = null) {
		return {
			body: this.#body.dump(),
			witnesses: this.#witnesses.dump(params, this.#body),
			metadata: this.#metadata !== null ? this.#metadata.dump() : null,
			valid: this.#valid,
			id: this.#valid ? this.id().toString() : "invalid"
		};
	}

	/**
	 * Set the start of the valid time range by specifying either a Date or a slot.
	 * 
	 * Mutates the transaction.
	 * Only available during building the transaction. 
	 * Returns the transaction instance so build methods can be chained.
	 * 
	 * > **Note**: since Helios v0.13.29 this is set automatically if any of the Helios validator scripts call `tx.time_range`.
	 * @param {bigint | Date } slotOrTime
	 * @returns {Tx}
	 */
	validFrom(slotOrTime) {
		assert(!this.#valid);

		this.#validFrom = slotOrTime;

		return this;
	}

	/**
	 * Set the end of the valid time range by specifying either a Date or a slot. 
	 * 
	 * Mutates the transaction.
	 * Only available during transaction building. 
	 * Returns the transaction instance so build methods can be chained.
	 * 
	 * > **Note**: since Helios v0.13.29 this is set automatically if any of the Helios validator scripts call `tx.time_range`.
	 * @param {bigint | Date } slotOrTime
	 * @returns {Tx}
	 */
	validTo(slotOrTime) {
		assert(!this.#valid);

		this.#validTo = slotOrTime;

		return this;
	}

	/**
	 * Mint a list of tokens associated with a given `MintingPolicyHash`.
	 * Throws an error if the given `MintingPolicyHash` was already used in a previous call to `mintTokens()`.
	 * The token names can either by a list of bytes or a hexadecimal string.
	 * 
	 * Mutates the transaction. 
	 * Only available during transaction building the transaction.
	 * Returns the transaction instance so build methods can be chained.
	 * 
	 * Also throws an error if the redeemer is `null`, and the minting policy isn't a known `NativeScript`.
	 * @param {MintingPolicyHash | MintingPolicyHashProps} mph 
	 * @param {[ByteArray | ByteArrayProps, HInt | HIntProps][]} tokens - list of pairs of [tokenName, quantity], tokenName can be list of bytes or hex-string
	 * @param {UplcDataValue | UplcData | null} redeemer
	 * @returns {Tx}
	 */
	mintTokens(mph, tokens, redeemer) {
		const mph_ = MintingPolicyHash.fromProps(mph);

		assert(!this.#valid);

		this.#body.addMint(mph_, tokens);

		if (!redeemer) {
			if (!this.#witnesses.isNativeScript(mph_)) {
				throw new Error("no redeemer specified for minted tokens (hint: if this policy is a NativeScript, attach that script before calling tx.mintTokens())");
			}
		} else {
			this.#witnesses.addMintingRedeemer(mph_, UplcDataValue.unwrap(redeemer));
		}
		

		return this;
	}

	/**
	 * Add a UTxO instance as an input to the transaction being built.
	 * Throws an error if the UTxO is locked at a script address but a redeemer isn't specified (unless the script is a known `NativeScript`).
	 * 
	 * Mutates the transaction. 
	 * Only available during transaction building.
	 * Returns the transaction instance so build methods can be chained.
	 * @param {TxInput} input
	 * @param {null | UplcDataValue | UplcData | HeliosData} rawRedeemer
	 * @returns {Tx}
	 */
	addInput(input, rawRedeemer = null) {
		assert(!this.#valid);

		if (input.origOutput === null) {
			throw new Error("TxInput.origOutput must be set when building transaction");
		} else {
			void this.#body.addInput(input);

			if (rawRedeemer !== null) {
				assert(input.origOutput.address.validatorHash !== null, "input isn't locked by a script");

				const redeemer = rawRedeemer instanceof HeliosData ? rawRedeemer._toUplcData() : UplcDataValue.unwrap(rawRedeemer);

				this.#witnesses.addSpendingRedeemer(input, redeemer);

				if (input.origOutput.datum === null) {
					throw new Error("expected non-null datum");
				} else {
					let datum = input.origOutput.datum;

					if (datum instanceof HashedDatum) {
						let datumData = datum.data;
						if (datumData === null) {
							throw new Error("expected non-null datum data");
						} else {
							this.#witnesses.addDatumData(datumData);
						}
					}
				}
			} else {
				if (input.origOutput.address.pubKeyHash === null) {
					if (!this.#witnesses.isNativeScript(assertDefined(input.origOutput.address.validatorHash))) {
						throw new Error("input is locked by a script, but redeemer isn't specified (hint: if this is a NativeScript, attach that script before calling tx.addInput())");
					}
				}
			}
		}

		return this;
	}

	/**
	 * Add multiple UTxO instances as inputs to the transaction being built. 
	 * Throws an error if the UTxOs are locked at a script address but a redeemer isn't specified (unless the script is a known `NativeScript`).
	 * 
	 * Mutates the transaction.
	 * Only available during transaction building. Returns the transaction instance so build methods can be chained.
	 * @param {TxInput[]} inputs
	 * @param {?(UplcDataValue | UplcData | HeliosData)} redeemer
	 * @returns {Tx}
	 */
	addInputs(inputs, redeemer = null) {
		for (let input of inputs) {
			this.addInput(input, redeemer);
		}

		return this;
	}

	/**
	 * Add a `TxInput` instance as a reference input to the transaction being built.
	 * Any associated reference script, as a `UplcProgram` instance, must also be included in the transaction at this point (so the that the execution budget can be calculated correctly).
	 * 
	 * Mutates the transaction.
	 * Only available during transaction building.
	 * Returns the transaction instance so build methods can be chained.
	 * @param {TxInput} input
	 * @param {null | UplcProgram} refScript
	 * @returns {Tx}
	 */
	addRefInput(input, refScript = null) {
		assert(!this.#valid);

		this.#body.addRefInput(input);

		if (refScript !== null) {
			this.#witnesses.attachPlutusScript(refScript, true);
		}

		return this;
	}

	/**
	 * Add multiple `TxInput` instances as reference inputs to the transaction being built.
	 * 
	 * Mutates the transaction.
	 * Only available during transaction building.
	 * Returns the transaction instance so build methods can be chained.
	 * @param {TxInput[]} inputs
	 * @returns {Tx}
	 */
	addRefInputs(inputs) {
		for (let input of inputs) {
			const refScript = input.origOutput.refScript;
			this.addRefInput(input, refScript);
		}

		return this;
	}

	/**
	 * Add a `TxOutput` instance to the transaction being built.
	 * 
	 * Mutates the transaction.
	 * Only available during transaction building.
	 * Returns the transaction instance so build methods can be chained.
	 * @param {TxOutput} output 
	 * @returns {Tx}
	 */
	addOutput(output) {
		assert(!this.#valid);
		
		// min lovelace isn't checked here but during finalize()
		this.#body.addOutput(output);

		return this;
	}

	/**
	 * Add multiple `TxOutput` instances at once.
	 * 
	 * Mutates the transaction.
	 * Only available during transaction building.
	 * Returns the transaction instance so build methods can be chained.
	 * @param {TxOutput[]} outputs 
	 * @returns {Tx}
	 */
	addOutputs(outputs) {
		for (let output of outputs) {
			this.addOutput(output);
		}

		return this;
	}

	/**
	 * Add a signatory `PubKeyHash` to the transaction being built.
	 * The added entry becomes available in the `tx.signatories` field in the Helios script.
	 * 
	 * Mutates the transaction.
	 * Only available during transaction building.
	 * Returns the transaction instance so build methods can be chained.
	 * @param {PubKeyHash} hash
	 * @returns {Tx}
	 */
	addSigner(hash) {
		assert(!this.#valid);

		this.#body.addSigner(hash);

		return this;
	}

	/**
	 * Add a `DCert` to the transactions being built. `DCert` contains information about a staking-related action.
	 * 
	 * TODO: implement all DCert (de)serialization methods.
	 * 
	 * Returns the transaction instance so build methods can be chained.
	 * @param {DCert} dcert
	 * @returns {Tx}
	 */
	addDCert(dcert) {
		this.#body.addDCert(dcert);

		return this;
	}

	/**
	 * Attaches a script witness to the transaction being built.
	 * The script witness can be either a `UplcProgram` or a legacy `NativeScript`.
	 * A `UplcProgram` instance can be created by compiling a Helios `Program`.
	 * A legacy `NativeScript` instance can be created by deserializing its original CBOR representation.
	 * 
	 * Throws an error if script has already been added.
	 * Throws an error if the script isn't used upon finalization.
	 * 
	 * Mutates the transaction. 
	 * Only available during transaction building.
	 * Returns the transaction instance so build methods can be chained.
	 * 
	 * > **Note**: a `NativeScript` must be attached before associated inputs are added or tokens are minted.
	 * @param {UplcProgram | NativeScript} program
	 * @returns {Tx}
	 */
	attachScript(program) {
		assert(!this.#valid);

		if (program instanceof NativeScript) { 
			this.#witnesses.attachNativeScript(program);
		} else {
			this.#witnesses.attachPlutusScript(program);
		}

		return this;
	}

	/**
	 * Add a UTxO instance as collateral to the transaction being built.
	 * Usually adding only one collateral input is enough.
	 * The number of collateral inputs must be greater than 0 if script witnesses are used in the transaction,
	 * and must be less than the limit defined in the `NetworkParams`.
	 * 
	 * Mutates the transaction. 
	 * Only available during transaction building. 
	 * Returns the transaction instance so build methods can be chained.
	 * @param {TxInput} input 
	 * @returns {Tx}
	 */
	addCollateral(input) {
		assert(!this.#valid);

		this.#body.addCollateral(input);

		return this;
	}

	/**
	 * Calculates tx fee (including script execution)
	 * Shouldn't be used directly
	 * @internal
	 * @param {NetworkParams} networkParams
	 * @returns {bigint}
	 */
	estimateFee(networkParams) {
		let [a, b] = networkParams.txFeeParams;

		if (!this.#valid) {
			// add dummy signatures
			let nUniquePubKeyHashes = this.#body.countUniqueSigners();
			
			this.#witnesses.addDummySignatures(nUniquePubKeyHashes);
		}

		let size = this.toCbor().length;

		if (!this.#valid) {
			// clean up the dummy signatures
			this.#witnesses.removeDummySignatures();
		}

		let sizeFee = BigInt(a) + BigInt(size)*BigInt(b);

		let exFee = this.#witnesses.estimateFee(networkParams);

		return sizeFee + exFee;
	}

	/**
	 * Iterates until fee is exact
	 * Shouldn't be used directly
	 * @internal
	 * @param {NetworkParams} networkParams
	 * @param {bigint} fee
	 * @returns {bigint}
	 */
	setFee(networkParams, fee) {
		let oldFee = this.#body.fee;

		while (oldFee != fee) {
			this.#body.setFee(fee);

			oldFee = fee;

			fee = this.estimateFee(networkParams);
		}

		return fee;
	}

	/**
	 * Checks that all necessary scripts are included, and that all included scripts are used
	 * Shouldn't be used directly
	 * @internal
	 */
	checkScripts() {
		let scripts = this.#witnesses.scripts;

		/**
		 * @type {Set<string>}
		 */
		const currentScripts = new Set();

		scripts.forEach(script => {
			currentScripts.add(bytesToHex(script.hash()))
		});

		/** 
		 * @type {Map<string, number>} 
		 */
		let wantedScripts = new Map();

		this.#body.collectScriptHashes(wantedScripts);

		if (wantedScripts.size < scripts.length) {
			throw new Error("too many scripts included, not all are needed");
		} else if (wantedScripts.size > scripts.length) {
			wantedScripts.forEach((value, key) => {
				if (!currentScripts.has(key)) {
					if (value >= 0) {
						console.error(JSON.stringify(this.dump(), null, "  "));
						throw new Error(`missing script for input ${value}`);
					} else if (value < 0) {
						console.error(JSON.stringify(this.dump(), null, "  "));
						throw new Error(`missing script for minting policy ${-value-1}`);
					}
				}
			});
		}

		currentScripts.forEach((key) => {
			if (!wantedScripts.has(key)) {
				console.log(wantedScripts, currentScripts)
				throw new Error("detected unused script");
			}
		});
	}

	/**
	 * @internal
	 * @param {NetworkParams} networkParams 
	 * @param {Address} changeAddress
	 * @returns {Promise<void>}
	 */
	async executeRedeemers(networkParams, changeAddress) {
		await this.#witnesses.executeScripts(networkParams, this.#body, changeAddress);
	}

	/**
	 * @internal
	 * @param {NetworkParams} networkParams 
	 * @returns {Promise<void>}
	 */
	async checkExecutionBudgets(networkParams) {
		await this.#witnesses.checkExecutionBudgets(networkParams, this.#body);
	}

	/**
	 * @internal
	 * @param {Address} changeAddress 
	 */
	balanceAssets(changeAddress) {
		const inputAssets = this.#body.sumInputAndMintedAssets();

		const outputAssets = this.#body.sumOutputAssets();

		if (inputAssets.eq(outputAssets)) {
			return;
		} else if (outputAssets.ge(inputAssets)) {
			throw new Error("not enough input assets");
		} else {
			const diff = inputAssets.sub(outputAssets);

			if (config.MAX_ASSETS_PER_CHANGE_OUTPUT) {
				const maxAssetsPerOutput = config.MAX_ASSETS_PER_CHANGE_OUTPUT;

				let changeAssets = new Assets();
				let tokensAdded = 0;

				diff.mintingPolicies.forEach((mph) => {
					const tokens = diff.getTokens(mph);
					tokens.forEach(([token, quantity], i) => {
						changeAssets.addComponent(mph, token, quantity);
						tokensAdded += 1;
						if (tokensAdded == maxAssetsPerOutput) {
							this.#body.addOutput(new TxOutput(changeAddress, new Value(0n, changeAssets)));
							changeAssets = new Assets();
							tokensAdded = 0;
						}
					});
				});

				// If we are here and have No assets, they we're done
				if (!changeAssets.isZero()) {
					this.#body.addOutput(new TxOutput(changeAddress, new Value(0n, changeAssets)));
				}
			} else {
				const changeOutput = new TxOutput(changeAddress, new Value(0n, diff));
	
				this.#body.addOutput(changeOutput);
			}
		}
	}

	/**
	 * Calculate the base fee which will be multiplied by the required min collateral percentage 
	 * @internal
	 * @param {NetworkParams} networkParams 
	 * @param {Address} changeAddress 
	 * @param {TxInput[]} spareUtxos 
	 */
	estimateCollateralBaseFee(networkParams, changeAddress, spareUtxos) {
		assert(config.N_DUMMY_INPUTS == 1 || config.N_DUMMY_INPUTS == 2, "expected N_DUMMY_INPUTs == 1 or N_DUMMY_INPUTS == 2");

		// create the collateral return output (might not actually be added if there isn't enough lovelace)
		const dummyOutput = new TxOutput(changeAddress, new Value(0n));
		dummyOutput.correctLovelace(networkParams);

		// some dummy UTxOs on to be able to correctly calculate the collateral (assuming it uses full body fee)
		const dummyCollateral = spareUtxos.map(spare => spare).concat(this.#body.inputs).slice(0, 3);
		dummyCollateral.forEach(input => {
			this.#body.collateral.push(input);
		});

		const dummyInputs = dummyCollateral.slice(0, config.N_DUMMY_INPUTS);

		this.#body.setCollateralReturn(dummyOutput);
		dummyInputs.forEach(dummyInput => this.#body.addInput(dummyInput, false));
		this.#body.addOutput(dummyOutput);

		const baseFee = this.estimateFee(networkParams);

		// remove the dummy inputs and outputs
		while(this.#body.collateral.length) {
			this.#body.collateral.pop();
		}
		this.#body.setCollateralReturn(null);
		dummyInputs.forEach(dummyInput => this.#body.removeInput(dummyInput));
		this.#body.removeOutput(dummyOutput);

		return baseFee;
	}
	
	/**
	 * @internal
	 * @param {NetworkParams} networkParams
	 * @param {Address} changeAddress
	 * @param {TxInput[]} spareUtxos
	 */
	balanceCollateral(networkParams, changeAddress, spareUtxos) {
		// don't do this step if collateral was already added explicitly
		if (this.#body.collateral.length > 0 || !this.isSmart()) {
			return;
		}

		const baseFee = this.estimateCollateralBaseFee(networkParams, changeAddress, spareUtxos);

		const minCollateral = ((baseFee*BigInt(networkParams.minCollateralPct)) + 100n)/100n; // integer division that rounds up

		let collateral = 0n;
		/**
		 * @type {TxInput[]}
		 */
		const collateralInputs = [];

		/**
		 * @param {TxInput[]} inputs 
		 */
		function addCollateralInputs(inputs) {
			// first try using the UTxOs that already form the inputs, but are locked at script
			const cleanInputs = inputs.filter(utxo => (!utxo.address.validatorHash) && utxo.value.assets.isZero()).sort((a, b) => Number(a.value.lovelace - b.value.lovelace));

			for (let input of cleanInputs) {
				if (collateral > minCollateral) {
					break;
				}

				while (collateralInputs.length >= networkParams.maxCollateralInputs) {
					collateralInputs.shift();
				}
	
				collateralInputs.push(input);
				collateral += input.value.lovelace;
			}
		}
		
		addCollateralInputs(this.#body.inputs.slice());

		addCollateralInputs(spareUtxos.map(utxo => utxo));

		// create the collateral return output if there is enough lovelace
		const changeOutput = new TxOutput(changeAddress, new Value(0n));
		changeOutput.correctLovelace(networkParams);

		if (collateral < minCollateral) {
			throw new Error("unable to find enough collateral input");
		} else {
			if (collateral > minCollateral + changeOutput.value.lovelace) {
				changeOutput.setValue(new Value(0n));

				changeOutput.correctLovelace(networkParams);

				if (collateral > minCollateral + changeOutput.value.lovelace) {
					changeOutput.setValue(new Value(collateral - minCollateral));
					this.#body.setCollateralReturn(changeOutput);
				} else {
					console.log(`not setting collateral return: collateral input too low (${collateral})`);
				}
			}
		}

		collateralInputs.forEach(utxo => {
			this.#body.addCollateral(utxo);
		});
	}

	/**
	 * Calculates fee and balances transaction by sending an output back to changeAddress
	 * First assumes that change output isn't needed, and if that assumption doesn't result in a balanced transaction the change output is created.
	 * Iteratively increments the fee because the fee increase the tx size which in turn increases the fee (always converges within two steps though).
	 * Throws error if transaction can't be balanced.
	 * Shouldn't be used directly
	 * @internal
	 * @param {NetworkParams} networkParams 
	 * @param {Address} changeAddress
	 * @param {TxInput[]} spareUtxos - used when there are yet enough inputs to cover everything (eg. due to min output lovelace requirements, or fees)
	 * @returns {TxOutput} - changeOutput so the fee can be mutated furthers
	 */
	balanceLovelace(networkParams, changeAddress, spareUtxos) {
		// don't include the changeOutput in this value
		let nonChangeOutputValue = this.#body.sumOutputValue();

		// assume a change output is always needed
		const changeOutput = new TxOutput(changeAddress, new Value(0n));
		
		changeOutput.correctLovelace(networkParams);

		this.#body.addOutput(changeOutput);
		
		const minLovelace = changeOutput.value.lovelace;

		let fee = networkParams.maxTxFee;

		this.#body.setFee(fee);
		
		let inputValue = this.#body.sumInputAndMintedValue();

		let feeValue = new Value(fee);

		nonChangeOutputValue = feeValue.add(nonChangeOutputValue);

		// stake certificates
		const stakeAddrDeposit = new Value(networkParams.stakeAddressDeposit);
		this.#body.dcerts.forEach(dcert => {
			// in case of stake registrations, count stake key deposits as additional output ADA
			if (dcert.certType == 0) nonChangeOutputValue = nonChangeOutputValue.add(stakeAddrDeposit);
			// in case of stake de-registrations, count stake key deposits as additional input ADA
			if (dcert.certType == 1) inputValue = inputValue.add(stakeAddrDeposit);
		});

		// this is quite restrictive, but we really don't want to touch UTxOs containing assets just for balancing purposes
		const spareAssetUTxOs = spareUtxos.some(utxo => !utxo.value.assets.isZero());
		spareUtxos = spareUtxos.filter(utxo => utxo.value.assets.isZero());
		
		// use some spareUtxos if the inputValue doesn't cover the outputs and fees
		const totalOutputValue = nonChangeOutputValue.add(changeOutput.value);
		while (!inputValue.ge(totalOutputValue)) {
			let spare = spareUtxos.pop();

			if (spare === undefined) {
				if (spareAssetUTxOs) {
					throw new Error(`UTxOs too fragmented`);
				} else {
					throw new Error(`need ${totalOutputValue.lovelace} lovelace, but only have ${inputValue.lovelace}`);
				}
			} else {
				this.#body.addInput(spare);

				inputValue = inputValue.add(spare.value);
			}
		}

		// use to the exact diff, which is >= minLovelace
		let diff = inputValue.sub(nonChangeOutputValue);

		assert(diff.assets.isZero(), "unexpected unbalanced assets");
		assert(diff.lovelace >= minLovelace, `diff.lovelace=${diff.lovelace} ${typeof diff.lovelace} vs minLovelace=${minLovelace} ${typeof minLovelace}`);

		changeOutput.setValue(diff);

		// we can mutate the lovelace value of 'changeOutput' until we have a balanced transaction with precisely the right fee

		return changeOutput;
	}

	/**
	 * @internal
	 * @param {NetworkParams} networkParams
	 * @param {TxOutput} changeOutput 
	 */
	correctChangeOutput(networkParams, changeOutput) {
		const origFee = this.#body.fee;

		const fee = this.setFee(networkParams, this.estimateFee(networkParams));
		
		const diff = origFee - fee;

		const changeLovelace = changeOutput.value.lovelace + diff;

		changeOutput.value.setLovelace(changeLovelace);
	}

	/**
	 * @internal
	 * @param {NetworkParams} networkParams
	 */
	checkBalanced(networkParams) {
		const stakeAddrDeposit = new Value(networkParams.stakeAddressDeposit);
		let v = new Value(0n);

		v = this.#body.inputs.reduce((prev, inp) => inp.value.add(prev), v);
		v = this.#body.dcerts.reduce((prev, dcert) => { // add released stakeAddrDeposit
            return dcert.certType === 1 ? prev.add(stakeAddrDeposit) : prev
        }, v);
		v = v.sub(new Value(this.#body.fee));
		v = v.add(new Value(0, this.#body.minted));
		v = this.#body.outputs.reduce((prev, out) => {
			return prev.sub(out.value)
		}, v);
		v = this.#body.dcerts.reduce((prev, dcert) => { // deduct locked stakeAddrDeposit
            return dcert.certType === 0 ? prev.sub(stakeAddrDeposit) : prev
        }, v);

		assert(v.lovelace == 0n, `tx not balanced, net lovelace not zero (${v.lovelace})`);
		assert(v.assets.isZero(), "tx not balanced, net assets not zero");
	}

	/**
	 * Shouldn't be used directly
	 * @internal
	 * @param {NetworkParams} networkParams
	 */
	syncScriptDataHash(networkParams) {
		const hash = this.#witnesses.calcScriptDataHash(networkParams);

		this.#body.setScriptDataHash(hash);
	}

	/**
	 * @internal
	 * @returns {boolean}
	 */
	isSmart() {
		return this.#witnesses.scripts.length > 0;
	}

	/**
	 * Throws an error if there isn't enough collateral
	 * Also throws an error if the script doesn't require collateral, but collateral was actually included
	 * Shouldn't be used directly
	 * @internal
	 * @param {NetworkParams} networkParams 
	 */
	checkCollateral(networkParams) {
		if (this.isSmart()) {
			let minCollateralPct = networkParams.minCollateralPct;

			// only use the exBudget 

			const fee = this.#body.fee;

			this.#body.checkCollateral(networkParams, BigInt(Math.ceil(minCollateralPct*Number(fee)/100.0)));
		} else {
			this.#body.checkCollateral(networkParams, null);
		}
	}

	/**
	 * Throws error if tx is too big
	 * Shouldn't be used directly
	 * @internal
	 * @param {NetworkParams} networkParams 
	 */
	checkSize(networkParams) {
		let size = this.toCbor().length;

		if (size > networkParams.maxTxSize) {
			throw new Error("tx too big");
		}
	}

	/**
	 * Final check that fee is big enough
	 * @internal
	 * @param {NetworkParams} networkParams 
	 */
	checkFee(networkParams) {
		assert(this.estimateFee(networkParams) <= this.#body.fee, `fee too small (${this.#body.fee} < ${this.estimateFee(networkParams)})`);
	}

	/**
	 * @internal
	 * @param {NetworkParams} networkParams 
	 */
	finalizeValidityTimeRange(networkParams) {
		if (this.#witnesses.anyScriptCallsTxTimeRange() && this.#validFrom === null && this.#validTo === null) {
			const currentSlot = networkParams.liveSlot;
			const now = currentSlot !== null ? new Date(Number(networkParams.slotToTime(currentSlot))) : new Date();

			if (config.VALIDITY_RANGE_START_OFFSET !== null) {
				this.#validFrom = new Date(now.getTime() - 1000*config.VALIDITY_RANGE_START_OFFSET);
			}

			if (config.VALIDITY_RANGE_END_OFFSET !== null) {
				this.#validTo = new Date(now.getTime() + 1000*config.VALIDITY_RANGE_END_OFFSET);
			}

			if (!config.AUTO_SET_VALIDITY_RANGE) {
				console.error("Warning: validity interval is unset but detected usage of tx.time_range in one of the scripts.\nSetting the tx validity interval to a sane default\n(hint: set helios.config.AUTO_SET_VALIDITY_RANGE to true to avoid this warning)");
			}
		}

		if (this.#validTo !== null) {
			this.#body.validTo(
				(typeof this.#validTo === "bigint") ? 
					this.#validTo : 
					networkParams.timeToSlot(BigInt(this.#validTo.getTime()))
			);
		}

		if (this.#validFrom !== null) {
			this.#body.validFrom(
				(typeof this.#validFrom === "bigint") ?
					this.#validFrom :
					networkParams.timeToSlot(BigInt(this.#validFrom.getTime()))
			);
		}
	}

	/**
	 * Executes all the attached scripts with appropriate redeemers and calculates execution budgets.
	 * Balances the transaction, and optionally uses some spare UTxOs if the current inputs don't contain enough lovelace to cover the fees and min output deposits.
	 * 
	 * Inputs, minted assets, and withdrawals are sorted.
	 * 
	 * Sets the validatity range automatically if a call to `tx.time_range` is detected in any of the attached Helios scripts.
	 * @param {NetworkParams} networkParams
	 * @param {Address}       changeAddress
	 * @param {TxInput[]}        spareUtxos - might be used during balancing if there currently aren't enough inputs
	 * @returns {Promise<Tx>}
	 */
	async finalize(networkParams, changeAddress, spareUtxos = []) {
		assert(!this.#valid);

		if (this.#metadata !== null) {
			// Calculate the Metadata hash and add to the TxBody
			this.#body.setMetadataHash(
				new Hash(Crypto.blake2b(this.#metadata.toCbor()))
			);
		}

		// initially dummy for more correct body size, recalculated later
		if (this.#witnesses.redeemers.length > 0) {
			this.#body.setScriptDataHash(new Hash((new Array(32)).fill(0)));
		}

		// auto set the validity time range if the script call tx.time_range
		//  and translate the time range dates to slots
		this.finalizeValidityTimeRange(networkParams);

		// inputs, minted assets, and withdrawals must all be in a particular order
		this.#body.sortInputs();

		// after inputs etc. have been sorted we can calculate the indices of the redeemers referring to those inputs
		this.#witnesses.updateRedeemerIndices(this.#body);

		this.checkScripts();

		// balance the non-ada assets
		this.balanceAssets(changeAddress);

		// sort the assets in the outputs, including the asset change output
		this.#body.sortOutputs();

		// make sure that each output contains the necessary minimum amount of lovelace	
		this.#body.correctOutputs(networkParams);

		// balance the lovelace using maxTxFee as the fee
		const changeOutput = this.balanceLovelace(networkParams, changeAddress, spareUtxos.slice());

		// the scripts executed at this point will not see the correct txHash nor the correct fee
		await this.executeRedeemers(networkParams, changeAddress);

		// balance collateral (if collateral wasn't already set manually)
		this.balanceCollateral(networkParams, changeAddress, spareUtxos.slice());

		// correct the changeOutput now the exact fee is known
		this.correctChangeOutput(networkParams, changeOutput);

		// run updateRedeemerIndices again because new inputs may have been added and sorted
		this.#witnesses.updateRedeemerIndices(this.#body);

		// we can only sync scriptDataHash after the redeemer execution costs have been estimated, and final redeemer indices have been determined
		this.syncScriptDataHash(networkParams);

		// a bunch of checks
		this.#body.checkOutputs(networkParams);

		this.checkCollateral(networkParams);

		await this.checkExecutionBudgets(networkParams);

		this.#witnesses.checkExecutionBudgetLimits(networkParams);

		this.checkSize(networkParams);

		this.checkFee(networkParams);

		this.checkBalanced(networkParams);

		this.#valid = true;

		return this;
	}

	/**
	 * @type {string}
	 */
	get profileReport() {
		return this.#witnesses.profileReport;
	}

	/**
	 * Adds a signature created by a wallet. Only available after the transaction has been finalized.
	 * Optionally verifies that the signature is correct.
	 * @param {Signature} signature 
	 * @param {boolean} verify Defaults to `true`
	 * @returns {Tx}
	 */
	addSignature(signature, verify = true) {
		assert(this.#valid);

		if (verify) {
			signature.verify(this.bodyHash);
		}

		this.#witnesses.addSignature(signature);

		return this;
	}

	/**
	 * Adds multiple signatures at once. Only available after the transaction has been finalized.
	 * Optionally verifies each signature is correct.
	 * @param {Signature[]} signatures 
	 * @param {boolean} verify 
	 * @returns {Tx}
	 */
	addSignatures(signatures, verify = true) {
		for (let s of signatures) {
			this.addSignature(s, verify);
		}

		return this;
	}

	/**
	 * Add metadata to a transaction.
	 * Metadata can be used to store data on-chain,
	 * but can't be consumed by validator scripts.
	 * Metadata can for example be used for [CIP 25](https://cips.cardano.org/cips/cip25/). 
	 * @param {number} tag
	 * @param {Metadata} data
	 * @returns {Tx}
	 */
	addMetadata(tag, data) {
		if (this.#metadata === null) {
			this.#metadata = new TxMetadata();
		}

		this.#metadata.add(tag, data);

		return this;
	}

	/**
	 * @returns {TxId}
	 */
	id() {
		assert(this.#valid, "can't get TxId of unfinalized Tx");
		return new TxId(this.bodyHash);
	}
}

/**
 * inputs, minted assets, and withdrawals need to be sorted in order to form a valid transaction
 */
export class TxBody extends CborData {
	/**
	 * Inputs must be sorted before submitting (first by TxId, then by utxoIndex)
	 * Spending redeemers must point to the sorted inputs
	 * @type {TxInput[]} 
	 */
	#inputs;

	/** @type {TxOutput[]} */
	#outputs;

	/** @type {bigint} in lovelace */
	#fee;

	/** @type {null | bigint} */
	#lastValidSlot;

	/** @type {DCert[]} */
	#dcerts;

	/**
	 * Withdrawals must be sorted by address
	 * Stake rewarding redeemers must point to the sorted withdrawals
	 * @type {Map<Address, bigint>} 
	 */
	#withdrawals;

	/** @type {null | bigint} */
	#firstValidSlot;

	/**
	 * Internally the assets must be sorted by mintingpolicyhash
	 * Minting redeemers must point to the sorted minted assets
	 * @type {Assets} 
	 */
	#minted;

	/** @type {null | Hash} */
	#scriptDataHash;

	/** @type {TxInput[]} */
	#collateral;

	/** @type {PubKeyHash[]} */
	#signers;

	/** @type {null | TxOutput} */
	#collateralReturn;

	/** @type {bigint} */
	#totalCollateral;

	/** @type {TxInput[]} */
	#refInputs;

	/** @type {?Hash} */
	#metadataHash;

	constructor() {
		super();

		this.#inputs = [];
		this.#outputs = [];
		this.#fee = 0n;
		this.#lastValidSlot = null;
		this.#dcerts = [];
		this.#withdrawals = new Map();
		this.#firstValidSlot = null;
		this.#minted = new Assets(); // starts as zero value (i.e. empty map)
		this.#scriptDataHash = null; 
		this.#collateral = [];
		this.#signers = [];
		this.#collateralReturn = null;
		this.#totalCollateral = 0n; // doesn't seem to be used anymore
		this.#refInputs = [];
		this.#metadataHash = null;
	}

	/**
	 * @type {TxInput[]}
	 */
	get inputs() {
		return this.#inputs;
	}

	/**
	 * @type {TxInput[]}
	 */
	get refInputs() {
		return this.#refInputs;
	}

	/**
	 * @type {TxOutput[]}
	 */
	get outputs() {
		return this.#outputs;
	}

	/**
	 * @type {bigint}
	 */
	get fee() {
		return this.#fee;
	}

	/**
	 * @internal
	 * @param {bigint} fee
	 */
	setFee(fee) {
		this.#fee = fee;
	}

	/**
	 * @type {Assets}
	 */
	get minted() {
		return this.#minted;
	}

	/**
	 * @type {TxInput[]}
	 */
	get collateral() {
		return this.#collateral;
	}

	/**
	 * @type {bigint | null}
	 */
	get firstValidSlot() {
		return this.#firstValidSlot;
	}

	/**
	 * @type {bigint | null}
	 */
	get lastValidSlot() {
		return this.#lastValidSlot;
	}

	/**
	 * @type {PubKeyHash[]}
	 */
	get signers() {
		return this.#signers.slice();
	}

	/**
	 * @type {DCert[]}
	 */
	get dcerts() {
		return this.#dcerts.slice();
	}

	/**
	 * @returns {number[]}
	 */
	toCbor() {
		/**
		 * @type {Map<number, number[]>}
		 */
		let object = new Map();

		object.set(0, Cbor.encodeDefList(this.#inputs));
		object.set(1, Cbor.encodeDefList(this.#outputs));
		object.set(2, Cbor.encodeInteger(this.#fee));
		
		if (this.#lastValidSlot !== null) {
			object.set(3, Cbor.encodeInteger(this.#lastValidSlot));
		}

		if (this.#dcerts.length != 0) {
			object.set(4, Cbor.encodeDefList(this.#dcerts));
		}

		if (this.#withdrawals.size != 0) {
			throw new Error("not yet implemented");
		}

		if (this.#metadataHash !== null) {
			object.set(7, this.#metadataHash.toCbor());
		}

		if (this.#firstValidSlot !== null) {
			object.set(8, Cbor.encodeInteger(this.#firstValidSlot));
		}

		if (!this.#minted.isZero()) {
			object.set(9, this.#minted.toCbor());
		}

		if (this.#scriptDataHash !== null) {
			object.set(11, this.#scriptDataHash.toCbor());
		}

		if (this.#collateral.length != 0) {
			object.set(13, Cbor.encodeDefList(this.#collateral));
		}

		if (this.#signers.length != 0) {
			object.set(14, Cbor.encodeDefList(this.#signers));
		}

		// what is NetworkId used for, seems a bit useless?
		// object.set(15, Cbor.encodeInteger(2n));

		if (this.#collateralReturn !== null) {
			object.set(16, this.#collateralReturn.toCbor());
		}

		if (this.#totalCollateral > 0n) {
			object.set(17, Cbor.encodeInteger(this.#totalCollateral));
		}

		if (this.#refInputs.length != 0) {
			object.set(18, Cbor.encodeDefList(this.#refInputs));
		}

		return Cbor.encodeObject(object);
	}

	/**
	 * @param {number[]} bytes
	 * @returns {TxBody}
	 */
	static fromCbor(bytes) {
		const txBody = new TxBody();

		const done = Cbor.decodeObject(bytes, (i, fieldBytes) => {
			switch(i) {
				case 0:
					Cbor.decodeList(fieldBytes, (_, itemBytes) => {
						txBody.#inputs.push(TxInput.fromCbor(itemBytes));
					});
					break;
				case 1:
					Cbor.decodeList(fieldBytes, (_, itemBytes) => {
						txBody.#outputs.push(TxOutput.fromCbor(itemBytes));
					})
					break;
				case 2:
					txBody.#fee = Cbor.decodeInteger(fieldBytes);
					break;
				case 3:
					txBody.#lastValidSlot = Cbor.decodeInteger(fieldBytes);
					break;
				case 4:
					Cbor.decodeList(fieldBytes, (_, itemBytes) => {
						txBody.#dcerts.push(DCert.fromCbor(itemBytes));
					});
					break;
				case 5:
					throw new Error("not yet implemented");
				case 6:
					throw new Error("not yet implemented");
				case 7:
					txBody.#metadataHash = Hash.fromCbor(fieldBytes);
					break;
				case 8:
					txBody.#firstValidSlot = Cbor.decodeInteger(fieldBytes);
					break;
				case 9:
					txBody.#minted = Assets.fromCbor(fieldBytes);
					break;
				case 10:
					throw new Error("unhandled field");
				case 11:
					txBody.#scriptDataHash = Hash.fromCbor(fieldBytes);
					break;
				case 12:
					throw new Error("unhandled field");
				case 13:
					Cbor.decodeList(fieldBytes, (_, itemBytes) => {
						txBody.#collateral.push(TxInput.fromCbor(itemBytes));
					});
					break;
				case 14:
					Cbor.decodeList(fieldBytes, (_, itemBytes) => {
						txBody.#signers.push(PubKeyHash.fromCbor(itemBytes));
					});
					break;
				case 15:
					// ignore the network Id
					void Cbor.decodeInteger(fieldBytes);
					break;
				case 16:
					txBody.#collateralReturn = TxOutput.fromCbor(fieldBytes);
					break;
				case 17:
					txBody.#totalCollateral = Cbor.decodeInteger(fieldBytes);
					break;
				case 18:
					Cbor.decodeList(fieldBytes, itemBytes => {
						txBody.#refInputs.push(TxInput.fromCbor(fieldBytes));
					});
					break;
				default:
					throw new Error("unrecognized field");
			}
		});

		assert(done.has(0) && done.has(1) && done.has(2));

		return txBody;
	}

	/**
	 * @returns {Object}
	 */
	dump() {
		return {
			inputs: this.#inputs.map(input => input.dump()),
			outputs: this.#outputs.map(output => output.dump()),
			fee: this.#fee.toString(),
			lastValidSlot: this.#lastValidSlot === null ? null : this.#lastValidSlot.toString(),
			firstValidSlot: this.#firstValidSlot === null ? null : this.#firstValidSlot.toString(),
			minted: this.#minted.isZero() ? null : this.#minted.dump(),
			metadataHash: this.#metadataHash === null ? null : this.#metadataHash.dump(),
			scriptDataHash: this.#scriptDataHash === null ? null : this.#scriptDataHash.dump(),
			certificates: this.#dcerts.length == 0 ? null : this.#dcerts.map(dc => dc.dump()),
			collateral: this.#collateral.length == 0 ? null : this.#collateral.map(c => c.dump()),
			signers: this.#signers.length == 0 ? null : this.#signers.map(rs => rs.dump()),
			collateralReturn: this.#collateralReturn === null ? null : this.#collateralReturn.dump(),
			//totalCollateral: this.#totalCollateral.toString(), // doesn't seem to be used anymore
			refInputs: this.#refInputs.map(ri => ri.dump()),
		};
	}

	/**
	 * For now simply returns minus infinity to plus infinity (WiP)
	 * @internal
	 * @param {NetworkParams} networkParams
	 * @returns {ConstrData}
	 */
	toValidTimeRangeData(networkParams) {
		return new ConstrData(0, [
			new ConstrData(0, [ // LowerBound
				this.#firstValidSlot === null ? new ConstrData(0, []) : new ConstrData(1, [new IntData(networkParams.slotToTime(this.#firstValidSlot))]), // NegInf
				new ConstrData(1, []), // true
			]),
			new ConstrData(0, [ // UpperBound
				this.#lastValidSlot === null ? new ConstrData(2, []) : new ConstrData(1, [new IntData(networkParams.slotToTime(this.#lastValidSlot))]), // PosInf
				new ConstrData(this.#lastValidSlot === null ? 1 : 0, []), // false if slot is set, true if slot isn't set
			]),
		]);
	}

	/**
	 * A serialized tx throws away input information
	 * This must be refetched from the network if the tx needs to be analyzed
	 * @internal
	 * @param {(id: TxOutputId) => Promise<TxOutput>} fn
	 * @param {TxWitnesses} witnesses
	 */
	async completeInputData(fn, witnesses) {
		const indices = [];
		const ids = [];

		for (let i = 0; i < this.#inputs.length; i++) {
			const input = this.#inputs[i];

			if (!input.hasOrigOutput()) {
				indices.push(i);
				ids.push(input.outputId);
			}
		}

		const offset = this.#inputs.length;

		for (let i = 0; i < this.#refInputs.length; i++) {
			const refInput = this.#refInputs[i];

			if (!refInput.hasOrigOutput()) {
				indices.push(offset + i);
				ids.push(refInput.outputId);
			}
		}

		const outputs = await Promise.all(ids.map(id => fn(id)));

		outputs.forEach((output, j) => {
			const i = indices[j];

			if (output.refScript) {
				witnesses.attachRefScript(output.refScript)
			}
			
			if (i < offset) {
				this.#inputs[i].setOrigOutput(output)
			} else {
				this.#refInputs[i-offset].setOrigOutput(output)
			}
		});
	}

	/**
	 * @internal
	 * @param {NetworkParams} networkParams
	 * @param {Redeemer[]} redeemers
	 * @param {ListData} datums 
	 * @param {TxId} txId
	 * @returns {ConstrData}
	 */
	toTxData(networkParams, redeemers, datums, txId) {
		return new ConstrData(0, [
			new ListData(this.#inputs.map(input => input.toData())),
			new ListData(this.#refInputs.map(input => input.toData())),
			new ListData(this.#outputs.map(output => output.toData())),
			(new Value(this.#fee))._toUplcData(),
			// NOTE: all other Value instances in ScriptContext contain some lovelace, but #minted can never contain any lovelace, yet cardano-node always prepends 0 lovelace to the #minted MapData
			(new Value(0n, this.#minted))._toUplcData(true), 
			new ListData(this.#dcerts.map(cert => cert.toData())),
			new MapData(Array.from(this.#withdrawals.entries()).map(w => [w[0].toStakingData(), new IntData(w[1])])),
			this.toValidTimeRangeData(networkParams),
			new ListData(this.#signers.map(rs => new ByteArrayData(rs.bytes))),
			new MapData(redeemers.map(r => [r.toScriptPurposeData(this), r.data])),
			new MapData(datums.list.map(d => [
				new ByteArrayData(Crypto.blake2b(d.toCbor())), 
				d
			])),
			new ConstrData(0, [new ByteArrayData(txId.bytes)])
		]);
	}

	/**
	 * @internal
	 * @param {NetworkParams} networkParams 
	 * @param {Redeemer[]} redeemers
	 * @param {ListData} datums
	 * @param {number} redeemerIdx
	 * @param {TxId} txId
	 * @returns {UplcData}
	 */
	toScriptContextData(networkParams, redeemers, datums, redeemerIdx, txId = TxId.dummy()) {		
		return new ConstrData(0, [
			// tx (we can't know the txId right now, because we don't know the execution costs yet, but a dummy txId should be fine)
			this.toTxData(networkParams, redeemers, datums, txId),
			redeemers[redeemerIdx].toScriptPurposeData(this),
		]);
	}

	/**
	 * @returns {Value}
	 */
	sumInputValue() {
		let sum = new Value();

		for (let input of this.#inputs) {
			if (input.origOutput !== null) {
				sum = sum.add(input.origOutput.value);
			}
		}

		return sum;
	}

	/**
	 * Throws error if any part of the sum is negative (i.e. more is burned than input)
	 * @returns {Value}
	 */
	sumInputAndMintedValue() {
		return this.sumInputValue().add(new Value(0n, this.#minted)).assertAllPositive();
	}

	/**
	 * @returns {Assets}
	 */
	sumInputAndMintedAssets() {
		return this.sumInputAndMintedValue().assets;
	}

	/**
	 * @returns {Value}
	 */
	sumOutputValue() {
		let sum = new Value();

		for (let output of this.#outputs) {
			sum = sum.add(output.value);
		}

		return sum;
	}

	/**
	 * @returns {Assets}
	 */
	sumOutputAssets() {
		return this.sumOutputValue().assets;
	}

	/**
	 * @internal
	 * @param {bigint} slot
	 */
	validFrom(slot) {
		this.#firstValidSlot = slot;
	}

	/**
	 * @internal
	 * @param {bigint} slot
	 */
	validTo(slot) {
		this.#lastValidSlot = slot;
	}

	/**
	 * Throws error if this.#minted already contains mph
	 * @internal
	 * @param {MintingPolicyHash | MintingPolicyHashProps} mph - minting policy hash
	 * @param {[ByteArray | ByteArrayProps, HInt | HIntProps][]} tokens
	 */
	addMint(mph, tokens) {
		this.#minted.addTokens(mph, tokens);
	}

	/**
	 * @internal
	 * @param {TxInput} input 
	 * @param {boolean} checkUniqueness
	 */
	addInput(input, checkUniqueness = true) {
		if (input.origOutput === null) {
			throw new Error("TxInput.origOutput must be set when building transaction");
		}

		input.origOutput.value.assertAllPositive();

		if (checkUniqueness) {
			assert(this.#inputs.every(prevInput => {
				return  !prevInput.txId.eq(input.txId) || prevInput.utxoIdx != input.utxoIdx
			}), "input already added before");
		}

		// push, then sort immediately
		this.#inputs.push(input);
		this.#inputs.sort(TxInput.comp);
	}

	/**
	 * Used to remove dummy inputs
	 * Dummy inputs are needed to be able to correctly estimate fees
	 * Throws an error if input doesn't exist in list of inputs
	 * @internal
	 * @param {TxInput} input
	 */
	removeInput(input) {
		let idx = -1;

		// search from end, so removal is exact inverse of addition
		for (let i = this.#inputs.length - 1; i >= 0; i--) {
			if (this.#inputs[i] == input) {
				idx = i;
				break;
			}
		}

		const n = this.#inputs.length;

		assert(idx != -1, "input not found");

		this.#inputs = this.#inputs.filter((_, i) => i != idx);

		assert(this.#inputs.length == n - 1, "input not removed");
	}

	/**
	 * @internal
	 * @param {TxInput} input 
	 * @param {boolean} checkUniqueness
	 */
	addRefInput(input, checkUniqueness = true) {
		if (input.origOutput === null) {
			throw new Error("TxInput.origOutput must be set when building transaction");
		}

		input.origOutput.value.assertAllPositive();

		if (checkUniqueness) {
			assert(this.#refInputs.every(prevInput => {
				return  !prevInput.txId.eq(input.txId) || prevInput.utxoIdx != input.utxoIdx
			}), "refInput already added before");
		}

		// push, then sort immediately
		this.#refInputs.push(input);
		this.#refInputs.sort(TxInput.comp);
	}

	/**
	 * @internal
	 * @param {TxOutput} output
	 */
	addOutput(output) {
		output.value.assertAllPositive();

		this.#outputs.push(output);
	}

	/**
	 * Used to remove dummy outputs
	 * Dummy outputs are needed to be able to correctly estimate fees
	 * Throws an error if the output doesn't exist in list of outputs
	 * @internal
	 * @param {TxOutput} output 
	 */
	removeOutput(output) {
		let idx = -1;

		// search from end, so removal is exact inverse of addition
		for (let i = this.#outputs.length - 1; i >= 0; i--) {
			if (this.#outputs[i] == output) {
				idx = i;
				break;
			}
		}

		const n = this.#outputs.length;

		assert(idx != -1, "output not found");

		this.#outputs = this.#outputs.filter((_, i) => i != idx);

		assert(this.#outputs.length == n - 1, "output not removed");
	}

	/**
	 * @internal
	 * @param {PubKeyHash} hash 
	 * @param {boolean} checkUniqueness
	 */
	addSigner(hash, checkUniqueness = true) {
		if (checkUniqueness) {
			assert(this.#signers.every(prevSigner => {
				return  !prevSigner.eq(hash);
			}), "signer already added before");
		}


		this.#signers.push(hash);
		this.#signers.sort(Hash.compare);
	}

	/**
	 * @internal
	 * @param {DCert} dcert 
	 */
	addDCert(dcert) {
		this.#dcerts.push(dcert);

		const reqSigTypes = [1,2]; // stake de-reg, stake deleg
		if (reqSigTypes.includes(dcert.certType)){
			if (dcert.credentialType === 0) { // for address key hash only
				this.addSigner(dcert.stakeHash);
			}
		}
	}

	/**
	 * @internal
	 * @param {TxInput} input 
	 */
	addCollateral(input) {
		this.#collateral.push(input);
	}
	
	/**
	 * @internal
	 * @param {Hash | null} scriptDataHash
	 */
	setScriptDataHash(scriptDataHash) {
		this.#scriptDataHash = scriptDataHash;
	}

	/**
	 * @internal
	 * @param {Hash} metadataHash
	 */
	setMetadataHash(metadataHash) {
		this.#metadataHash = metadataHash;
	}

	/**
	 * @internal
	 * @param {TxOutput | null} output 
	 */
	setCollateralReturn(output) {
		this.#collateralReturn = output;
	}

	/**
	 * Calculates the number of dummy signatures needed to get precisely the right tx size.
	 * @internal
	 * @returns {number}
	 */
	countUniqueSigners() {
		/** @type {Set<PubKeyHash>} */
		let set = new Set();

		const inputs = this.#inputs.concat(this.#collateral);

		for (let input of inputs) {
			let origOutput = input.origOutput;

			if (origOutput !== null) {
				let pubKeyHash = origOutput.address.pubKeyHash;

				if (pubKeyHash !== null) {
					set.add(pubKeyHash);
				}
			}
		}

		for (let rs of this.#signers) {
			set.add(rs);
		}

		return set.size;
	}

	/**
	 * Script hashes are found in addresses of TxInputs and hashes of the minted MultiAsset.
	 * @internal
	 * @param {Map<string, number>} set - hashes in hex format
	 */
	collectScriptHashes(set) {
		for (let i = 0; i < this.#inputs.length; i++) {
			const input = this.#inputs[i];

			if (input.origOutput !== null) {
				let scriptHash = input.origOutput.address.validatorHash;

				if (scriptHash !== null) {
					const hash = bytesToHex(scriptHash.bytes);

					if (!set.has(hash)) { 
						set.set(hash, i);
					}
				}
			}
		}

		let mphs = this.#minted.mintingPolicies;

		for (let i = 0; i < mphs.length; i++) {
			const mph = mphs[i];

			const hash = bytesToHex(mph.bytes);

			if (!set.has(hash)) {
				set.set(hash, -i-1);
			}
		}
	}

	/**
	 * Makes sure each output contains the necessary min lovelace.
	 * @internal
	 * @param {NetworkParams} networkParams
	 */
	correctOutputs(networkParams) {
		for (let output of this.#outputs) {
			output.correctLovelace(networkParams);
		}
	}

	/**
	 * Checks that each output contains enough lovelace
	 * @internal
	 * @param {NetworkParams} networkParams
	 */
	checkOutputs(networkParams) {
		for (let output of this.#outputs) {
			let minLovelace = output.calcMinLovelace(networkParams);

			assert(minLovelace <= output.value.lovelace, `not enough lovelace in output (expected at least ${minLovelace.toString()}, got ${output.value.lovelace})`);

			output.value.assets.assertSorted();
		}
	}
	
	/**
	 * @internal
	 * @param {NetworkParams} networkParams
	 * @param {null | bigint} minCollateral 
	 */
	checkCollateral(networkParams, minCollateral) {
		assert(this.#collateral.length <= networkParams.maxCollateralInputs);

		if (minCollateral === null) {
			assert(this.#collateral.length == 0, "unnecessary collateral included");
		} else {
			let sum = new Value();

			for (let col of this.#collateral) {
				if (col.origOutput === null) {
					throw new Error("expected collateral TxInput.origOutput to be set");
				} else if (!col.origOutput.value.assets.isZero()) {
					throw new Error("collateral can only contain lovelace");
				} else {
					sum = sum.add(col.origOutput.value);
				}
			}

			if (this.#collateralReturn != null) {
				sum = sum.sub(this.#collateralReturn.value);
			}

			assert(sum.lovelace >= minCollateral, "not enough collateral");

			if (sum.lovelace > minCollateral*5n){
				console.error("Warning: way too much collateral");
			}
		}
	}

	/**
	 * Makes sore inputs, withdrawals, and minted assets are in correct order, this is needed for the redeemer indices
	 * Mutates
	 * @internal
	 */
	sortInputs() {
		// inputs should've been added in sorted manner, so this is just a check
		this.#inputs.forEach((input, i) => {
			if (i > 0) {
				const prev = this.#inputs[i-1];

				// can be less than -1 if utxoIds aren't consecutive
				assert(TxInput.comp(prev, input) <= -1, "inputs not sorted");
			}
		});

		// same for ref inputs
		this.#refInputs.forEach((input, i) => {
			if (i > 0) {
				const prev = this.#refInputs[i-1];

				// can be less than -1 if utxoIds aren't consecutive
				assert(TxInput.comp(prev, input) <= -1, "refInputs not sorted");
			}
		});

		// TODO: also add withdrawals in sorted manner
		this.#withdrawals = new Map(Array.from(this.#withdrawals.entries()).sort((a, b) => {
			return Address.compStakingHashes(a[0], b[0]);
		}));

		// minted assets should've been added in sorted manner, so this is just a check
		this.#minted.assertSorted();
	}


	/**
	 * Not done in the same routine as sortInputs(), because balancing of assets happens after redeemer indices are set
	 * @internal
	 */
	sortOutputs() {
		// sort the tokens in the outputs, needed by the flint wallet
		this.#outputs.forEach(output => {
			output.value.assets.sort();
		});
	}

	/**
	 * Used by (indirectly) by emulator to check if slot range is valid.
	 * Note: firstValidSlot == lastValidSlot is allowed
	 * @internal
	 * @param {bigint} slot
	 */
	isValid(slot) {
		if (this.#lastValidSlot != null) {
			if (slot > this.#lastValidSlot) {
				return false;
			}
		}

		if (this.#firstValidSlot != null) {
			if (slot < this.#firstValidSlot) {
				return false;
			}
		}

		return true;
	}

	/**
	 * @internal
	 * @returns {number[]}
	 */
	hash() {
		return Crypto.blake2b(this.toCbor());
	}
}

/**
 * Represents the pubkey signatures, and datums/redeemers/scripts that are witnessing a transaction.
 */
export class TxWitnesses extends CborData {
	/** @type {Signature[]} */
	#signatures;

	/** @type {ListData} */
	#datums;

	/** @type {Redeemer[]} */
	#redeemers;

	/**
	 * @type {number[][]}
	 */
	#v1Scripts;

	/** @type {UplcProgram[]} */
	#scripts;

	/** @type {UplcProgram[]} */
	#refScripts;

	/** @type {NativeScript[]} */
	#nativeScripts;

	constructor() {
		super();
		this.#signatures = [];
		this.#datums = new ListData([]);
		this.#redeemers = [];
		this.#v1Scripts = []; // for backward compatibility with some wallets
		this.#scripts = []; // always plutus v2
		this.#refScripts = [];
		this.#nativeScripts = [];
	}

	/**
	 * Gets the list of `Signature` instances contained in this witness set.
	 * @type {Signature[]}
	 */
	get signatures() {
		return this.#signatures;
	}

	/**
	 * Returns all the scripts, including the reference scripts
	 * @type {(UplcProgram | NativeScript)[]}
	 */
	get scripts() {
		/**
		 * @type {(UplcProgram | NativeScript)[]}
		 */
		let allScripts = this.#scripts.slice().concat(this.#refScripts.slice())
		
		allScripts = allScripts.concat(this.#nativeScripts.slice());

		return allScripts;
	}

	/**
	 * @type {Redeemer[]}
	 */
	get redeemers() {
		return this.#redeemers;
	}

	/**
	 * @type {ListData}
	 */
	get datums() {
		return this.#datums;
	}

	/**
	 * @param {ValidatorHash | MintingPolicyHash} h 
	 * @returns {boolean}
	 */
	isNativeScript(h) {
		return this.#nativeScripts.some(s => eq(s.hash(), h.bytes));
	}

	/**
	 * @internal
	 * @returns {boolean}
	 */
	anyScriptCallsTxTimeRange() {
		return this.scripts.some(s => (s instanceof UplcProgram) && s.properties.callsTxTimeRange);
	}

	/**
	 * @returns {number[]}
	 */
	toCbor() {
		/**
		 * @type {Map<number, number[]>}
		 */
		let object = new Map();

		if (this.#signatures.length > 0) {
			object.set(0, Cbor.encodeDefList(this.#signatures));
		}
		
		if (this.#nativeScripts.length > 0) {
			object.set(1, Cbor.encodeDefList(this.#nativeScripts));
		}

		if (this.#v1Scripts.length > 0) {
			object.set(3, Cbor.encodeDefList(this.#v1Scripts));
		}

		if (this.#datums.list.length > 0) {
			object.set(4, this.#datums.toCbor());
		}

		if (this.#redeemers.length > 0) {
			object.set(5, Cbor.encodeDefList(this.#redeemers));
		}

		if (this.#scripts.length > 0) {
			/**
			 * @type {number[][]}
			 */
			let scriptBytes = this.#scripts.map(s => s.toCbor());

			object.set(6, Cbor.encodeDefList(scriptBytes));
		}

		return Cbor.encodeObject(object);
	}

	/**
	 * @param {number[]} bytes 
	 * @returns {TxWitnesses}
	 */
	static fromCbor(bytes) {
		let txWitnesses = new TxWitnesses();

		Cbor.decodeObject(bytes, (i, fieldBytes) => {
			switch(i) {
				case 0:
					Cbor.decodeList(fieldBytes, (_, itemBytes) => {
						txWitnesses.#signatures.push(Signature.fromCbor(itemBytes));
					});
					break;
				case 1:
					Cbor.decodeList(fieldBytes, (_, itemBytes) => {
						txWitnesses.#nativeScripts.push(NativeScript.fromCbor(itemBytes));
					});
					break;
				case 2:
					throw new Error(`unhandled TxWitnesses field ${i}`);
				case 3:
					Cbor.decodeList(fieldBytes, (_, itemBytes) => {
						txWitnesses.#v1Scripts.push(itemBytes);
					});
					break;
				case 4:
					txWitnesses.#datums = ListData.fromCbor(fieldBytes);
					break;
				case 5:
					Cbor.decodeList(fieldBytes, (_, itemBytes) => {
						txWitnesses.#redeemers.push(Redeemer.fromCbor(itemBytes));
					});
					break;
				case 6:
					Cbor.decodeList(fieldBytes, (_, itemBytes) => {
						txWitnesses.#scripts.push(UplcProgram.fromCbor(itemBytes));
					});
					break;
				default:
					throw new Error("unrecognized field");
			}
		});

		return txWitnesses;
	}

	/**
	 * Throws error if signatures are incorrect
	 * @internal
	 * @param {number[]} bodyBytes 
	 */
	verifySignatures(bodyBytes) {
		for (let signature of this.#signatures) {
			signature.verify(Crypto.blake2b(bodyBytes));
		}
	}

	/**
	 * @param {null | NetworkParams} params 
	 * @param {null | TxBody} body
	 * @returns {Object}
	 */
	dump(params = null, body = null) {
		return {
			signatures: this.#signatures.map(pkw => pkw.dump()),
			datums: this.#datums.list.map(datum => datum.toString()),
			redeemers: this.#redeemers.map((redeemer, i) => {
				const obj = redeemer.dump()
				if (params && body) {
					const scriptContext = body.toScriptContextData(params, this.#redeemers, this.#datums, i);
					
					obj["ctx"] = scriptContext.toCborHex();

					if (redeemer instanceof SpendingRedeemer) {
						const idx = redeemer.inputIndex;
			
						const origOutput = body.inputs[idx].origOutput;
			
						if (origOutput === null) {
							throw new Error("expected origOutput to be non-null");
						} else {
							const datumData = origOutput.getDatumData();

							obj["datum"] = datumData.toCborHex();
						}
					}
				}

				return obj;
			}),
			nativeScripts: this.#nativeScripts.map(script => script.toJson()),
			scripts: this.#scripts.map(script => bytesToHex(script.toCbor())),
			refScripts: this.#refScripts.map(script => bytesToHex(script.toCbor())),
		};
	}

	/**
	 * @internal
	 * @param {NetworkParams} networkParams
	 * @returns {bigint}
	 */
	estimateFee(networkParams) {
		let sum = 0n;

		for (let redeemer of this.#redeemers) {
			sum += redeemer.estimateFee(networkParams);
		}

		return sum;
	}

	/**
	 * @internal
	 * @param {Signature} signature 
	 */
	addSignature(signature) {
		// only add unique signautres
		if (this.#signatures.every(s => !s.isDummy() && !s.pubKeyHash.eq(signature.pubKeyHash))) {
			this.#signatures.push(signature);
		}
	}

	/**
	 * @internal
	 * @param {number} n
	 */
	addDummySignatures(n) {
		for (let i = 0 ; i < n; i++) {
			this.#signatures.push(Signature.dummy());
		}
	}

	/**
	 * @internal
	 */
	removeDummySignatures() {
		this.#signatures = this.#signatures.filter(pkw => !pkw.isDummy());
	}

	/**
	 * Index is calculated later
	 * @internal
	 * @param {TxInput} input
	 * @param {UplcData} redeemerData 
	 */
	addSpendingRedeemer(input, redeemerData) {
		this.#redeemers.push(new SpendingRedeemer(input, -1, redeemerData)); // actual input index is determined later
	}

	/**
	 * @internal
	 * @param {MintingPolicyHash} mph
	 * @param {UplcData} redeemerData
	 */
	addMintingRedeemer(mph, redeemerData) {
		this.#redeemers.push(new MintingRedeemer(mph, -1, redeemerData));
	}

	/**
	 * @internal
	 * @param {UplcData} data 
	 */
	addDatumData(data) {
		// check that it hasn't already been included
		for (let prev of this.#datums.list) {
			if (eq(prev.toCbor(), data.toCbor())) {
				return;
			}
		}

		let lst = this.#datums.list;
		lst.push(data);

		this.#datums = new ListData(lst);
	}

	/**
	 * @internal
	 * @param {NativeScript} script 
	 */
	attachNativeScript(script) {
		const h = script.hash();

		if (this.#nativeScripts.some(other => eq(h, other.hash()))) {
			return;
		} else {
			this.#nativeScripts.push(script);
		}
	}

	/**
	 * @internal
	 * @param {UplcProgram} script 
	 */
	attachRefScript(script) {
		if (this.#refScripts.some(s => eq(s.hash(), script.hash()))) {
			return;
		}

		this.#refScripts.push(script);
	}

	/**
	 * Throws error if script was already added before.
	 * @internal
	 * @param {UplcProgram} program 
	 * @param {boolean} isRef
	 */
	attachPlutusScript(program, isRef = false) {
		const h = program.hash();

		if (isRef) {
			assert(this.#scripts.every(s => !eq(s.hash(), h)));

			if (this.#refScripts.some(s => eq(s.hash(), h))) {
				return;
			} else {
				this.#refScripts.push(program);
			}
		} else {
			assert(this.#refScripts.every(s => !eq(s.hash(), h)));

			if (this.#scripts.some(s => eq(s.hash(), h))) {
				return;
			} else {
				this.#scripts.push(program);
			}
		}
	}

	/**
	 * Retrieves either a regular script or a reference script.
	 * @internal
	 * @param {Hash} scriptHash - can be ValidatorHash or MintingPolicyHash
	 * @returns {UplcProgram}
	 */
	getUplcProgram(scriptHash) {
		const p = this.scripts.find(s => eq(s.hash(), scriptHash.bytes));

		if (!(p instanceof UplcProgram)) {
			throw new Error("not a uplc program");
		}

		return p;
	}

	/**
	 * @internal
	 * @param {TxBody} body
	 */
	updateRedeemerIndices(body) {
		for (let redeemer of this.#redeemers) {
			redeemer.updateIndex(body);
		}
	}

	/**
	 * @internal
	 * @param {NetworkParams} networkParams 
	 * @returns {Hash | null} - returns null if there are no redeemers
	 */
	calcScriptDataHash(networkParams) {
		if (this.#redeemers.length > 0) {
			let bytes = Cbor.encodeDefList(this.#redeemers);

			if (this.#datums.list.length > 0) {
				bytes = bytes.concat(this.#datums.toCbor());
			}

			// language view encodings?
			let sortedCostParams = networkParams.sortedCostParams;

			bytes = bytes.concat(Cbor.encodeMap([[
				Cbor.encodeInteger(1n), 
				Cbor.encodeDefList(sortedCostParams.map(cp => Cbor.encodeInteger(BigInt(cp)))),
			]]));

			return new Hash(Crypto.blake2b(bytes));
		} else {
			return null;
		}
	}

	/**
	 * @internal
	 * @param {NetworkParams} networkParams 
	 * @param {TxBody} body
	 * @param {Redeemer} redeemer 
	 * @param {UplcData} scriptContext
	 * @returns {Promise<Profile>} 
	 */
	async executeRedeemer(networkParams, body, redeemer, scriptContext) {
		if (redeemer instanceof SpendingRedeemer) {
			const idx = redeemer.inputIndex;

			const origOutput = body.inputs[idx].origOutput;

			if (origOutput === null) {
				throw new Error("expected origOutput to be non-null");
			} else {
				const datumData = origOutput.getDatumData();

				const validatorHash = origOutput.address.validatorHash;

				if (validatorHash === null || validatorHash === undefined) {
					throw new Error("expected validatorHash to be non-null");
				} else {
					const script = this.getUplcProgram(validatorHash);

					if (script.properties.name) {
						redeemer.setProgramName(script.properties.name);
					}

					const args = [
						new UplcDataValue(Site.dummy(), datumData), 
						new UplcDataValue(Site.dummy(), redeemer.data), 
						new UplcDataValue(Site.dummy(), scriptContext),
					];

					const profile = await script.profile(args, networkParams);

					profile.messages?.forEach(m => console.log(m));

					if (profile.result instanceof UserError || profile.result instanceof RuntimeError) {	
						if (script.properties.name) {
							profile.result.context["name"] = script.properties.name;
						}
						profile.result.context["Datum"] = bytesToHex(datumData.toCbor());
						profile.result.context["Redeemer"] = bytesToHex(redeemer.data.toCbor());
						profile.result.context["ScriptContext"] = bytesToHex(scriptContext.toCbor());
						throw profile.result;
					} else {
						return profile;
					}
				}
			}
		} else if (redeemer instanceof MintingRedeemer) {
			const mph = body.minted.mintingPolicies[redeemer.mphIndex];

			const script = this.getUplcProgram(mph);

			if (script.properties.name) {
				redeemer.setProgramName(script.properties.name);
			}

			const args = [
				new UplcDataValue(Site.dummy(), redeemer.data),
				new UplcDataValue(Site.dummy(), scriptContext),
			];

			const profile = await script.profile(args, networkParams);

			profile.messages?.forEach(m => console.log(m));

			if (profile.result instanceof UserError || profile.result instanceof RuntimeError) {
				if (script.properties.name) {
					profile.result.context["name"] = script.properties.name;
				}
				profile.result.context["Redeemer"] = bytesToHex(redeemer.data.toCbor());
				profile.result.context["ScriptContext"] = bytesToHex(scriptContext.toCbor());
				throw profile.result;
			} else {
				return profile;
			}
		} else {
			throw new Error("unhandled redeemer type");
		}
	}

	/**
	 * Executes the redeemers in order to calculate the necessary ex units
	 * @internal
	 * @param {NetworkParams} networkParams 
	 * @param {TxBody} body - needed in order to create correct ScriptContexts
	 * @param {Address} changeAddress - needed for dummy input and dummy output
	 * @returns {Promise<void>}
	 */
	async executeScripts(networkParams, body, changeAddress) {
		await this.executeRedeemers(networkParams, body, changeAddress);

		this.executeNativeScripts(body);
	}
	
	/**
	 * @internal
	 * @param {TxBody} body
	 */
	executeNativeScripts(body) {
		const ctx = new NativeContext(body.firstValidSlot, body.lastValidSlot, body.signers);

		this.#nativeScripts.forEach(s => {
			if (!s.eval(ctx)) {
				throw new Error("native script execution returned false");
			}
		});
	}

	/**
	 * Executes the redeemers in order to calculate the necessary ex units
	 * @internal
	 * @param {NetworkParams} networkParams 
	 * @param {TxBody} body - needed in order to create correct ScriptContexts
	 * @param {Address} changeAddress - needed for dummy input and dummy output
	 * @returns {Promise<void>}
	 */
	async executeRedeemers(networkParams, body, changeAddress) {
		assert(config.N_DUMMY_INPUTS == 1 || config.N_DUMMY_INPUTS == 2, "expected N_DUMMY_INPUTS==1 or N_DUMMY_INPUTS==2");
		const twoDummyInputs = config.N_DUMMY_INPUTS == 2;

		const fee = networkParams.maxTxFee;

		// Additional 2 dummy inputs and 1 dummy output to compensate for balancing inputs and outputs that might be added later
		// The reason for needing 2 dummy inputs is that one needs to be at the beginning of the body.inputs list (TxId 0000...), and the other needs TxId ffffff (at the end of the list)
		// TxId ffffff overestimates the cost of printing the TxIds, and the dummy TxId 00000 overestimates iterating over body.inputs
		// We can't just prepend a dummy input with TxId ffffff, because some scripts might be relying on the order of the inputs (eg. counting votes in DAOs)

		// 1000 ADA should be enough as a dummy input/output
		const dummyInput1 = new TxInput(
			new TxOutputId(TxId.dummy(0), 0),
			new TxOutput(
				changeAddress,
				new Value(fee + 1000_000_000n)
			)
		);
		
		const dummyInput2 = new TxInput(
			new TxOutputId(TxId.dummy(255), 999),
			new TxOutput(
				changeAddress,
				new Value(1000_000_000n)
			)
		);

		const dummyOutput = new TxOutput(
			changeAddress,
			new Value(twoDummyInputs ? 2000_000_000n : 1000_000_000n)
		);

		body.setFee(fee);
		body.addInput(dummyInput1, false);
		if (twoDummyInputs) {
			body.addInput(dummyInput2, false);
		}
		body.addOutput(dummyOutput);

		this.updateRedeemerIndices(body);

		for (let i = 0; i < this.#redeemers.length; i++) {
			const redeemer = this.#redeemers[i];

			const scriptContext = body.toScriptContextData(networkParams, this.#redeemers, this.#datums, i);

			const cost = await this.executeRedeemer(networkParams, body, redeemer, scriptContext);

			redeemer.setProfile(cost);
		}

		body.removeInput(dummyInput1);
		if (twoDummyInputs) {
			body.removeInput(dummyInput2);
		}
		body.removeOutput(dummyOutput);

		this.updateRedeemerIndices(body);
	}

	/**
	 * Reruns all the redeemers to make sure the ex budgets are still correct (can change due to outputs added during rebalancing)
	 * @internal
	 * @param {NetworkParams} networkParams 
	 * @param {TxBody} body 
	 */
	async checkExecutionBudgets(networkParams, body) {
		// when check the tx is assumed to be finalized, so we can use the actual txId
		const txId = new TxId(body.hash());

		for (let i = 0; i < this.#redeemers.length; i++) {
			const redeemer = this.#redeemers[i];
			
			const scriptContext = body.toScriptContextData(networkParams, this.#redeemers, this.#datums, i, txId);

			const cost = await this.executeRedeemer(networkParams, body, redeemer, scriptContext);

			if (redeemer.memCost < cost.mem) {
				throw new Error(`internal finalization error, redeemer mem budget too low (${redeemer.memCost} < ${cost.mem})`);
			} else if (redeemer.cpuCost < cost.cpu) {
				throw new Error(`internal finalization error, redeemer cpu budget too low (${redeemer.cpuCost} < ${cost.cpu})`);
			}
		}
	}

	/**
	 * Throws error if execution budget is exceeded
	 * @internal
	 * @param {NetworkParams} networkParams
	 */
	checkExecutionBudgetLimits(networkParams) {
		let totalMem = 0n;
		let totalCpu = 0n;

		for (let redeemer of this.#redeemers) {
			totalMem += redeemer.memCost;
			totalCpu += redeemer.cpuCost;
		}

		let [maxMem, maxCpu] = networkParams.maxTxExecutionBudget;

		if (totalMem > BigInt(maxMem)) {
			throw new Error(`execution budget exceeded for mem (${totalMem.toString()} > ${maxMem.toString()})\n${this.profileReport.split("\n").map(l => "  " + l).join("\n")}`);
		}

		if (totalCpu > BigInt(maxCpu)) {
			throw new Error(`execution budget exceeded for cpu (${totalCpu.toString()} > ${maxCpu.toString()})\n${this.profileReport.split("\n").map(l => "  " + l).join("\n")}`);
		}
	}

	/**
	 * Compiles a report of each redeemer execution.
	 * Only works after the tx has been finalized.
	 * @type {string}
	 */
	get profileReport() {
		/**
		 * @type {string[]}
		 */
		let report = [];

		for (let redeemer of this.#redeemers) {
			let header = "";

			if (redeemer instanceof SpendingRedeemer) {
				header = `SpendingRedeemer ${redeemer.inputIndex.toString()}`;
			} else if (redeemer instanceof MintingRedeemer) {
				header = `MintingRedeemer ${redeemer.mphIndex.toString()}`;
			} else {
				throw new Error("unhandled Redeemer type");
			}

			header += `${redeemer.programName ? ` (${redeemer.programName})` : ""}: mem=${redeemer.memCost.toString()}, cpu=${redeemer.cpuCost.toString()}`;

			report.push(header);

			if (redeemer.profile.builtins) {
				report.push(`  builtins`);

				for (let k in redeemer.profile.builtins) {
					const c = redeemer.profile.builtins[k];
					report.push(`    ${k}: mem=${c.mem}, cpu=${c.cpu}`);
				}
			}

			if (redeemer.profile.terms) {
				report.push(`  terms`);

				for (let k in redeemer.profile.terms) {
					const c = redeemer.profile.terms[k];

					report.push(`    ${k}: mem=${c.mem}, cpu=${c.cpu}`);
				}
			}
		}

		return report.join("\n");
	}
}

/**
 * TxInput base-type
 */
export class TxInput extends CborData {
	/** 
	 * @readonly
	 * @type {TxOutputId} 
	 */
	outputId;

	/** 
	 * @type {null | TxOutput} 
	 */
	#output;

	/**
	 * @param {TxOutputId} outputId 
	 * @param {null | TxOutput} output - used during building, not part of serialization
	 */
	constructor(outputId, output = null) {
		super();
		this.outputId = outputId;
		this.#output = output;
	}
	
	/**
	 * @deprecated
	 * @type {TxId}
	 */
	get txId() {
		return this.outputId.txId;
	}

	/**
	 * @deprecated
	 * @type {number}
	 */
	get utxoIdx() {
		return Number(this.outputId.utxoIdx);
	}

	/**
	 * 
	 * @param {TxInput} other 
	 * @returns {boolean}
	 */
	eq(other) {
		return other.outputId.eq(this.outputId);
	}

	/**
	 * @internal
	 * @returns {boolean}
	 */
	hasOrigOutput() {
		return this.#output !== null;
	}

	/**
	 * @internal
	 * @param {TxOutput} output 
	 */
	setOrigOutput(output) {
		this.#output = output;
	}

	/**
	 * 
	 * @type {TxOutput}
	 */
	get output() {
		if (this.#output === null) {
			throw new Error("underlying output data not set");
		} else {
			return this.#output;
		}
	}

	/**
	 * Backward compatible alias for `TxInput.output`
	 * @type {TxOutput}
	 */
	get origOutput() {
		return this.output
	}

	/**
	 * Shortcut
	 * @type {Value}
	 */
	get value() {
		return assertDefined(this.#output).value;
	}

	/**
	 * Shortcut
	 * @type {Address}
	 */
	get address() {
		return assertDefined(this.#output).address;
	}

	/**
	 * @internal
	 * @returns {ConstrData}
	 */
	toOutputIdData() {
		return this.outputId._toUplcData();
	}

	/**
	 * @internal
	 * @returns {ConstrData}
	 */
	toData() {
		if (this.#output === null) {
			throw new Error("expected to be non-null");
		} else {
			return new ConstrData(0, [
				this.toOutputIdData(),
				this.#output.toData(),
			]);
		}
	}

	/**
	 * @internal
	 * @param {UplcData} data 
	 * @returns {TxInput}
	 */
	static fromUplcData(data) {
		assert(data.index == 0);
		const fields = data.fields;

		const outputId = TxOutputId.fromUplcData(fields[0]);

		return new TxInput(
			outputId,
			TxOutput.fromUplcData(fields[1])
		);
	}

	/**
	 * @returns {number[]}
	 */
	toCbor() {
		//return Cbor.encodeTuple([
		return this.outputId.toCbor();//,
		//	this.origOutput.toCbor()
		//]);
	}

	/**
	 * @returns {number[]}
	 */
	toFullCbor() {
		return Cbor.encodeTuple([
			this.outputId.toCbor(),
			this.origOutput.toCbor()
		]);
	}

	/**
	 * Deserializes TxOutput format used by wallet connector
	 * @param {string | number[]} rawBytes
	 * @returns {TxInput}
	 */
	static fromFullCbor(rawBytes) {
		const bytes = Array.isArray(rawBytes) ? rawBytes : hexToBytes(rawBytes);

		/** @type {null | TxOutputId} */
		let outputId = null;

		/** @type {null | TxOutput} */
		let output = null;

		Cbor.decodeTuple(bytes, (i, fieldBytes) => {
			switch(i) {
				case 0:
					outputId = TxOutputId.fromCbor(fieldBytes);
					break;
				case 1:
					output = TxOutput.fromCbor(fieldBytes);
					break;
				default:
					throw new Error("unrecognized field");
			}
		});

		if (outputId !== null && output !== null) {
			return new TxInput(outputId, output);
		} else {
			throw new Error("unexpected");
		}
	}

	/**
	 * @param {string | number[]} rawBytes 
	 * @returns {TxInput}
	 */
	static fromCbor(rawBytes) {
		const outputId = TxOutputId.fromCbor(rawBytes);

		return new TxInput(outputId, null);
	}

	/**
	 * Tx inputs must be ordered. 
	 * The following function can be used directly by a js array sort
	 * @internal
	 * @param {TxInput} a
	 * @param {TxInput} b
	 * @returns {number}
	 */
	static comp(a, b) {
		return TxOutputId.comp(a.outputId, b.outputId);
	} 

	/**
	 * @param {TxInput[]} inputs
	 * @returns {Value}
	 */
	static sumValue(inputs) {
		let sum = new Value();

		for (let input of inputs) {
			sum = sum.add(input.value);
		}

		return sum;
	}

	/**
	 * @returns {Object}
	 */
	dump() {
		return {
			outputId: this.outputId.toString(),
			output: this.#output !== null ? this.#output.dump() : null
		};
	}
}

/**
 * Use TxInput instead
 * @deprecated
 */
export class UTxO extends TxInput {}

/**
 * Use TxInput instead
 * @deprecated
 */
export class TxRefInput extends TxInput {}

/**
 * Represents a transaction output that is used when building a transaction.
 */
export class TxOutput extends CborData {
	/** 
	 * @type {Address} 
	 */
	#address;

	/** 
	 * @type {Value} 
	 */
	#value;

	/** 
	 * @type {null | Datum} 
	 */
	#datum;

	/**
	 * @type {null | UplcProgram} 
	 */
	#refScript;

	/**
	 * Constructs a `TxOutput` instance using an `Address`, a `Value`, an optional `Datum`, and optional `UplcProgram` reference script.
	 * @param {Address} address 
	 * @param {Value} value 
	 * @param {null | Datum} datum 
	 * @param {null | UplcProgram} refScript 
	 */
	constructor(address, value, datum = null, refScript = null) {
		assert(datum === null || datum instanceof Datum); // check this explicitely because caller might be using this constructor without proper type-checking
		super();
		this.#address = address;
		this.#value = value;
		this.#datum = datum;
		this.#refScript = refScript;
	}

	/**
	 * Get the `Address` to which the `TxOutput` will be sent.
	 * @type {Address}
	 */
	get address() {
		return this.#address;
	}

	/**
	 * Mutation is handy when correctin the quantity of lovelace in a utxo
	 * @param {Address} addr
	 */
	setAddress(addr) {
		this.#address = addr;
	}

	/**
	 * Get the `Value` contained in the `TxOutput`.
	 * @type {Value}
	 */
	get value() {
		return this.#value;
	}

	/**
	 * Mutation is handy when correcting the quantity of lovelace in a utxo
	 * @param {Value} val
	 */
	setValue(val) {
		this.#value = val;
	}

	/**
	 * Get the optional `Datum` associated with the `TxOutput`.
	 * @type {null | Datum}
	 */
	get datum() {
		return this.#datum;
	}

	/**
	 * Mutation is handy when correctin the quantity of lovelace in a utxo 
	 * @param {Datum} datum 
	 */
	setDatum(datum) {
		this.#datum = datum;
	}

	/**
	 * @returns {UplcData}
	 */
	getDatumData() {
		if (this.#datum === null) {
			throw new Error("no datum data available");
		} else {
			let data = this.#datum.data;
			if (data === null) {
				throw new Error("no datum data available");
			} else {
				return data;
			}
		}
	}

	/**
	 * @type {null | UplcProgram}
	 */
	get refScript() {
		return this.#refScript;
	}

	/**
	 * @returns {number[]}
	 */
	toCbor() {
		if ((this.#datum === null || this.#datum instanceof HashedDatum) && this.#refScript === null && !config.STRICT_BABBAGE) {
			// this is needed to match eternl wallet (de)serialization (annoyingly eternl deserializes the tx and then signs its own serialization)
			// hopefully cardano-cli signs whatever serialization we choose (so we use the eternl variant in order to be compatible with both)

			let fields = [
				this.#address.toCbor(),
				this.#value.toCbor()
			];

			if (this.#datum !== null) {
				if (this.#datum instanceof HashedDatum) {
					fields.push(this.#datum.hash.toCbor());
				} else {
					throw new Error("unexpected");
				}
			}

			return Cbor.encodeTuple(fields);
		} else {
			/** @type {Map<number, number[]>} */
			let object = new Map();

			object.set(0, this.#address.toCbor());
			object.set(1, this.#value.toCbor());

			if (this.#datum !== null) {
				object.set(2, this.#datum.toCbor());
			}

			if (this.#refScript !== null) {
				object.set(3, Cbor.encodeTag(24n).concat(Cbor.encodeBytes(
					Cbor.encodeTuple([
						Cbor.encodeInteger(BigInt(this.#refScript.versionTag())),
						this.#refScript.toCbor()
					])
				)));
			}

			return Cbor.encodeObject(object);
		}
	}

	/**
	 * @param {number[]} bytes 
	 * @returns {TxOutput}
	 */
	static fromCbor(bytes) {
		/** 
		 * @type {null | Address} 
		 */
		let address = null;

		/** 
		 * @type {null | Value} 
		 */
		let value = null;

		/** 
		 * @type {null | Datum} 
		 */
		let outputDatum = null;

		/** 
		 * @type {null | UplcProgram} 
		 */
		let refScript = null;

		if (Cbor.isObject(bytes)) {
			Cbor.decodeObject(bytes, (i, fieldBytes) => {
				switch(i) { 
					case 0:
						address = Address.fromCbor(fieldBytes);
						break;
					case 1:
						value = Value.fromCbor(fieldBytes);
						break;
					case 2:
						outputDatum = Datum.fromCbor(fieldBytes);
						break;
					case 3:
						assert(Cbor.decodeTag(fieldBytes) == 24n);

						let tupleBytes = Cbor.decodeBytes(fieldBytes);

						let refScriptType = -1;

						Cbor.decodeTuple(tupleBytes, (tupleIdx, innerTupleBytes) => {
							assert(refScript === null);

							switch(tupleIdx) {
								case 0:
									refScriptType = Number(Cbor.decodeInteger(innerTupleBytes));
									break;
								case 1:
									switch(refScriptType) {
										case 0:
											throw new Error("native refScript not handled");
										case 1:
											console.log("Warning: deserializing PlutusV1 refScript as PlutusV2 refScript");
											refScript = UplcProgram.fromCbor(innerTupleBytes);		
											break;
										case 2:
											refScript = UplcProgram.fromCbor(innerTupleBytes);		
											break;
										default:
											throw new Error(`unhandled refScript type ${refScriptType}`);
									}
									break;
								default:
									throw new Error("unhandled refScript format");
							}
						});

						break;
					default:
						throw new Error("unrecognized field");
				}
			});
		} else if (Cbor.isTuple(bytes)) {
			// this is the pre-vasil format, which is still sometimes returned by wallet connector functions
			Cbor.decodeTuple(bytes, (i, fieldBytes) => {
				switch(i) { 
					case 0:
						address = Address.fromCbor(fieldBytes);
						break;
					case 1:
						value = Value.fromCbor(fieldBytes);
						break;
					case 2:
						outputDatum = new HashedDatum(DatumHash.fromCbor(fieldBytes));
						break;
					default:
						throw new Error("unrecognized field");
				}
			});
		} else {
			throw new Error("expected object or tuple for TxOutput");
		}

		if (address === null || value === null) {
			throw new Error("unexpected");
		} else {
			return new TxOutput(address, value, outputDatum, refScript);
		}
	}

	/**
	 * @returns {Object}
	 */
	dump() {
		return {
			address: this.#address.dump(),
			value: this.#value.dump(),
			datum: this.#datum === null ? null : this.#datum.dump(),
			refScript: this.#refScript === null ? null : bytesToHex(this.#refScript.toCbor()),
		};
	}

	/**
	 * @returns {ConstrData}
	 */
	toData() {
		let datum = new ConstrData(0, []); // none
		if (this.#datum !== null) {
			datum = this.#datum.toData();
		}

		return new ConstrData(0, [
			this.#address._toUplcData(),
			this.#value._toUplcData(),
			datum,
			this.#refScript ? new ConstrData(0, [new ByteArrayData(this.#refScript.hash())]) : new ConstrData(1, [])
		]);
	}

	/**
	 * @param {UplcData} data 
	 * @returns {TxOutput}
	 */
	static fromUplcData(data) {
		assert(data.index == 0);
		assert(data.fields.length == 4);

		return new TxOutput(
			Address.fromUplcData(data.fields[0]),
			Value.fromUplcData(data.fields[1]),
			Datum.fromUplcData(data.fields[2])
		);
	}

	/**
	 * Each UTxO must contain some minimum quantity of lovelace to avoid that the blockchain is used for data storage.
	 * @param {NetworkParams} networkParams
	 * @returns {bigint}
	 */
	calcMinLovelace(networkParams) {
		let lovelacePerByte = networkParams.lovelacePerUTXOByte;

		let correctedSize = this.toCbor().length + 160; // 160 accounts for some database overhead?

		return BigInt(correctedSize)*BigInt(lovelacePerByte);
	}

	/**
	 * Makes sure the `TxOutput` contains the minimum quantity of lovelace.
	 * The network requires this to avoid the creation of unusable dust UTxOs.
	 * 
	 * Optionally an update function can be specified that allows mutating the datum of the `TxOutput` to account for an increase of the lovelace quantity contained in the value.
	 * @param {NetworkParams} networkParams 
	 * @param {null | ((output: TxOutput) => void)} updater
	 */
	correctLovelace(networkParams, updater = null) {
		let minLovelace = this.calcMinLovelace(networkParams);

		while (this.#value.lovelace < minLovelace) {
			this.#value.setLovelace(minLovelace);

			if (updater != null) {
				updater(this);
			}

			minLovelace = this.calcMinLovelace(networkParams);
		}
	}
}

/**
 * A `DCert` represents a staking action (eg. withdrawing rewards, delegating to another pool).
 */
export class DCert extends CborData {
	#certType;

	/**
     * @param {number} certType
     */
	constructor(certType) {
		super();
		this.#certType = certType;
	}

	/**
	 * Get certificate type.
	 * @type {number}
	 */
	get certType() {
		return this.#certType;
	}

	/**
	 * Get stake hash.
	 * @type {PubKeyHash | StakingValidatorHash}
	 */
	get stakeHash() {
		throw new Error("not yet implemented");
	}

	/**
	 * Get stake credential type.
	 * @type {number}
	 */
	get credentialType() {
		throw new Error("not yet implemented");
	}


	/**
     * @returns {number[]}
     */
    typeToCbor() {
        return Cbor.encodeInteger(BigInt(this.#certType));
    }

	/**
	 * @param {string | number[]} raw
	 * @returns {DCert}
	 */
	static fromCbor(raw) {
		const bytes = (typeof raw == "string") ? hexToBytes(raw) : raw;

		if (bytes[0] == 0) {
            bytes.shift();
        }

		let certType = -1;
		let stakeHash, stakeHashType, poolHash;

		/**
         * @type {DCert | null}
         */
        let cert = null;

		Cbor.decodeTuple(bytes, (i, fieldBytes) => {
			if (i == 0) {
                certType = Number(Cbor.decodeInteger(fieldBytes))
            } else {
				switch(certType) {
					case 0: // fall through to case 1
					case 1: // fall through to case 2
					case 2:
						if (i == 1)	{
							Cbor.decodeList(fieldBytes, (i, itemBytes) => {
								if (i == 0){
									stakeHashType = Number(Cbor.decodeInteger(itemBytes));
								} else {
									if (stakeHashType == 0) { stakeHash = PubKeyHash.fromCbor(itemBytes) }
									else if (stakeHashType == 1) { stakeHash = StakingValidatorHash.fromCbor(itemBytes) }
									else { throw new Error("invalid stake credential") }
								}
							});
							if (certType == 0) cert = new DCertRegister(stakeHash);
							if (certType == 1) cert = new DCertDeregister(stakeHash);
						} else if (i == 2){
							poolHash = PubKeyHash.fromCbor(Cbor.decodeBytes(fieldBytes));
							if (certType == 2) cert = new DCertDelegate(stakeHash, poolHash);
						}
						break;
					case 3: // fall through
					case 4: // fall through
					case 5: // fall through
					case 6:
						throw new Error("DCert type not yet implemented");
					default:
						throw new Error("invalid DCert type");
				}
			}
		});

		if (!cert) {
            throw new Error("unable to deserialize certificate");
        } else {
            return cert;
        }
	}

	/**
	 * `DCertProps.type` can be:
	 *     `0` for stake registration
	 *     `1` for stake de-registration
	 *     `2` for stake delegation
	 *     `3` for stake pool registration (not yet implemented)
	 *     `4` for stake pool retirement (not yet implemented)
	 *     `5` for genesis key delegation (not yet implemented)
	 *     `6` for moving instantaneous rewards (not yet implemented)
	 *
	 * `DCertProps.credential.type` can be:
	 *     `0` for staking address key hash
	 *     `1` for staking validator key hash (script hash)
	 *
	 * `DCertProps.poolHash` is needed only for stake delegation.
	 * @typedef {{
	 *   type: 0 | 1 | 2,
	 *   credential: {
	 *     type: 0 | 1,
	 *     hash: string
	 *   },
	 *   poolHash?: string
	 * }} DCertProps
	 */

	/**
	 * Create a DCert from a given json parameter.
     * @param {string | DCertProps} json
     * @returns {DCert}
     */
    static fromJson(json) {
		const obj = (typeof json == "string") ? JSON.parse(json) : json;
        const certType = obj?.type;
		const stakeHashType = obj.credential.type;

		if (typeof certType !== "number") {
            throw new Error("invalid or no certificate type specified");
        }

		let stakeHash, poolHash;

		/**
         * @type {DCert | null}
         */
        let cert = null;

		switch (certType) {
			case 0: // fall through to case 1
			case 1: // fall through to case 2
			case 2:
				stakeHash = stakeHashType === 0
				          ? PubKeyHash.fromHex(obj.credential.hash)
						  : StakingValidatorHash.fromHex(obj.credential.hash);
				if (certType == 0) cert = new DCertRegister(stakeHash);
				if (certType == 1) cert = new DCertDeregister(stakeHash);
				if (certType == 2){
					poolHash = PubKeyHash.fromHex(obj.poolHash)
					cert = new DCertDelegate(stakeHash, poolHash);
				}
				break;
			case 3: // fall through
			case 4: // fall through
			case 5: // fall through
			case 6:
				throw new Error("DCert type not yet implemented");
			default:
				throw new Error("invalid DCert type");
		}

		if (!cert) {
            throw new Error("unable to deserialize certificate");
        } else {
            return cert;
        }
	}

	/**
	 * @param {UplcData} data 
	 * @returns {DCert}
	 */
	static fromUplcData(data) {
		throw new Error("not yet implemented");
	}

	/**
	 * @returns {ConstrData}
	 */
	toData() {
		throw new Error("not yet implemented");
	}

	/**
	 * @returns {Object}
	 */
	dump() {
		return {}; // placeholder here only, to satisfy call from TxBody.dump (type checking); overwritten by child classes
	}
}

/**
 * @internal
 */
export class DCertRegister extends DCert {
	/**
	 * @type {PubKeyHash | StakingValidatorHash}
	 */
	#stakeHash;

	/**
	 * @type {number}
	 */
	#credentialType;

	/**
     * @param {PubKeyHash | StakingValidatorHash} stakeHash
     */
    constructor(stakeHash) {
        super(0);
        assert(stakeHash instanceof PubKeyHash || stakeHash instanceof StakingValidatorHash);
        this.#stakeHash = stakeHash;
		this.#credentialType = stakeHash instanceof PubKeyHash ? 0 : 1;
    }

	/**
	 * Get stake hash.
	 * @type {PubKeyHash | StakingValidatorHash}
	 */
	get stakeHash() {
		return this.#stakeHash;
	}
	
	/**
	 * Get stake credential type.
	 * @type {number}
	 */
	get credentialType() {
		return this.#credentialType;
	}

	/**
     * @returns {number[]}
     */
    toCbor() {
        return Cbor.encodeTuple([
            this.typeToCbor(),
            Cbor.encodeDefList([
				Cbor.encodeInteger(BigInt(this.#credentialType)),
				this.#stakeHash.toCbor()
			])
        ]);
    }

    /**
	 * @returns {Object}
	 */
	dump() {
		return {
			certType: "stake_registration",
			stakeCredential: {
				type: this.#credentialType,
				hash: this.#stakeHash.dump()
			}
		};
	}
}

/**
 * @internal
 */
export class DCertDeregister extends DCert {
	/**
	 * @type {PubKeyHash | StakingValidatorHash}
	 */
	#stakeHash;

	/**
	 * @type {number}
	 */
	#credentialType;

	/**
     * @param {PubKeyHash | StakingValidatorHash} stakeHash
     */
    constructor(stakeHash) {
        super(1);
        assert(stakeHash instanceof PubKeyHash || stakeHash instanceof StakingValidatorHash);
        this.#stakeHash = stakeHash;
		this.#credentialType = stakeHash instanceof PubKeyHash ? 0 : 1;
    }

	/**
	 * Get stake hash.
	 * @type {PubKeyHash | StakingValidatorHash}
	 */
	get stakeHash() {
		return this.#stakeHash;
	}
	
	/**
	 * Get stake credential type.
	 * @type {number}
	 */
	get credentialType() {
		return this.#credentialType;
	}

	/**
     * @returns {number[]}
     */
    toCbor() {
        return Cbor.encodeTuple([
            this.typeToCbor(),
            Cbor.encodeDefList([
				Cbor.encodeInteger(BigInt(this.#credentialType)),
				this.#stakeHash.toCbor()
			])
        ]);
    }

    /**
	 * @returns {Object}
	 */
	dump() {
		return {
			certType: "stake_deregistration",
			stakeCredential: {
				type: this.#credentialType,
				hash: this.#stakeHash.dump()
			}
		};
	}
}

/**
 * @internal
 */
export class DCertDelegate extends DCert {
	/**
	 * @type {PubKeyHash | StakingValidatorHash}
	 */
	#stakeHash;

	/**
	 * @type {number}
	 */
	#credentialType;

	/**
	 * @type {PubKeyHash}
	 */
	#poolHash;

	/**
     * @param {PubKeyHash | StakingValidatorHash} stakeHash
	 * @param {PubKeyHash} poolHash
     */
    constructor(stakeHash, poolHash) {
        super(2);
        assert(stakeHash instanceof PubKeyHash || stakeHash instanceof StakingValidatorHash);
		assert(poolHash instanceof PubKeyHash);
        this.#stakeHash = stakeHash;
		this.#credentialType = stakeHash instanceof PubKeyHash ? 0 : 1;
		this.#poolHash = poolHash;
    }

	/**
	 * Get stake hash.
	 * @type {PubKeyHash | StakingValidatorHash}
	 */
	get stakeHash() {
		return this.#stakeHash;
	}

	/**
	 * Get stake credential type.
	 * @type {number}
	 */
	get credentialType() {
		return this.#credentialType;
	}

	/**
	 * Get stake pool hash.
	 * @type {PubKeyHash}
	 */
	get poolHash() {
		return this.#poolHash;
	}

	/**
     * @returns {number[]}
     */
    toCbor() {
        return Cbor.encodeTuple([
            this.typeToCbor(),
            Cbor.encodeDefList([
				Cbor.encodeInteger(BigInt(this.#credentialType)),
				this.#stakeHash.toCbor()
			]),
			this.#poolHash.toCbor()
        ]);
    }

    /**
	 * @returns {Object}
	 */
	dump() {
		return {
			certType: "stake_delegation",
			stakeCredential: {
				type: this.#credentialType,
				hash: this.#stakeHash.dump()
			},
			poolHash: this.#poolHash.dump()
		};
	}
}

/**
 * @internal
 */
export class DCertRegisterPool extends DCert {

}

/**
 * @internal
 */
export class DCertRetirePool extends DCert {

}

/**
 * Wrapper for Cardano stake address bytes. An StakeAddress consists of two parts internally:
 *   - Header (1 byte, see CIP 8)
 *   - Staking witness hash (28 bytes that represent the `PubKeyHash` or `StakingValidatorHash`)
 * 
 * Stake addresses are used to query the assets held by given staking credentials.
 */
export class StakeAddress {
	#bytes;

	/**
	 * @param {number[]} bytes 
	 */
	constructor(bytes) {
		assert(bytes.length == 29);

		this.#bytes = bytes;
	}

	/**
	 * @type {number[]}
	 */
	get bytes() {
		return this.#bytes;
	}

	/**
	 * Returns `true` if the given `StakeAddress` is a testnet address.
	 * @param {StakeAddress} sa
	 * @returns {boolean}
	 */
	static isForTestnet(sa) {
		return Address.isForTestnet(new Address(sa.bytes));
	}

	/**
	 * Convert a regular `Address` into a `StakeAddress`. 
	 * Throws an error if the Address doesn't have a staking credential.
	 * @param {Address} addr 
	 * @returns {StakeAddress}
	 */
	static fromAddress(addr) {
		const sh = addr.stakingHash;

		if (sh === null) {
			throw new Error("address doesn't have a staking part");
		} else {
			return StakeAddress.fromHash(Address.isForTestnet(addr), sh);
		}
	}

	/**
	 * Converts a `StakeAddress` into its CBOR representation.
	 * @returns {number[]}
	 */
	toCbor() {
		return Cbor.encodeBytes(this.#bytes);
	}

	/**
	 * @param {number[]} bytes
	 * @returns {StakeAddress}
	 */
	static fromCbor(bytes) {
		return new StakeAddress(Cbor.decodeBytes(bytes));
	}

	/**
	 * Converts a `StakeAddress` into its Bech32 representation.
	 * @returns {string}
	 */
	toBech32() {
		return Crypto.encodeBech32(
			StakeAddress.isForTestnet(this) ? "stake_test" : "stake",
			this.bytes
		);
	}

	/**
	 * @param {string} str
	 * @returns {StakeAddress}
	 */
	static fromBech32(str) {
		let [prefix, bytes] = Crypto.decodeBech32(str);

		let result = new StakeAddress(bytes);

		assert(prefix == (StakeAddress.isForTestnet(result) ? "stake_test" : "stake"), "invalid StakeAddress prefix");

		return result;
	}

	/**
	 * Converts a `StakeAddress` into its hexadecimal representation.
	 * @returns {string}
	 */
	toHex() {
		return bytesToHex(this.#bytes);
	}

	/**
	 * Converts a `StakeAddress` into its hexadecimal representation.
	 * @type {string}
	 */
	get hex() {
		return this.toHex()
	}

	/**
	 * Doesn't check validity
	 * @param {string} hex
	 * @returns {StakeAddress}
	 */
	static fromHex(hex) {
		return new StakeAddress(hexToBytes(hex));
	}

	/**
	 * Address with only staking part (regular PubKeyHash)
	 * @internal
	 * @param {boolean} isTestnet
	 * @param {PubKeyHash} hash
	 * @returns {StakeAddress}
	 */
	static fromPubKeyHash(isTestnet, hash) {
		return new StakeAddress(
			[isTestnet ? 0xe0 : 0xe1].concat(hash.bytes)
		);
	}

	/**
	 * Address with only staking part (script StakingValidatorHash)
	 * @internal
	 * @param {boolean} isTestnet
	 * @param {StakingValidatorHash} hash
	 * @returns {StakeAddress}
	 */
	static fromStakingValidatorHash(isTestnet, hash) {
		return new StakeAddress(
			[isTestnet ? 0xf0 : 0xf1].concat(hash.bytes)
		);
	}

	/**
	 * Converts a `PubKeyHash` or `StakingValidatorHash` into `StakeAddress`.
	 * @param {boolean} isTestnet
	 * @param {PubKeyHash | StakingValidatorHash} hash
	 * @returns {StakeAddress}
	 */
	static fromHash(isTestnet, hash) {
		if (hash instanceof PubKeyHash) {
			return StakeAddress.fromPubKeyHash(isTestnet, hash);
		} else {
			return StakeAddress.fromStakingValidatorHash(isTestnet, hash);
		}
	}

	/**
	 * Returns the underlying `PubKeyHash` or `StakingValidatorHash`.
	 * @returns {PubKeyHash | StakingValidatorHash}
	 */
	get stakingHash() {
		const type = this.bytes[0];

		if (type == 0xe0 || type == 0xe1) {
			return new PubKeyHash(this.bytes.slice(1));
		} else if (type == 0xf0 || type == 0xf1) {
			return new StakingValidatorHash(this.bytes.slice(1));
		} else {
			throw new Error("bad StakeAddress header");
		}
	}
}

/**
 * Represents a Ed25519 signature.
 * 
 * Also contains a reference to the PubKey that did the signing.
 */
export class Signature extends CborData {
	/**
	 * @type {PubKey} 
	 */
	#pubKey;

	/** @type {number[]} */
	#signature;

	/**
	 * @param {number[] | PubKey} pubKey 
	 * @param {number[]} signature 
	 */
	constructor(pubKey, signature) {
		super();
		this.#pubKey = (pubKey instanceof PubKey) ? pubKey : new PubKey(pubKey);
		this.#signature = signature;
	}

	/**
	 * @type {number[]}
	 */
	get bytes() {
		return this.#signature;
	}

	/**
	 * @type {PubKey}
	 */
	get pubKey() {
		return this.#pubKey;
	}

	/**
	 * @type {PubKeyHash}
	 */
	get pubKeyHash() {
		return this.#pubKey.pubKeyHash;
	}

	/**
	 * @returns {Signature}
	 */
	static dummy() {
		return new Signature(PubKey.dummy(), (new Array(64)).fill(0));
	}

	/**
	 * @returns {boolean}
	 */
	isDummy() {
		return this.#pubKey.isDummy() && this.#signature.every(b => b == 0);
	}

	/**
	 * @returns {number[]}
	 */
	toCbor() {
		return Cbor.encodeTuple([
			this.#pubKey.toCbor(),
			Cbor.encodeBytes(this.#signature),
		]);
	}

	/**
	 * @param {number[]} bytes 
	 * @returns {Signature}
	 */
	static fromCbor(bytes) {
		/** @type {null | PubKey} */
		let pubKey = null;

		/** @type {null | number[]} */
		let signature = null;

		let n = Cbor.decodeTuple(bytes, (i, fieldBytes) => {
			switch(i) {
				case 0:
					pubKey = PubKey.fromCbor(fieldBytes);
					break;
				case 1:
					signature = Cbor.decodeBytes(fieldBytes);
					break;
				default:
					throw new Error("unrecognized field");
			}
		});

		assert(n == 2);

		if (pubKey === null || signature === null) {
			throw new Error("unexpected");
		} else {
			return new Signature(pubKey, signature);
		}
	}

	/**
	 * @returns {Object}
	 */
	dump() {
		return {
			pubKey: this.#pubKey.dump,
			pubKeyHash: this.pubKeyHash.dump(),
			signature: bytesToHex(this.#signature),
		};
	}

	/**
	 * Throws error if incorrect
	 * @param {number[]} msg
	 */
	verify(msg) {
		if (this.#signature === null) {
			throw new Error("signature can't be null");
		} else {
			if (this.#pubKey === null) {
				throw new Error("pubKey can't be null");
			} else {
				if (!Ed25519.verify(this.#signature, msg, this.#pubKey.bytes)) {
					throw new Error("incorrect signature");
				}
			}
		}
	}
}

/**
 * @interface
 * @typedef {object} PrivateKey
 * @property {() => PubKey} derivePubKey Generates the corresponding public key.
 * @property {(msg: number[]) => Signature} sign Signs a byte-array payload, returning the signature.
 */

/**
 * @implements {PrivateKey}
 */
export class Ed25519PrivateKey extends HeliosData {
	/**
	 * @type {number[]}
	 */
	#bytes;

	/**
	 * cache the derived pubKey
	 * @type {null | PubKey}
	 */
	#pubKey

	/**
	 * @param {string | number[]} bytes
	 */
	constructor(bytes) {
		super();
		this.#bytes = Array.isArray(bytes) ? bytes : hexToBytes(bytes);
		this.#pubKey = null;
	}

 	/**
     * Generate a private key from a random number generator.
	 * This is not cryptographically secure, only use this for testing purpose
     * @param {NumberGenerator} random 
     * @returns {Ed25519PrivateKey} - Ed25519 private key is 32 bytes long
     */
	static random(random) {
		return new Ed25519PrivateKey(randomBytes(random, 32));
	}

	/**
	 * @type {number[]}
	 */
	get bytes() {
		return this.#bytes;
	}

	/**
	 * @type {string}
	 */
	get hex() {
		return bytesToHex(this.#bytes);
	}

	/**
	 * NOT the Ed25519-Bip32 hierarchial extension algorithm (see ExtendedPrivateKey below)
	 * @returns {Ed25519PrivateKey}
	 */
	extend() {
		return new Ed25519PrivateKey(Crypto.sha2_512(this.#bytes));
	}

	/**
	 * @returns {PubKey}
	 */
	derivePubKey() {
		if (this.#pubKey) {
			return this.#pubKey;
		} else {
			this.#pubKey = new PubKey(Ed25519.derivePublicKey(this.#bytes));
			
			return this.#pubKey;
		}
	}

	/**
	 * @param {number[]} message 
	 * @returns {Signature}
	 */
	sign(message) {
		return new Signature(
			this.derivePubKey(),
			Ed25519.sign(message, this.#bytes)
		);
	}
}

/**
 * Used during `Bip32PrivateKey` derivation, to create a new `Bip32PrivateKey` instance with a non-publicly deriveable `PubKey`.
 */
export const BIP32_HARDEN = 0x80000000;

/**
 * Ed25519-Bip32 extendable `PrivateKey`.
 * @implements {PrivateKey}
 */
export class Bip32PrivateKey {
	/**
	 * 96 bytes
	 * @type {number[]}
	 */
	#bytes;

	/**
	 * @type {PubKey | null}
	 */
	#pubKey;

	/**
	 * @param {number[]} bytes
	 */
	constructor(bytes) {
		assert(bytes.length == 96);
		this.#bytes = bytes;
		this.#pubKey = null;
	}

	/**
	 * @type {number[]}
	 */
	get bytes() {
		return this.#bytes.slice();
	}

	/**
	 * @private
	 * @type {number[]}
	 */
	get k() {
		return this.#bytes.slice(0, 64);
	}

	/**
	 * @private
	 * @type {number[]}
	 */
	get kl() {
		return this.#bytes.slice(0, 32);
	}

	/**
	 * @private
	 * @type {number[]}
	 */
	get kr() {
		return this.#bytes.slice(32, 64);
	}

	/**
	 * @private
	 * @type {number[]}
	 */
	get c() {
		return this.#bytes.slice(64, 96);
	}

	/**
     * Generate a Bip32PrivateKey from a random number generator.
	 * This is not cryptographically secure, only use this for testing purpose
     * @param {NumberGenerator} random 
     * @returns {Bip32PrivateKey}
     */
	static random(random = Crypto.rand(Math.random())) {
		return new Bip32PrivateKey(randomBytes(random, 96));
	}

	/**
	 * @param {number[]} entropy
	 * @param {boolean} force
	 */
	static fromBip39Entropy(entropy, force = true) {
		const bytes = Crypto.pbkdf2(Crypto.hmacSha2_512, [], entropy, 4096, 96);

		const kl = bytes.slice(0, 32);
		const kr = bytes.slice(32, 64);

		if (!force) {
			assert((kl[31] & 0b00100000) == 0, "invalid root secret");
		}

		kl[0]  &= 0b11111000;
		kl[31] &= 0b00011111;
		kl[31] |= 0b01000000;

		const c = bytes.slice(64, 96);

		return new Bip32PrivateKey(kl.concat(kr).concat(c));
	}

	/**
	 * @private
	 * @param {number} i - child index
	 */
	calcChildZ(i) {
		const ib = bigIntToBytes(BigInt(i)).reverse();
		while (ib.length < 4) {
			ib.push(0);
		}

		assert(ib.length == 4, "child index too big");
			
		if (i < BIP32_HARDEN) {
			const A = this.derivePubKey().bytes;
			
			return Crypto.hmacSha2_512(this.c, [0x02].concat(A).concat(ib));
		} else {
			return Crypto.hmacSha2_512(this.c, [0x00].concat(this.k).concat(ib));
		}
	}

	/**
	 * @private
	 * @param {number} i 
	 */
	calcChildC(i) {
		const ib = bigIntToBytes(BigInt(i)).reverse();
		while (ib.length < 4) {
			ib.push(0);
		}

		assert(ib.length == 4, "child index too big");
			
		if (i < BIP32_HARDEN) {
			const A = this.derivePubKey().bytes;
			
			return Crypto.hmacSha2_512(this.c, [0x03].concat(A).concat(ib));
		} else {
			return Crypto.hmacSha2_512(this.c, [0x01].concat(this.k).concat(ib));
		}
	}

	/**
	 * @param {number} i
	 * @returns {Bip32PrivateKey}
	 */
	derive(i) {
		const Z = this.calcChildZ(i);

		const kl = bigIntToLe32Bytes(8n*leBytesToBigInt(Z.slice(0, 28)) + leBytesToBigInt(this.kl)).slice(0, 32);
		const kr = bigIntToLe32Bytes(leBytesToBigInt(Z.slice(32, 64)) + (leBytesToBigInt(this.kr)%115792089237316195423570985008687907853269984665640564039457584007913129639936n)).slice(0, 32);

		const c = this.calcChildC(i).slice(32, 64);

		// TODO: discard child key whose public key is the identity point
		return new Bip32PrivateKey(kl.concat(kr).concat(c));
	}

	/**
	 * @param {number[]} path 
	 * @returns {Bip32PrivateKey}
	 */
	derivePath(path) {
		/**
		 * @type {Bip32PrivateKey}
		 */
		let pk = this;

		path.forEach(i => {
			pk = pk.derive(i);
		});

		return pk;
	}

	/**
	 * @returns {PubKey}
	 */ 
	derivePubKey() {
		if (this.#pubKey) {
			return this.#pubKey;
		} else {
			this.#pubKey = new PubKey(Ed25519.deriveBip32PublicKey(this.k));

			return this.#pubKey;
		}
	}

	/**
	 * @example
	 * (new Bip32PrivateKey([0x60, 0xd3, 0x99, 0xda, 0x83, 0xef, 0x80, 0xd8, 0xd4, 0xf8, 0xd2, 0x23, 0x23, 0x9e, 0xfd, 0xc2, 0xb8, 0xfe, 0xf3, 0x87, 0xe1, 0xb5, 0x21, 0x91, 0x37, 0xff, 0xb4, 0xe8, 0xfb, 0xde, 0xa1, 0x5a, 0xdc, 0x93, 0x66, 0xb7, 0xd0, 0x03, 0xaf, 0x37, 0xc1, 0x13, 0x96, 0xde, 0x9a, 0x83, 0x73, 0x4e, 0x30, 0xe0, 0x5e, 0x85, 0x1e, 0xfa, 0x32, 0x74, 0x5c, 0x9c, 0xd7, 0xb4, 0x27, 0x12, 0xc8, 0x90, 0x60, 0x87, 0x63, 0x77, 0x0e, 0xdd, 0xf7, 0x72, 0x48, 0xab, 0x65, 0x29, 0x84, 0xb2, 0x1b, 0x84, 0x97, 0x60, 0xd1, 0xda, 0x74, 0xa6, 0xf5, 0xbd, 0x63, 0x3c, 0xe4, 0x1a, 0xdc, 0xee, 0xf0, 0x7a])).sign(textToBytes("Hello World")).bytes == [0x90, 0x19, 0x4d, 0x57, 0xcd, 0xe4, 0xfd, 0xad, 0xd0, 0x1e, 0xb7, 0xcf, 0x16, 0x17, 0x80, 0xc2, 0x77, 0xe1, 0x29, 0xfc, 0x71, 0x35, 0xb9, 0x77, 0x79, 0xa3, 0x26, 0x88, 0x37, 0xe4, 0xcd, 0x2e, 0x94, 0x44, 0xb9, 0xbb, 0x91, 0xc0, 0xe8, 0x4d, 0x23, 0xbb, 0xa8, 0x70, 0xdf, 0x3c, 0x4b, 0xda, 0x91, 0xa1, 0x10, 0xef, 0x73, 0x56, 0x38, 0xfa, 0x7a, 0x34, 0xea, 0x20, 0x46, 0xd4, 0xbe, 0x04]
	 * @param {number[]} message 
	 * @returns {Signature}
	 */
	sign(message) {
		return new Signature(
			this.derivePubKey(),
			Ed25519.signBip32(message, this.k)
		);
	}
}

/**
 * @implements {PrivateKey}
 */
export class RootPrivateKey {
	#entropy;
	#key;

	/**
	 * @param {number[]} entropy 
	 */
	constructor(entropy) {
		assert(entropy.length == 16 || entropy.length == 20 || entropy.length == 24 || entropy.length == 28 || entropy.length == 32, `expected 16, 20, 24, 28 or 32 bytes for the root entropy, got ${entropy.length}`);
		
		this.#entropy = entropy;
		this.#key = Bip32PrivateKey.fromBip39Entropy(entropy);
	}

	/**
	 * @param {string[]} phrase 
	 * @param {string[]} dict 
	 * @returns {boolean}
	 */
	static isValidPhrase(phrase, dict = BIP39_DICT_EN) {
		if (phrase.length != 12 && phrase.length != 15 && phrase.length != 18 && phrase.length != 21 && phrase.length != 24) {
			return false;
		} else {
			return phrase.every(w => dict.findIndex(dw => dw == w) != -1);
		}
	}

	/**
	 * @param {string[]} phrase 
	 * @param {string[]} dict 
	 * @returns {RootPrivateKey}
	 */
	static fromPhrase(phrase, dict = BIP39_DICT_EN) {
		assert(phrase.length == 12 || phrase.length == 15 || phrase.length == 18 || phrase.length == 21 || phrase.length == 24, `expected phrase with 12, 15, 18, 21 or 24 words, got ${phrase.length} words`);

		const bw = new BitWriter();

		phrase.forEach(w => {
			const i = dict.findIndex(dw => dw == w);
			assert(i != -1, `invalid phrase, ${w} not found in dict`);

			bw.write(padZeroes(i.toString(2), 11));
		});

		const nChecksumBits = phrase.length/3;
		assert(nChecksumBits%1.0 == 0.0, "bad nChecksumBits");
		assert(nChecksumBits >= 4.0 && nChecksumBits <= 8.0, "too many or too few nChecksumBits");

		const checksum = bw.pop(nChecksumBits);

		const bytes = bw.finalize(false);

		assert(padZeroes(Crypto.sha2_256(bytes)[0].toString(2).slice(0, nChecksumBits), nChecksumBits) == checksum, "invalid checksum");

		return new RootPrivateKey(bytes);
	}

	/**
	 * @type {number[]}
	 */
	get bytes() {
		return this.#key.bytes;
	}

	/**
	 * @type {number[]}
	 */
	get entropy() {
		return this.#entropy;
	}

	/**
	 * @param {string[]} dict 
	 * @returns {string[]}
	 */
	toPhrase(dict = BIP39_DICT_EN) {
		const nChecksumBits = this.#entropy.length/4;
		const checksum = padZeroes(Crypto.sha2_256(this.#entropy)[0].toString(2).slice(0, nChecksumBits), nChecksumBits);

		/**
		 * @type {string[]}
		 */
		const parts = [];

		this.#entropy.forEach(b => {
			parts.push(padZeroes(b.toString(2), 8));
		});

		parts.push(checksum);

		let bits = parts.join('');

		assert(bits.length%11 == 0.0);

		/**
		 * @type {string[]}
		 */
		const words = [];

		while (bits.length > 0) {
			const part = bits.slice(0, 11);
			assert(part.length == 11, "didn't slice of exactly 11 bits");

			const i = parseInt(part, 2);

			words.push(assertDefined(dict[i], `dict entry ${i} not found`));

			bits = bits.slice(11);
		}

		assert(RootPrivateKey.isValidPhrase(words, dict), "internal error: invalid phrase");

		return words;
	}

	/**
	 * @param {number} i - childIndex
	 * @returns {Bip32PrivateKey}
	 */
	derive(i) {
		return this.#key.derive(i);
	}

	/**
	 * @param {number[]} path 
	 * @returns {Bip32PrivateKey}
	 */
	derivePath(path) {
		return this.#key.derivePath(path);
	}

	/**
	 * @param {number} accountIndex
	 * @returns {Bip32PrivateKey}
	 */
	deriveSpendingRootKey(accountIndex = 0) {
		return this.derivePath([
			1852 + BIP32_HARDEN,
			1815 + BIP32_HARDEN,
			accountIndex + BIP32_HARDEN,
			0
		]);
	}

	/**
	 * @param {number} accountIndex
	 * @returns {Bip32PrivateKey}
	 */
	deriveStakingRootKey(accountIndex) {
		return this.derivePath([
			1852 + BIP32_HARDEN,
			1815 + BIP32_HARDEN,
			accountIndex + BIP32_HARDEN,
			2
		]);
	}

	/**
	 * @param {number} accountIndex
	 * @param {number} i
	 * @returns {Bip32PrivateKey}
	 */
	deriveSpendingKey(accountIndex = 0, i = 0) {
		return this.deriveSpendingRootKey(accountIndex).derive(i);
	}

	/**
	 * @param {number} accountIndex
	 * @param {number} i
	 * @returns {Bip32PrivateKey}
	 */
	deriveStakingKey(accountIndex = 0, i = 0) {
		return this.deriveStakingRootKey(accountIndex).derive(i);
	}

	/**
	 * @returns {PubKey}
	 */ 
	derivePubKey() {
		return this.#key.derivePubKey();
	}

	/**
	 * @param {number[]} message 
	 * @returns {Signature}
	 */
	sign(message) {
		return this.#key.sign(message);
	}
}

/**
 * Base-type of SpendingRedeemer and MintingRedeemer
 */
export class Redeemer extends CborData {
	/** @type {UplcData} */
	#data;

	/** @type {Profile} */
	#profile;

	/**
	 * @type {null | string}
	 */
	#programName;

	/**
	 * @param {UplcData} data 
	 * @param {Profile} profile 
	 */
	constructor(data, profile = {mem: 0n, cpu: 0n}) {
		super();
		this.#data = data;
		this.#profile = profile;
		this.#programName = null;
	}

	/**
	 * @type {UplcData}
	 */
	get data() {
		return this.#data;
	}

	/**
	 * @type {bigint}
	 */
	get memCost() {
		return this.#profile.mem;
	}

	/**
	 * @type {bigint}
	 */
	get cpuCost() {
		return this.#profile.cpu;
	}

	/**
	 * @param {string} name 
	 */
	setProgramName(name) {
		this.#programName = name;
	}

	/**
	 * @type {null | string}
	 */
	get programName() {
		return this.#programName;
	}

	/**
	 * type:
	 *   0 -> spending
	 *   1 -> minting 
	 *   2 -> certifying
	 *   3 -> rewarding
	 * @param {number} type 
	 * @param {number} index 
	 * @returns {number[]}
	 */
	toCborInternal(type, index) {
		return Cbor.encodeTuple([
			Cbor.encodeInteger(BigInt(type)),
			Cbor.encodeInteger(BigInt(index)),
			this.#data.toCbor(),
			Cbor.encodeTuple([
				Cbor.encodeInteger(this.#profile.mem),
				Cbor.encodeInteger(this.#profile.cpu),
			]),
		]);
	}

	/**
	 * @param {number[]} bytes 
	 * @returns {Redeemer}
	 */
	static fromCbor(bytes) {
		/** @type {null | number} */
		let type = null;

		/** @type {null | number} */
		let index = null;

		/** @type {null | UplcData} */
		let data = null;

		/** @type {null | Cost} */
		let cost = null;

		let n = Cbor.decodeTuple(bytes, (i, fieldBytes) => {
			switch(i) {
				case 0:
					type = Number(Cbor.decodeInteger(fieldBytes));
					break;
				case 1:
					index = Number(Cbor.decodeInteger(fieldBytes));
					break;
				case 2:
					data = UplcData.fromCbor(fieldBytes);
					break;
				case 3: 
					/** @type {null | bigint} */
					let mem = null;

					/** @type {null | bigint} */
					let cpu = null;

					let m = Cbor.decodeTuple(fieldBytes, (j, subFieldBytes) => {
						switch (j) {
							case 0:
								mem = Cbor.decodeInteger(subFieldBytes);
								break;
							case 1:
								cpu = Cbor.decodeInteger(subFieldBytes);
								break;
							default:
								throw new Error("unrecognized field");
						}
					});

					assert(m == 2);

					if (mem === null || cpu === null) {
						throw new Error("unexpected");
					} else {
						cost = {mem: mem, cpu: cpu};
					}
					break;
				default:
					throw new Error("unrecognized field");
			}
		});

		assert(n == 4);

		if (type === null || index === null || data === null || cost === null) {
			throw new Error("unexpected");
		} else {

			switch(type) {
				case 0:
					return new SpendingRedeemer(null, index, data, cost);
				case 1:
					return new MintingRedeemer(null, index, data, cost);
				default:
					throw new Error("unhandled redeemer type (Todo)");	
			}
		}
	}

	/**
	 * @returns {Object}
	 */
	dumpInternal() {
		return {
			json: this.#data.toSchemaJson(),
			cbor: this.#data.toCborHex(),
			exUnits: {
				mem: this.#profile.mem.toString(),
				cpu: this.#profile.cpu.toString(),
			}
		}
	}

	/**
	 * @returns {Object}
	 */
	dump() {
		throw new Error("not yet implemented");
	}

	/**
	 * @param {TxBody} body 
	 * @returns {ConstrData}
	 */
	toScriptPurposeData(body) {
		throw new Error("not yet implemented");
	}

	/**
	 * @param {TxBody} body 
	 */
	updateIndex(body) {
		throw new Error("not yet implemented");
	}

	/**
	 * @param {Profile} profile
	 */
	setProfile(profile) {
		this.#profile = profile;
	}

	/**
	 * @type {Profile}
	 */
	get profile() {
		return this.#profile;
	}

	/**
	 * @param {NetworkParams} networkParams 
	 * @returns {bigint}
	 */
	estimateFee(networkParams) {
		// this.#exUnits.mem and this.#exUnits can be 0 if we are estimating the fee for an initial balance
		
		let [memFee, cpuFee] = networkParams.exFeeParams;

		return BigInt(Math.ceil(Number(this.#profile.mem)*memFee + Number(this.#profile.cpu)*cpuFee));
	}
}

export class SpendingRedeemer extends Redeemer {
	#input;
	#inputIndex;

	/**
	 * @param {null | TxInput} input
	 * @param {number} inputIndex
	 * @param {UplcData} data 
	 * @param {Cost} exUnits 
	 */
	constructor(input, inputIndex, data, exUnits = {mem: 0n, cpu: 0n}) {
		super(data, exUnits);

		this.#input = input
		this.#inputIndex = inputIndex;
	}

	/**
	 * @type {number}
	 */
	get inputIndex() {
		return this.#inputIndex;
	}

	/**
	 * @returns {number[]}
	 */
	toCbor() {
		return this.toCborInternal(0, this.#inputIndex);
	}

	/**
	 * @returns {Object}
	 */
	dump() {
		let obj = super.dumpInternal();

		obj["type"] = 0;
		obj["typeName"] = "spending";
		obj["inputIndex"] = this.#inputIndex;

		return obj;
	}

	/**
	 * @param {TxBody} body 
	 * @returns {ConstrData}
	 */
	toScriptPurposeData(body) {
		return new ConstrData(1, [
			body.inputs[this.#inputIndex].toOutputIdData(),
		]);
	}

	/**
	 * @param {TxBody} body
	 */
	updateIndex(body) {
		if (this.#input == null) {
			throw new Error("input can't be null");
		} else {
			this.#inputIndex = body.inputs.findIndex(i => {
				return i.txId.eq(assertDefined(this.#input).txId) && (i.utxoIdx == assertDefined(this.#input).utxoIdx)
			});

			assert(this.#inputIndex != -1);
		}
	}
}

export class MintingRedeemer extends Redeemer {
	#mph;
	#mphIndex;

	/**
	 * @param {?MintingPolicyHash} mph
	 * @param {number} mphIndex
	 * @param {UplcData} data
	 * @param {Cost} exUnits
	 */
	constructor(mph, mphIndex, data, exUnits = {mem: 0n, cpu: 0n}) {
		super(data, exUnits);

		this.#mph = mph;
		this.#mphIndex = mphIndex;
	}

	/**
	 * @type {number}
	 */
	get mphIndex() {
		return this.#mphIndex;
	}

	/**
	 * @returns {number[]}
	 */
	toCbor() {
		return this.toCborInternal(1, this.#mphIndex);
	}

	/** 
	 * @returns {Object}
	 */
	dump() {
		let obj = super.dumpInternal();

		obj["type"] = 1;
		obj["typeName"] = "minting";
		obj["mphIndex"] = this.#mphIndex;

		return obj;
	}

	/**
	 * @param {TxBody} body 
	 * @returns {ConstrData}
	 */
	toScriptPurposeData(body) {
		let mph = body.minted.mintingPolicies[this.#mphIndex];

		return new ConstrData(0, [
			new ByteArrayData(mph.bytes),
		]);
	}

	/**
	 * @param {TxBody} body 
	 */
	updateIndex(body) {
		if (this.#mph === null) {
			throw new Error("can't have null mph at this point");
		} else {
			this.#mphIndex = body.minted.mintingPolicies.findIndex(mph => mph.eq(assertDefined(this.#mph)));

			assert(this.#mphIndex != -1);
		}
	}
}

/**
 * Represents either an inline datum, or a hashed datum.
 * 
 * Inside the Helios language this type is named `OutputDatum` in order to distinguish it from user defined Datums,
 * But outside helios scripts there isn't much sense to keep using the name 'OutputDatum' instead of Datum.
 */
export class Datum extends CborData {
	constructor() {
		super();
	}

	/**
	 * @param {number[]} bytes 
	 * @returns {Datum}
	 */
	static fromCbor(bytes) {
		/** @type {null | number} */
		let type = null;

		/** @type {null | Datum} */
		let res = null;

		let n = Cbor.decodeTuple(bytes, (i, fieldBytes) => {
			switch(i) {
				case 0:
					type = Number(Cbor.decodeInteger(fieldBytes));
					break;
				case 1:
					if (type == 0) {
						res = new HashedDatum(DatumHash.fromCbor(fieldBytes));
					} else if (type == 1) {
						assert(Cbor.decodeTag(fieldBytes) == 24n);

						let dataBytes = Cbor.decodeBytes(fieldBytes);
						let data = UplcData.fromCbor(dataBytes);

						res = new InlineDatum(data);
					}
					break;
				default:
					throw new Error("unrecognized field label");
			}
		});

		assert(n == 2);

		if (type === null || res === null) {
			throw new Error("unexpected");
		} else {
			return res;
		}
	}

	/**
	 * @param {UplcData} data
	 * @returns {null | Datum}
	 */
	static fromUplcData(data) {
		if (data.index == 0) {
			assert(data.fields.length == 0);
			return null;
		} else if (data.index == 1) {
			assert(data.fields.length == 1);
			return new HashedDatum(DatumHash.fromUplcData(data.fields[0]));
		} else if (data.index == 2) {
			assert(data.fields.length == 1);
			return new InlineDatum(data.fields[0]);
		} else {
			throw new Error("unhandled constr index");
		}
	}

	/**
	 * Constructs a `HashedDatum`. The input data is hashed internally.
	 * @param {UplcDataValue | UplcData | HeliosData} data
	 * @returns {Datum}
	 */
	static hashed(data) {
		if (data instanceof HeliosData) {
			return HashedDatum.fromData(data._toUplcData());
		} else {
			return HashedDatum.fromData(UplcDataValue.unwrap(data));
		}
	}

	/**
	 * @param {UplcDataValue | UplcData | HeliosData} data
	 * @returns {Datum}
	 */
	static inline(data) {
		if (data instanceof HeliosData) {
			return new InlineDatum(data._toUplcData());
		} else {
			return new InlineDatum(UplcDataValue.unwrap(data));
		}
	}

	/**
	 * @returns {boolean}
	 */
	isInline() {
		throw new Error("not yet implemented");
	}

	/**
	 * @returns {boolean}
	 */
	isHashed() {
		throw new Error("not yet implemented");
	}

	/**
	 * @type {DatumHash}
	 */
	get hash() {
		throw new Error("not yet implemented");
	}

	/**
	 * @type {?UplcData}
	 */
	get data() {
		throw new Error("not yet implemented");
	}

	/**
	 * @returns {Object}
	 */
	dump() {
		throw new Error("not yet implemented");
	}

	/**
	 * @returns {ConstrData}
	 */
	toData() {
		throw new Error("not yet implemented");
	}
}

/**
 * Inside helios this type is named OutputDatum::Hash in order to distinguish it from the user defined Datum,
 * but outside helios scripts there isn't much sense to keep using the name 'OutputDatum' instead of Datum
 */
export class HashedDatum extends Datum {
	/** @type {DatumHash} */
	#hash;

	/** @type {null | UplcData} */
	#origData;

	/**
	 * @param {DatumHash} hash 
	 * @param {null | UplcData} origData
	 */
	constructor(hash, origData = null) {
		super();
		this.#hash = hash;
		this.#origData = origData;

		if (this.#origData !== null) {
			assert(eq(this.#hash.bytes, Crypto.blake2b(this.#origData.toCbor())));
		}
	}

	/**
	 * @returns {boolean}
	 */
	isInline() {
		return false;
	}

	/**
	 * @returns {boolean}
	 */
	isHashed() {
		return true;
	}

	/**
	 * @type {DatumHash}
	 */
	get hash() {
		return this.#hash;
	}

	/**
	 * @type {?UplcData}
	 */
	get data() {
		return this.#origData;
	}

	/**
	 * Used by script context emulation
	 * @returns {ConstrData}
	 */
	toData() {
		return new ConstrData(1, [new ByteArrayData(this.#hash.bytes)]);
	}

	/**
	 * @returns {number[]}
	 */
	toCbor() {
		return Cbor.encodeTuple([
			Cbor.encodeInteger(0n),
			this.#hash.toCbor(),
		]);
	}

	/**
	 * Constructs a `HashedDatum`. The input data is hashed internally.
	 * @param {UplcData} data 
	 * @returns {HashedDatum}
	 */
	static fromData(data) {
		return new HashedDatum(new Hash(Crypto.blake2b(data.toCbor())), data);
	}

	/**
	 * @returns {Object}
	 */
	dump() {
		return {
			hash: this.#hash.dump(),
			cbor: this.#origData === null ? null : bytesToHex(this.#origData.toCbor()),
			schema: this.#origData === null ? null : JSON.parse(this.#origData.toSchemaJson())
		};
	}
}

/**
 * Inside helios this type is named OutputDatum::Inline in order to distinguish it from the user defined Datum,
 * but outside helios scripts there isn't much sense to keep using the name 'OutputDatum' instead of Datum
 */
class InlineDatum extends Datum {
	/** @type {UplcData} */
	#data;

	/**
	 * @param {UplcData} data
	 */
	constructor(data) {
		super();
		this.#data = data;
	}

	/**
	 * @returns {boolean}
	 */
	isInline() {
		return true;
	}

	/**
	 * @returns {boolean}
	 */
	isHashed() {
		return false;
	}

	/**
	 * @type {DatumHash}
	 */
	get hash() {
		return new DatumHash(Crypto.blake2b(this.#data.toCbor()));
	}

	/**
	 * @type {UplcData}
	 */
	get data() {
		return this.#data;
	}

	/**
	 * Used by script context emulation
	 * @returns {ConstrData}
	 */
	toData() {
		return new ConstrData(2, [this.#data]);
	}

	/**
	 * @returns {number[]}
	 */
	toCbor() {
		return Cbor.encodeTuple([
			Cbor.encodeInteger(1n),
			Cbor.encodeTag(24n).concat(Cbor.encodeBytes(this.#data.toCbor()))
		]);
	}

	/**
	 * @returns {Object}
	 */
	dump() {
		return {
			inlineCbor: bytesToHex(this.#data.toCbor()),
			inlineSchema: JSON.parse(this.#data.toSchemaJson())
		};
	}
}

/**
 * The inner 'any' is also Metadata, but jsdoc doesn't allow declaring recursive types
 * Metadata is essentially a JSON schema object
 * @typedef {{map: [any, any][]} | any[] | string | number} Metadata
 */

/**
 * @param {Metadata} metadata 
 * @returns {number[]}
 */
function encodeMetadata(metadata) {
	if (typeof metadata === 'string') {
		return Cbor.encodeUtf8(metadata, true);
	} else if (typeof metadata === 'number') {
		assert(metadata % 1.0 == 0.0);

		return Cbor.encodeInteger(BigInt(metadata));
	} else if (Array.isArray(metadata)) {
		return Cbor.encodeDefList(metadata.map(item => encodeMetadata(item)));
	} else if (metadata instanceof Object && "map" in metadata && Object.keys(metadata).length == 1) {
		let pairs = metadata["map"];

		if (Array.isArray(pairs)) {
			return Cbor.encodeMap(pairs.map(pair => {
				if (Array.isArray(pair) && pair.length == 2) {
					return [
						encodeMetadata(pair[0]),
						encodeMetadata(pair[1])
					];
				} else {
					throw new Error("invalid metadata schema");		
				}
			}));
		} else {
			throw new Error("invalid metadata schema");
		}
	} else {
		throw new Error("invalid metadata schema");
	}
}

/**
 * Shifts bytes to next Cbor element
 * @param {number[]} bytes 
 * @returns {Metadata}
 */
function decodeMetadata(bytes) {
	if (Cbor.isUtf8(bytes)) {
		return Cbor.decodeUtf8(bytes);
	} else if (Cbor.isList(bytes)) {
		/**
		 * @type {Metadata[]}
		 */
		let items = [];

		Cbor.decodeList(bytes, (_, itemBytes) => {
			items.push(decodeMetadata(itemBytes));
		});

		return items;
	} else if (Cbor.isMap(bytes)) {
		/**
		 * @type {[Metadata, Metadata][]}
		 */
		let pairs = [];

		Cbor.decodeMap(bytes, (_, pairBytes) => {
			pairs.push([
				decodeMetadata(pairBytes),
				decodeMetadata(pairBytes)
			]);
		});

		return {"map": pairs};
	} else {
		return Number(Cbor.decodeInteger(bytes));
	}
}

export class TxMetadata {
	/**
	 * @type {Object.<number, Metadata>} 
	 */
	#metadata;

	constructor() {
		this.#metadata = {};
	}

	/**
	 *
	 * @param {number} tag
	 * @param {Metadata} data
	 */
	add(tag, data) {
		this.#metadata[tag] = data;
	}

	/**
	 * @type {number[]}
	 */
	get keys() {
		return Object.keys(this.#metadata).map(key => parseInt(key)).sort();
	}

	/**
	 * @returns {Object}
	 */
	dump() {
		let obj = {};

		for (let key of this.keys) {
			obj[key] =this.#metadata[key];
		}

		return obj;
	}

	/**
	 * @returns {number[]}
	 */
	toCbor() {
		/**
		 * @type {[number[], number[]][]}
		 */
		const pairs = this.keys.map(key => [
			Cbor.encodeInteger(BigInt(key)),
			encodeMetadata(this.#metadata[key])
		]);
		
		return Cbor.encodeMap(pairs);
	}

	/**
	* Decodes a TxMetadata instance from Cbor
	* @param {number[]} data
	* @returns {TxMetadata}
	*/
	static fromCbor(data) {
		const txMetadata = new TxMetadata();

		Cbor.decodeMap(data, (_, pairBytes) => {
			txMetadata.add(
				Number(Cbor.decodeInteger(pairBytes)), 
				decodeMetadata(pairBytes)
			);
		});

		return txMetadata;
	}
}



////////////////////////////////////
// Section 36: Highlighting function
////////////////////////////////////

/**
 * Categories for syntax highlighting
 */
const SyntaxCategory = {
	Normal:     0,
	Comment:    1,
	Literal:    2,
	Symbol:     3,
	Type:       4,
	Keyword:    5,
	Error:      6,
};

/**
 * Returns Uint8Array with the same length as the number of chars in the script.
 * Each resulting byte respresents a different syntax category.
 * This approach should be faster than a RegExp based a approach.
 * @param {string} src
 * @returns {Uint8Array}
 */
export function highlight(src) {
	let n = src.length;

	const SyntaxState = {
		Normal:        0,
		SLComment:     1,
		MLComment:     2,
		String:        3,
		NumberStart:   4,
		HexNumber:     5,
		BinaryNumber:  6,
		OctalNumber:   7,
		DecimalNumber: 8,
		ByteArray:     9,
	};

	// array of categories
	let data = new Uint8Array(n);

	let j = 0; // position in data
	let state = SyntaxState.Normal;

	/** @type {SymbolToken[]} */
	let groupStack = [];
	
	for (let i = 0; i < n; i++) {
		let c = src[i];
		let isLast = i == n - 1;

		switch (state) {
			case SyntaxState.Normal:
				if (c == "/") {
					// maybe comment
					if (!isLast && src[i+1] == "/") {
						data[j++] = SyntaxCategory.Comment;
						data[j++] = SyntaxCategory.Comment;
		
						i++;
						state = SyntaxState.SLComment;
					} else if (!isLast && src[i+1] == "*") {
						data[j++] = SyntaxCategory.Comment;
						data[j++] = SyntaxCategory.Comment;

						i++;
						state = SyntaxState.MLComment;
					} else {
						data[j++] = SyntaxCategory.Symbol;
					}
				} else if (c == "[" || c == "]" || c == "{" || c == "}" || c == "(" || c == ")") {
					let s = new SymbolToken(new Site(new Source(src, ""), i), c);

					if (Group.isOpenSymbol(s)) {
						groupStack.push(s);
						data[j++] = SyntaxCategory.Normal;
					} else {
						let prevGroup = groupStack.pop();

						if (prevGroup === undefined) {
							data[j++] = SyntaxCategory.Error;
						} else if (c == Group.matchSymbol(prevGroup)) {
							data[j++] = SyntaxCategory.Normal;
						} else {
							data[prevGroup.site.startPos] = SyntaxCategory.Error;
							data[j++] = SyntaxCategory.Error;
						}
					}
				} else if (c == "%" || c == "!" || c == "&" || c == "*" || c == "+" || c == "-" || c == "<" || c == "=" || c == ">" || c == "|") {
					// symbol
					switch (c) {
						case "&":
							if (!isLast && src[i+1] == "&") {
								data[j++] = SyntaxCategory.Symbol;
								data[j++] = SyntaxCategory.Symbol;
								i++;
							} else {
								data[j++] = SyntaxCategory.Normal;
							}
							break;
						case "|":
							if (!isLast && src[i+1] == "|") {
								data[j++] = SyntaxCategory.Symbol;
								data[j++] = SyntaxCategory.Symbol;
								i++;
							} else {
								data[j++] = SyntaxCategory.Normal;
							}
							break;
						case "!":
							if (!isLast && src[i+1] == "=") {
								data[j++] = SyntaxCategory.Symbol;
								data[j++] = SyntaxCategory.Symbol;
								i++;
							} else {
								data[j++] = SyntaxCategory.Symbol;
							}
							break;
						case "=":
							if (!isLast && (src[i+1] == "=" || src[i+1] == ">")) {
								data[j++] = SyntaxCategory.Symbol;
								data[j++] = SyntaxCategory.Symbol;
								i++;
							} else {
								data[j++] = SyntaxCategory.Symbol;
							}
							break;
						case ">":
							if (!isLast && src[i+1] == "=") {
								data[j++] = SyntaxCategory.Symbol;
								data[j++] = SyntaxCategory.Symbol;
								i++;
							} else {
								data[j++] = SyntaxCategory.Symbol;
							}
							break;
						case "<":
							if (!isLast && src[i+1] == "=") {
								data[j++] = SyntaxCategory.Symbol;
								data[j++] = SyntaxCategory.Symbol;
								i++;
							} else {
								data[j++] = SyntaxCategory.Symbol;
							}
							break;
						case "-":
							if (!isLast && src[i+1] == ">") {
								data[j++] = SyntaxCategory.Symbol;
								data[j++] = SyntaxCategory.Symbol;
								i++;
							} else {
								data[j++] = SyntaxCategory.Symbol;
							}
							break;
						default:
							data[j++] = SyntaxCategory.Symbol;
					}
				} else if (c == "\"") {
					// literal string
					data[j++] = SyntaxCategory.Literal;
					state = SyntaxState.String;
				} else if (c == "0") {
					// literal number
					data[j++] = SyntaxCategory.Literal;
					state = SyntaxState.NumberStart;
				} else if (c >= "1" && c <= "9") {
					// literal decimal number
					data[j++] = SyntaxCategory.Literal;
					state = SyntaxState.DecimalNumber;
				} else if (c == "#") {
					data[j++] = SyntaxCategory.Literal;
					state = SyntaxState.ByteArray;
				} else if ((c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || c == "_") {
					// maybe keyword, builtin type, or boolean
					let i0 = i;
					let chars = [c];
					// move i to the last word char
					while (i + 1 < n) {
						let d = src[i+1];

						if ((d >= "a" && d <= "z") || (d >= "A" && d <= "Z") || d == "_" || (d >= "0" && d <= "9")) {
							chars.push(d);
							i++;
						} else {
							break;
						}
					}

					let word = chars.join("");
					/** @type {number} */
					let type;
					switch (word) {
						case "true":
						case "false":
							type = SyntaxCategory.Literal;
							break;
						case "Bool":
						case "Int":
						case "ByteArray":
						case "String":
						case "Option":
							type = SyntaxCategory.Type;
							break;
						case "if":
						case "else":
						case "switch":
						case "func":
						case "const":
						case "struct":
						case "enum":
						case "import":
						case "print":
						case "error":
						case "self":
							type = SyntaxCategory.Keyword;
							break;
						case "testing":
						case "spending":
						case "staking":
						case "minting":
						case "endpoint":
						case "module":
							if (i0 == 0) {
								type = SyntaxCategory.Keyword;
							} else {
								type = SyntaxCategory.Normal;
							}
							break;
						default:
							type = SyntaxCategory.Normal;
					}

					for (let ii = i0; ii < i0 + chars.length; ii++) {
						data[j++] = type;
					}
				} else {
					data[j++] = SyntaxCategory.Normal;
				}
				break;
			case SyntaxState.SLComment:
				data[j++] = SyntaxCategory.Comment;
				if (c == "\n") {
					state = SyntaxState.Normal;
				}
				break;
			case SyntaxState.MLComment:
				data[j++] = SyntaxCategory.Comment;

				if (c == "*" && !isLast && src[i+1] == "/") {
					i++;
					data[j++] = SyntaxCategory.Comment;
					state = SyntaxState.Normal;
				}
				break;
			case SyntaxState.String:
				data[j++] = SyntaxCategory.Literal;

				if (c == "\"") {
					state = SyntaxState.Normal;
				}
				break;
			case SyntaxState.NumberStart:
				if (c == "x") {
					data[j++] = SyntaxCategory.Literal;
					state = SyntaxState.HexNumber;
				} else if (c == "o") {
					data[j++] = SyntaxCategory.Literal;
					state = SyntaxState.OctalNumber;
				} else if (c == "b") {
					data[j++] = SyntaxCategory.Literal;
					state = SyntaxState.BinaryNumber;
				} else if (c >= "0" && c <= "9") {
					data[j++] = SyntaxCategory.Literal;
					state = SyntaxState.DecimalNumber;
				} else {
					i--;
					state = SyntaxState.Normal;
				}
				break;
			case SyntaxState.DecimalNumber:
				if (c >= "0" && c <= "9") {
					data[j++] = SyntaxCategory.Literal;
				} else {
					i--;
					state = SyntaxState.Normal;
				}
				break;
			case SyntaxState.HexNumber:
			case SyntaxState.ByteArray:
				if ((c >= "a" && c <= "f") || (c >= "0" && c <= "9")) {
					data[j++] = SyntaxCategory.Literal;
				} else {
					i--;
					state = SyntaxState.Normal;
				}
				break;
			case SyntaxState.OctalNumber:
				if (c >= "0" && c <= "7") {
					data[j++] = SyntaxCategory.Literal;
				} else {
					i--;
					state = SyntaxState.Normal;
				}
				break;
			case SyntaxState.BinaryNumber:
				if (c == "0" || c == "1") {
					data[j++] = SyntaxCategory.Literal;
				} else {
					i--;
					state = SyntaxState.Normal;
				}
				break;
			default:
				throw new Error("unhandled SyntaxState");
		}		
	}

	for (let s of groupStack) {
		data[s.site.startPos] = SyntaxCategory.Error;
	}

	return data;
}


////////////////////////////
// Section 37: CoinSelection
////////////////////////////

/**
 * Returns two lists. The first list contains the selected UTxOs, the second list contains the remaining UTxOs.
 * @typedef {(utxos: TxInput[], amount: Value) => [TxInput[], TxInput[]]} CoinSelectionAlgorithm
 */

/**
 * Collection of common [coin selection algorithms](https://cips.cardano.org/cips/cip2/).
 * @namespace
 */
export const CoinSelection = {
    /**
     * @internal
     * @param {TxInput[]} utxos 
     * @param {Value} amount 
     * @param {boolean} largestFirst
     * @returns {[TxInput[], TxInput[]]} - [picked, not picked that can be used as spares]
     */
    selectExtremumFirst: (utxos, amount, largestFirst) => {
        let sum = new Value();

        /** @type {TxInput[]} */
        let notSelected = utxos.slice();

        /** @type {TxInput[]} */
        const selected = [];

        /**
         * Selects smallest utxos until 'needed' is reached
         * @param {bigint} neededQuantity
         * @param {(utxo: TxInput) => bigint} getQuantity
         */
        function select(neededQuantity, getQuantity) {
            // first sort notYetPicked in ascending order when picking smallest first,
            // and in descending order when picking largest first
            // sort UTxOs that contain more assets last
            notSelected.sort((a, b) => {
                const qa = getQuantity(a);
                const qb = getQuantity(b);

                const sign = largestFirst ? -1 : 1;

                if (qa != 0n && qb == 0n) {
                    return sign;
                } else if (qa == 0n && qb != 0n) {
                    return -sign;
                } else if (qa == 0n && qb == 0n) {
                    return 0;
                } else {
                    const na = a.value.assets.nTokenTypes;
                    const nb = b.value.assets.nTokenTypes;

                    if (na == nb) {
                        return Number(qa - qb)*sign;
                    } else if (na < nb) {
                        return sign;
                    } else {
                        return -sign
                    }
                }
            });

            let count = 0n;
            const remaining = [];

            while (count < neededQuantity || count == 0n) { // must select at least one utxo if neededQuantity == 0n
                const utxo = notSelected.shift();

                if (utxo === undefined) {
                    console.error(selected.map(s => JSON.stringify(s.dump(), undefined, "  ")));
                    console.error(JSON.stringify(amount.dump(), undefined, "  "));
                    throw new Error("not enough utxos to cover amount");
                } else {
                    const qty = getQuantity(utxo);

                    if (qty > 0n) {
                        count += qty;
                        selected.push(utxo);
                        sum = sum.add(utxo.value);
                    } else {
                        remaining.push(utxo);
                    }
                }
            }

            notSelected = notSelected.concat(remaining);
        }

        /**
         * Select UTxOs while looping through (MintingPolicyHash,TokenName) entries
         */
        const mphs = amount.assets.mintingPolicies;

        for (const mph of mphs) {
            const tokenNames = amount.assets.getTokenNames(mph);

            for (const tokenName of tokenNames) {
                const need = amount.assets.get(mph, tokenName);
                const have = sum.assets.get(mph, tokenName);

                if (have < need) {
                    const diff = need - have;

                    select(diff, (utxo) => utxo.value.assets.get(mph, tokenName));
                }
            }
        }

        // now use the same strategy for lovelace
        const need = amount.lovelace;
        const have = sum.lovelace;

        if (have < need) {
            const diff = need - have;

            select(diff, (utxo) => utxo.value.lovelace);
        }

        assert(selected.length + notSelected.length == utxos.length, "internal error: select algorithm doesn't conserve utxos");

        return [selected, notSelected];
    },

    /**
     * Selects UTxOs from a list by iterating through the tokens in the given `Value` and picking the UTxOs containing the smallest corresponding amount first.
     * This method can be used to eliminate dust UTxOs from a wallet.
     * @type {CoinSelectionAlgorithm}
     */
    selectSmallestFirst: (utxos, amount) => {
        return CoinSelection.selectExtremumFirst(utxos, amount, false);
    },

    /**
     * * Selects UTxOs from a list by iterating through the tokens in the given `Value` and picking the UTxOs containing the largest corresponding amount first.
     * @type {CoinSelectionAlgorithm}
     */
    selectLargestFirst: (utxos, amount) => {
        return CoinSelection.selectExtremumFirst(utxos, amount, true);
    }
}


//////////////////////
// Section 38: Wallets
//////////////////////

/**
 * An interface type for a wallet that manages a user's UTxOs and addresses.
 * @interface
 * @typedef {object} Wallet
 * @property {() => Promise<boolean>} isMainnet Returns `true` if the wallet is connected to the mainnet.
 * @property {Promise<StakeAddress[]>} rewardAddresses Returns a list of the reward addresses.
 * @property {Promise<Address[]>} usedAddresses Returns a list of addresses which already contain UTxOs.
 * @property {Promise<Address[]>} unusedAddresses Returns a list of unique unused addresses which can be used to send UTxOs to with increased anonimity.
 * @property {Promise<TxInput[]>} utxos Returns a list of all the utxos controlled by the wallet.
 * @property {Promise<TxInput[]>} collateral
 * @property {(addr: Address, sigStructure: string) => Promise<{signature: string, key: string}>} signData Signs a message, returning an object containing the signature and key that can be used to verify/authenticate the message later.
 * @property {(tx: Tx) => Promise<Signature[]>} signTx Signs a transaction, returning a list of signatures needed for submitting a valid transaction.
 * @property {(tx: Tx) => Promise<TxId>} submitTx Submits a transaction to the blockchain and returns the id of that transaction upon success.
 */

/**
 * Convenience type for browser plugin wallets supporting the CIP 30 dApp connector standard (eg. Eternl, Nami, ...).
 * 
 * This is useful in typescript projects to avoid type errors when accessing the handles in `window.cardano`.
 * 
 * ```ts
 * // refer to this file in the 'typeRoots' list in tsconfig.json
 *
 * type Cip30SimpleHandle = {
 *   name: string,
 *   icon: string,
 *   enable(): Promise<helios.Cip30Handle>,
 *   isEnabled(): boolean
 * }
 *
 * declare global {
 *   interface Window {
 *     cardano: {
 *       [walletName: string]: Cip30SimpleHandle
 *     };
 *   }
 * }
 * ```
 * 
 * @typedef {{
 *     getNetworkId(): Promise<number>,
 *     getUsedAddresses(): Promise<string[]>,
 *     getUnusedAddresses(): Promise<string[]>,
 *     getUtxos(): Promise<string[]>,
 *     getCollateral(): Promise<string[]>,
 *     getRewardAddresses(): Promise<string[]>,
 *     signData(addr: string, sigStructure: string): Promise<{signature: string, key: string}>,
 *     signTx(txHex: string, partialSign: boolean): Promise<string>,
 *     submitTx(txHex: string): Promise<string>,
 *     experimental: {
 *         getCollateral(): Promise<string[]>
 *     },
 * }} Cip30Handle
 */

/**
 * Implementation of `Wallet` that lets you connect to a browser plugin wallet.
 * @implements {Wallet}
 */
export class Cip30Wallet {
    #handle;

    /**
     * Constructs Cip30Wallet using the Cip30Handle which is available in the browser window.cardano context.
     * 
     * ```ts
     * const handle: helios.Cip30Handle = await window.cardano.eternl.enable()
     * const wallet = new helios.Cip30Wallet(handle)
     * ```
     * @param {Cip30Handle} handle
     */
    constructor(handle) {
        this.#handle = handle;
    }

    /**
     * Returns `true` if the wallet is connected to the mainnet.
     * @returns {Promise<boolean>}
     */
    async isMainnet() {
        return (await this.#handle.getNetworkId()) == 1;
    }

    /**
     * Gets a list of unique reward addresses which can be used to UTxOs to.
     * @type {Promise<StakeAddress[]>}
     */
     get rewardAddresses() {
        return this.#handle.getRewardAddresses().then(
            addresses => {
                if (!Array.isArray(addresses)) {
                    throw new Error(`The wallet getRewardAddresses() call did not return an array.`);
                }

                return addresses.map(a => new StakeAddress(hexToBytes(a)));
            });
    }

    /**
     * Gets a list of addresses which contain(ed) UTxOs.
     * @type {Promise<Address[]>}
     */
    get usedAddresses() {
        return this.#handle.getUsedAddresses().then(addresses => addresses.map(a => new Address(a)));
    }

    /**
     * Gets a list of unique unused addresses which can be used to UTxOs to.
     * @type {Promise<Address[]>}
     */
    get unusedAddresses() {
        return this.#handle.getUnusedAddresses().then(addresses => addresses.map(a => new Address(a)));
    }

    /**
     * Gets the complete list of UTxOs (as `TxInput` instances) sitting at the addresses owned by the wallet.
     * @type {Promise<TxInput[]>}
     */
    get utxos() {
        return this.#handle.getUtxos().then(utxos => utxos.map(u => TxInput.fromFullCbor(hexToBytes(u))));
    }

    /**
     * @type {Promise<TxInput[]>}
     */
    get collateral() {
        const getCollateral = this.#handle.getCollateral || this.#handle.experimental.getCollateral;
        return getCollateral().then(utxos => utxos.map(u => TxInput.fromFullCbor(hexToBytes(u))));
    }

    /**
     * Sign a data payload with the users wallet.
     *
     * @param {Address} addr - A Cardano address object
     * @param {string} sigStructure - The message to sign, in string format.
     * @return {Promise<{signature: string, key: string}>}
     */
    async signData(addr, sigStructure) {
        if (!(addr instanceof Address)) {
            throw new Error(`The value in the addr parameter is not a Cardano Address object.`);
        } else if (typeof sigStructure !== 'string' || sigStructure.length < 1) {
            throw new Error(`The sigStructure parameter is empty or invalid.  Must be a non-empty string`);
        }

        // Convert the string to a hex string since that is what
        //  the underlying signData() method expects.
        const hexStr = bytesToHex(textToBytes(sigStructure));

        return await this.#handle.signData(addr.toHex(), hexStr);
    }

    /**
     * Signs a transaction, returning a list of signatures needed for submitting a valid transaction.
     * @param {Tx} tx
     * @returns {Promise<Signature[]>}
     */
    async signTx(tx) {
        const res = await this.#handle.signTx(bytesToHex(tx.toCbor()), true);

        return TxWitnesses.fromCbor(hexToBytes(res)).signatures;
    }

    /**
     * Submits a transaction to the blockchain.
     * @param {Tx} tx
     * @returns {Promise<TxId>}
     */
    async submitTx(tx) {
        const responseText = await this.#handle.submitTx(bytesToHex(tx.toCbor()));

        return new TxId(responseText);
    }
}

/**
 * High-level helper class for instances that implement the `Wallet` interface.
 */
export class WalletHelper {
    #wallet;
    #getUtxosFallback;

    /**
     * @param {Wallet} wallet
     * @param {undefined | ((addr: Address[]) => Promise<TxInput[]>)} getUtxosFallback
     */
    constructor(wallet, getUtxosFallback = undefined) {
        this.#wallet = wallet;
        this.#getUtxosFallback = getUtxosFallback;
    }

    /**
     * Concatenation of `usedAddresses` and `unusedAddresses`.
     * @type {Promise<Address[]>}
     */
    get allAddresses() {
        return this.#wallet.usedAddresses.then(usedAddress => this.#wallet.unusedAddresses.then(unusedAddresses => usedAddress.concat(unusedAddresses)));
    }

    /**
     * @returns {Promise<Value>}
     */
    async calcBalance() {
        let sum = new Value();

        const utxos = await this.getUtxos();

        for (const utxo of utxos) {
            sum = sum.add(utxo.value);
        }

        return sum;
    }

    /**
     * First `Address` in `allAddresses`.
     * @type {Promise<Address>}
     */
    get baseAddress() {
        return this.allAddresses.then(addresses => assertDefined(addresses[0]));
    }

    /**
     * First `Address` in `unusedAddresses` (falls back to last `Address` in `usedAddresses` if not defined).
     * @type {Promise<Address>}
     */
    get changeAddress() {
        return this.#wallet.unusedAddresses.then(addresses => {
            if (addresses.length == 0) {
                return this.#wallet.usedAddresses.then(addresses => {
                    if (addresses.length == 0) {
                        throw new Error("no addresses found")
                    } else {
                        return addresses[addresses.length-1];
                    }
                })
            } else {
                return addresses[0];
            }
        });
    }

    /**
     * First UTxO in `utxos`. Can be used to distinguish between preview and preprod networks.
     * @type {Promise<null | TxInput>}
     */
    get refUtxo() {
        return this.getUtxos().then(utxos => {
            if(utxos.length == 0) {
                return null;
            } else {
                return assertDefined(utxos[0]);
            }
        });
    }

    /**
     * @returns {Promise<TxInput[]>}
     */
    async getUtxos() {
        try {
            const utxos = await this.#wallet.utxos;

            if (utxos.length > 0) {
                return utxos;
            }
        } catch (e) {
            if (!this.#getUtxosFallback) {
                console.error("fallback not set");
                throw e;
            }
        }

        if (this.#getUtxosFallback) {
            console.log("falling back to retrieving UTxOs through query layer");
            return this.#getUtxosFallback(await this.#wallet.usedAddresses);
        } else {
            throw new Error("wallet returned 0 utxos, set the helper getUtxosFallback callback to use an Api query layer instead");
        }
    }
    /**
     * Pick a number of UTxOs needed to cover a given Value. The default coin selection strategy is to pick the smallest first.
     * @param {Value} amount
     * @param {CoinSelectionAlgorithm} algorithm
     * @returns {Promise<[TxInput[], TxInput[]]>} The first list contains the selected UTxOs, the second list contains the remaining UTxOs.
     */
    async pickUtxos(amount, algorithm = CoinSelection.selectSmallestFirst) {
        return algorithm(await this.getUtxos(), amount);
    }

    /**
     * Picks a single UTxO intended as collateral.
     * @param {bigint} amount - 2 Ada should cover most things
     * @returns {Promise<TxInput>}
     */
    async pickCollateral(amount = 2000000n) {
        const pureUtxos = (await this.getUtxos()).filter(utxo => utxo.value.assets.isZero());

        if (pureUtxos.length == 0) {
            throw new Error("no pure UTxOs in wallet (needed for collateral)");
        }

        const bigEnough = pureUtxos.filter(utxo => utxo.value.lovelace >= amount);

        if (bigEnough.length == 0) {
            throw new Error("no UTxO in wallet that is big enough to cover collateral");
        }

        bigEnough.sort((a,b) => Number(a.value.lovelace - b.value.lovelace));

        return bigEnough[0];
    }

    /**
     * Returns `true` if the `PubKeyHash` in the given `Address` is controlled by the wallet.
     * @param {Address} addr
     * @returns {Promise<boolean>}
     */
    async isOwnAddress(addr) {
        const pkh = addr.pubKeyHash;

        if (pkh === null) {
            return false;
        } else {
            return this.isOwnPubKeyHash(pkh);
        }
    }

    /**
     * Returns `true` if the given `PubKeyHash` is controlled by the wallet.
     * @param {PubKeyHash} pkh
     * @returns {Promise<boolean>}
     */
    async isOwnPubKeyHash(pkh) {
        const addresses = await this.allAddresses;

        for (const addr of addresses) {
            const aPkh = addr.pubKeyHash;

            if (aPkh !== null && aPkh.eq(pkh)) {
                return true;
            }
        }

        return false;
    }

    /**
     * @returns {Promise<any>}
     */
    async toJson() {
        const isMainnet = (await this.#wallet.isMainnet());
        const usedAddresses = (await this.#wallet.usedAddresses);
        const unusedAddresses = (await this.#wallet.unusedAddresses);

        return {
            isMainnet: isMainnet,
            usedAddresses: usedAddresses.map(a => a.toBech32()),
            unusedAddresses: unusedAddresses.map(a => a.toBech32()),
            utxos: (await this.getUtxos()).map(u => bytesToHex(u.toFullCbor()))
        };
    }
}

/**
 * @implements {Wallet}
 */
export class RemoteWallet {
    #isMainnet;
    #usedAddresses;
    #unusedAddresses;
    #utxos;

    /**
     * @param {boolean} isMainnet
     * @param {Address[]} usedAddresses 
     * @param {Address[]} unusedAddresses 
     * @param {TxInput[]} utxos 
     */
    constructor(isMainnet, usedAddresses, unusedAddresses, utxos) {
        this.#isMainnet = isMainnet;
        this.#usedAddresses = usedAddresses;
        this.#unusedAddresses = unusedAddresses;
        this.#utxos = utxos;
    }

    /**
     * @param {string | Object} obj 
     * @returns {RemoteWallet}
     */
    static fromJson(obj) {
        if (typeof obj == "string") {
            return RemoteWallet.fromJson(JSON.parse(obj));
        } else {
            return new RemoteWallet(
                obj.isMainnet,
                obj.usedAddresses.map(a => Address.fromBech32(a)),
                obj.unusedAddresses.map(a => Address.fromBech32(a)),
                obj.utxos.map(u => TxInput.fromFullCbor(u))
            )
        }
    }

    /**
     * @returns {Promise<boolean>}
     */
    async isMainnet() {
        return this.#isMainnet;
    }

    /**
     * @type {Promise<StakeAddress[]>}
     */
    get rewardAddresses() {
        throw new Error("not yet implemented")
    }

    /**
     * @type {Promise<Address[]>}
     */
    get usedAddresses() {
        return new Promise((resolve, _) => resolve(this.#usedAddresses));
    }
    
    /**
     * @type {Promise<Address[]>}
     */
    get unusedAddresses() {
        return new Promise((resolve, _) => resolve(this.#unusedAddresses));
    }

    /**
     * @type {Promise<TxInput[]>}
     */
    get utxos() {
        return new Promise((resolve, _) => resolve(this.#utxos));
    }

    /**
     * @type {Promise<TxInput[]>}
     */
    get collateral() {
        return new Promise((resolve, _) => resolve([]));
    }

    /**
     * @param {Address} addr
     * @param {string} message
     * @return {Promise<{signature: string, key: string}>}
     */
    async signData(addr, message) {
        throw new Error("not yet implemented")
    }

    /**
     * @param {Tx} tx 
     * @returns {Promise<Signature[]>}
     */
    async signTx(tx) {
        throw new Error("a RemoteWallet can't sign a transaction");
    }

    /**
     * @param {Tx} tx 
     * @returns {Promise<TxId>}
     */
    async submitTx(tx) {
        throw new Error("a RemoteWallet can't submit a transaction");
    }
}



//////////////////////
// Section 39: Network
//////////////////////


/**
 * Blockchain query interface.
 * @interface
 * @typedef {object} Network
 * @property {(address: Address) => Promise<TxInput[]>} getUtxos Returns a complete list of UTxOs at a given address.
 * @property {(id: TxOutputId) => Promise<TxInput>} getUtxo Returns a single TxInput (that might already have been spent).
 * @property {() => Promise<NetworkParams>} getParameters Returns the latest network parameters.
 * @property {(tx: Tx) => Promise<TxId>} submitTx Submits a transaction to the blockchain and returns the id of that transaction upon success.
 */

/**
 * Blockfrost specific implementation of `Network`.
 * @implements {Network}
 */
export class BlockfrostV0 {
    #networkName;
    #projectId;

    /**
     * Constructs a BlockfrostV0 using the network name (preview, preprod or mainnet) and your Blockfrost `project_id`.
     * @param {"preview" | "preprod" | "mainnet"} networkName
     * @param {string} projectId
     */
    constructor(networkName, projectId) {
        this.#networkName = networkName;
        this.#projectId = projectId
    }

    /**
     * @type {string}
     */
    get networkName() {
        return this.#networkName;
    }

    /**
     * Throws an error if a Blockfrost project_id is missing for that specific network.
     * @param {TxInput} refUtxo
     * @param {{
     *     preview?: string,
     *     preprod?: string,
     *     mainnet?: string
     * }} projectIds
     * @returns {Promise<BlockfrostV0>}
     */
    static async resolveUsingUtxo(refUtxo, projectIds) {
        const mainnetProjectId = projectIds["mainnet"];
        const preprodProjectId = projectIds["preprod"];
        const previewProjectId = projectIds["preview"];

        if (preprodProjectId !== undefined) {
            const preprodNetwork = new BlockfrostV0("preprod", preprodProjectId);

            if (await preprodNetwork.hasUtxo(refUtxo)) {
                return preprodNetwork;
            }
        }

        if (previewProjectId !== undefined) {
            const previewNetwork = new BlockfrostV0("preview", previewProjectId);

            if (await previewNetwork.hasUtxo(refUtxo)) {
                return previewNetwork;
            }
        }

        if (mainnetProjectId !== undefined) {
            const mainnetNetwork = new BlockfrostV0("mainnet", mainnetProjectId);

            if (await mainnetNetwork.hasUtxo(refUtxo)) {
                return mainnetNetwork;
            }
        }

        throw new Error("refUtxo not found on a network for which you have a project id");
    }

    /**
     * Connects to the same network a given `Wallet` is connected to (preview, preprod or mainnet).
     * 
     * Throws an error if a Blockfrost project_id is missing for that specific network.
     * @param {Wallet} wallet
     * @param {{
     *     preview?: string,
     *     preprod?: string,
     *     mainnet?: string
     * }} projectIds
     * @returns {Promise<BlockfrostV0>}
     */
    static async resolveUsingWallet(wallet, projectIds) {
        if (await wallet.isMainnet()) {
            return new BlockfrostV0("mainnet", assertDefined(projectIds["mainnet"]));
        } else {
            const helper = new WalletHelper(wallet);

            const refUtxo = await helper.refUtxo;

            if (refUtxo === null) {
                throw new Error("empty wallet, can't determine which testnet you are connecting to");
            } else {
                return BlockfrostV0.resolveUsingUtxo(refUtxo, projectIds);
            }
        }
    }

     /**
     * Connects to the same network a given `Wallet` or the given `TxInput` (preview, preprod or mainnet).
     * 
     * Throws an error if a Blockfrost project_id is missing for that specific network.
     * @param {TxInput | Wallet} utxoOrWallet
     * @param {{
     *     preview?: string,
     *     preprod?: string,
     *     mainnet?: string
     * }} projectIds
     * @returns {Promise<BlockfrostV0>}
     */
    static async resolve(utxoOrWallet, projectIds) {
        if (utxoOrWallet instanceof TxInput) {
            return BlockfrostV0.resolveUsingUtxo(utxoOrWallet, projectIds);
        } else {
            return BlockfrostV0.resolveUsingWallet(utxoOrWallet, projectIds);
        }
    }

    /**
     * @internal
     * @param {{unit: string, quantity: string}[]} obj
     * @returns {Value}
     */
    static parseValue(obj) {
        let value = new Value();

        for (let item of obj) {
            let qty = BigInt(item.quantity);

            if (item.unit == "lovelace") {
                value = value.add(new Value(qty));
            } else {
                let policyID = item.unit.substring(0, 56);
                let mph = MintingPolicyHash.fromHex(policyID);

                let token = hexToBytes(item.unit.substring(56));

                value = value.add(new Value(0n, new Assets([
                    [mph, [
                        [token, qty]
                    ]]
                ])));
            }
        }

        return value;
    }

    /**
     * @returns {Promise<NetworkParams>}
     */
    async getParameters() {
        const response = await fetch(`https://d1t0d7c2nekuk0.cloudfront.net/${this.#networkName}.json`);

        // TODO: build networkParams from Blockfrost endpoints instead
        return new NetworkParams(await response.json());
    }

    /**
     * @returns {Promise<any>}
     */
    async getLatestEpoch() {
        const response = await fetch(`https://cardano-${this.#networkName}.blockfrost.io/api/v0/epochs/latest`, {
            method: "GET",
            headers: {
                "project_id": this.#projectId
            }
        });

        return (await response.json());
    }

    /**
     * If the UTxO isn't found an error is throw with the following message format: "UTxO <txId.utxoId> not found".
     * @param {TxOutputId} id
     * @returns {Promise<TxInput>}
     */
    async getUtxo(id) {
        const txId = id.txId;

        const url = `https://cardano-${this.#networkName}.blockfrost.io/api/v0/txs/${txId.hex}/utxos`;

        const response = await fetch(url, {
            method: "GET",
            headers: {
                "project_id": this.#projectId
            }
        });

        if (!response.ok) {
            throw new Error(`UTxO ${id.toString()} not found`);
        } else if (response.status != 200) {
            throw new Error(`Blockfrost error: ${await response.text()}`);
        }

        const responseObj = await response.json();

        
        
        const outputs = responseObj.outputs;

        if (!outputs) {
            console.log(responseObj);
            throw new Error(`unexpected response from Blockfrost`);
        }

        const obj = outputs[id.utxoIdx];

        if (!obj) {
            console.log(responseObj);
            throw new Error(`UTxO ${id.toString()} not found`);
        }

        obj["tx_hash"] = txId.hex;
        obj["output_index"] = Number(id.utxoIdx);

        return await this.restoreTxInput(obj);
    }

    /**
     * Used by `BlockfrostV0.resolve()`.
     * @param {TxInput} utxo
     * @returns {Promise<boolean>}
     */
    async hasUtxo(utxo) {
        const txId = utxo.outputId.txId;

        const url = `https://cardano-${this.#networkName}.blockfrost.io/api/v0/txs/${txId.hex}/utxos`;

        const response = await fetch(url, {
            method: "GET",
            headers: {
                "project_id": this.#projectId
            }
        });

        return response.ok;
    }

    /**
     * @internal
     * @param {{
     *   address: string
     *   tx_hash: string
     *   output_index: number
     *   amount: {unit: string, quantity: string}[]
     *   inline_datum: null | string
     *   data_hash: null | string
     *   collateral: boolean
     *   reference_script_hash: null | string
     * }} obj 
     */
    async restoreTxInput(obj) {
        /**
         * @type {null | UplcProgram}
         */
        let refScript = null;
        if (obj.reference_script_hash !== null) {
            const url = `https://cardano-${this.#networkName}.blockfrost.io/api/v0/scripts/${obj.reference_script_hash}/cbor`;

            const response = await fetch(url, {
                method: "GET",
                headers: {
                    "project_id": this.#projectId
                }
            });

            const cbor = (await response.json()).cbor;

            refScript = UplcProgram.fromCbor(cbor);
        }

        return new TxInput(
            new TxOutputId(TxId.fromHex(obj.tx_hash), obj.output_index),
            new TxOutput(
                Address.fromBech32(obj.address),
                BlockfrostV0.parseValue(obj.amount),
                obj.inline_datum ? Datum.inline(UplcData.fromCbor(hexToBytes(obj.inline_datum))) : null,
                refScript
            )
        );
    }

    /**
     * Gets a complete list of UTxOs at a given `Address`.
     * Returns oldest UTxOs first, newest last.
     * @param {Address} address
     * @returns {Promise<TxInput[]>}
     */
    async getUtxos(address) {
        /**
         * TODO: pagination
         */

        const url = `https://cardano-${this.#networkName}.blockfrost.io/api/v0/addresses/${address.toBech32()}/utxos?order=asc`;

        try {
            const response = await fetch(url, {
                headers: {
                    "project_id": this.#projectId
                }
            });

            if (response.status == 404) {
                return []; 
            }

            /**
             * @type {any}
             */
            let all = await response.json();

            if (all?.status_code >= 300) {
                all = [];
            }

            try {
                return await Promise.all(all.map(obj => {
                    return this.restoreTxInput(obj);
                }));
            } catch (e) {
                console.error("unable to parse blockfrost utxo format:", all);
                throw e;
            }
        } catch (e) {
            if (e.message.includes("The requested component has not been found")) {
                return []
            } else {
                throw e
            }
        }
    }

    /**
     * Submits a transaction to the blockchain.
     * @param {Tx} tx
     * @returns {Promise<TxId>}
     */
    async submitTx(tx) {
        const data = new Uint8Array(tx.toCbor());
        const url = `https://cardano-${this.#networkName}.blockfrost.io/api/v0/tx/submit`;

        const response = await fetch(url, {
            method: "POST",
            headers: {
                "content-type": "application/cbor",
                "project_id": this.#projectId
            },
            body: data
        }).catch(e => {
            console.error(e);
            throw e;
        });

        const responseText = await response.text();

        if (response.status != 200) {
            // analyze error and throw a different error if it was detected that an input UTxO might not exist
            throw new Error(responseText);
        } else {
            return new TxId(JSON.parse(responseText));
        }
    }

    /**
     * Allows inspecting the live Blockfrost mempool.
     */
    async dumpMempool() {
        const url = `https://cardano-${this.#networkName}.blockfrost.io/api/v0/mempool`;

        const response = await fetch(url, {
            method: "GET",
            headers: {
                "project_id": this.#projectId
            }
        });

        console.log(await response.text());
    }
}

/**
 * Koios network interface.
 * @implements {Network}
 */
export class KoiosV0 {
    #networkName;

    /**
     * @param {"preview" | "preprod" | "mainnet"} networkName 
     */
    constructor(networkName) {
        this.#networkName = networkName;
    }

    /**
     * @private
     * @type {string}
     */
    get rootUrl() {
        return {
            preview: "https://preview.koios.rest",
            preprod: "https://preprod.koios.rest",
            guildnet: "https://guild.koios.rest",
            mainnet: "https://api.koios.rest"
        }[this.#networkName];
    }

     /**
     * @returns {Promise<NetworkParams>}
     */
     async getParameters() {
        const response = await fetch(`https://d1t0d7c2nekuk0.cloudfront.net/${this.#networkName}.json`);

        // TODO: build networkParams from Koios endpoints instead
        return new NetworkParams(await response.json());
    }

    /**
     * @private
     * @param {TxOutputId[]} ids 
     * @returns {Promise<TxInput[]>}
     */
    async getUtxosInternal(ids) {
        const url = `${this.rootUrl}/api/v0/tx_info`;

        /**
         * @type {Map<string, number[]>}
         */
        const txIds = new Map();
        
        ids.forEach(id => {
            const prev = txIds.get(id.txId.hex);

            if (prev) {
                prev.push(id.utxoIdx);
            } else {
                txIds.set(id.txId.hex, [id.utxoIdx]);
            }
        });

        const response = await fetch(url, {
            method: "POST",
            headers: {
                "accept": "application/json",
                "content-type": "application/json"
            },
            body: JSON.stringify({
                _tx_hashes: Array.from(txIds.keys())
            })
        });

        const responseText = await response.text();

        if (response.status != 200) {
            // analyze error and throw a different error if it was detected that an input UTxO might not exist
            throw new Error(responseText);
        }

        const obj = JSON.parse(responseText);

        /**
         * @type {Map<string, TxInput>}
         */
        const result = new Map();

        const rawTxs = obj;

        if (!Array.isArray(rawTxs)) {
            throw new Error(`unexpected tx_info format: ${responseText}`);
        }

        rawTxs.forEach(rawTx => {
            const rawOutputs = rawTx["outputs"];
            
            if (!rawOutputs) {
                throw new Error(`unexpected tx_info format: ${JSON.stringify(rawTx)}`);
            }

            const utxoIdxs = assertDefined(txIds.get(rawTx.tx_hash));

            for (let utxoIdx of utxoIdxs) {
                const id = new TxOutputId(new TxId(rawTx.tx_hash), utxoIdx);
                
                const rawOutput = rawOutputs[id.utxoIdx]

                if (!rawOutput) {
                    throw new Error(`UTxO ${id.toString()} doesn't exist`);
                }

                const rawPaymentAddr = rawOutput.payment_addr?.bech32;

                if (!rawPaymentAddr || typeof rawPaymentAddr != "string") {
                    throw new Error(`unexpected tx_info format: ${JSON.stringify(rawTx)}`);
                }

                const rawStakeAddr = rawOutput.stake_addr;

                if (rawStakeAddr === undefined) {
                    throw new Error(`unexpected tx_info format: ${JSON.stringify(rawTx)}`);
                }

                const paymentAddr = Address.fromBech32(rawPaymentAddr);
                
                const stakeAddr = rawStakeAddr ? StakeAddress.fromBech32(rawStakeAddr) : null;

                const address = Address.fromHashes(
                    assertDefined(paymentAddr.pubKeyHash ?? paymentAddr.validatorHash),
                    stakeAddr?.stakingHash ?? null,
                    this.#networkName != "mainnet"
                );

                const lovelace = BigInt(parseInt(assertDefined(rawOutput.value)));

                assert(lovelace.toString() == rawOutput.value, `unexpected tx_info format: ${JSON.stringify(rawTx)}`)

                /**
                 * @type {[AssetClass, bigint][]}
                 */
                const assets = [];

                for (let rawAsset of rawOutput.asset_list) {
                    const qty = BigInt(parseInt(rawAsset.quantity));
                    assert(qty.toString() == rawAsset.quantity, `unexpected tx_info format: ${JSON.stringify(rawTx)}`)

                    assets.push([
                        new AssetClass(`${rawAsset.policy_id}.${rawAsset.asset_name ?? ""}`),
                        qty
                    ]);
                }

                const datum = rawOutput.inline_datum ? 
                    (Datum.inline(UplcData.fromCbor(rawOutput.inline_datum.bytes))) : 
                    (rawOutput.datum_hash ? new HashedDatum(new DatumHash(rawOutput.datum_hash)) : null);

                const refScript = rawOutput.reference_script ? UplcProgram.fromCbor(rawOutput.reference_script) : null;

                const txInput =  new TxInput(
                    id,
                    new TxOutput(
                        address,
                        new Value(lovelace, new Assets(assets)),
                        datum,
                        refScript
                    )
                );

                result.set(id.toString(), txInput);
            }
        });

        return ids.map(id => assertDefined(result.get(id.toString())));
    }

     /**
     * @param {TxInput} refUtxo
     * @returns {Promise<KoiosV0>}
     */
    static async resolveUsingUtxo(refUtxo) {
        const preprodNetwork = new KoiosV0("preprod");

        if (await preprodNetwork.hasUtxo(refUtxo)) {
            return preprodNetwork;
        }
        
        const previewNetwork = new KoiosV0("preview");

        if (await previewNetwork.hasUtxo(refUtxo)) {
            return previewNetwork;
        }

        const mainnetNetwork = new KoiosV0("mainnet");

        if (await mainnetNetwork.hasUtxo(refUtxo)) {
            return mainnetNetwork;
        }

        throw new Error("refUtxo not found on any network");
    }

    /** 
     * @param {TxOutputId} id 
     * @returns {Promise<TxInput>}
     */
    async getUtxo(id) {
        return assertDefined(await this.getUtxosInternal([id])[0]);
    }

     /**
     * Used by `KoiosV0.resolveUsingUtxo()`.
     * @param {TxInput} utxo
     * @returns {Promise<boolean>}
     */
     async hasUtxo(utxo) {
        const url = `${this.rootUrl}/api/v0/tx_info`;

        const response = await fetch(url, {
            method: "GET",
            headers: {
                "accept": "application/json",
                "content-type": "application/json"
            },
            body: JSON.stringify({
                _tx_hashes: [utxo.outputId.txId.hex]
            })
        });

        return response.ok;
    }

    /**
     * @param {Address} address 
     * @returns {Promise<TxInput[]>}
     */
    async getUtxos(address) {
        const url = `${this.rootUrl}/api/v0/credential_utxos`;

        const response = await fetch(url, {
            method: "POST",
            headers: {
                "accept": "application/json",
                "content-type": "application/json"
            },
            body: JSON.stringify({
                _payment_credentials: [assertDefined(address.pubKeyHash ?? address.validatorHash).hex]
            })
        });

        const responseText = await response.text();

        if (response.status != 200) {
            // analyze error and throw a different error if it was detected that an input UTxO might not exist
            throw new Error(responseText);
        }

        const obj = JSON.parse(responseText);

        if (!Array.isArray(obj)) {
            throw new Error(`unexpected credential_utxos format: ${responseText}`);
        }

        const ids = obj.map(rawId => {
            const utxoIdx = Number(rawId.tx_index);
            const id = new TxOutputId(new TxId(rawId.tx_hash), utxoIdx);

            return id;
        });

        return this.getUtxosInternal(ids);
    }

    /**
     * @param {Tx} tx 
     * @returns {Promise<TxId>}
     */
    async submitTx(tx) {
        const url = `${this.rootUrl}/api/v0/submittx`;

        const response = await fetch(url, {
            method: "POST",
            headers: {
                "accept": "application/json",
                "content-type": "application/cbor"
            },
            body: new Uint8Array(tx.toCbor())
        });

        const responseText = await response.text();

        if (response.status != 200) {
            // analyze error and throw a different error if it was detected that an input UTxO might not exist
            throw new Error(responseText);
        }

        return new TxId(responseText);
    }
}


///////////////////////
// Section 40: Emulator
///////////////////////
/**
 * Raw network parameters used by Emulator
 * @internal
 */
export const rawNetworkEmulatorParams = {
    shelleyGenesis: {
        activeSlotsCoeff: 0.05,
        epochLength: 432000,
        genDelegs: {
            "637f2e950b0fd8f8e3e811c5fbeb19e411e7a2bf37272b84b29c1a0b": {
                delegate: "aae9293510344ddd636364c2673e34e03e79e3eefa8dbaa70e326f7d",
                vrf: "227116365af2ed943f1a8b5e6557bfaa34996f1578eec667a5e2b361c51e4ce7"
            },
            "8a4b77c4f534f8b8cc6f269e5ebb7ba77fa63a476e50e05e66d7051c": {
                delegate: "d15422b2e8b60e500a82a8f4ceaa98b04e55a0171d1125f6c58f8758",
                vrf: "0ada6c25d62db5e1e35d3df727635afa943b9e8a123ab83785e2281605b09ce2"
            },
            "b00470cd193d67aac47c373602fccd4195aad3002c169b5570de1126": {
                delegate: "b3b539e9e7ed1b32fbf778bf2ebf0a6b9f980eac90ac86623d11881a",
                vrf:"0ff0ce9b820376e51c03b27877cd08f8ba40318f1a9f85a3db0b60dd03f71a7a"
            },
            "b260ffdb6eba541fcf18601923457307647dce807851b9d19da133ab": {
                delegate: "7c64eb868b4ef566391a321c85323f41d2b95480d7ce56ad2abcb022",
                vrf: "7fb22abd39d550c9a022ec8104648a26240a9ff9c88b8b89a6e20d393c03098e"
            },
            "ced1599fd821a39593e00592e5292bdc1437ae0f7af388ef5257344a": {
                delegate: "de7ca985023cf892f4de7f5f1d0a7181668884752d9ebb9e96c95059",
                vrf:"c301b7fc4d1b57fb60841bcec5e3d2db89602e5285801e522fce3790987b1124"
            },
            "dd2a7d71a05bed11db61555ba4c658cb1ce06c8024193d064f2a66ae":{
                delegate:"1e113c218899ee7807f4028071d0e108fc790dade9fd1a0d0b0701ee",
                vrf:"faf2702aa4893c877c622ab22dfeaf1d0c8aab98b837fe2bf667314f0d043822"
            },
            "f3b9e74f7d0f24d2314ea5dfbca94b65b2059d1ff94d97436b82d5b4":{
                delegate: "fd637b08cc379ef7b99c83b416458fcda8a01a606041779331008fb9",
                vrf: "37f2ea7c843a688159ddc2c38a2f997ab465150164a9136dca69564714b73268"
            }
        },
        initialFunds: {},
        maxKESEvolutions: 120,
        maxLovelaceSupply: 45000000000000000,
        networkId: "Testnet",
        networkMagic: 1,
        protocolParams: {
            a0:0.1,
            decentralisationParam:1,
            eMax:18,
            extraEntropy:{
                tag: "NeutralNonce"
            },
            keyDeposit:400000,
            maxBlockBodySize:65536,
            maxBlockHeaderSize:1100,
            maxTxSize:16384,
            minFeeA:44,
            minFeeB:155381,
            minPoolCost:0,
            minUTxOValue:0,
            nOpt:50,
            poolDeposit:500000000,
            protocolVersion:{
                major:2,
                minor:0
            },
            rho:0.00178650067,
            tau:0.1
        },
        securityParam: 2160,
        slotLength:1,
        slotsPerKESPeriod:86400,
        staking:{
            pools:{},
            stake:{}
        },
        systemStart:"2022-06-01T00:00:00Z",
        updateQuorum:5
    },
    alonzoGenesis:{
        lovelacePerUTxOWord:34482,
        executionPrices:{
            prSteps:{
                numerator:721,
                denominator:10000000
            },
            prMem:{
                numerator:577,
                denominator:10000
            }
        },
        maxTxExUnits:{
            exUnitsMem:10000000,
            exUnitsSteps:10000000000
        },
        maxBlockExUnits:{
            exUnitsMem:50000000,
            exUnitsSteps:40000000000
        },
        maxValueSize:5000,
        collateralPercentage:150,
        maxCollateralInputs:3,
        costModels:{
            PlutusV1:{
                "sha2_256-memory-arguments":4,
                "equalsString-cpu-arguments-constant":1000,
                "cekDelayCost-exBudgetMemory":100,
                "lessThanEqualsByteString-cpu-arguments-intercept":103599,
                "divideInteger-memory-arguments-minimum":1,
                "appendByteString-cpu-arguments-slope":621,
                "blake2b-cpu-arguments-slope":29175,
                "iData-cpu-arguments":150000,
                "encodeUtf8-cpu-arguments-slope":1000,
                "unBData-cpu-arguments":150000,
                "multiplyInteger-cpu-arguments-intercept":61516,
                "cekConstCost-exBudgetMemory":100,
                "nullList-cpu-arguments":150000,
                "equalsString-cpu-arguments-intercept":150000,
                "trace-cpu-arguments":150000,
                "mkNilData-memory-arguments":32,
                "lengthOfByteString-cpu-arguments":150000,
                "cekBuiltinCost-exBudgetCPU":29773,
                "bData-cpu-arguments":150000,
                "subtractInteger-cpu-arguments-slope":0,
                "unIData-cpu-arguments":150000,
                "consByteString-memory-arguments-intercept":0,
                "divideInteger-memory-arguments-slope":1,
                "divideInteger-cpu-arguments-model-arguments-slope":118,
                "listData-cpu-arguments":150000,
                "headList-cpu-arguments":150000,
                "chooseData-memory-arguments":32,
                "equalsInteger-cpu-arguments-intercept":136542,
                "sha3_256-cpu-arguments-slope":82363,
                "sliceByteString-cpu-arguments-slope":5000,
                "unMapData-cpu-arguments":150000,
                "lessThanInteger-cpu-arguments-intercept":179690,
                "mkCons-cpu-arguments":150000,
                "appendString-memory-arguments-intercept":0,
                "modInteger-cpu-arguments-model-arguments-slope":118,
                "ifThenElse-cpu-arguments":1,
                "mkNilPairData-cpu-arguments":150000,
                "lessThanEqualsInteger-cpu-arguments-intercept":145276,
                "addInteger-memory-arguments-slope":1,
                "chooseList-memory-arguments":32,"constrData-memory-arguments":32,
                "decodeUtf8-cpu-arguments-intercept":150000,
                "equalsData-memory-arguments":1,
                "subtractInteger-memory-arguments-slope":1,
                "appendByteString-memory-arguments-intercept":0,
                "lengthOfByteString-memory-arguments":4,
                "headList-memory-arguments":32,
                "listData-memory-arguments":32,
                "consByteString-cpu-arguments-intercept":150000,
                "unIData-memory-arguments":32,
                "remainderInteger-memory-arguments-minimum":1,
                "bData-memory-arguments":32,
                "lessThanByteString-cpu-arguments-slope":248,
                "encodeUtf8-memory-arguments-intercept":0,
                "cekStartupCost-exBudgetCPU":100,
                "multiplyInteger-memory-arguments-intercept":0,
                "unListData-memory-arguments":32,
                "remainderInteger-cpu-arguments-model-arguments-slope":118,
                "cekVarCost-exBudgetCPU":29773,
                "remainderInteger-memory-arguments-slope":1,
                "cekForceCost-exBudgetCPU":29773,
                "sha2_256-cpu-arguments-slope":29175,
                "equalsInteger-memory-arguments":1,
                "indexByteString-memory-arguments":1,
                "addInteger-memory-arguments-intercept":1,
                "chooseUnit-cpu-arguments":150000,
                "sndPair-cpu-arguments":150000,
                "cekLamCost-exBudgetCPU":29773,
                "fstPair-cpu-arguments":150000,
                "quotientInteger-memory-arguments-minimum":1,
                "decodeUtf8-cpu-arguments-slope":1000,
                "lessThanInteger-memory-arguments":1,
                "lessThanEqualsInteger-cpu-arguments-slope":1366,
                "fstPair-memory-arguments":32,
                "modInteger-memory-arguments-intercept":0,
                "unConstrData-cpu-arguments":150000,
                "lessThanEqualsInteger-memory-arguments":1,
                "chooseUnit-memory-arguments":32,
                "sndPair-memory-arguments":32,
                "addInteger-cpu-arguments-intercept":197209,
                "decodeUtf8-memory-arguments-slope":8,
                "equalsData-cpu-arguments-intercept":150000,
                "mapData-cpu-arguments":150000,
                "mkPairData-cpu-arguments":150000,
                "quotientInteger-cpu-arguments-constant":148000,
                "consByteString-memory-arguments-slope":1,
                "cekVarCost-exBudgetMemory":100,
                "indexByteString-cpu-arguments":150000,
                "unListData-cpu-arguments":150000,
                "equalsInteger-cpu-arguments-slope":1326,
                "cekStartupCost-exBudgetMemory":100,
                "subtractInteger-cpu-arguments-intercept":197209,
                "divideInteger-cpu-arguments-model-arguments-intercept":425507,
				"divideInteger-memory-arguments-intercept":0,
				"cekForceCost-exBudgetMemory":100,
				"blake2b-cpu-arguments-intercept":2477736,
				"remainderInteger-cpu-arguments-constant":148000,
				"tailList-cpu-arguments":150000,
				"encodeUtf8-cpu-arguments-intercept":150000,
				"equalsString-cpu-arguments-slope":1000,
				"lessThanByteString-memory-arguments":1,
				"multiplyInteger-cpu-arguments-slope":11218,
				"appendByteString-cpu-arguments-intercept":396231,
				"lessThanEqualsByteString-cpu-arguments-slope":248,
				"modInteger-memory-arguments-slope":1,
				"addInteger-cpu-arguments-slope":0,
				"equalsData-cpu-arguments-slope":10000,
				"decodeUtf8-memory-arguments-intercept":0,
				"chooseList-cpu-arguments":150000,
				"constrData-cpu-arguments":150000,
				"equalsByteString-memory-arguments":1,
				"cekApplyCost-exBudgetCPU":29773,
				"quotientInteger-memory-arguments-slope":1,
				"verifySignature-cpu-arguments-intercept":3345831,
				"unMapData-memory-arguments":32,
				"mkCons-memory-arguments":32,
				"sliceByteString-memory-arguments-slope":1,
				"sha3_256-memory-arguments":4,
				"ifThenElse-memory-arguments":1,
				"mkNilPairData-memory-arguments":32,
				"equalsByteString-cpu-arguments-slope":247,
				"appendString-cpu-arguments-intercept":150000,
				"quotientInteger-cpu-arguments-model-arguments-slope":118,
				"cekApplyCost-exBudgetMemory":100,
				"equalsString-memory-arguments":1,
				"multiplyInteger-memory-arguments-slope":1,
				"cekBuiltinCost-exBudgetMemory":100,
				"remainderInteger-memory-arguments-intercept":0,
				"sha2_256-cpu-arguments-intercept":2477736,
				"remainderInteger-cpu-arguments-model-arguments-intercept":425507,
				"lessThanEqualsByteString-memory-arguments":1,
				"tailList-memory-arguments":32,
				"mkNilData-cpu-arguments":150000,
				"chooseData-cpu-arguments":150000,
				"unBData-memory-arguments":32,
				"blake2b-memory-arguments":4,
				"iData-memory-arguments":32,
				"nullList-memory-arguments":32,
				"cekDelayCost-exBudgetCPU":29773,
				"subtractInteger-memory-arguments-intercept":1,
				"lessThanByteString-cpu-arguments-intercept":103599,
				"consByteString-cpu-arguments-slope":1000,
				"appendByteString-memory-arguments-slope":1,
				"trace-memory-arguments":32,
				"divideInteger-cpu-arguments-constant":148000,
				"cekConstCost-exBudgetCPU":29773,
				"encodeUtf8-memory-arguments-slope":8,
				"quotientInteger-cpu-arguments-model-arguments-intercept":425507,
				"mapData-memory-arguments":32,
				"appendString-cpu-arguments-slope":1000,
				"modInteger-cpu-arguments-constant":148000,
				"verifySignature-cpu-arguments-slope":1,
				"unConstrData-memory-arguments":32,
				"quotientInteger-memory-arguments-intercept":0,
				"equalsByteString-cpu-arguments-constant":150000,
				"sliceByteString-memory-arguments-intercept":0,
				"mkPairData-memory-arguments":32,
				"equalsByteString-cpu-arguments-intercept":112536,
				"appendString-memory-arguments-slope":1,
				"lessThanInteger-cpu-arguments-slope":497,
				"modInteger-cpu-arguments-model-arguments-intercept":425507,
				"modInteger-memory-arguments-minimum":1,
				"sha3_256-cpu-arguments-intercept":0,
				"verifySignature-memory-arguments":1,
				"cekLamCost-exBudgetMemory":100,
				"sliceByteString-cpu-arguments-intercept":150000
			}
		}
	},
	latestParams:{
		collateralPercentage:150,
		costModels:{
			PlutusScriptV1:{
				"addInteger-cpu-arguments-intercept":205665,
				"addInteger-cpu-arguments-slope":812,
				"addInteger-memory-arguments-intercept":1,
				"addInteger-memory-arguments-slope":1,
				"appendByteString-cpu-arguments-intercept":1000,
				"appendByteString-cpu-arguments-slope":571,
				"appendByteString-memory-arguments-intercept":0,
				"appendByteString-memory-arguments-slope":1,
				"appendString-cpu-arguments-intercept":1000,
				"appendString-cpu-arguments-slope":24177,
				"appendString-memory-arguments-intercept":4,
				"appendString-memory-arguments-slope":1,
				"bData-cpu-arguments":1000,
				"bData-memory-arguments":32,
				"blake2b_256-cpu-arguments-intercept":117366,
				"blake2b_256-cpu-arguments-slope":10475,
				"blake2b_256-memory-arguments":4,
				"cekApplyCost-exBudgetCPU":23000,
				"cekApplyCost-exBudgetMemory":100,
				"cekBuiltinCost-exBudgetCPU":23000,
				"cekBuiltinCost-exBudgetMemory":100,
				"cekConstCost-exBudgetCPU":23000,
				"cekConstCost-exBudgetMemory":100,
				"cekDelayCost-exBudgetCPU":23000,
				"cekDelayCost-exBudgetMemory":100,
				"cekForceCost-exBudgetCPU":23000,
				"cekForceCost-exBudgetMemory":100,
				"cekLamCost-exBudgetCPU":23000,
				"cekLamCost-exBudgetMemory":100,
				"cekStartupCost-exBudgetCPU":100,
				"cekStartupCost-exBudgetMemory":100,
				"cekVarCost-exBudgetCPU":23000,
				"cekVarCost-exBudgetMemory":100,
				"chooseData-cpu-arguments":19537,
				"chooseData-memory-arguments":32,
				"chooseList-cpu-arguments":175354,
				"chooseList-memory-arguments":32,
				"chooseUnit-cpu-arguments":46417,
				"chooseUnit-memory-arguments":4,
				"consByteString-cpu-arguments-intercept":221973,
				"consByteString-cpu-arguments-slope":511,
				"consByteString-memory-arguments-intercept":0,
				"consByteString-memory-arguments-slope":1,
				"constrData-cpu-arguments":89141,
				"constrData-memory-arguments":32,
				"decodeUtf8-cpu-arguments-intercept":497525,
				"decodeUtf8-cpu-arguments-slope":14068,
				"decodeUtf8-memory-arguments-intercept":4,
				"decodeUtf8-memory-arguments-slope":2,
				"divideInteger-cpu-arguments-constant":196500,
				"divideInteger-cpu-arguments-model-arguments-intercept":453240,
				"divideInteger-cpu-arguments-model-arguments-slope":220,
				"divideInteger-memory-arguments-intercept":0,
				"divideInteger-memory-arguments-minimum":1,
				"divideInteger-memory-arguments-slope":1,
				"encodeUtf8-cpu-arguments-intercept":1000,
				"encodeUtf8-cpu-arguments-slope":28662,
				"encodeUtf8-memory-arguments-intercept":4,
				"encodeUtf8-memory-arguments-slope":2,
				"equalsByteString-cpu-arguments-constant":245000,
				"equalsByteString-cpu-arguments-intercept":216773,
				"equalsByteString-cpu-arguments-slope":62,
				"equalsByteString-memory-arguments":1,
				"equalsData-cpu-arguments-intercept":1060367,
				"equalsData-cpu-arguments-slope":12586,
				"equalsData-memory-arguments":1,
				"equalsInteger-cpu-arguments-intercept":208512,
				"equalsInteger-cpu-arguments-slope":421,
				"equalsInteger-memory-arguments":1,
				"equalsString-cpu-arguments-constant":187000,
				"equalsString-cpu-arguments-intercept":1000,
				"equalsString-cpu-arguments-slope":52998,
				"equalsString-memory-arguments":1,
				"fstPair-cpu-arguments":80436,
				"fstPair-memory-arguments":32,
				"headList-cpu-arguments":43249,
				"headList-memory-arguments":32,
				"iData-cpu-arguments":1000,
				"iData-memory-arguments":32,
				"ifThenElse-cpu-arguments":80556,
				"ifThenElse-memory-arguments":1,
				"indexByteString-cpu-arguments":57667,
				"indexByteString-memory-arguments":4,
				"lengthOfByteString-cpu-arguments":1000,
				"lengthOfByteString-memory-arguments":10,
				"lessThanByteString-cpu-arguments-intercept":197145,
				"lessThanByteString-cpu-arguments-slope":156,
				"lessThanByteString-memory-arguments":1,
				"lessThanEqualsByteString-cpu-arguments-intercept":197145,
				"lessThanEqualsByteString-cpu-arguments-slope":156,
				"lessThanEqualsByteString-memory-arguments":1,
				"lessThanEqualsInteger-cpu-arguments-intercept":204924,
				"lessThanEqualsInteger-cpu-arguments-slope":473,
				"lessThanEqualsInteger-memory-arguments":1,
				"lessThanInteger-cpu-arguments-intercept":208896,
				"lessThanInteger-cpu-arguments-slope":511,
				"lessThanInteger-memory-arguments":1,
				"listData-cpu-arguments":52467,
				"listData-memory-arguments":32,
				"mapData-cpu-arguments":64832,
				"mapData-memory-arguments":32,
				"mkCons-cpu-arguments":65493,
				"mkCons-memory-arguments":32,
				"mkNilData-cpu-arguments":22558,
				"mkNilData-memory-arguments":32,
				"mkNilPairData-cpu-arguments":16563,
				"mkNilPairData-memory-arguments":32,
				"mkPairData-cpu-arguments":76511,
				"mkPairData-memory-arguments":32,
				"modInteger-cpu-arguments-constant":196500,
				"modInteger-cpu-arguments-model-arguments-intercept":453240,
				"modInteger-cpu-arguments-model-arguments-slope":220,
				"modInteger-memory-arguments-intercept":0,
				"modInteger-memory-arguments-minimum":1,
				"modInteger-memory-arguments-slope":1,
				"multiplyInteger-cpu-arguments-intercept":69522,
				"multiplyInteger-cpu-arguments-slope":11687,
				"multiplyInteger-memory-arguments-intercept":0,
				"multiplyInteger-memory-arguments-slope":1,
				"nullList-cpu-arguments":60091,
				"nullList-memory-arguments":32,
				"quotientInteger-cpu-arguments-constant":196500,
				"quotientInteger-cpu-arguments-model-arguments-intercept":453240,
				"quotientInteger-cpu-arguments-model-arguments-slope":220,
				"quotientInteger-memory-arguments-intercept":0,
				"quotientInteger-memory-arguments-minimum":1,
				"quotientInteger-memory-arguments-slope":1,
				"remainderInteger-cpu-arguments-constant":196500,
				"remainderInteger-cpu-arguments-model-arguments-intercept":453240,
				"remainderInteger-cpu-arguments-model-arguments-slope":220,
				"remainderInteger-memory-arguments-intercept":0,
				"remainderInteger-memory-arguments-minimum":1,
				"remainderInteger-memory-arguments-slope":1,
				"sha2_256-cpu-arguments-intercept":806990,
				"sha2_256-cpu-arguments-slope":30482,
				"sha2_256-memory-arguments":4,
				"sha3_256-cpu-arguments-intercept":1927926,
				"sha3_256-cpu-arguments-slope":82523,
				"sha3_256-memory-arguments":4,
				"sliceByteString-cpu-arguments-intercept":265318,
				"sliceByteString-cpu-arguments-slope":0,
				"sliceByteString-memory-arguments-intercept":4,
				"sliceByteString-memory-arguments-slope":0,
				"sndPair-cpu-arguments":85931,
				"sndPair-memory-arguments":32,
				"subtractInteger-cpu-arguments-intercept":205665,
				"subtractInteger-cpu-arguments-slope":812,
				"subtractInteger-memory-arguments-intercept":1,
				"subtractInteger-memory-arguments-slope":1,
				"tailList-cpu-arguments":41182,
				"tailList-memory-arguments":32,
				"trace-cpu-arguments":212342,
				"trace-memory-arguments":32,
				"unBData-cpu-arguments":31220,
				"unBData-memory-arguments":32,
				"unConstrData-cpu-arguments":32696,
				"unConstrData-memory-arguments":32,
				"unIData-cpu-arguments":43357,
				"unIData-memory-arguments":32,
				"unListData-cpu-arguments":32247,
				"unListData-memory-arguments":32,
				"unMapData-cpu-arguments":38314,
				"unMapData-memory-arguments":32,
				"verifyEd25519Signature-cpu-arguments-intercept":9462713,
				"verifyEd25519Signature-cpu-arguments-slope":1021,
				"verifyEd25519Signature-memory-arguments":10
			},
			PlutusScriptV2:{
				"addInteger-cpu-arguments-intercept":205665,
				"addInteger-cpu-arguments-slope":812,
				"addInteger-memory-arguments-intercept":1,
				"addInteger-memory-arguments-slope":1,
				"appendByteString-cpu-arguments-intercept":1000,
				"appendByteString-cpu-arguments-slope":571,
				"appendByteString-memory-arguments-intercept":0,
				"appendByteString-memory-arguments-slope":1,
				"appendString-cpu-arguments-intercept":1000,
				"appendString-cpu-arguments-slope":24177,
				"appendString-memory-arguments-intercept":4,
				"appendString-memory-arguments-slope":1,
				"bData-cpu-arguments":1000,
				"bData-memory-arguments":32,
				"blake2b_256-cpu-arguments-intercept":117366,
				"blake2b_256-cpu-arguments-slope":10475,
				"blake2b_256-memory-arguments":4,
				"cekApplyCost-exBudgetCPU":23000,
				"cekApplyCost-exBudgetMemory":100,
				"cekBuiltinCost-exBudgetCPU":23000,
				"cekBuiltinCost-exBudgetMemory":100,
				"cekConstCost-exBudgetCPU":23000,
				"cekConstCost-exBudgetMemory":100,
				"cekDelayCost-exBudgetCPU":23000,
				"cekDelayCost-exBudgetMemory":100,
				"cekForceCost-exBudgetCPU":23000,
				"cekForceCost-exBudgetMemory":100,
				"cekLamCost-exBudgetCPU":23000,
				"cekLamCost-exBudgetMemory":100,
				"cekStartupCost-exBudgetCPU":100,
				"cekStartupCost-exBudgetMemory":100,
				"cekVarCost-exBudgetCPU":23000,
				"cekVarCost-exBudgetMemory":100,
				"chooseData-cpu-arguments":19537,
				"chooseData-memory-arguments":32,
				"chooseList-cpu-arguments":175354,
				"chooseList-memory-arguments":32,
				"chooseUnit-cpu-arguments":46417,
				"chooseUnit-memory-arguments":4,
				"consByteString-cpu-arguments-intercept":221973,
				"consByteString-cpu-arguments-slope":511,
				"consByteString-memory-arguments-intercept":0,
				"consByteString-memory-arguments-slope":1,
				"constrData-cpu-arguments":89141,
				"constrData-memory-arguments":32,
				"decodeUtf8-cpu-arguments-intercept":497525,
				"decodeUtf8-cpu-arguments-slope":14068,
				"decodeUtf8-memory-arguments-intercept":4,
				"decodeUtf8-memory-arguments-slope":2,
				"divideInteger-cpu-arguments-constant":196500,
				"divideInteger-cpu-arguments-model-arguments-intercept":453240,
				"divideInteger-cpu-arguments-model-arguments-slope":220,
				"divideInteger-memory-arguments-intercept":0,
				"divideInteger-memory-arguments-minimum":1,
				"divideInteger-memory-arguments-slope":1,
				"encodeUtf8-cpu-arguments-intercept":1000,
				"encodeUtf8-cpu-arguments-slope":28662,
				"encodeUtf8-memory-arguments-intercept":4,
				"encodeUtf8-memory-arguments-slope":2,
				"equalsByteString-cpu-arguments-constant":245000,
				"equalsByteString-cpu-arguments-intercept":216773,
				"equalsByteString-cpu-arguments-slope":62,
				"equalsByteString-memory-arguments":1,
				"equalsData-cpu-arguments-intercept":1060367,
				"equalsData-cpu-arguments-slope":12586,
				"equalsData-memory-arguments":1,
				"equalsInteger-cpu-arguments-intercept":208512,
				"equalsInteger-cpu-arguments-slope":421,
				"equalsInteger-memory-arguments":1,
				"equalsString-cpu-arguments-constant":187000,
				"equalsString-cpu-arguments-intercept":1000,
				"equalsString-cpu-arguments-slope":52998,
				"equalsString-memory-arguments":1,
				"fstPair-cpu-arguments":80436,
				"fstPair-memory-arguments":32,
				"headList-cpu-arguments":43249,
				"headList-memory-arguments":32,
				"iData-cpu-arguments":1000,
				"iData-memory-arguments":32,
				"ifThenElse-cpu-arguments":80556,
				"ifThenElse-memory-arguments":1,
				"indexByteString-cpu-arguments":57667,
				"indexByteString-memory-arguments":4,
				"lengthOfByteString-cpu-arguments":1000,
				"lengthOfByteString-memory-arguments":10,
				"lessThanByteString-cpu-arguments-intercept":197145,
				"lessThanByteString-cpu-arguments-slope":156,
				"lessThanByteString-memory-arguments":1,
				"lessThanEqualsByteString-cpu-arguments-intercept":197145,
				"lessThanEqualsByteString-cpu-arguments-slope":156,
				"lessThanEqualsByteString-memory-arguments":1,
				"lessThanEqualsInteger-cpu-arguments-intercept":204924,
				"lessThanEqualsInteger-cpu-arguments-slope":473,
				"lessThanEqualsInteger-memory-arguments":1,
				"lessThanInteger-cpu-arguments-intercept":208896,
				"lessThanInteger-cpu-arguments-slope":511,
				"lessThanInteger-memory-arguments":1,
				"listData-cpu-arguments":52467,
				"listData-memory-arguments":32,
				"mapData-cpu-arguments":64832,
				"mapData-memory-arguments":32,
				"mkCons-cpu-arguments":65493,
				"mkCons-memory-arguments":32,
				"mkNilData-cpu-arguments":22558,
				"mkNilData-memory-arguments":32,
				"mkNilPairData-cpu-arguments":16563,
				"mkNilPairData-memory-arguments":32,
				"mkPairData-cpu-arguments":76511,
				"mkPairData-memory-arguments":32,
				"modInteger-cpu-arguments-constant":196500,
				"modInteger-cpu-arguments-model-arguments-intercept":453240,
				"modInteger-cpu-arguments-model-arguments-slope":220,
				"modInteger-memory-arguments-intercept":0,
				"modInteger-memory-arguments-minimum":1,
				"modInteger-memory-arguments-slope":1,
				"multiplyInteger-cpu-arguments-intercept":69522,
				"multiplyInteger-cpu-arguments-slope":11687,
				"multiplyInteger-memory-arguments-intercept":0,
				"multiplyInteger-memory-arguments-slope":1,
				"nullList-cpu-arguments":60091,
				"nullList-memory-arguments":32,
				"quotientInteger-cpu-arguments-constant":196500,
				"quotientInteger-cpu-arguments-model-arguments-intercept":453240,
				"quotientInteger-cpu-arguments-model-arguments-slope":220,
				"quotientInteger-memory-arguments-intercept":0,
				"quotientInteger-memory-arguments-minimum":1,
				"quotientInteger-memory-arguments-slope":1,
				"remainderInteger-cpu-arguments-constant":196500,
				"remainderInteger-cpu-arguments-model-arguments-intercept":453240,
				"remainderInteger-cpu-arguments-model-arguments-slope":220,
				"remainderInteger-memory-arguments-intercept":0,
				"remainderInteger-memory-arguments-minimum":1,
				"remainderInteger-memory-arguments-slope":1,
				"serialiseData-cpu-arguments-intercept":1159724,
				"serialiseData-cpu-arguments-slope":392670,
				"serialiseData-memory-arguments-intercept":0,
				"serialiseData-memory-arguments-slope":2,
				"sha2_256-cpu-arguments-intercept":806990,
				"sha2_256-cpu-arguments-slope":30482,
				"sha2_256-memory-arguments":4,
				"sha3_256-cpu-arguments-intercept":1927926,
				"sha3_256-cpu-arguments-slope":82523,
				"sha3_256-memory-arguments":4,
				"sliceByteString-cpu-arguments-intercept":265318,
				"sliceByteString-cpu-arguments-slope":0,
				"sliceByteString-memory-arguments-intercept":4,
				"sliceByteString-memory-arguments-slope":0,
				"sndPair-cpu-arguments":85931,
				"sndPair-memory-arguments":32,
				"subtractInteger-cpu-arguments-intercept":205665,
				"subtractInteger-cpu-arguments-slope":812,
				"subtractInteger-memory-arguments-intercept":1,
				"subtractInteger-memory-arguments-slope":1,
				"tailList-cpu-arguments":41182,
				"tailList-memory-arguments":32,
				"trace-cpu-arguments":212342,
				"trace-memory-arguments":32,
				"unBData-cpu-arguments":31220,
				"unBData-memory-arguments":32,
				"unConstrData-cpu-arguments":32696,
				"unConstrData-memory-arguments":32,
				"unIData-cpu-arguments":43357,
				"unIData-memory-arguments":32,
				"unListData-cpu-arguments":32247,
				"unListData-memory-arguments":32,
				"unMapData-cpu-arguments":38314,
				"unMapData-memory-arguments":32,
				"verifyEcdsaSecp256k1Signature-cpu-arguments":20000000000,
				"verifyEcdsaSecp256k1Signature-memory-arguments":20000000000,
				"verifyEd25519Signature-cpu-arguments-intercept":9462713,
				"verifyEd25519Signature-cpu-arguments-slope":1021,
				"verifyEd25519Signature-memory-arguments":10,
				"verifySchnorrSecp256k1Signature-cpu-arguments-intercept":20000000000,
				"verifySchnorrSecp256k1Signature-cpu-arguments-slope":0,
				"verifySchnorrSecp256k1Signature-memory-arguments":20000000000
			}
		},
		executionUnitPrices:{
			priceMemory:0.0577,
			priceSteps:0.0000721
		},
		maxBlockBodySize:90112,
		maxBlockExecutionUnits:{
			memory:62000000,
			steps:40000000000
		},
		maxBlockHeaderSize:1100,
		maxCollateralInputs:3,
		maxTxExecutionUnits:{
			memory:14000000,
			steps:10000000000
		},
		maxTxSize:16384,
		maxValueSize:5000,
		minPoolCost:340000000,
		monetaryExpansion:0.003,
		poolPledgeInfluence:0.3,
		poolRetireMaxEpoch:18,
		protocolVersion:{
			major:7,
			minor:0
		},
		stakeAddressDeposit:2000000,
		stakePoolDeposit:500000000,
		stakePoolTargetNum:500,
		treasuryCut:0.2,
		txFeeFixed:155381,
		txFeePerByte:44,
		utxoCostPerByte:4310
	},
	latestTip:{
		epoch:29,
		hash:"0de380c16222470e4cf4f7cce8af9a7b54d63e5aa4228520df9f2d252a0efcb5",
		slot:11192926,
		time:1666876126000
	}
};

/**
 * This wallet only has a single private/public key, which isn't rotated. Staking is not yet supported.
 * @implements {Wallet}
 */
export class SimpleWallet {
    /**
     * @type {Network}
     */
    #network;

    /**
     * @type {Bip32PrivateKey}
     */
    #privateKey;

    /**
     * @type {PubKey}
     */
    #pubKey;

    /**
     * @param {Network} network
     * @param {Bip32PrivateKey} privateKey
     */
    constructor(network, privateKey) {
        this.#network = network;
        this.#privateKey = privateKey;
        this.#pubKey = this.#privateKey.derivePubKey();

        // TODO: staking credentials
    }

    /**
     * @type {Bip32PrivateKey}
     */
    get privateKey() {
        return this.#privateKey;
    }

    /**
     * @type {PubKey}
     */
    get pubKey() {
        return this.#pubKey;
    }

    /**
     * @type {PubKeyHash}
     */
    get pubKeyHash() {
        return this.#pubKey.pubKeyHash;
    }

    /**
     * @type {Address}
     */
    get address() {
        return Address.fromPubKeyHash(this.pubKeyHash);
    }

    /**
     * @returns {Promise<boolean>}
     */
    async isMainnet() {
        return false;
    }

    /**
     * Not yet implemented.
     * @type {Promise<StakeAddress[]>}
     */
    get rewardAddresses() {
        throw new Error("not yet implemented")
    }

    /**
     * Assumed wallet was initiated with at least 1 UTxO at the pubkeyhash address.
     * @type {Promise<Address[]>}
     */
    get usedAddresses() {
        return new Promise((resolve, _) => {
            resolve([this.address])
        });
    }

    /**
     * @type {Promise<Address[]>}
     */
    get unusedAddresses() {
        return new Promise((resolve, _) => {
            resolve([])
        });
    }

    /**
     * @type {Promise<TxInput[]>}
     */
    get utxos() {
        return new Promise((resolve, _) => {
            resolve(this.#network.getUtxos(this.address));
        });
    }

    /**
     * @type {Promise<TxInput[]>}
     */
     get collateral() {
        return new Promise((resolve, _) => {
            resolve([])
        });
    }

    /**
     * Not yet implemented.
     * @param {Address} addr 
     * @param {string} message 
     * @return {Promise<{signature: string, key: string}>}
     */
    async signData(addr, message) {
        throw new Error("not yet implemented")
    }

    /**
     * Simply assumed the tx needs to by signed by this wallet without checking.
     * @param {Tx} tx
     * @returns {Promise<Signature[]>}
     */
    async signTx(tx) {
        return [
            this.#privateKey.sign(tx.bodyHash)
        ];
    }

    /**
     * @param {Tx} tx
     * @returns {Promise<TxId>}
     */
    async submitTx(tx) {
        return await this.#network.submitTx(tx);
    }
}

/**
 * collectUtxos removes tx inputs from the list, and appends txoutputs sent to the address to the end.
 * @internal
 * @typedef {{
 *     id(): TxId
 *     consumes(utxo: TxInput): boolean
 *     collectUtxos(address: Address, utxos: TxInput[]): TxInput[]
 *     getUtxo(id: TxOutputId): (null | TxInput)
 *     dump(): void
 * }} EmulatorTx
 */

/**
 * @implements {EmulatorTx}
 */
class GenesisTx {
    #id;
    #address;
    #lovelace;
    #assets;

    /**
     * @param {number} id
     * @param {Address} address
     * @param {bigint} lovelace
     * @param {Assets} assets
     */
    constructor(id, address, lovelace, assets) {
        this.#id = id;
        this.#address = address;
        this.#lovelace = lovelace;
        this.#assets = assets;
    }

    /**
     * Simple incremental txId for genesis transactions.
     * It's very unlikely that regular transactions have the same hash.
     * @return {TxId}
     */
    id() {
        let bytes = bigIntToBytes(BigInt(this.#id));

        if (bytes.length < 32) {
            bytes = (new Array(32 - bytes.length)).fill(0).concat(bytes);
        }

        return new TxId(bytes);
    }

    /**
     * @param {TxInput} utxo
     * @returns {boolean}
     */
    consumes(utxo) {
        return false;
    }

    /**
     * @param {Address} address
     * @param {TxInput[]} utxos
     * @returns {TxInput[]}
     */
    collectUtxos(address, utxos) {
        if (eq(this.#address.bytes, address.bytes)) {
            utxos = utxos.slice();

            utxos.push(new TxInput(
                new TxOutputId(this.id(), 0),
                new TxOutput(
                    this.#address,
                    new Value(this.#lovelace, this.#assets)
                )
            ));

            return utxos;
        } else {
            return utxos;
        }
    }

    /**
     * @param {TxOutputId} id 
     * @returns {null | TxInput}
     */
    getUtxo(id) {
        if (!(this.id().eq(id.txId) && id.utxoIdx == 0)) {
            return null;
        }

        return new TxInput(
            new TxOutputId(this.id(), 0),
            new TxOutput(
                this.#address,
                new Value(this.#lovelace, this.#assets)
            )
        );
    }

    dump() {
        console.log("GENESIS TX");
        console.log(`id: ${this.#id.toString()},\naddress: ${this.#address.toBech32()},\nlovelace: ${this.#lovelace.toString()},\nassets: ${JSON.stringify(this.#assets.dump(), undefined, "    ")}`);
    }
}

/**
 * @implements {EmulatorTx}
 */
class RegularTx {
    #tx;

    /**
     * @param {Tx} tx
     */
    constructor(tx) {
        this.#tx = tx;
    }

    /**
     * @returns {TxId}
     */
    id() {
        return this.#tx.id();
    }

    /**
     * @param {TxInput} utxo
     * @returns {boolean}
     */
    consumes(utxo) {
        const txInputs = this.#tx.body.inputs;

        return txInputs.some(txInput => txInput.eq(utxo));
    }

    /**
     * @param {Address} address
     * @param {TxInput[]} utxos
     * @returns {TxInput[]}
     */
    collectUtxos(address, utxos) {
        utxos = utxos.filter(utxo => !this.consumes(utxo));

        const txOutputs = this.#tx.body.outputs;

        txOutputs.forEach((txOutput, utxoId) => {
            if (eq(txOutput.address.bytes, address.bytes)) {
                utxos.push(new TxInput(
                    new TxOutputId(this.id(), utxoId),
                    txOutput
                ));
            }
        });

        return utxos;
    }

    /**
     * @param {TxOutputId} id 
     * @returns {null | TxInput}
     */
    getUtxo(id) {
        if (!id.txId.eq(this.id())) {
            return null;
        }

        /**
         * @type {null | TxInput}
         */
        let utxo = null;

        this.#tx.body.outputs.forEach((output, i) => {
            if (i == id.utxoIdx) {
                utxo = new TxInput(
                    id,
                    output
                );
            }
        });

        return utxo;
    }

    dump() {
        console.log("REGULAR TX");
        console.log(JSON.stringify(this.#tx.dump(), undefined, "  "));
    }
}

/**
 * A simple emulated Network.
 * This can be used to do integration tests of whole dApps.
 * Staking is not yet supported.
 * @implements {Network}
 */
export class NetworkEmulator {
    /**
     * @type {bigint}
     */
    #slot;

    /**
     * @type {NumberGenerator}
     */
    #random;

    /**
     * @type {GenesisTx[]}
     */
    #genesis;

    /**
     * @type {EmulatorTx[]}
     */
    #mempool;

    /**
     * @type {EmulatorTx[][]}
     */
    #blocks;

    /**
     * Instantiates a NetworkEmulator at slot 0.
     * An optional seed number can be specified, from which all emulated randomness is derived.
     * @param {number} seed
     */
    constructor(seed = 0) {
        this.#slot = 0n;
        this.#random = Crypto.mulberry32(seed);
        this.#genesis = [];
        this.#mempool = [];
        this.#blocks = [];
    }

    /**
     * @type {bigint}
     */
    get currentSlot() {
        return this.#slot;
    }

    /**
     * Creates a new `NetworkParams` instance that has access to current slot 
     * (so that the `Tx` validity range can be set automatically during `Tx.finalize()`).
     * @param {NetworkParams} networkParams
     * @returns {NetworkParams}
     */
    initNetworkParams(networkParams) {
        const raw = Object.assign({}, networkParams.raw);

        raw.latestTip = {
            epoch: 0,
            hash: "",
            slot: 0,
            time: 0
        };

        return new NetworkParams(
            raw,
            () => {
                return this.#slot;
            }
        );
    }

    /**
     * Creates a new SimpleWallet and populates it with a given lovelace quantity and assets.
     * Special genesis transactions are added to the emulated chain in order to create these assets.
     * @param {bigint} lovelace
     * @param {Assets} assets
     * @returns {SimpleWallet}
     */
    createWallet(lovelace = 0n, assets = new Assets([])) {
        const wallet = new SimpleWallet(this, Bip32PrivateKey.random(this.#random));

        this.createUtxo(wallet, lovelace, assets);

        return wallet;
    }

    /**
     * Creates a UTxO using a GenesisTx.
     * @param {SimpleWallet} wallet
     * @param {bigint} lovelace
     * @param {Assets} assets
     */
    createUtxo(wallet, lovelace, assets = new Assets([])) {
        if (lovelace != 0n || !assets.isZero()) {
            const tx = new GenesisTx(
                this.#genesis.length,
                wallet.address,
                lovelace,
                assets
            );

            this.#genesis.push(tx);
            this.#mempool.push(tx);
        }
    }

    /**
     * Mint a block with the current mempool, and advance the slot by a number of slots.
     * @param {bigint} nSlots
     */
    tick(nSlots) {
        assert(nSlots > 0, `nSlots must be > 0, got ${nSlots.toString()}`);

        if (this.#mempool.length > 0) {
            this.#blocks.push(this.#mempool);

            this.#mempool = [];
        }

        this.#slot += nSlots;
    }
    
    /**
     * @returns {Promise<NetworkParams>}
     */
    async getParameters() {
        return this.initNetworkParams(new NetworkParams(rawNetworkEmulatorParams));
    }

    warnMempool() {
        if (this.#mempool.length > 0) {
            console.error("Warning: mempool not empty (hint: use 'network.tick()')");
        }
    }

    /**
     * Throws an error if the UTxO isn't found
     * @param {TxOutputId} id 
     * @returns {Promise<TxInput>}
     */
    async getUtxo(id) {
        this.warnMempool();

        for (let block of this.#blocks) {
            for (let tx of block) {
                const utxo = tx.getUtxo(id)
                if (utxo) {
                    return utxo;
                }
            }
        }

        throw new Error(`utxo with id ${id.toString()} doesn't exist`);
    }

    /**
     * @param {Address} address
     * @returns {Promise<TxInput[]>}
     */
    async getUtxos(address) {
        this.warnMempool();

        /**
         * @type {TxInput[]}
         */
        let utxos = [];

        for (let block of this.#blocks) {
            for (let tx of block) {
                utxos = tx.collectUtxos(address, utxos);
            }
        }

        return utxos;
    }

    dump() {
        console.log(`${this.#blocks.length} BLOCKS`);
        this.#blocks.forEach((block, i) => {
            console.log(`${block.length} TXs in BLOCK ${i}`);
            for (let tx of block) {
                tx.dump();
            }
        })
    }

    /**
     * @param {TxInput} utxo
     * @returns {boolean}
     */
    isConsumed(utxo) {
        return this.#blocks.some(b => {
            return b.some(tx => {
                return tx.consumes(utxo)
            })
        }) || this.#mempool.some(tx => {
            return tx.consumes(utxo);
        })
    }

    /**
     * @param {Tx} tx
     * @returns {Promise<TxId>}
     */
    async submitTx(tx) {
        this.warnMempool();
        
        assert(tx.isValid(this.#slot), "tx invalid (not finalized or slot out of range)");

        // make sure that none of the inputs have been consumed before
        assert(tx.body.inputs.every(input => !this.isConsumed(input)), "input already consumed before");

        this.#mempool.push(new RegularTx(tx));

        return tx.id();
    }
}

/**
 * @internal
 * @implements {Wallet}
 */
class TxChainWallet {
    #base;
    #chain;
    
    /**
     * 
     * @param {Wallet} base 
     * @param {TxChain} chain 
     */
    constructor(base, chain) {
        this.#base = base;
        this.#chain = chain;
    }

    /**
     * @returns {Promise<boolean>}
     */
    async isMainnet() {
        return this.#base.isMainnet()
    }

    /**
     * @param {Address} addr 
     * @param {string} message 
     * @return {Promise<{signature: string, key: string}>}
     */
    async signData(addr, message) {
        return this.#base.signData(addr, message)
    }

    /**
     * @param {Tx} tx 
     * @returns {Promise<Signature[]>}
     */
    async signTx(tx) {
        return this.#base.signTx(tx)
    }

    /**
     * @param {Tx} tx 
     * @returns {Promise<TxId>}
     */
    async submitTx(tx) {
        return this.#chain.submitTx(tx)
    }

    get rewardAddresses() {
        return this.#base.rewardAddresses;
    }

    get unusedAddresses() {
        return this.#base.unusedAddresses;
    }

    get usedAddresses() {
        return this.#base.usedAddresses;
    }

    /**
     * @internal
     * @param {TxInput[]} utxos 
     * @returns {Promise<TxInput[]>}
     */
    async filterUtxos(utxos) {
        const ua = await this.usedAddresses;
        const una = await this.unusedAddresses;

        const addrs = ua.concat(una);

        return await this.#chain.getUtxosInternal(utxos, addrs);
    }

    /**
     * @type {Promise<TxInput[]>}
     */
    get collateral() {
        return new Promise((resolve, reject) => {
            this.#base.collateral.then(utxos => {
                this.filterUtxos(utxos).then(utxos => {
                    resolve(utxos);
                });
            });
        });
    }

    /**
     * @type {Promise<TxInput[]>}
     */
    get utxos() {
        return new Promise((resolve, reject) => {
            this.#base.utxos.then(utxos => {
                this.filterUtxos(utxos).then(utxos => {
                    resolve(utxos);
                });
            });
        });
    }
}

/**
 * Helper that 
 * @implements {Network}
 */
export class TxChain {
    #network;

    /**
     * @type {RegularTx[]}
     */
    #txs;

    /**
     * @param {Network} network 
     */
    constructor(network) {
        this.#network = network;
        this.#txs = [];
    }

    /**
     * @param {Tx} tx 
     * @returns {Promise<TxId>}
     */
    async submitTx(tx) {
        const id = await this.#network.submitTx(tx);

        this.#txs.push(new RegularTx(tx));

        return id;
    }

    /**
     * @returns {Promise<NetworkParams>}
     */
    async getParameters() {
        return this.#network.getParameters()
    }

    /**
     * @param {TxOutputId} id 
     * @returns {Promise<TxInput>}
     */
    async getUtxo(id) {
        for (let i = 0; i < this.#txs.length; i++) {
            const txInput = this.#txs[i].getUtxo(id);

            if (txInput) {
                return txInput;
            }
        }

        return this.#network.getUtxo(id);
    }

    /**
     * @param {TxInput[]} utxos
     * @param {Address[]} addrs
     * @returns {Promise<TxInput[]>}
     */
    async getUtxosInternal(utxos, addrs) {
        for (let tx of this.#txs) {
            addrs.forEach(addr => {
                utxos = tx.collectUtxos(addr, utxos);
            });
        }

        return utxos;
    }
    /**
     * @param {Address} addr
     * @returns {Promise<TxInput[]>}
     */
    async getUtxos(addr) {
        let utxos = await this.#network.getUtxos(addr);

        return this.getUtxosInternal(utxos, [addr])
    }

    /**
     * @param {Wallet} baseWallet 
     * @returns {Wallet}
     */
    asWallet(baseWallet) {
        return new TxChainWallet(baseWallet, this);
    }
}

/**
 * @typedef {{
 *   [address: string]: TxInput[]
 * }} NetworkSliceUTxOs
 */

/**
 * @implements {Network}
 */
export class NetworkSlice {
    #params;
    #utxos;

    /**
     * @param {NetworkParams} params
     * @param {NetworkSliceUTxOs} utxos 
     */
    constructor(params, utxos) {
        this.#params = params;
        this.#utxos = utxos;
    }

    /**
     * @param {Network} network
     * @param {Address[]} addresses
     * @returns {Promise<NetworkSlice>}
     */
    static async init(network, addresses) {
        /**
         * @type {[Promise<NetworkParams>, ...Promise<TxInput[]>[]]}
         */
        const promises = [
            network.getParameters(),
            ...addresses.map(a => network.getUtxos(a))
        ];

        const resolved = await Promise.all(promises);

        const [params, ...utxos] = resolved;

        /**
         * @type {NetworkSliceUTxOs}
         */
        const obj = {};

        addresses.forEach((a, i) => {
            obj[a.toBech32()] = utxos[i];
        });

        return new NetworkSlice(params, obj);
    }

    /**
     * @returns {any}
     */
    toJson() {
        const obj = {};

        for (let a in this.#utxos) {
            obj[a] = this.#utxos[a].map(utxo => bytesToHex(utxo.toFullCbor()));
        }

        return {
            params: this.#params.raw,
            utxos: obj
        };
    }

    /**
     * @param {any} obj 
     * @returns {NetworkSlice}
     */
    static fromJson(obj) {
        const params = new NetworkParams(obj.params);

        /**
         * @type {NetworkSliceUTxOs}
         */
        const utxos = {};

        for (let a in obj.utxos) {
            utxos[a] = obj.utxos[a].map(utxo => TxInput.fromFullCbor(utxo));
        }

        return new NetworkSlice(params, utxos);
    }

    /**
     * @returns {Promise<NetworkParams>}
     */
    async getParameters() {
        return this.#params;
    }

    /**
     * @param {TxOutputId} id 
     * @returns {Promise<TxInput>}
     */
    async getUtxo(id) {
        for (let utxos of Object.values(this.#utxos)) {
            for (let utxo of utxos) {
                if (utxo.outputId.eq(id)) {
                    return utxo;
                }
            }
        }

        throw new Error(`utxo ${id.toString()} not found`);
    }

    /**
     * @param {Address} addr 
     * @returns {Promise<TxInput[]>}
     */
    async getUtxos(addr) {
        const key = addr.toBech32();

        if (key in this.#utxos) {
            return this.#utxos[key];
        } else {
            return [];
        }
    }

    /**
     * @param {Tx} tx 
     * @returns {Promise<TxId>}
     */
    async submitTx(tx) {
        throw new Error("can't submit tx through network slice");
    }
}


//////////////////////////////////////
// Section 41: Fuzzy testing framework
//////////////////////////////////////

/**
 * @typedef {() => UplcData} ValueGenerator
 */

/**
 * @typedef {(args: UplcValue[], res: (UplcValue | RuntimeError), isSimplfied?: boolean) => (boolean | Object.<string, boolean>)} PropertyTest
 */

/**
 * Helper class for performing fuzzy property-based tests of Helios scripts.
 */
export class FuzzyTest {
	/**
	 * @type {number}
	 */
	#seed;

	/**
	 * @type {NumberGenerator} - seed generator
	 */
	#rand;

	#runsPerTest;

	#simplify;

	#printMessages;

	/**
	 * @type {NetworkParams}
	 */
	#dummyNetworkParams;

	/**
	 * The simplify argument specifies whether optimized versions of the Helios sources should also be tested.
	 * @param {number} seed
	 * @param {number} runsPerTest
	 * @param {boolean} simplify If true then also test the simplified program
	 */
	constructor(seed = 0, runsPerTest = 100, simplify = false, printMessages = false) {
		console.log("starting fuzzy testing  with seed", seed);

		this.#seed = seed;
		this.#rand = Crypto.rand(seed);
		this.#runsPerTest = runsPerTest;
		this.#simplify = simplify;
		this.#dummyNetworkParams = new NetworkParams(rawNetworkEmulatorParams);
		this.#printMessages = printMessages;
	}

	reset() {
		this.#rand = Crypto.rand(this.#seed);
	}

	/**
	 * @returns {NumberGenerator}
	 */
	newRand() {
		let seed = this.#rand()*1000000;

		return Crypto.rand(seed);
	}

	/**
	 * Returns a gernator for whole numbers between min and max
	 * @param {number} min
	 * @param {number} max
	 * @returns {() => bigint}
	 */
	rawInt(min = -10000000, max = 10000000) {
		let rand = this.newRand();

		return function() {
			return BigInt(Math.floor(rand()*(max - min)) + min);
		}
	}

	/**
	 * Returns a generator for whole numbers between min and max, wrapped with IntData
	 * @param {number} min
	 * @param {number} max
	 * @returns {ValueGenerator}
	 */
	int(min = -10000000, max = 10000000) {		
		let rand = this.rawInt(min, max);

		return function() {
			return new IntData(rand());
		}
	}

	/**
	 * @param {number} min 
	 * @param {number} max 
	 * @returns {ValueGenerator}
	 */
	real(min = -1000, max = 1000) {
		let rand = this.newRand();

		return function() {
			return new IntData(BigInt(Math.floor(((rand()*(max - min)) + min)*1000000)))
		}
	}

	/**
	 * Returns a generator for strings containing any utf-8 character.
	 * @param {number} minLength
	 * @param {number} maxLength
	 * @returns {ValueGenerator}
	 */
	string(minLength = 0, maxLength = 64) {
		let rand = this.newRand();

		return function() {
			let n = Math.round(rand()*(maxLength - minLength)) + minLength;
			if (n < 0) {
				n = 0;
			}

			let chars = [];
			for (let i = 0; i < n; i++) {
				chars.push(String.fromCodePoint(Math.round(rand()*1112064)));
			}
			
			return ByteArrayData.fromString(chars.join(""));
		}
	}

	/** 
	 * Returns a generator for strings with ascii characters from 32 (space) to 126 (tilde).
	 * @param {number} minLength
	 * @param {number} maxLength
	 * @returns {ValueGenerator}
	 */
	ascii(minLength = 0, maxLength = 64) {
		let rand = this.newRand();

		return function() {
			let n = Math.round(rand()*(maxLength - minLength)) + minLength;
			if (n < 0) {
				n = 0;
			}

			let chars = [];
			for (let i = 0; i < n; i++) {
				chars.push(String.fromCharCode(Math.round(rand()*94 + 32)));
			}
			
			return ByteArrayData.fromString(chars.join(""));
		}
	}

	/**
	 * Returns a generator for bytearrays containing only valid ascii characters
	 * @param {number} minLength
	 * @param {number} maxLength
	 * @returns {ValueGenerator}
	 */
	asciiBytes(minLength = 0, maxLength = 64) {
		let rand = this.newRand();

		return function() {
			let n = Math.round(rand()*(maxLength - minLength)) + minLength;
			if (n < 0) {
				n = 0;
			}

			let bytes = [];
			for (let i = 0; i < n; i++) {
				bytes.push(Math.floor(rand()*94 + 32));
			}

			return new ByteArrayData(bytes);
		}
	}

	/**
	 * Returns a generator for bytearrays the are also valid utf8 strings
	 * @param {number} minLength - length of the string, not of the bytearray!
	 * @param {number} maxLength - length of the string, not of the bytearray!
	 * @returns {ValueGenerator}
	 */
	utf8Bytes(minLength = 0, maxLength = 64) {
		return this.string(minLength, maxLength);
	}

	/**
	 * Returns a generator for number[]
	 * @param {number} minLength
	 * @param {number} maxLength
	 * @returns {() => number[]}
	 */
	rawBytes(minLength = 0, maxLength = 64) {
		let rand = this.newRand();

		return function() {
			let n = Math.round(rand()*(maxLength - minLength)) + minLength;
			if (n < 0) {
				n = 0;
			}

			let bytes = [];
			for (let i = 0; i < n; i++) {
				bytes.push(Math.floor(rand()*256));
			}

			return bytes;
		}
	}

	/**
	 * Returns a generator for bytearrays 
	 * @param {number} minLength
	 * @param {number} maxLength
	 * @returns {ValueGenerator}
	 */
	bytes(minLength = 0, maxLength = 64) {
		let rand = this.rawBytes(minLength, maxLength);

		return function() {
			let bytes = rand();

			return new ByteArrayData(bytes);
		}
	}
	/**
	 * Returns a generator for booleans,
	 * @returns {() => boolean}
	 */
	rawBool() {
		let rand = this.newRand();

		return function() {
			let x = rand();

			return x >= 0.5;
		}
	}

	/**
	 * Returns a generator for booleans, wrapped with ConstrData
	 * @returns {ValueGenerator}
	 */
	bool() {
		let rand = this.rawBool();

		return function() {
			return new ConstrData(rand() ? 1 : 0, []);
		}
	}

	/**
	 * Returns a generator for options
	 * @param {ValueGenerator} someGenerator
	 * @param {number} noneProbability
	 * @returns {ValueGenerator}
	 */
	option(someGenerator, noneProbability = 0.5) {
		let rand = this.newRand();

		return function() {
			let x = rand();

			if (x < noneProbability) {
				return new ConstrData(1, []);
			} else {
				return new ConstrData(0, [someGenerator()]);
			}
		}
	}

	/**
	 * Returns a generator for lists
	 * @param {ValueGenerator} itemGenerator
	 * @param {number} minLength
	 * @param {number} maxLength
	 * @returns {ValueGenerator}
	 */
	list(itemGenerator, minLength = 0, maxLength = 10) {
		let rand = this.newRand();

		if (minLength < 0) {
			minLength = 0;
		}

		if (maxLength < 0) {
			maxLength = 0;
		}

		return function() {
			let n = Math.round(rand()*(maxLength - minLength)) + minLength;
			if (n < 0) {
				n = 0;
			}

			/**
			 * @type {UplcData[]}
			 */
			let items = [];

			for (let i = 0; i < n; i++) {
				items.push(itemGenerator());
			}

			return new ListData(items);
		}
	}

	/**
	 * Returns a generator for maps
	 * @param {ValueGenerator} keyGenerator
	 * @param {ValueGenerator} valueGenerator
	 * @param {number} minLength
	 * @param {number} maxLength
	 * @returns {ValueGenerator}
	 */
	map(keyGenerator, valueGenerator, minLength = 0, maxLength = 10) {
		let rand = this.newRand();

		if (minLength < 0) {
			minLength = 0;
		}

		if (maxLength < 0) {
			maxLength = 0;
		}

		return function() {
			let n = Math.round(rand()*(maxLength - minLength)) + minLength;

			if (n < 0) {
				n = 0;
			}

			/**
			 * @type {[UplcData, UplcData][]}
			 */
			let pairs = [];

			for (let i = 0; i < n; i++) {
				pairs.push([keyGenerator(), valueGenerator()]);
			}

			return new MapData(pairs);
		};
	}

	/**
	 * Returns a generator for objects
	 * @param {...ValueGenerator} itemGenerators
	 * @returns {ValueGenerator}
	 */
	object(...itemGenerators) {
		return function() {
			let items = itemGenerators.map(g => g());

			return new ConstrData(0, items);
		}
	}

	/**
	 * Returns a generator for tagged constr
	 * @param {number | NumberGenerator} tag
	 * @param {...ValueGenerator} fieldGenerators
	 * @returns {ValueGenerator}
	 */
	constr(tag, ...fieldGenerators) {
		return function() {
			const fields = fieldGenerators.map(g => g());

			const finalTag = (typeof tag == "number") ? tag : Math.round(tag()*100);
			
			return new ConstrData(finalTag, fields);
		}
	}

	/**
	 * Perform a fuzzy/property-based test-run of a Helios source. One value generator must be specified per argument of main.
	 * 
	 * Throws an error if the propTest fails. 
	 * 
	 * The propTest can simply return a boolean, or can return an object with boolean values, and if any of these booleans is false the propTest fails (the keys can be used to provide extra information).
	 * @param {ValueGenerator[]} argGens
	 * @param {string} src
	 * @param {PropertyTest} propTest
	 * @param {number} nRuns
	 * @param {boolean} simplify
	 * @returns {Promise<void>} - throws an error if any of the property tests fail
	 */
	async test(argGens, src, propTest, nRuns = this.#runsPerTest, simplify = false) {
		// compilation errors here aren't caught

		let purposeName = extractScriptPurposeAndName(src);

		if (purposeName === null) {
			throw new Error("failed to get script purpose and name");
		} else {
			const [_, testName] = purposeName;

			const program = Program.new(src);
			const uplcProgram = program.compile(simplify);

			/**
			 * @type {Cost}
			 */
			const totalCost = {
				mem: 0n,
				cpu: 0n
			};

			let nonErrorRuns = 0;

			for (let it = 0; it < nRuns; it++) {
				let args = argGens.map(gen => new UplcDataValue(Site.dummy(), gen()));
			
				/**
				 * @type {Cost}
				 */
				const cost = {
					mem: 0n,
					cpu: 0n
				};

				try {
					let result = await uplcProgram.run(
						args, {
							...DEFAULT_UPLC_RTE_CALLBACKS,
							onPrint: this.#printMessages ? async (msg) => {console.log(msg)} : async (msg) => {return},
							onIncrCost: (name, isTerm, c) => {cost.mem = cost.mem + c.mem; cost.cpu = cost.cpu + c.cpu}
						},
						this.#dummyNetworkParams
					);
				
					let obj = propTest(args, result, simplify);

					if (!(result instanceof RuntimeError)) {
						totalCost.mem += cost.mem;
						totalCost.cpu += cost.cpu;
						nonErrorRuns += 1;
					}

					if (typeof obj == "boolean") {
						if (!obj) {
							console.log(program.dumpIR(simplify, true));
							throw new Error(`property test '${testName}' failed (info: (${args.map(a => a.toString()).join(', ')}) => ${result.toString()})`);
						}
					} else {
						// check for failures
						for (let key in obj) {
							if (!obj[key]) {
								console.log(program.dumpIR(simplify, true));
								throw new Error(`property test '${testName}:${key}' failed (info: (${args.map(a => a.toString()).join(', ')}) => ${result.toString()})`);
							}
						}
					}
				} catch (e) {
					console.log("UNSIMPLIFIED:", program.dumpIR(false, true));
					console.log("SIMPLIFIED:", program.dumpIR(true, true));
					
					throw e;
				}
			}

			console.log(`property tests for '${testName}' succeeded${simplify ? " (simplified)":""} (${uplcProgram.calcSize()} bytes, ${nonErrorRuns > 0 ? totalCost.mem/BigInt(nonErrorRuns): "N/A"} mem, ${nonErrorRuns > 0 ? totalCost.cpu/BigInt(nonErrorRuns): "N/A"} cpu)`);
		}

		if (!simplify && this.#simplify) {
			await this.test(argGens, src, propTest, nRuns, true);
		}
	}

	/**
	 * @param {Object.<string, ValueGenerator>} paramGenerators
	 * @param {string[]} paramArgs
	 * @param {string} src
	 * @param {PropertyTest} propTest
	 * @param {number} nRuns
	 * @param {boolean} simplify
	 * @returns {Promise<void>}
	 */
	async testParams(paramGenerators, paramArgs, src, propTest, nRuns = this.#runsPerTest, simplify = false) {
		let program = Program.new(src);

		let purposeName = extractScriptPurposeAndName(src);

		if (purposeName === null) {
			throw new Error("failed to get script purpose and name");
		} else {
			let [_, testName] = purposeName;

			for (let it = 0; it < nRuns; it++) {

				for (let key in paramGenerators) {
					program.changeParamSafe(key, paramGenerators[key]())
				}

				let args = paramArgs.map(paramArg => program.evalParam(paramArg));
			
				program = Program.new(src);

				let coreProgram = program.compile(simplify);

				let result = await coreProgram.run(
					args,
					{
						...DEFAULT_UPLC_RTE_CALLBACKS,
						onPrint: this.#printMessages ? async (msg) => {console.log(msg)} : async (msg) => {return}
					}
				);

				let obj = propTest(args, result, simplify);

				if (typeof obj == "boolean") {
					if (!obj) {
						console.log(program.dumpIR(simplify, true));

						throw new Error(`property test '${testName}' failed (info: (${args.map(a => a.toString()).join(', ')}) => ${result.toString()})`);
					}
				} else {
					// check for failures
					for (let key in obj) {
						if (!obj[key]) {
							console.log(program.dumpIR(simplify, true));

							throw new Error(`property test '${testName}:${key}' failed (info: (${args.map(a => a.toString()).join(', ')}) => ${result.toString()})`);
						}
					}
				}
			}

			console.log(`property tests for '${testName}' succeeded${simplify ? " (simplified)":""}`);
		}

		if (!simplify && this.#simplify) {
			await this.testParams(paramGenerators, paramArgs, src, propTest, nRuns, true);
		}
	}
}


//////////////////////////////////////////
// Section 42: Bundling specific functions
//////////////////////////////////////////


/**
 * @internal
 * @param {TypeSchema} schema
 * @param {any} obj
 * @param {JsToUplcHelpers} helpers
 * @returns {Promise<UplcData>}
 */
export async function jsToUplcInternal(schema, obj, helpers) {
    if (schema.type == "List" && "itemType" in schema) {
        if (!Array.isArray(obj)) {
            throw new Error(`expected Array, got '${obj}'`);
        }

        const items = obj.map(item => jsToUplcInternal(schema.itemType, item, helpers))

        return new ListData(await Promise.all(items));
    } else if (schema.type == "Map" && "keyType" in schema && "valueType" in schema) {
        if (!Array.isArray(obj)) {
            throw new Error(`expected Array, got '${obj}'`);
        }

        /**
         * @type {[Promise<UplcData>, Promise<UplcData>][]}
         */
        const pairs = obj.map(entry => {
            if (!Array.isArray(entry)) {
                throw new Error(`expected Array of Arrays, got '${obj}'`);
            }

            const [key, value] = entry;

            if (!key || !value) {
                throw new Error(`expected Array of Array[2], got '${obj}'`);
            }

            return [
                jsToUplcInternal(schema.keyType, key, helpers),
                jsToUplcInternal(schema.valueType, value, helpers)
            ];
        });

        const keys = await Promise.all(pairs.map(p => p[0]));
        const values = await Promise.all(pairs.map(p => p[1]));

        return new MapData(keys.map((k, i) => [k, values[i]]));
    } else if (schema.type == "Option" && "someType" in schema) {
        if (obj === null) {
            return new ConstrData(1, []);
        } else {
            return new ConstrData(0, [await jsToUplcInternal(schema.someType, obj, helpers)]);
        }
    } else if (schema.type == "Struct" && "fieldTypes" in schema) {
        const fields = schema.fieldTypes.map((fieldSchema) => {
            const fieldName = fieldSchema.name;
            const fieldObj = obj[fieldName];

            if (fieldObj === undefined) {
                throw new Error(`field ${fieldName} not found in '${obj}'`);
            }

            return jsToUplcInternal(fieldSchema, fieldObj, helpers);
        });

        if (fields.length == 1) {
            return fields[0];
        } else {
            return new ListData(await Promise.all(fields));
        }
    } else if (schema.type == "Enum" && "variantTypes" in schema) {
        const keys = Object.keys(obj);

        if (keys.length != 1) {
            throw new Error("expected a single key for enum");
        }

        const key = keys[0];

        const index = schema.variantTypes.findIndex(variant => variant.name == key);

        if (index == -1) {
            throw new Error(`invalid variant ${key}`);
        }

        const fields = schema.variantTypes[index].fieldTypes.map((fieldSchema) => {
            const fieldName = fieldSchema.name;
            const fieldObj = obj[key][fieldName];

            if (fieldObj === undefined) {
                throw new Error(`field ${fieldName} not found in '${obj[key]}'`);
            }

            return jsToUplcInternal(fieldSchema, fieldObj, helpers);
        });

        return new ConstrData(index, await Promise.all(fields));
    } else {
        const builtinType = builtinTypes[schema.type];

        if (!builtinType) {
            throw new Error(`${schema.type} isn't a valid builtin type`);
        }

        return builtinType.jsToUplc(obj, helpers);
    }
}

/**
 * @internal
 * @param {TypeSchema} schema
 * @param {any} obj
 * @param {JsToUplcHelpers} helpers
 * @returns {Promise<UplcData>}
 */
export function jsToUplc(schema, obj, helpers) {
    return jsToUplcInternal(schema, obj, helpers);
}

/**
 * @internal
 * @param {TypeSchema} schema
 * @param {UplcData} data
 * @param {UplcToJsHelpers} helpers
 * @returns {Promise<any>}
 */
async function uplcToJsInternal(schema, data, helpers) {
    if (schema.type == "List" && "itemType" in schema) {
        return await Promise.all(data.list.map(item => uplcToJsInternal(schema.itemType, item, helpers)));
    } else if (schema.type == "Map" && "keyType" in schema && "valueType" in schema) {
        /**
         * @type {[Promise<any>, Promise<any>][]}
         */
        const pairs = data.map.map(([key, value]) => [uplcToJsInternal(schema.keyType, key, helpers), uplcToJsInternal(schema.valueType, value, helpers)]);

        const keys = await Promise.all(pairs.map(p => p[0]));
        const values = await Promise.all(pairs.map(p => p[1]));

        return keys.map((k, i) => [k, values[i]]);
    } else if (schema.type == "Option" && "someType" in schema) {
        if (data.index == 1) {
            assert(data.fields.length == 0, "not an Option ConstrData");
            return null;
        } else if (data.index == 0) {
            assert(data.fields.length == 1, "not an Option ConstrData");
            return uplcToJsInternal(schema.someType, data.fields[0], helpers);
        } else {
            throw new Error("not an Option ConstrData");
        }
    } else if (schema.type == "Struct" && "fieldTypes" in schema) {
        const obj = {};

        const fields = schema.fieldTypes.length == 1 ? [data] : data.list;

        for (let i = 0; i < fields.length; i++) {
            const field = fields[i];

            const fieldType = schema.fieldTypes[i];

            if (!fieldType) {
                throw new Error("field out-of-range");
            }

            obj[fieldType.name] = await uplcToJsInternal(fieldType, field, helpers);
        }

        return obj;
    } else if (schema.type == "Enum" && "variantTypes" in schema) {
        const index = data.index;

        const variant = schema.variantTypes[index];

        if (!variant) {
            throw new Error("constr index out-of-range");
        }

        const obj = {};

        const fields = data.fields;

        for (let i = 0; i< fields.length; i++) {
            const field = fields[i];

            const fieldType = variant.fieldTypes[i];

            if (!fieldType) {
                throw new Error("field out-of-range");
            }

            obj[fieldType.name] = await uplcToJsInternal(fieldType, field, helpers);
        }

        return {[variant.name]: obj};
    } else {
        const builtinType = builtinTypes[schema.type];

        if (!builtinType) {
            throw new Error(`${schema.type} isn't a valid builtin type`);
        }

        return builtinType.uplcToJs(data, helpers);
    }
}

/**
 * @internal
 * @param {TypeSchema} schema
 * @param {UplcData} data
 * @param {UplcToJsHelpers} helpers
 * @returns {Promise<any>}
 */
export function uplcToJs(schema, data, helpers) {
    return uplcToJsInternal(schema, data, helpers);
}