code:
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const limitNumber_1 = require("./limitNumber");
const multipleOf_1 = require("./multipleOf");
const limitLength_1 = require("./limitLength");
const pattern_1 = require("./pattern");
const limitProperties_1 = require("./limitProperties");
const required_1 = require("./required");
const limitItems_1 = require("./limitItems");
const uniqueItems_1 = require("./uniqueItems");
const const_1 = require("./const");
const enum_1 = require("./enum");
const validation = [
    // number
    limitNumber_1.default,
    multipleOf_1.default,
    // string
    limitLength_1.default,
    pattern_1.default,
    // object
    limitProperties_1.default,
    required_1.default,
    // array
    limitItems_1.default,
    uniqueItems_1.default,
    // any
    { keyword: "type", schemaType: ["string", "array"] },
    { keyword: "nullable", schemaType: "boolean" },
    const_1.default,
    enum_1.default,
];
exports.default = validation;
//# sourceMappingURL=index.js.map


code:
import type { CodeKeywordDefinition } from "../../types";
declare const def: CodeKeywordDefinition;
export default def;

license: mit


code:
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const util_1 = require("../../compile/util");
const def = {
    keyword: ["maxContains", "minContains"],
    type: "array",
    schemaType: "number",
    code({ keyword, parentSchema, it }) {
        if (parentSchema.contains === undefined) {
            (0, util_1.checkStrictMode)(it, `"${keyword}" without "contains" is ignored`);
        }
    },
};
exports.default = def;
//# sourceMappingURL=limitContains.js.map


code:
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const codegen_1 = require("../../compile/codegen");
const error = {
    message({ keyword, schemaCode }) {
        const comp = keyword === "maxItems" ? "more" : "fewer";
        return (0, codegen_1.str) `must NOT have ${comp} than ${schemaCode} items`;
    },
    params: ({ schemaCode }) => (0, codegen_1._) `{limit: ${schemaCode}}`,
};
const def = {
    keyword: ["maxItems", "minItems"],
    type: "array",
    schemaType: "number",
    $data: true,
    error,
    code(cxt) {
        const { keyword, data, schemaCode } = cxt;
        const op = keyword === "maxItems" ? codegen_1.operators.GT : codegen_1.operators.LT;
        cxt.fail$data((0, codegen_1._) `${data}.length ${op} ${schemaCode}`);
    },
};
exports.default = def;
//# sourceMappingURL=limitItems.js.map


code:
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const codegen_1 = require("../../compile/codegen");
const util_1 = require("../../compile/util");
const ucs2length_1 = require("../../runtime/ucs2length");
const error = {
    message({ keyword, schemaCode }) {
        const comp = keyword === "maxLength" ? "more" : "fewer";
        return (0, codegen_1.str) `must NOT have ${comp} than ${schemaCode} characters`;
    },
    params: ({ schemaCode }) => (0, codegen_1._) `{limit: ${schemaCode}}`,
};
const def = {
    keyword: ["maxLength", "minLength"],
    type: "string",
    schemaType: "number",
    $data: true,
    error,
    code(cxt) {
        const { keyword, data, schemaCode, it } = cxt;
        const op = keyword === "maxLength" ? codegen_1.operators.GT : codegen_1.operators.LT;
        const len = it.opts.unicode === false ? (0, codegen_1._) `${data}.length` : (0, codegen_1._) `${(0, util_1.useFunc)(cxt.gen, ucs2length_1.default)}(${data})`;
        cxt.fail$data((0, codegen_1._) `${len} ${op} ${schemaCode}`);
    },
};
exports.default = def;
//# sourceMappingURL=limitLength.js.map


code:
import type { CodeKeywordDefinition, ErrorObject } from "../../types";
type Kwd = "maximum" | "minimum" | "exclusiveMaximum" | "exclusiveMinimum";
type Comparison = "<=" | ">=" | "<" | ">";
export type LimitNumberError = ErrorObject<Kwd, {
    limit: number;
    comparison: Comparison;
}, number | {
    $data: string;
}>;
declare const def: CodeKeywordDefinition;
export default def;


code:
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const codegen_1 = require("../../compile/codegen");
const ops = codegen_1.operators;
const KWDs = {
    maximum: { okStr: "<=", ok: ops.LTE, fail: ops.GT },
    minimum: { okStr: ">=", ok: ops.GTE, fail: ops.LT },
    exclusiveMaximum: { okStr: "<", ok: ops.LT, fail: ops.GTE },
    exclusiveMinimum: { okStr: ">", ok: ops.GT, fail: ops.LTE },
};
const error = {
    message: ({ keyword, schemaCode }) => (0, codegen_1.str) `must be ${KWDs[keyword].okStr} ${schemaCode}`,
    params: ({ keyword, schemaCode }) => (0, codegen_1._) `{comparison: ${KWDs[keyword].okStr}, limit: ${schemaCode}}`,
};
const def = {
    keyword: Object.keys(KWDs),
    type: "number",
    schemaType: "number",
    $data: true,
    error,
    code(cxt) {
        const { keyword, data, schemaCode } = cxt;
        cxt.fail$data((0, codegen_1._) `${data} ${KWDs[keyword].fail} ${schemaCode} || isNaN(${data})`);
    },
};
exports.default = def;
//# sourceMappingURL=limitNumber.js.map


code:
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const codegen_1 = require("../../compile/codegen");
const error = {
    message({ keyword, schemaCode }) {
        const comp = keyword === "maxProperties" ? "more" : "fewer";
        return (0, codegen_1.str) `must NOT have ${comp} than ${schemaCode} properties`;
    },
    params: ({ schemaCode }) => (0, codegen_1._) `{limit: ${schemaCode}}`,
};
const def = {
    keyword: ["maxProperties", "minProperties"],
    type: "object",
    schemaType: "number",
    $data: true,
    error,
    code(cxt) {
        const { keyword, data, schemaCode } = cxt;
        const op = keyword === "maxProperties" ? codegen_1.operators.GT : codegen_1.operators.LT;
        cxt.fail$data((0, codegen_1._) `Object.keys(${data}).length ${op} ${schemaCode}`);
    },
};
exports.default = def;
//# sourceMappingURL=limitProperties.js.map


code:
import type { CodeKeywordDefinition, ErrorObject } from "../../types";
export type MultipleOfError = ErrorObject<"multipleOf", {
    multipleOf: number;
}, number | {
    $data: string;
}>;
declare const def: CodeKeywordDefinition;
export default def;


code:
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const codegen_1 = require("../../compile/codegen");
const error = {
    message: ({ schemaCode }) => (0, codegen_1.str) `must be multiple of ${schemaCode}`,
    params: ({ schemaCode }) => (0, codegen_1._) `{multipleOf: ${schemaCode}}`,
};
const def = {
    keyword: "multipleOf",
    type: "number",
    schemaType: "number",
    $data: true,
    error,
    code(cxt) {
        const { gen, data, schemaCode, it } = cxt;
        // const bdt = bad$DataType(schemaCode, <string>def.schemaType, $data)
        const prec = it.opts.multipleOfPrecision;
        const res = gen.let("res");
        const invalid = prec
            ? (0, codegen_1._) `Math.abs(Math.round(${res}) - ${res}) > 1e-${prec}`
            : (0, codegen_1._) `${res} !== parseInt(${res})`;
        cxt.fail$data((0, codegen_1._) `(${schemaCode} === 0 || (${res} = ${data}/${schemaCode}, ${invalid}))`);
    },
};
exports.default = def;
//# sourceMappingURL=multipleOf.js.map


code:
import type { CodeKeywordDefinition, ErrorObject } from "../../types";
export type PatternError = ErrorObject<"pattern", {
    pattern: string;
}, string | {
    $data: string;
}>;
declare const def: CodeKeywordDefinition;
export default def;


code:
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const code_1 = require("../code");
const codegen_1 = require("../../compile/codegen");
const error = {
    message: ({ schemaCode }) => (0, codegen_1.str) `must match pattern "${schemaCode}"`,
    params: ({ schemaCode }) => (0, codegen_1._) `{pattern: ${schemaCode}}`,
};
const def = {
    keyword: "pattern",
    type: "string",
    schemaType: "string",
    $data: true,
    error,
    code(cxt) {
        const { data, $data, schema, schemaCode, it } = cxt;
        // TODO regexp should be wrapped in try/catchs
        const u = it.opts.unicodeRegExp ? "u" : "";
        const regExp = $data ? (0, codegen_1._) `(new RegExp(${schemaCode}, ${u}))` : (0, code_1.usePattern)(cxt, schema);
        cxt.fail$data((0, codegen_1._) `!${regExp}.test(${data})`);
    },
};
exports.default = def;
//# sourceMappingURL=pattern.js.map


code:
import type { CodeKeywordDefinition, ErrorObject } from "../../types";
export type RequiredError = ErrorObject<"required", {
    missingProperty: string;
}, string[] | {
    $data: string;
}>;
declare const def: CodeKeywordDefinition;
export default def;


code:
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const code_1 = require("../code");
const codegen_1 = require("../../compile/codegen");
const util_1 = require("../../compile/util");
const error = {
    message: ({ params: { missingProperty } }) => (0, codegen_1.str) `must have required property '${missingProperty}'`,
    params: ({ params: { missingProperty } }) => (0, codegen_1._) `{missingProperty: ${missingProperty}}`,
};
const def = {
    keyword: "required",
    type: "object",
    schemaType: "array",
    $data: true,
    error,
    code(cxt) {
        const { gen, schema, schemaCode, data, $data, it } = cxt;
        const { opts } = it;
        if (!$data && schema.length === 0)
            return;
        const useLoop = schema.length >= opts.loopRequired;
        if (it.allErrors)
            allErrorsMode();
        else
            exitOnErrorMode();
        if (opts.strictRequired) {
            const props = cxt.parentSchema.properties;
            const { definedProperties } = cxt.it;
            for (const requiredKey of schema) {
                if ((props === null || props === void 0 ? void 0 : props[requiredKey]) === undefined && !definedProperties.has(requiredKey)) {
                    const schemaPath = it.schemaEnv.baseId + it.errSchemaPath;
                    const msg = `required property "${requiredKey}" is not defined at "${schemaPath}" (strictRequired)`;
                    (0, util_1.checkStrictMode)(it, msg, it.opts.strictRequired);
                }
            }
        }
        function allErrorsMode() {
            if (useLoop || $data) {
                cxt.block$data(codegen_1.nil, loopAllRequired);
            }
            else {
                for (const prop of schema) {
                    (0, code_1.checkReportMissingProp)(cxt, prop);
                }
            }
        }
        function exitOnErrorMode() {
            const missing = gen.let("missing");
            if (useLoop || $data) {
                const valid = gen.let("valid", true);
                cxt.block$data(valid, () => loopUntilMissing(missing, valid));
                cxt.ok(valid);
            }
            else {
                gen.if((0, code_1.checkMissingProp)(cxt, schema, missing));
                (0, code_1.reportMissingProp)(cxt, missing);
                gen.else();
            }
        }
        function loopAllRequired() {
            gen.forOf("prop", schemaCode, (prop) => {
                cxt.setParams({ missingProperty: prop });
                gen.if((0, code_1.noPropertyInData)(gen, data, prop, opts.ownProperties), () => cxt.error());
            });
        }
        function loopUntilMissing(missing, valid) {
            cxt.setParams({ missingProperty: missing });
            gen.forOf(missing, schemaCode, () => {
                gen.assign(valid, (0, code_1.propertyInData)(gen, data, missing, opts.ownProperties));
                gen.if((0, codegen_1.not)(valid), () => {
                    cxt.error();
                    gen.break();
                });
            }, codegen_1.nil);
        }
    },
};
exports.default = def;
//# sourceMappingURL=required.js.map


code:
import type { CodeKeywordDefinition, ErrorObject } from "../../types";
export type UniqueItemsError = ErrorObject<"uniqueItems", {
    i: number;
    j: number;
}, boolean | {
    $data: string;
}>;
declare const def: CodeKeywordDefinition;
export default def;


code:
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const dataType_1 = require("../../compile/validate/dataType");
const codegen_1 = require("../../compile/codegen");
const util_1 = require("../../compile/util");
const equal_1 = require("../../runtime/equal");
const error = {
    message: ({ params: { i, j } }) => (0, codegen_1.str) `must NOT have duplicate items (items ## ${j} and ${i} are identical)`,
    params: ({ params: { i, j } }) => (0, codegen_1._) `{i: ${i}, j: ${j}}`,
};
const def = {
    keyword: "uniqueItems",
    type: "array",
    schemaType: "boolean",
    $data: true,
    error,
    code(cxt) {
        const { gen, data, $data, schema, parentSchema, schemaCode, it } = cxt;
        if (!$data && !schema)
            return;
        const valid = gen.let("valid");
        const itemTypes = parentSchema.items ? (0, dataType_1.getSchemaTypes)(parentSchema.items) : [];
        cxt.block$data(valid, validateUniqueItems, (0, codegen_1._) `${schemaCode} === false`);
        cxt.ok(valid);
        function validateUniqueItems() {
            const i = gen.let("i", (0, codegen_1._) `${data}.length`);
            const j = gen.let("j");
            cxt.setParams({ i, j });
            gen.assign(valid, true);
            gen.if((0, codegen_1._) `${i} > 1`, () => (canOptimize() ? loopN : loopN2)(i, j));
        }
        function canOptimize() {
            return itemTypes.length > 0 && !itemTypes.some((t) => t === "object" || t === "array");
        }
        function loopN(i, j) {
            const item = gen.name("item");
            const wrongType = (0, dataType_1.checkDataTypes)(itemTypes, item, it.opts.strictNumbers, dataType_1.DataType.Wrong);
            const indices = gen.const("indices", (0, codegen_1._) `{}`);
            gen.for((0, codegen_1._) `;${i}--;`, () => {
                gen.let(item, (0, codegen_1._) `${data}[${i}]`);
                gen.if(wrongType, (0, codegen_1._) `continue`);
                if (itemTypes.length > 1)
                    gen.if((0, codegen_1._) `typeof ${item} == "string"`, (0, codegen_1._) `${item} += "_"`);
                gen
                    .if((0, codegen_1._) `typeof ${indices}[${item}] == "number"`, () => {
                    gen.assign(j, (0, codegen_1._) `${indices}[${item}]`);
                    cxt.error();
                    gen.assign(valid, false).break();
                })
                    .code((0, codegen_1._) `${indices}[${item}] = ${i}`);
            });
        }
        function loopN2(i, j) {
            const eql = (0, util_1.useFunc)(gen, equal_1.default);
            const outer = gen.name("outer");
            gen.label(outer).for((0, codegen_1._) `;${i}--;`, () => gen.for((0, codegen_1._) `${j} = ${i}; ${j}--;`, () => gen.if((0, codegen_1._) `${eql}(${data}[${i}], ${data}[${j}])`, () => {
                cxt.error();
                gen.assign(valid, false).break(outer);
            })));
        }
    },
};
exports.default = def;
//# sourceMappingURL=uniqueItems.js.map


code:
import type {AnySchemaObject} from "./types"
import AjvCore, {Options} from "./core"

import draft7Vocabularies from "./vocabularies/draft7"
import dynamicVocabulary from "./vocabularies/dynamic"
import nextVocabulary from "./vocabularies/next"
import unevaluatedVocabulary from "./vocabularies/unevaluated"
import discriminator from "./vocabularies/discriminator"
import addMetaSchema2019 from "./refs/json-schema-2019-09"

const META_SCHEMA_ID = "https://json-schema.org/draft/2019-09/schema"

export class Ajv2019 extends AjvCore {
  constructor(opts: Options = {}) {
    super({
      ...opts,
      dynamicRef: true,
      next: true,
      unevaluated: true,
    })
  }

  _addVocabularies(): void {
    super._addVocabularies()
    this.addVocabulary(dynamicVocabulary)
    draft7Vocabularies.forEach((v) => this.addVocabulary(v))
    this.addVocabulary(nextVocabulary)
    this.addVocabulary(unevaluatedVocabulary)
    if (this.opts.discriminator) this.addKeyword(discriminator)
  }

  _addDefaultMetaSchema(): void {
    super._addDefaultMetaSchema()
    const {$data, meta} = this.opts
    if (!meta) return
    addMetaSchema2019.call(this, $data)
    this.refs["http://json-schema.org/schema"] = META_SCHEMA_ID
  }

  defaultMeta(): string | AnySchemaObject | undefined {
    return (this.opts.defaultMeta =
      super.defaultMeta() || (this.getSchema(META_SCHEMA_ID) ? META_SCHEMA_ID : undefined))
  }
}

module.exports = exports = Ajv2019
module.exports.Ajv2019 = Ajv2019
Object.defineProperty(exports, "__esModule", {value: true})

export default Ajv2019

export {
  Format,
  FormatDefinition,
  AsyncFormatDefinition,
  KeywordDefinition,
  KeywordErrorDefinition,
  CodeKeywordDefinition,
  MacroKeywordDefinition,
  FuncKeywordDefinition,
  Vocabulary,
  Schema,
  SchemaObject,
  AnySchemaObject,
  AsyncSchema,
  AnySchema,
  ValidateFunction,
  AsyncValidateFunction,
  ErrorObject,
  ErrorNoParams,
} from "./types"

export {Plugin, Options, CodeOptions, InstanceOptions, Logger, ErrorsTextOptions} from "./core"
export {SchemaCxt, SchemaObjCxt} from "./compile"
export {KeywordCxt} from "./compile/validate"
export {DefinedError} from "./vocabularies/errors"
export {JSONType} from "./compile/rules"
export {JSONSchemaType} from "./types/json-schema"
export {_, str, stringify, nil, Name, Code, CodeGen, CodeGenOptions} from "./compile/codegen"
export {default as ValidationError} from "./runtime/validation_error"
export {default as MissingRefError} from "./compile/ref_error"


code:
import type {AnySchemaObject} from "./types"
import AjvCore, {Options} from "./core"

import draft2020Vocabularies from "./vocabularies/draft2020"
import discriminator from "./vocabularies/discriminator"
import addMetaSchema2020 from "./refs/json-schema-2020-12"

const META_SCHEMA_ID = "https://json-schema.org/draft/2020-12/schema"

export class Ajv2020 extends AjvCore {
  constructor(opts: Options = {}) {
    super({
      ...opts,
      dynamicRef: true,
      next: true,
      unevaluated: true,
    })
  }

  _addVocabularies(): void {
    super._addVocabularies()
    draft2020Vocabularies.forEach((v) => this.addVocabulary(v))
    if (this.opts.discriminator) this.addKeyword(discriminator)
  }

  _addDefaultMetaSchema(): void {
    super._addDefaultMetaSchema()
    const {$data, meta} = this.opts
    if (!meta) return
    addMetaSchema2020.call(this, $data)
    this.refs["http://json-schema.org/schema"] = META_SCHEMA_ID
  }

  defaultMeta(): string | AnySchemaObject | undefined {
    return (this.opts.defaultMeta =
      super.defaultMeta() || (this.getSchema(META_SCHEMA_ID) ? META_SCHEMA_ID : undefined))
  }
}

module.exports = exports = Ajv2020
module.exports.Ajv2020 = Ajv2020
Object.defineProperty(exports, "__esModule", {value: true})

export default Ajv2020

export {
  Format,
  FormatDefinition,
  AsyncFormatDefinition,
  KeywordDefinition,
  KeywordErrorDefinition,
  CodeKeywordDefinition,
  MacroKeywordDefinition,
  FuncKeywordDefinition,
  Vocabulary,
  Schema,
  SchemaObject,
  AnySchemaObject,
  AsyncSchema,
  AnySchema,
  ValidateFunction,
  AsyncValidateFunction,
  ErrorObject,
  ErrorNoParams,
} from "./types"

export {Plugin, Options, CodeOptions, InstanceOptions, Logger, ErrorsTextOptions} from "./core"
export {SchemaCxt, SchemaObjCxt} from "./compile"
export {KeywordCxt} from "./compile/validate"
export {DefinedError} from "./vocabularies/errors"
export {JSONType} from "./compile/rules"
export {JSONSchemaType} from "./types/json-schema"
export {_, str, stringify, nil, Name, Code, CodeGen, CodeGenOptions} from "./compile/codegen"
export {default as ValidationError} from "./runtime/validation_error"
export {default as MissingRefError} from "./compile/ref_error"


code:
import type {AnySchemaObject} from "./types"
import AjvCore from "./core"
import draft7Vocabularies from "./vocabularies/draft7"
import discriminator from "./vocabularies/discriminator"
import * as draft7MetaSchema from "./refs/json-schema-draft-07.json"

const META_SUPPORT_DATA = ["/properties"]

const META_SCHEMA_ID = "http://json-schema.org/draft-07/schema"

export class Ajv extends AjvCore {
  _addVocabularies(): void {
    super._addVocabularies()
    draft7Vocabularies.forEach((v) => this.addVocabulary(v))
    if (this.opts.discriminator) this.addKeyword(discriminator)
  }

  _addDefaultMetaSchema(): void {
    super._addDefaultMetaSchema()
    if (!this.opts.meta) return
    const metaSchema = this.opts.$data
      ? this.$dataMetaSchema(draft7MetaSchema, META_SUPPORT_DATA)
      : draft7MetaSchema
    this.addMetaSchema(metaSchema, META_SCHEMA_ID, false)
    this.refs["http://json-schema.org/schema"] = META_SCHEMA_ID
  }

  defaultMeta(): string | AnySchemaObject | undefined {
    return (this.opts.defaultMeta =
      super.defaultMeta() || (this.getSchema(META_SCHEMA_ID) ? META_SCHEMA_ID : undefined))
  }
}

module.exports = exports = Ajv
module.exports.Ajv = Ajv
Object.defineProperty(exports, "__esModule", {value: true})

export default Ajv

export {
  Format,
  FormatDefinition,
  AsyncFormatDefinition,
  KeywordDefinition,
  KeywordErrorDefinition,
  CodeKeywordDefinition,
  MacroKeywordDefinition,
  FuncKeywordDefinition,
  Vocabulary,
  Schema,
  SchemaObject,
  AnySchemaObject,
  AsyncSchema,
  AnySchema,
  ValidateFunction,
  AsyncValidateFunction,
  SchemaValidateFunction,
  ErrorObject,
  ErrorNoParams,
} from "./types"

export {Plugin, Options, CodeOptions, InstanceOptions, Logger, ErrorsTextOptions} from "./core"
export {SchemaCxt, SchemaObjCxt} from "./compile"
export {KeywordCxt} from "./compile/validate"
export {DefinedError} from "./vocabularies/errors"
export {JSONType} from "./compile/rules"
export {JSONSchemaType} from "./types/json-schema"
export {_, str, stringify, nil, Name, Code, CodeGen, CodeGenOptions} from "./compile/codegen"
export {default as ValidationError} from "./runtime/validation_error"
export {default as MissingRefError} from "./compile/ref_error"


code:
export abstract class _CodeOrName {
  abstract readonly str: string
  abstract readonly names: UsedNames
  abstract toString(): string
  abstract emptyStr(): boolean
}

export const IDENTIFIER = /^[a-z$_][a-z$_0-9]*$/i

export class Name extends _CodeOrName {
  readonly str: string
  constructor(s: string) {
    super()
    if (!IDENTIFIER.test(s)) throw new Error("CodeGen: name must be a valid identifier")
    this.str = s
  }

  toString(): string {
    return this.str
  }

  emptyStr(): boolean {
    return false
  }

  get names(): UsedNames {
    return {[this.str]: 1}
  }
}

export class _Code extends _CodeOrName {
  readonly _items: readonly CodeItem[]
  private _str?: string
  private _names?: UsedNames

  constructor(code: string | readonly CodeItem[]) {
    super()
    this._items = typeof code === "string" ? [code] : code
  }

  toString(): string {
    return this.str
  }

  emptyStr(): boolean {
    if (this._items.length > 1) return false
    const item = this._items[0]
    return item === "" || item === '""'
  }

  get str(): string {
    return (this._str ??= this._items.reduce((s: string, c: CodeItem) => `${s}${c}`, ""))
  }

  get names(): UsedNames {
    return (this._names ??= this._items.reduce((names: UsedNames, c) => {
      if (c instanceof Name) names[c.str] = (names[c.str] || 0) + 1
      return names
    }, {}))
  }
}

export type CodeItem = Name | string | number | boolean | null

export type UsedNames = Record<string, number | undefined>

export type Code = _Code | Name

export type SafeExpr = Code | number | boolean | null

export const nil = new _Code("")

type CodeArg = SafeExpr | string | undefined

export function _(strs: TemplateStringsArray, ...args: CodeArg[]): _Code {
  const code: CodeItem[] = [strs[0]]
  let i = 0
  while (i < args.length) {
    addCodeArg(code, args[i])
    code.push(strs[++i])
  }
  return new _Code(code)
}

const plus = new _Code("+")

export function str(strs: TemplateStringsArray, ...args: (CodeArg | string[])[]): _Code {
  const expr: CodeItem[] = [safeStringify(strs[0])]
  let i = 0
  while (i < args.length) {
    expr.push(plus)
    addCodeArg(expr, args[i])
    expr.push(plus, safeStringify(strs[++i]))
  }
  optimize(expr)
  return new _Code(expr)
}

export function addCodeArg(code: CodeItem[], arg: CodeArg | string[]): void {
  if (arg instanceof _Code) code.push(...arg._items)
  else if (arg instanceof Name) code.push(arg)
  else code.push(interpolate(arg))
}

function optimize(expr: CodeItem[]): void {
  let i = 1
  while (i < expr.length - 1) {
    if (expr[i] === plus) {
      const res = mergeExprItems(expr[i - 1], expr[i + 1])
      if (res !== undefined) {
        expr.splice(i - 1, 3, res)
        continue
      }
      expr[i++] = "+"
    }
    i++
  }
}

function mergeExprItems(a: CodeItem, b: CodeItem): CodeItem | undefined {
  if (b === '""') return a
  if (a === '""') return b
  if (typeof a == "string") {
    if (b instanceof Name || a[a.length - 1] !== '"') return
    if (typeof b != "string") return `${a.slice(0, -1)}${b}"`
    if (b[0] === '"') return a.slice(0, -1) + b.slice(1)
    return
  }
  if (typeof b == "string" && b[0] === '"' && !(a instanceof Name)) return `"${a}${b.slice(1)}`
  return
}

export function strConcat(c1: Code, c2: Code): Code {
  return c2.emptyStr() ? c1 : c1.emptyStr() ? c2 : str`${c1}${c2}`
}

// TODO do not allow arrays here
function interpolate(x?: string | string[] | number | boolean | null): SafeExpr | string {
  return typeof x == "number" || typeof x == "boolean" || x === null
    ? x
    : safeStringify(Array.isArray(x) ? x.join(",") : x)
}

export function stringify(x: unknown): Code {
  return new _Code(safeStringify(x))
}

export function safeStringify(x: unknown): string {
  return JSON.stringify(x)
    .replace(/\u2028/g, "\\u2028")
    .replace(/\u2029/g, "\\u2029")
}

export function getProperty(key: Code | string | number): Code {
  return typeof key == "string" && IDENTIFIER.test(key) ? new _Code(`.${key}`) : _`[${key}]`
}

//Does best effort to format the name properly
export function getEsmExportName(key: Code | string | number): Code {
  if (typeof key == "string" && IDENTIFIER.test(key)) {
    return new _Code(`${key}`)
  }
  throw new Error(`CodeGen: invalid export name: ${key}, use explicit $id name mapping`)
}

export function regexpCode(rx: RegExp): Code {
  return new _Code(rx.toString())
}


code:
import type {ScopeValueSets, NameValue, ValueScope, ValueScopeName} from "./scope"
import {_, nil, _Code, Code, Name, UsedNames, CodeItem, addCodeArg, _CodeOrName} from "./code"
import {Scope, varKinds} from "./scope"

export {_, str, strConcat, nil, getProperty, stringify, regexpCode, Name, Code} from "./code"
export {Scope, ScopeStore, ValueScope, ValueScopeName, ScopeValueSets, varKinds} from "./scope"

// type for expressions that can be safely inserted in code without quotes
export type SafeExpr = Code | number | boolean | null

// type that is either Code of function that adds code to CodeGen instance using its methods
export type Block = Code | (() => void)

export const operators = {
  GT: new _Code(">"),
  GTE: new _Code(">="),
  LT: new _Code("<"),
  LTE: new _Code("<="),
  EQ: new _Code("==="),
  NEQ: new _Code("!=="),
  NOT: new _Code("!"),
  OR: new _Code("||"),
  AND: new _Code("&&"),
  ADD: new _Code("+"),
}

abstract class Node {
  abstract readonly names: UsedNames

  optimizeNodes(): this | ChildNode | ChildNode[] | undefined {
    return this
  }

  optimizeNames(_names: UsedNames, _constants: Constants): this | undefined {
    return this
  }

  // get count(): number {
  //   return 1
  // }
}

class Def extends Node {
  constructor(
    private readonly varKind: Name,
    private readonly name: Name,
    private rhs?: SafeExpr
  ) {
    super()
  }

  render({es5, _n}: CGOptions): string {
    const varKind = es5 ? varKinds.var : this.varKind
    const rhs = this.rhs === undefined ? "" : ` = ${this.rhs}`
    return `${varKind} ${this.name}${rhs};` + _n
  }

  optimizeNames(names: UsedNames, constants: Constants): this | undefined {
    if (!names[this.name.str]) return
    if (this.rhs) this.rhs = optimizeExpr(this.rhs, names, constants)
    return this
  }

  get names(): UsedNames {
    return this.rhs instanceof _CodeOrName ? this.rhs.names : {}
  }
}

class Assign extends Node {
  constructor(
    readonly lhs: Code,
    public rhs: SafeExpr,
    private readonly sideEffects?: boolean
  ) {
    super()
  }

  render({_n}: CGOptions): string {
    return `${this.lhs} = ${this.rhs};` + _n
  }

  optimizeNames(names: UsedNames, constants: Constants): this | undefined {
    if (this.lhs instanceof Name && !names[this.lhs.str] && !this.sideEffects) return
    this.rhs = optimizeExpr(this.rhs, names, constants)
    return this
  }

  get names(): UsedNames {
    const names = this.lhs instanceof Name ? {} : {...this.lhs.names}
    return addExprNames(names, this.rhs)
  }
}

class AssignOp extends Assign {
  constructor(
    lhs: Code,
    private readonly op: Code,
    rhs: SafeExpr,
    sideEffects?: boolean
  ) {
    super(lhs, rhs, sideEffects)
  }

  render({_n}: CGOptions): string {
    return `${this.lhs} ${this.op}= ${this.rhs};` + _n
  }
}

class Label extends Node {
  readonly names: UsedNames = {}
  constructor(readonly label: Name) {
    super()
  }

  render({_n}: CGOptions): string {
    return `${this.label}:` + _n
  }
}

class Break extends Node {
  readonly names: UsedNames = {}
  constructor(readonly label?: Code) {
    super()
  }

  render({_n}: CGOptions): string {
    const label = this.label ? ` ${this.label}` : ""
    return `break${label};` + _n
  }
}

class Throw extends Node {
  constructor(readonly error: Code) {
    super()
  }

  render({_n}: CGOptions): string {
    return `throw ${this.error};` + _n
  }

  get names(): UsedNames {
    return this.error.names
  }
}

class AnyCode extends Node {
  constructor(private code: SafeExpr) {
    super()
  }

  render({_n}: CGOptions): string {
    return `${this.code};` + _n
  }

  optimizeNodes(): this | undefined {
    return `${this.code}` ? this : undefined
  }

  optimizeNames(names: UsedNames, constants: Constants): this {
    this.code = optimizeExpr(this.code, names, constants)
    return this
  }

  get names(): UsedNames {
    return this.code instanceof _CodeOrName ? this.code.names : {}
  }
}

abstract class ParentNode extends Node {
  constructor(readonly nodes: ChildNode[] = []) {
    super()
  }

  render(opts: CGOptions): string {
    return this.nodes.reduce((code, n) => code + n.render(opts), "")
  }

  optimizeNodes(): this | ChildNode | ChildNode[] | undefined {
    const {nodes} = this
    let i = nodes.length
    while (i--) {
      const n = nodes[i].optimizeNodes()
      if (Array.isArray(n)) nodes.splice(i, 1, ...n)
      else if (n) nodes[i] = n
      else nodes.splice(i, 1)
    }
    return nodes.length > 0 ? this : undefined
  }

  optimizeNames(names: UsedNames, constants: Constants): this | undefined {
    const {nodes} = this
    let i = nodes.length
    while (i--) {
      // iterating backwards improves 1-pass optimization
      const n = nodes[i]
      if (n.optimizeNames(names, constants)) continue
      subtractNames(names, n.names)
      nodes.splice(i, 1)
    }
    return nodes.length > 0 ? this : undefined
  }

  get names(): UsedNames {
    return this.nodes.reduce((names: UsedNames, n) => addNames(names, n.names), {})
  }

  // get count(): number {
  //   return this.nodes.reduce((c, n) => c + n.count, 1)
  // }
}

abstract class BlockNode extends ParentNode {
  render(opts: CGOptions): string {
    return "{" + opts._n + super.render(opts) + "}" + opts._n
  }
}

class Root extends ParentNode {}

class Else extends BlockNode {
  static readonly kind = "else"
}

class If extends BlockNode {
  static readonly kind = "if"
  else?: If | Else
  constructor(
    private condition: Code | boolean,
    nodes?: ChildNode[]
  ) {
    super(nodes)
  }

  render(opts: CGOptions): string {
    let code = `if(${this.condition})` + super.render(opts)
    if (this.else) code += "else " + this.else.render(opts)
    return code
  }

  optimizeNodes(): If | ChildNode[] | undefined {
    super.optimizeNodes()
    const cond = this.condition
    if (cond === true) return this.nodes // else is ignored here
    let e = this.else
    if (e) {
      const ns = e.optimizeNodes()
      e = this.else = Array.isArray(ns) ? new Else(ns) : (ns as Else | undefined)
    }
    if (e) {
      if (cond === false) return e instanceof If ? e : e.nodes
      if (this.nodes.length) return this
      return new If(not(cond), e instanceof If ? [e] : e.nodes)
    }
    if (cond === false || !this.nodes.length) return undefined
    return this
  }

  optimizeNames(names: UsedNames, constants: Constants): this | undefined {
    this.else = this.else?.optimizeNames(names, constants)
    if (!(super.optimizeNames(names, constants) || this.else)) return
    this.condition = optimizeExpr(this.condition, names, constants)
    return this
  }

  get names(): UsedNames {
    const names = super.names
    addExprNames(names, this.condition)
    if (this.else) addNames(names, this.else.names)
    return names
  }

  // get count(): number {
  //   return super.count + (this.else?.count || 0)
  // }
}

abstract class For extends BlockNode {
  static readonly kind = "for"
}

class ForLoop extends For {
  constructor(private iteration: Code) {
    super()
  }

  render(opts: CGOptions): string {
    return `for(${this.iteration})` + super.render(opts)
  }

  optimizeNames(names: UsedNames, constants: Constants): this | undefined {
    if (!super.optimizeNames(names, constants)) return
    this.iteration = optimizeExpr(this.iteration, names, constants)
    return this
  }

  get names(): UsedNames {
    return addNames(super.names, this.iteration.names)
  }
}

class ForRange extends For {
  constructor(
    private readonly varKind: Name,
    private readonly name: Name,
    private readonly from: SafeExpr,
    private readonly to: SafeExpr
  ) {
    super()
  }

  render(opts: CGOptions): string {
    const varKind = opts.es5 ? varKinds.var : this.varKind
    const {name, from, to} = this
    return `for(${varKind} ${name}=${from}; ${name}<${to}; ${name}++)` + super.render(opts)
  }

  get names(): UsedNames {
    const names = addExprNames(super.names, this.from)
    return addExprNames(names, this.to)
  }
}

class ForIter extends For {
  constructor(
    private readonly loop: "of" | "in",
    private readonly varKind: Name,
    private readonly name: Name,
    private iterable: Code
  ) {
    super()
  }

  render(opts: CGOptions): string {
    return `for(${this.varKind} ${this.name} ${this.loop} ${this.iterable})` + super.render(opts)
  }

  optimizeNames(names: UsedNames, constants: Constants): this | undefined {
    if (!super.optimizeNames(names, constants)) return
    this.iterable = optimizeExpr(this.iterable, names, constants)
    return this
  }

  get names(): UsedNames {
    return addNames(super.names, this.iterable.names)
  }
}

class Func extends BlockNode {
  static readonly kind = "func"
  constructor(
    public name: Name,
    public args: Code,
    public async?: boolean
  ) {
    super()
  }

  render(opts: CGOptions): string {
    const _async = this.async ? "async " : ""
    return `${_async}function ${this.name}(${this.args})` + super.render(opts)
  }
}

class Return extends ParentNode {
  static readonly kind = "return"

  render(opts: CGOptions): string {
    return "return " + super.render(opts)
  }
}

class Try extends BlockNode {
  catch?: Catch
  finally?: Finally

  render(opts: CGOptions): string {
    let code = "try" + super.render(opts)
    if (this.catch) code += this.catch.render(opts)
    if (this.finally) code += this.finally.render(opts)
    return code
  }

  optimizeNodes(): this {
    super.optimizeNodes()
    this.catch?.optimizeNodes() as Catch | undefined
    this.finally?.optimizeNodes() as Finally | undefined
    return this
  }

  optimizeNames(names: UsedNames, constants: Constants): this {
    super.optimizeNames(names, constants)
    this.catch?.optimizeNames(names, constants)
    this.finally?.optimizeNames(names, constants)
    return this
  }

  get names(): UsedNames {
    const names = super.names
    if (this.catch) addNames(names, this.catch.names)
    if (this.finally) addNames(names, this.finally.names)
    return names
  }

  // get count(): number {
  //   return super.count + (this.catch?.count || 0) + (this.finally?.count || 0)
  // }
}

class Catch extends BlockNode {
  static readonly kind = "catch"
  constructor(readonly error: Name) {
    super()
  }

  render(opts: CGOptions): string {
    return `catch(${this.error})` + super.render(opts)
  }
}

class Finally extends BlockNode {
  static readonly kind = "finally"
  render(opts: CGOptions): string {
    return "finally" + super.render(opts)
  }
}

type StartBlockNode = If | For | Func | Return | Try

type LeafNode = Def | Assign | Label | Break | Throw | AnyCode

type ChildNode = StartBlockNode | LeafNode

type EndBlockNodeType =
  | typeof If
  | typeof Else
  | typeof For
  | typeof Func
  | typeof Return
  | typeof Catch
  | typeof Finally

type Constants = Record<string, SafeExpr | undefined>

export interface CodeGenOptions {
  es5?: boolean
  lines?: boolean
  ownProperties?: boolean
}

interface CGOptions extends CodeGenOptions {
  _n: "\n" | ""
}

export class CodeGen {
  readonly _scope: Scope
  readonly _extScope: ValueScope
  readonly _values: ScopeValueSets = {}
  private readonly _nodes: ParentNode[]
  private readonly _blockStarts: number[] = []
  private readonly _constants: Constants = {}
  private readonly opts: CGOptions

  constructor(extScope: ValueScope, opts: CodeGenOptions = {}) {
    this.opts = {...opts, _n: opts.lines ? "\n" : ""}
    this._extScope = extScope
    this._scope = new Scope({parent: extScope})
    this._nodes = [new Root()]
  }

  toString(): string {
    return this._root.render(this.opts)
  }

  // returns unique name in the internal scope
  name(prefix: string): Name {
    return this._scope.name(prefix)
  }

  // reserves unique name in the external scope
  scopeName(prefix: string): ValueScopeName {
    return this._extScope.name(prefix)
  }

  // reserves unique name in the external scope and assigns value to it
  scopeValue(prefixOrName: ValueScopeName | string, value: NameValue): Name {
    const name = this._extScope.value(prefixOrName, value)
    const vs = this._values[name.prefix] || (this._values[name.prefix] = new Set())
    vs.add(name)
    return name
  }

  getScopeValue(prefix: string, keyOrRef: unknown): ValueScopeName | undefined {
    return this._extScope.getValue(prefix, keyOrRef)
  }

  // return code that assigns values in the external scope to the names that are used internally
  // (same names that were returned by gen.scopeName or gen.scopeValue)
  scopeRefs(scopeName: Name): Code {
    return this._extScope.scopeRefs(scopeName, this._values)
  }

  scopeCode(): Code {
    return this._extScope.scopeCode(this._values)
  }

  private _def(
    varKind: Name,
    nameOrPrefix: Name | string,
    rhs?: SafeExpr,
    constant?: boolean
  ): Name {
    const name = this._scope.toName(nameOrPrefix)
    if (rhs !== undefined && constant) this._constants[name.str] = rhs
    this._leafNode(new Def(varKind, name, rhs))
    return name
  }

  // `const` declaration (`var` in es5 mode)
  const(nameOrPrefix: Name | string, rhs: SafeExpr, _constant?: boolean): Name {
    return this._def(varKinds.const, nameOrPrefix, rhs, _constant)
  }

  // `let` declaration with optional assignment (`var` in es5 mode)
  let(nameOrPrefix: Name | string, rhs?: SafeExpr, _constant?: boolean): Name {
    return this._def(varKinds.let, nameOrPrefix, rhs, _constant)
  }

  // `var` declaration with optional assignment
  var(nameOrPrefix: Name | string, rhs?: SafeExpr, _constant?: boolean): Name {
    return this._def(varKinds.var, nameOrPrefix, rhs, _constant)
  }

  // assignment code
  assign(lhs: Code, rhs: SafeExpr, sideEffects?: boolean): CodeGen {
    return this._leafNode(new Assign(lhs, rhs, sideEffects))
  }

  // `+=` code
  add(lhs: Code, rhs: SafeExpr): CodeGen {
    return this._leafNode(new AssignOp(lhs, operators.ADD, rhs))
  }

  // appends passed SafeExpr to code or executes Block
  code(c: Block | SafeExpr): CodeGen {
    if (typeof c == "function") c()
    else if (c !== nil) this._leafNode(new AnyCode(c))
    return this
  }

  // returns code for object literal for the passed argument list of key-value pairs
  object(...keyValues: [Name | string, SafeExpr | string][]): _Code {
    const code: CodeItem[] = ["{"]
    for (const [key, value] of keyValues) {
      if (code.length > 1) code.push(",")
      code.push(key)
      if (key !== value || this.opts.es5) {
        code.push(":")
        addCodeArg(code, value)
      }
    }
    code.push("}")
    return new _Code(code)
  }

  // `if` clause (or statement if `thenBody` and, optionally, `elseBody` are passed)
  if(condition: Code | boolean, thenBody?: Block, elseBody?: Block): CodeGen {
    this._blockNode(new If(condition))

    if (thenBody && elseBody) {
      this.code(thenBody).else().code(elseBody).endIf()
    } else if (thenBody) {
      this.code(thenBody).endIf()
    } else if (elseBody) {
      throw new Error('CodeGen: "else" body without "then" body')
    }
    return this
  }

  // `else if` clause - invalid without `if` or after `else` clauses
  elseIf(condition: Code | boolean): CodeGen {
    return this._elseNode(new If(condition))
  }

  // `else` clause - only valid after `if` or `else if` clauses
  else(): CodeGen {
    return this._elseNode(new Else())
  }

  // end `if` statement (needed if gen.if was used only with condition)
  endIf(): CodeGen {
    return this._endBlockNode(If, Else)
  }

  private _for(node: For, forBody?: Block): CodeGen {
    this._blockNode(node)
    if (forBody) this.code(forBody).endFor()
    return this
  }

  // a generic `for` clause (or statement if `forBody` is passed)
  for(iteration: Code, forBody?: Block): CodeGen {
    return this._for(new ForLoop(iteration), forBody)
  }

  // `for` statement for a range of values
  forRange(
    nameOrPrefix: Name | string,
    from: SafeExpr,
    to: SafeExpr,
    forBody: (index: Name) => void,
    varKind: Code = this.opts.es5 ? varKinds.var : varKinds.let
  ): CodeGen {
    const name = this._scope.toName(nameOrPrefix)
    return this._for(new ForRange(varKind, name, from, to), () => forBody(name))
  }

  // `for-of` statement (in es5 mode replace with a normal for loop)
  forOf(
    nameOrPrefix: Name | string,
    iterable: Code,
    forBody: (item: Name) => void,
    varKind: Code = varKinds.const
  ): CodeGen {
    const name = this._scope.toName(nameOrPrefix)
    if (this.opts.es5) {
      const arr = iterable instanceof Name ? iterable : this.var("_arr", iterable)
      return this.forRange("_i", 0, _`${arr}.length`, (i) => {
        this.var(name, _`${arr}[${i}]`)
        forBody(name)
      })
    }
    return this._for(new ForIter("of", varKind, name, iterable), () => forBody(name))
  }

  // `for-in` statement.
  // With option `ownProperties` replaced with a `for-of` loop for object keys
  forIn(
    nameOrPrefix: Name | string,
    obj: Code,
    forBody: (item: Name) => void,
    varKind: Code = this.opts.es5 ? varKinds.var : varKinds.const
  ): CodeGen {
    if (this.opts.ownProperties) {
      return this.forOf(nameOrPrefix, _`Object.keys(${obj})`, forBody)
    }
    const name = this._scope.toName(nameOrPrefix)
    return this._for(new ForIter("in", varKind, name, obj), () => forBody(name))
  }

  // end `for` loop
  endFor(): CodeGen {
    return this._endBlockNode(For)
  }

  // `label` statement
  label(label: Name): CodeGen {
    return this._leafNode(new Label(label))
  }

  // `break` statement
  break(label?: Code): CodeGen {
    return this._leafNode(new Break(label))
  }

  // `return` statement
  return(value: Block | SafeExpr): CodeGen {
    const node = new Return()
    this._blockNode(node)
    this.code(value)
    if (node.nodes.length !== 1) throw new Error('CodeGen: "return" should have one node')
    return this._endBlockNode(Return)
  }

  // `try` statement
  try(tryBody: Block, catchCode?: (e: Name) => void, finallyCode?: Block): CodeGen {
    if (!catchCode && !finallyCode) throw new Error('CodeGen: "try" without "catch" and "finally"')
    const node = new Try()
    this._blockNode(node)
    this.code(tryBody)
    if (catchCode) {
      const error = this.name("e")
      this._currNode = node.catch = new Catch(error)
      catchCode(error)
    }
    if (finallyCode) {
      this._currNode = node.finally = new Finally()
      this.code(finallyCode)
    }
    return this._endBlockNode(Catch, Finally)
  }

  // `throw` statement
  throw(error: Code): CodeGen {
    return this._leafNode(new Throw(error))
  }

  // start self-balancing block
  block(body?: Block, nodeCount?: number): CodeGen {
    this._blockStarts.push(this._nodes.length)
    if (body) this.code(body).endBlock(nodeCount)
    return this
  }

  // end the current self-balancing block
  endBlock(nodeCount?: number): CodeGen {
    const len = this._blockStarts.pop()
    if (len === undefined) throw new Error("CodeGen: not in self-balancing block")
    const toClose = this._nodes.length - len
    if (toClose < 0 || (nodeCount !== undefined && toClose !== nodeCount)) {
      throw new Error(`CodeGen: wrong number of nodes: ${toClose} vs ${nodeCount} expected`)
    }
    this._nodes.length = len
    return this
  }

  // `function` heading (or definition if funcBody is passed)
  func(name: Name, args: Code = nil, async?: boolean, funcBody?: Block): CodeGen {
    this._blockNode(new Func(name, args, async))
    if (funcBody) this.code(funcBody).endFunc()
    return this
  }

  // end function definition
  endFunc(): CodeGen {
    return this._endBlockNode(Func)
  }

  optimize(n = 1): void {
    while (n-- > 0) {
      this._root.optimizeNodes()
      this._root.optimizeNames(this._root.names, this._constants)
    }
  }

  private _leafNode(node: LeafNode): CodeGen {
    this._currNode.nodes.push(node)
    return this
  }

  private _blockNode(node: StartBlockNode): void {
    this._currNode.nodes.push(node)
    this._nodes.push(node)
  }

  private _endBlockNode(N1: EndBlockNodeType, N2?: EndBlockNodeType): CodeGen {
    const n = this._currNode
    if (n instanceof N1 || (N2 && n instanceof N2)) {
      this._nodes.pop()
      return this
    }
    throw new Error(`CodeGen: not in block "${N2 ? `${N1.kind}/${N2.kind}` : N1.kind}"`)
  }

  private _elseNode(node: If | Else): CodeGen {
    const n = this._currNode
    if (!(n instanceof If)) {
      throw new Error('CodeGen: "else" without "if"')
    }
    this._currNode = n.else = node
    return this
  }

  private get _root(): Root {
    return this._nodes[0] as Root
  }

  private get _currNode(): ParentNode {
    const ns = this._nodes
    return ns[ns.length - 1]
  }

  private set _currNode(node: ParentNode) {
    const ns = this._nodes
    ns[ns.length - 1] = node
  }

  // get nodeCount(): number {
  //   return this._root.count
  // }
}

function addNames(names: UsedNames, from: UsedNames): UsedNames {
  for (const n in from) names[n] = (names[n] || 0) + (from[n] || 0)
  return names
}

function addExprNames(names: UsedNames, from: SafeExpr): UsedNames {
  return from instanceof _CodeOrName ? addNames(names, from.names) : names
}

function optimizeExpr<T extends SafeExpr | Code>(expr: T, names: UsedNames, constants: Constants): T
function optimizeExpr(expr: SafeExpr, names: UsedNames, constants: Constants): SafeExpr {
  if (expr instanceof Name) return replaceName(expr)
  if (!canOptimize(expr)) return expr
  return new _Code(
    expr._items.reduce((items: CodeItem[], c: SafeExpr | string) => {
      if (c instanceof Name) c = replaceName(c)
      if (c instanceof _Code) items.push(...c._items)
      else items.push(c)
      return items
    }, [])
  )

  function replaceName(n: Name): SafeExpr {
    const c = constants[n.str]
    if (c === undefined || names[n.str] !== 1) return n
    delete names[n.str]
    return c
  }

  function canOptimize(e: SafeExpr): e is _Code {
    return (
      e instanceof _Code &&
      e._items.some(
        (c) => c instanceof Name && names[c.str] === 1 && constants[c.str] !== undefined
      )
    )
  }
}

function subtractNames(names: UsedNames, from: UsedNames): void {
  for (const n in from) names[n] = (names[n] || 0) - (from[n] || 0)
}

export function not<T extends Code | SafeExpr>(x: T): T
export function not(x: Code | SafeExpr): Code | SafeExpr {
  return typeof x == "boolean" || typeof x == "number" || x === null ? !x : _`!${par(x)}`
}

const andCode = mappend(operators.AND)

// boolean AND (&&) expression with the passed arguments
export function and(...args: Code[]): Code {
  return args.reduce(andCode)
}

const orCode = mappend(operators.OR)

// boolean OR (||) expression with the passed arguments
export function or(...args: Code[]): Code {
  return args.reduce(orCode)
}

type MAppend = (x: Code, y: Code) => Code

function mappend(op: Code): MAppend {
  return (x, y) => (x === nil ? y : y === nil ? x : _`${par(x)} ${op} ${par(y)}`)
}

function par(x: Code): Code {
  return x instanceof Name ? x : _`(${x})`
}


code:
import {_, nil, Code, Name} from "./code"

interface NameGroup {
  prefix: string
  index: number
}

export interface NameValue {
  ref: ValueReference // this is the reference to any value that can be referred to from generated code via `globals` var in the closure
  key?: unknown // any key to identify a global to avoid duplicates, if not passed ref is used
  code?: Code // this is the code creating the value needed for standalone code wit_out closure - can be a primitive value, function or import (`require`)
}

export type ValueReference = unknown // possibly make CodeGen parameterized type on this type

class ValueError extends Error {
  readonly value?: NameValue
  constructor(name: ValueScopeName) {
    super(`CodeGen: "code" for ${name} not defined`)
    this.value = name.value
  }
}

interface ScopeOptions {
  prefixes?: Set<string>
  parent?: Scope
}

interface ValueScopeOptions extends ScopeOptions {
  scope: ScopeStore
  es5?: boolean
  lines?: boolean
}

export type ScopeStore = Record<string, ValueReference[] | undefined>

type ScopeValues = {
  [Prefix in string]?: Map<unknown, ValueScopeName>
}

export type ScopeValueSets = {
  [Prefix in string]?: Set<ValueScopeName>
}

export enum UsedValueState {
  Started,
  Completed,
}

export type UsedScopeValues = {
  [Prefix in string]?: Map<ValueScopeName, UsedValueState | undefined>
}

export const varKinds = {
  const: new Name("const"),
  let: new Name("let"),
  var: new Name("var"),
}

export class Scope {
  protected readonly _names: {[Prefix in string]?: NameGroup} = {}
  protected readonly _prefixes?: Set<string>
  protected readonly _parent?: Scope

  constructor({prefixes, parent}: ScopeOptions = {}) {
    this._prefixes = prefixes
    this._parent = parent
  }

  toName(nameOrPrefix: Name | string): Name {
    return nameOrPrefix instanceof Name ? nameOrPrefix : this.name(nameOrPrefix)
  }

  name(prefix: string): Name {
    return new Name(this._newName(prefix))
  }

  protected _newName(prefix: string): string {
    const ng = this._names[prefix] || this._nameGroup(prefix)
    return `${prefix}${ng.index++}`
  }

  private _nameGroup(prefix: string): NameGroup {
    if (this._parent?._prefixes?.has(prefix) || (this._prefixes && !this._prefixes.has(prefix))) {
      throw new Error(`CodeGen: prefix "${prefix}" is not allowed in this scope`)
    }
    return (this._names[prefix] = {prefix, index: 0})
  }
}

interface ScopePath {
  property: string
  itemIndex: number
}

export class ValueScopeName extends Name {
  readonly prefix: string
  value?: NameValue
  scopePath?: Code

  constructor(prefix: string, nameStr: string) {
    super(nameStr)
    this.prefix = prefix
  }

  setValue(value: NameValue, {property, itemIndex}: ScopePath): void {
    this.value = value
    this.scopePath = _`.${new Name(property)}[${itemIndex}]`
  }
}

interface VSOptions extends ValueScopeOptions {
  _n: Code
}

const line = _`\n`

export class ValueScope extends Scope {
  protected readonly _values: ScopeValues = {}
  protected readonly _scope: ScopeStore
  readonly opts: VSOptions

  constructor(opts: ValueScopeOptions) {
    super(opts)
    this._scope = opts.scope
    this.opts = {...opts, _n: opts.lines ? line : nil}
  }

  get(): ScopeStore {
    return this._scope
  }

  name(prefix: string): ValueScopeName {
    return new ValueScopeName(prefix, this._newName(prefix))
  }

  value(nameOrPrefix: ValueScopeName | string, value: NameValue): ValueScopeName {
    if (value.ref === undefined) throw new Error("CodeGen: ref must be passed in value")
    const name = this.toName(nameOrPrefix) as ValueScopeName
    const {prefix} = name
    const valueKey = value.key ?? value.ref
    let vs = this._values[prefix]
    if (vs) {
      const _name = vs.get(valueKey)
      if (_name) return _name
    } else {
      vs = this._values[prefix] = new Map()
    }
    vs.set(valueKey, name)

    const s = this._scope[prefix] || (this._scope[prefix] = [])
    const itemIndex = s.length
    s[itemIndex] = value.ref
    name.setValue(value, {property: prefix, itemIndex})
    return name
  }

  getValue(prefix: string, keyOrRef: unknown): ValueScopeName | undefined {
    const vs = this._values[prefix]
    if (!vs) return
    return vs.get(keyOrRef)
  }

  scopeRefs(scopeName: Name, values: ScopeValues | ScopeValueSets = this._values): Code {
    return this._reduceValues(values, (name: ValueScopeName) => {
      if (name.scopePath === undefined) throw new Error(`CodeGen: name "${name}" has no value`)
      return _`${scopeName}${name.scopePath}`
    })
  }

  scopeCode(
    values: ScopeValues | ScopeValueSets = this._values,
    usedValues?: UsedScopeValues,
    getCode?: (n: ValueScopeName) => Code | undefined
  ): Code {
    return this._reduceValues(
      values,
      (name: ValueScopeName) => {
        if (name.value === undefined) throw new Error(`CodeGen: name "${name}" has no value`)
        return name.value.code
      },
      usedValues,
      getCode
    )
  }

  private _reduceValues(
    values: ScopeValues | ScopeValueSets,
    valueCode: (n: ValueScopeName) => Code | undefined,
    usedValues: UsedScopeValues = {},
    getCode?: (n: ValueScopeName) => Code | undefined
  ): Code {
    let code: Code = nil
    for (const prefix in values) {
      const vs = values[prefix]
      if (!vs) continue
      const nameSet = (usedValues[prefix] = usedValues[prefix] || new Map())
      vs.forEach((name: ValueScopeName) => {
        if (nameSet.has(name)) return
        nameSet.set(name, UsedValueState.Started)
        let c = valueCode(name)
        if (c) {
          const def = this.opts.es5 ? varKinds.var : varKinds.const
          code = _`${code}${def} ${name} = ${c};${this.opts._n}`
        } else if ((c = getCode?.(name))) {
          code = _`${code}${c}${this.opts._n}`
        } else {
          throw new ValueError(name)
        }
        nameSet.set(name, UsedValueState.Completed)
      })
    }
    return code
  }
}


code:
import type {KeywordErrorCxt, KeywordErrorDefinition} from "../types"
import type {SchemaCxt} from "./index"
import {CodeGen, _, str, strConcat, Code, Name} from "./codegen"
import {SafeExpr} from "./codegen/code"
import {getErrorPath, Type} from "./util"
import N from "./names"

export const keywordError: KeywordErrorDefinition = {
  message: ({keyword}) => str`must pass "${keyword}" keyword validation`,
}

export const keyword$DataError: KeywordErrorDefinition = {
  message: ({keyword, schemaType}) =>
    schemaType
      ? str`"${keyword}" keyword must be ${schemaType} ($data)`
      : str`"${keyword}" keyword is invalid ($data)`,
}

export interface ErrorPaths {
  instancePath?: Code
  schemaPath?: string
  parentSchema?: boolean
}

export function reportError(
  cxt: KeywordErrorCxt,
  error: KeywordErrorDefinition = keywordError,
  errorPaths?: ErrorPaths,
  overrideAllErrors?: boolean
): void {
  const {it} = cxt
  const {gen, compositeRule, allErrors} = it
  const errObj = errorObjectCode(cxt, error, errorPaths)
  if (overrideAllErrors ?? (compositeRule || allErrors)) {
    addError(gen, errObj)
  } else {
    returnErrors(it, _`[${errObj}]`)
  }
}

export function reportExtraError(
  cxt: KeywordErrorCxt,
  error: KeywordErrorDefinition = keywordError,
  errorPaths?: ErrorPaths
): void {
  const {it} = cxt
  const {gen, compositeRule, allErrors} = it
  const errObj = errorObjectCode(cxt, error, errorPaths)
  addError(gen, errObj)
  if (!(compositeRule || allErrors)) {
    returnErrors(it, N.vErrors)
  }
}

export function resetErrorsCount(gen: CodeGen, errsCount: Name): void {
  gen.assign(N.errors, errsCount)
  gen.if(_`${N.vErrors} !== null`, () =>
    gen.if(
      errsCount,
      () => gen.assign(_`${N.vErrors}.length`, errsCount),
      () => gen.assign(N.vErrors, null)
    )
  )
}

export function extendErrors({
  gen,
  keyword,
  schemaValue,
  data,
  errsCount,
  it,
}: KeywordErrorCxt): void {
  /* istanbul ignore if */
  if (errsCount === undefined) throw new Error("ajv implementation error")
  const err = gen.name("err")
  gen.forRange("i", errsCount, N.errors, (i) => {
    gen.const(err, _`${N.vErrors}[${i}]`)
    gen.if(_`${err}.instancePath === undefined`, () =>
      gen.assign(_`${err}.instancePath`, strConcat(N.instancePath, it.errorPath))
    )
    gen.assign(_`${err}.schemaPath`, str`${it.errSchemaPath}/${keyword}`)
    if (it.opts.verbose) {
      gen.assign(_`${err}.schema`, schemaValue)
      gen.assign(_`${err}.data`, data)
    }
  })
}

function addError(gen: CodeGen, errObj: Code): void {
  const err = gen.const("err", errObj)
  gen.if(
    _`${N.vErrors} === null`,
    () => gen.assign(N.vErrors, _`[${err}]`),
    _`${N.vErrors}.push(${err})`
  )
  gen.code(_`${N.errors}++`)
}

function returnErrors(it: SchemaCxt, errs: Code): void {
  const {gen, validateName, schemaEnv} = it
  if (schemaEnv.$async) {
    gen.throw(_`new ${it.ValidationError as Name}(${errs})`)
  } else {
    gen.assign(_`${validateName}.errors`, errs)
    gen.return(false)
  }
}

const E = {
  keyword: new Name("keyword"),
  schemaPath: new Name("schemaPath"), // also used in JTD errors
  params: new Name("params"),
  propertyName: new Name("propertyName"),
  message: new Name("message"),
  schema: new Name("schema"),
  parentSchema: new Name("parentSchema"),
}

function errorObjectCode(
  cxt: KeywordErrorCxt,
  error: KeywordErrorDefinition,
  errorPaths?: ErrorPaths
): Code {
  const {createErrors} = cxt.it
  if (createErrors === false) return _`{}`
  return errorObject(cxt, error, errorPaths)
}

function errorObject(
  cxt: KeywordErrorCxt,
  error: KeywordErrorDefinition,
  errorPaths: ErrorPaths = {}
): Code {
  const {gen, it} = cxt
  const keyValues: [Name, SafeExpr | string][] = [
    errorInstancePath(it, errorPaths),
    errorSchemaPath(cxt, errorPaths),
  ]
  extraErrorProps(cxt, error, keyValues)
  return gen.object(...keyValues)
}

function errorInstancePath({errorPath}: SchemaCxt, {instancePath}: ErrorPaths): [Name, Code] {
  const instPath = instancePath
    ? str`${errorPath}${getErrorPath(instancePath, Type.Str)}`
    : errorPath
  return [N.instancePath, strConcat(N.instancePath, instPath)]
}

function errorSchemaPath(
  {keyword, it: {errSchemaPath}}: KeywordErrorCxt,
  {schemaPath, parentSchema}: ErrorPaths
): [Name, string | Code] {
  let schPath = parentSchema ? errSchemaPath : str`${errSchemaPath}/${keyword}`
  if (schemaPath) {
    schPath = str`${schPath}${getErrorPath(schemaPath, Type.Str)}`
  }
  return [E.schemaPath, schPath]
}

function extraErrorProps(
  cxt: KeywordErrorCxt,
  {params, message}: KeywordErrorDefinition,
  keyValues: [Name, SafeExpr | string][]
): void {
  const {keyword, data, schemaValue, it} = cxt
  const {opts, propertyName, topSchemaRef, schemaPath} = it
  keyValues.push(
    [E.keyword, keyword],
    [E.params, typeof params == "function" ? params(cxt) : params || _`{}`]
  )
  if (opts.messages) {
    keyValues.push([E.message, typeof message == "function" ? message(cxt) : message])
  }
  if (opts.verbose) {
    keyValues.push(
      [E.schema, schemaValue],
      [E.parentSchema, _`${topSchemaRef}${schemaPath}`],
      [N.data, data]
    )
  }
  if (propertyName) keyValues.push([E.propertyName, propertyName])
}


code:
import type {
  AnySchema,
  AnySchemaObject,
  AnyValidateFunction,
  AsyncValidateFunction,
  EvaluatedProperties,
  EvaluatedItems,
} from "../types"
import type Ajv from "../core"
import type {InstanceOptions} from "../core"
import {CodeGen, _, nil, stringify, Name, Code, ValueScopeName} from "./codegen"
import ValidationError from "../runtime/validation_error"
import N from "./names"
import {LocalRefs, getFullPath, _getFullPath, inlineRef, normalizeId, resolveUrl} from "./resolve"
import {schemaHasRulesButRef, unescapeFragment} from "./util"
import {validateFunctionCode} from "./validate"
import * as URI from "uri-js"
import {JSONType} from "./rules"

export type SchemaRefs = {
  [Ref in string]?: SchemaEnv | AnySchema
}

export interface SchemaCxt {
  readonly gen: CodeGen
  readonly allErrors?: boolean // validation mode - whether to collect all errors or break on error
  readonly data: Name // Name with reference to the current part of data instance
  readonly parentData: Name // should be used in keywords modifying data
  readonly parentDataProperty: Code | number // should be used in keywords modifying data
  readonly dataNames: Name[]
  readonly dataPathArr: (Code | number)[]
  readonly dataLevel: number // the level of the currently validated data,
  // it can be used to access both the property names and the data on all levels from the top.
  dataTypes: JSONType[] // data types applied to the current part of data instance
  definedProperties: Set<string> // set of properties to keep track of for required checks
  readonly topSchemaRef: Code
  readonly validateName: Name
  evaluated?: Name
  readonly ValidationError?: Name
  readonly schema: AnySchema // current schema object - equal to parentSchema passed via KeywordCxt
  readonly schemaEnv: SchemaEnv
  readonly rootId: string
  baseId: string // the current schema base URI that should be used as the base for resolving URIs in references (\$ref)
  readonly schemaPath: Code // the run-time expression that evaluates to the property name of the current schema
  readonly errSchemaPath: string // this is actual string, should not be changed to Code
  readonly errorPath: Code
  readonly propertyName?: Name
  readonly compositeRule?: boolean // true indicates that the current schema is inside the compound keyword,
  // where failing some rule doesn't mean validation failure (`anyOf`, `oneOf`, `not`, `if`).
  // This flag is used to determine whether you can return validation result immediately after any error in case the option `allErrors` is not `true.
  // You only need to use it if you have many steps in your keywords and potentially can define multiple errors.
  props?: EvaluatedProperties | Name // properties evaluated by this schema - used by parent schema or assigned to validation function
  items?: EvaluatedItems | Name // last item evaluated by this schema - used by parent schema or assigned to validation function
  jtdDiscriminator?: string
  jtdMetadata?: boolean
  readonly createErrors?: boolean
  readonly opts: InstanceOptions // Ajv instance option.
  readonly self: Ajv // current Ajv instance
}

export interface SchemaObjCxt extends SchemaCxt {
  readonly schema: AnySchemaObject
}
interface SchemaEnvArgs {
  readonly schema: AnySchema
  readonly schemaId?: "$id" | "id"
  readonly root?: SchemaEnv
  readonly baseId?: string
  readonly schemaPath?: string
  readonly localRefs?: LocalRefs
  readonly meta?: boolean
}

export class SchemaEnv implements SchemaEnvArgs {
  readonly schema: AnySchema
  readonly schemaId?: "$id" | "id"
  readonly root: SchemaEnv
  baseId: string // TODO possibly, it should be readonly
  schemaPath?: string
  localRefs?: LocalRefs
  readonly meta?: boolean
  readonly $async?: boolean // true if the current schema is asynchronous.
  readonly refs: SchemaRefs = {}
  readonly dynamicAnchors: {[Ref in string]?: true} = {}
  validate?: AnyValidateFunction
  validateName?: ValueScopeName
  serialize?: (data: unknown) => string
  serializeName?: ValueScopeName
  parse?: (data: string) => unknown
  parseName?: ValueScopeName

  constructor(env: SchemaEnvArgs) {
    let schema: AnySchemaObject | undefined
    if (typeof env.schema == "object") schema = env.schema
    this.schema = env.schema
    this.schemaId = env.schemaId
    this.root = env.root || this
    this.baseId = env.baseId ?? normalizeId(schema?.[env.schemaId || "$id"])
    this.schemaPath = env.schemaPath
    this.localRefs = env.localRefs
    this.meta = env.meta
    this.$async = schema?.$async
    this.refs = {}
  }
}

// let codeSize = 0
// let nodeCount = 0

// Compiles schema in SchemaEnv
export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv {
  // TODO refactor - remove compilations
  const _sch = getCompilingSchema.call(this, sch)
  if (_sch) return _sch
  const rootId = getFullPath(this.opts.uriResolver, sch.root.baseId) // TODO if getFullPath removed 1 tests fails
  const {es5, lines} = this.opts.code
  const {ownProperties} = this.opts
  const gen = new CodeGen(this.scope, {es5, lines, ownProperties})
  let _ValidationError
  if (sch.$async) {
    _ValidationError = gen.scopeValue("Error", {
      ref: ValidationError,
      code: _`require("ajv/dist/runtime/validation_error").default`,
    })
  }

  const validateName = gen.scopeName("validate")
  sch.validateName = validateName

  const schemaCxt: SchemaCxt = {
    gen,
    allErrors: this.opts.allErrors,
    data: N.data,
    parentData: N.parentData,
    parentDataProperty: N.parentDataProperty,
    dataNames: [N.data],
    dataPathArr: [nil], // TODO can its length be used as dataLevel if nil is removed?
    dataLevel: 0,
    dataTypes: [],
    definedProperties: new Set<string>(),
    topSchemaRef: gen.scopeValue(
      "schema",
      this.opts.code.source === true
        ? {ref: sch.schema, code: stringify(sch.schema)}
        : {ref: sch.schema}
    ),
    validateName,
    ValidationError: _ValidationError,
    schema: sch.schema,
    schemaEnv: sch,
    rootId,
    baseId: sch.baseId || rootId,
    schemaPath: nil,
    errSchemaPath: sch.schemaPath || (this.opts.jtd ? "" : "#"),
    errorPath: _`""`,
    opts: this.opts,
    self: this,
  }

  let sourceCode: string | undefined
  try {
    this._compilations.add(sch)
    validateFunctionCode(schemaCxt)
    gen.optimize(this.opts.code.optimize)
    // gen.optimize(1)
    const validateCode = gen.toString()
    sourceCode = `${gen.scopeRefs(N.scope)}return ${validateCode}`
    // console.log((codeSize += sourceCode.length), (nodeCount += gen.nodeCount))
    if (this.opts.code.process) sourceCode = this.opts.code.process(sourceCode, sch)
    // console.log("\n\n\n *** \n", sourceCode)
    const makeValidate = new Function(`${N.self}`, `${N.scope}`, sourceCode)
    const validate: AnyValidateFunction = makeValidate(this, this.scope.get())
    this.scope.value(validateName, {ref: validate})

    validate.errors = null
    validate.schema = sch.schema
    validate.schemaEnv = sch
    if (sch.$async) (validate as AsyncValidateFunction).$async = true
    if (this.opts.code.source === true) {
      validate.source = {validateName, validateCode, scopeValues: gen._values}
    }
    if (this.opts.unevaluated) {
      const {props, items} = schemaCxt
      validate.evaluated = {
        props: props instanceof Name ? undefined : props,
        items: items instanceof Name ? undefined : items,
        dynamicProps: props instanceof Name,
        dynamicItems: items instanceof Name,
      }
      if (validate.source) validate.source.evaluated = stringify(validate.evaluated)
    }
    sch.validate = validate
    return sch
  } catch (e) {
    delete sch.validate
    delete sch.validateName
    if (sourceCode) this.logger.error("Error compiling schema, function code:", sourceCode)
    // console.log("\n\n\n *** \n", sourceCode, this.opts)
    throw e
  } finally {
    this._compilations.delete(sch)
  }
}

export function resolveRef(
  this: Ajv,
  root: SchemaEnv,
  baseId: string,
  ref: string
): AnySchema | SchemaEnv | undefined {
  ref = resolveUrl(this.opts.uriResolver, baseId, ref)
  const schOrFunc = root.refs[ref]
  if (schOrFunc) return schOrFunc

  let _sch = resolve.call(this, root, ref)
  if (_sch === undefined) {
    const schema = root.localRefs?.[ref] // TODO maybe localRefs should hold SchemaEnv
    const {schemaId} = this.opts
    if (schema) _sch = new SchemaEnv({schema, schemaId, root, baseId})
  }

  if (_sch === undefined) return
  return (root.refs[ref] = inlineOrCompile.call(this, _sch))
}

function inlineOrCompile(this: Ajv, sch: SchemaEnv): AnySchema | SchemaEnv {
  if (inlineRef(sch.schema, this.opts.inlineRefs)) return sch.schema
  return sch.validate ? sch : compileSchema.call(this, sch)
}

// Index of schema compilation in the currently compiled list
export function getCompilingSchema(this: Ajv, schEnv: SchemaEnv): SchemaEnv | void {
  for (const sch of this._compilations) {
    if (sameSchemaEnv(sch, schEnv)) return sch
  }
}

function sameSchemaEnv(s1: SchemaEnv, s2: SchemaEnv): boolean {
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId
}

// resolve and compile the references ($ref)
// TODO returns AnySchemaObject (if the schema can be inlined) or validation function
function resolve(
  this: Ajv,
  root: SchemaEnv, // information about the root schema for the current schema
  ref: string // reference to resolve
): SchemaEnv | undefined {
  let sch
  while (typeof (sch = this.refs[ref]) == "string") ref = sch
  return sch || this.schemas[ref] || resolveSchema.call(this, root, ref)
}

// Resolve schema, its root and baseId
export function resolveSchema(
  this: Ajv,
  root: SchemaEnv, // root object with properties schema, refs TODO below SchemaEnv is assigned to it
  ref: string // reference to resolve
): SchemaEnv | undefined {
  const p = this.opts.uriResolver.parse(ref)
  const refPath = _getFullPath(this.opts.uriResolver, p)
  let baseId = getFullPath(this.opts.uriResolver, root.baseId, undefined)
  // TODO `Object.keys(root.schema).length > 0` should not be needed - but removing breaks 2 tests
  if (Object.keys(root.schema).length > 0 && refPath === baseId) {
    return getJsonPointer.call(this, p, root)
  }

  const id = normalizeId(refPath)
  const schOrRef = this.refs[id] || this.schemas[id]
  if (typeof schOrRef == "string") {
    const sch = resolveSchema.call(this, root, schOrRef)
    if (typeof sch?.schema !== "object") return
    return getJsonPointer.call(this, p, sch)
  }

  if (typeof schOrRef?.schema !== "object") return
  if (!schOrRef.validate) compileSchema.call(this, schOrRef)
  if (id === normalizeId(ref)) {
    const {schema} = schOrRef
    const {schemaId} = this.opts
    const schId = schema[schemaId]
    if (schId) baseId = resolveUrl(this.opts.uriResolver, baseId, schId)
    return new SchemaEnv({schema, schemaId, root, baseId})
  }
  return getJsonPointer.call(this, p, schOrRef)
}

const PREVENT_SCOPE_CHANGE = new Set([
  "properties",
  "patternProperties",
  "enum",
  "dependencies",
  "definitions",
])

function getJsonPointer(
  this: Ajv,
  parsedRef: URI.URIComponents,
  {baseId, schema, root}: SchemaEnv
): SchemaEnv | undefined {
  if (parsedRef.fragment?.[0] !== "/") return
  for (const part of parsedRef.fragment.slice(1).split("/")) {
    if (typeof schema === "boolean") return
    const partSchema = schema[unescapeFragment(part)]
    if (partSchema === undefined) return
    schema = partSchema
    // TODO PREVENT_SCOPE_CHANGE could be defined in keyword def?
    const schId = typeof schema === "object" && schema[this.opts.schemaId]
    if (!PREVENT_SCOPE_CHANGE.has(part) && schId) {
      baseId = resolveUrl(this.opts.uriResolver, baseId, schId)
    }
  }
  let env: SchemaEnv | undefined
  if (typeof schema != "boolean" && schema.$ref && !schemaHasRulesButRef(schema, this.RULES)) {
    const $ref = resolveUrl(this.opts.uriResolver, baseId, schema.$ref)
    env = resolveSchema.call(this, root, $ref)
  }
  // even though resolution failed we need to return SchemaEnv to throw exception
  // so that compileAsync loads missing schema.
  const {schemaId} = this.opts
  env = env || new SchemaEnv({schema, schemaId, root, baseId})
  if (env.schema !== env.root.schema) return env
  return undefined
}


code:
import type Ajv from "../../core"
import type {SchemaObject} from "../../types"
import {jtdForms, JTDForm, SchemaObjectMap} from "./types"
import {SchemaEnv, getCompilingSchema} from ".."
import {_, str, and, or, nil, not, CodeGen, Code, Name, SafeExpr} from "../codegen"
import MissingRefError from "../ref_error"
import N from "../names"
import {hasPropFunc} from "../../vocabularies/code"
import {hasRef} from "../../vocabularies/jtd/ref"
import {intRange, IntType} from "../../vocabularies/jtd/type"
import {parseJson, parseJsonNumber, parseJsonString} from "../../runtime/parseJson"
import {useFunc} from "../util"
import validTimestamp from "../../runtime/timestamp"

type GenParse = (cxt: ParseCxt) => void

const genParse: {[F in JTDForm]: GenParse} = {
  elements: parseElements,
  values: parseValues,
  discriminator: parseDiscriminator,
  properties: parseProperties,
  optionalProperties: parseProperties,
  enum: parseEnum,
  type: parseType,
  ref: parseRef,
}

interface ParseCxt {
  readonly gen: CodeGen
  readonly self: Ajv // current Ajv instance
  readonly schemaEnv: SchemaEnv
  readonly definitions: SchemaObjectMap
  schema: SchemaObject
  data: Code
  parseName: Name
  char: Name
}

export default function compileParser(
  this: Ajv,
  sch: SchemaEnv,
  definitions: SchemaObjectMap
): SchemaEnv {
  const _sch = getCompilingSchema.call(this, sch)
  if (_sch) return _sch
  const {es5, lines} = this.opts.code
  const {ownProperties} = this.opts
  const gen = new CodeGen(this.scope, {es5, lines, ownProperties})
  const parseName = gen.scopeName("parse")
  const cxt: ParseCxt = {
    self: this,
    gen,
    schema: sch.schema as SchemaObject,
    schemaEnv: sch,
    definitions,
    data: N.data,
    parseName,
    char: gen.name("c"),
  }

  let sourceCode: string | undefined
  try {
    this._compilations.add(sch)
    sch.parseName = parseName
    parserFunction(cxt)
    gen.optimize(this.opts.code.optimize)
    const parseFuncCode = gen.toString()
    sourceCode = `${gen.scopeRefs(N.scope)}return ${parseFuncCode}`
    const makeParse = new Function(`${N.scope}`, sourceCode)
    const parse: (json: string) => unknown = makeParse(this.scope.get())
    this.scope.value(parseName, {ref: parse})
    sch.parse = parse
  } catch (e) {
    if (sourceCode) this.logger.error("Error compiling parser, function code:", sourceCode)
    delete sch.parse
    delete sch.parseName
    throw e
  } finally {
    this._compilations.delete(sch)
  }
  return sch
}

const undef = _`undefined`

function parserFunction(cxt: ParseCxt): void {
  const {gen, parseName, char} = cxt
  gen.func(parseName, _`${N.json}, ${N.jsonPos}, ${N.jsonPart}`, false, () => {
    gen.let(N.data)
    gen.let(char)
    gen.assign(_`${parseName}.message`, undef)
    gen.assign(_`${parseName}.position`, undef)
    gen.assign(N.jsonPos, _`${N.jsonPos} || 0`)
    gen.const(N.jsonLen, _`${N.json}.length`)
    parseCode(cxt)
    skipWhitespace(cxt)
    gen.if(N.jsonPart, () => {
      gen.assign(_`${parseName}.position`, N.jsonPos)
      gen.return(N.data)
    })
    gen.if(_`${N.jsonPos} === ${N.jsonLen}`, () => gen.return(N.data))
    jsonSyntaxError(cxt)
  })
}

function parseCode(cxt: ParseCxt): void {
  let form: JTDForm | undefined
  for (const key of jtdForms) {
    if (key in cxt.schema) {
      form = key
      break
    }
  }
  if (form) parseNullable(cxt, genParse[form])
  else parseEmpty(cxt)
}

const parseBoolean = parseBooleanToken(true, parseBooleanToken(false, jsonSyntaxError))

function parseNullable(cxt: ParseCxt, parseForm: GenParse): void {
  const {gen, schema, data} = cxt
  if (!schema.nullable) return parseForm(cxt)
  tryParseToken(cxt, "null", parseForm, () => gen.assign(data, null))
}

function parseElements(cxt: ParseCxt): void {
  const {gen, schema, data} = cxt
  parseToken(cxt, "[")
  const ix = gen.let("i", 0)
  gen.assign(data, _`[]`)
  parseItems(cxt, "]", () => {
    const el = gen.let("el")
    parseCode({...cxt, schema: schema.elements, data: el})
    gen.assign(_`${data}[${ix}++]`, el)
  })
}

function parseValues(cxt: ParseCxt): void {
  const {gen, schema, data} = cxt
  parseToken(cxt, "{")
  gen.assign(data, _`{}`)
  parseItems(cxt, "}", () => parseKeyValue(cxt, schema.values))
}

function parseItems(cxt: ParseCxt, endToken: string, block: () => void): void {
  tryParseItems(cxt, endToken, block)
  parseToken(cxt, endToken)
}

function tryParseItems(cxt: ParseCxt, endToken: string, block: () => void): void {
  const {gen} = cxt
  gen.for(_`;${N.jsonPos}<${N.jsonLen} && ${jsonSlice(1)}!==${endToken};`, () => {
    block()
    tryParseToken(cxt, ",", () => gen.break(), hasItem)
  })

  function hasItem(): void {
    tryParseToken(cxt, endToken, () => {}, jsonSyntaxError)
  }
}

function parseKeyValue(cxt: ParseCxt, schema: SchemaObject): void {
  const {gen} = cxt
  const key = gen.let("key")
  parseString({...cxt, data: key})
  parseToken(cxt, ":")
  parsePropertyValue(cxt, key, schema)
}

function parseDiscriminator(cxt: ParseCxt): void {
  const {gen, data, schema} = cxt
  const {discriminator, mapping} = schema
  parseToken(cxt, "{")
  gen.assign(data, _`{}`)
  const startPos = gen.const("pos", N.jsonPos)
  const value = gen.let("value")
  const tag = gen.let("tag")
  tryParseItems(cxt, "}", () => {
    const key = gen.let("key")
    parseString({...cxt, data: key})
    parseToken(cxt, ":")
    gen.if(
      _`${key} === ${discriminator}`,
      () => {
        parseString({...cxt, data: tag})
        gen.assign(_`${data}[${key}]`, tag)
        gen.break()
      },
      () => parseEmpty({...cxt, data: value}) // can be discarded/skipped
    )
  })
  gen.assign(N.jsonPos, startPos)
  gen.if(_`${tag} === undefined`)
  parsingError(cxt, str`discriminator tag not found`)
  for (const tagValue in mapping) {
    gen.elseIf(_`${tag} === ${tagValue}`)
    parseSchemaProperties({...cxt, schema: mapping[tagValue]}, discriminator)
  }
  gen.else()
  parsingError(cxt, str`discriminator value not in schema`)
  gen.endIf()
}

function parseProperties(cxt: ParseCxt): void {
  const {gen, data} = cxt
  parseToken(cxt, "{")
  gen.assign(data, _`{}`)
  parseSchemaProperties(cxt)
}

function parseSchemaProperties(cxt: ParseCxt, discriminator?: string): void {
  const {gen, schema, data} = cxt
  const {properties, optionalProperties, additionalProperties} = schema
  parseItems(cxt, "}", () => {
    const key = gen.let("key")
    parseString({...cxt, data: key})
    parseToken(cxt, ":")
    gen.if(false)
    parseDefinedProperty(cxt, key, properties)
    parseDefinedProperty(cxt, key, optionalProperties)
    if (discriminator) {
      gen.elseIf(_`${key} === ${discriminator}`)
      const tag = gen.let("tag")
      parseString({...cxt, data: tag}) // can be discarded, it is already assigned
    }
    gen.else()
    if (additionalProperties) {
      parseEmpty({...cxt, data: _`${data}[${key}]`})
    } else {
      parsingError(cxt, str`property ${key} not allowed`)
    }
    gen.endIf()
  })
  if (properties) {
    const hasProp = hasPropFunc(gen)
    const allProps: Code = and(
      ...Object.keys(properties).map((p): Code => _`${hasProp}.call(${data}, ${p})`)
    )
    gen.if(not(allProps), () => parsingError(cxt, str`missing required properties`))
  }
}

function parseDefinedProperty(cxt: ParseCxt, key: Name, schemas: SchemaObjectMap = {}): void {
  const {gen} = cxt
  for (const prop in schemas) {
    gen.elseIf(_`${key} === ${prop}`)
    parsePropertyValue(cxt, key, schemas[prop] as SchemaObject)
  }
}

function parsePropertyValue(cxt: ParseCxt, key: Name, schema: SchemaObject): void {
  parseCode({...cxt, schema, data: _`${cxt.data}[${key}]`})
}

function parseType(cxt: ParseCxt): void {
  const {gen, schema, data, self} = cxt
  switch (schema.type) {
    case "boolean":
      parseBoolean(cxt)
      break
    case "string":
      parseString(cxt)
      break
    case "timestamp": {
      parseString(cxt)
      const vts = useFunc(gen, validTimestamp)
      const {allowDate, parseDate} = self.opts
      const notValid = allowDate ? _`!${vts}(${data}, true)` : _`!${vts}(${data})`
      const fail: Code = parseDate
        ? or(notValid, _`(${data} = new Date(${data}), false)`, _`isNaN(${data}.valueOf())`)
        : notValid
      gen.if(fail, () => parsingError(cxt, str`invalid timestamp`))
      break
    }
    case "float32":
    case "float64":
      parseNumber(cxt)
      break
    default: {
      const t = schema.type as IntType
      if (!self.opts.int32range && (t === "int32" || t === "uint32")) {
        parseNumber(cxt, 16) // 2 ** 53 - max safe integer
        if (t === "uint32") {
          gen.if(_`${data} < 0`, () => parsingError(cxt, str`integer out of range`))
        }
      } else {
        const [min, max, maxDigits] = intRange[t]
        parseNumber(cxt, maxDigits)
        gen.if(_`${data} < ${min} || ${data} > ${max}`, () =>
          parsingError(cxt, str`integer out of range`)
        )
      }
    }
  }
}

function parseString(cxt: ParseCxt): void {
  parseToken(cxt, '"')
  parseWith(cxt, parseJsonString)
}

function parseEnum(cxt: ParseCxt): void {
  const {gen, data, schema} = cxt
  const enumSch = schema.enum
  parseToken(cxt, '"')
  // TODO loopEnum
  gen.if(false)
  for (const value of enumSch) {
    const valueStr = JSON.stringify(value).slice(1) // remove starting quote
    gen.elseIf(_`${jsonSlice(valueStr.length)} === ${valueStr}`)
    gen.assign(data, str`${value}`)
    gen.add(N.jsonPos, valueStr.length)
  }
  gen.else()
  jsonSyntaxError(cxt)
  gen.endIf()
}

function parseNumber(cxt: ParseCxt, maxDigits?: number): void {
  const {gen} = cxt
  skipWhitespace(cxt)
  gen.if(
    _`"-0123456789".indexOf(${jsonSlice(1)}) < 0`,
    () => jsonSyntaxError(cxt),
    () => parseWith(cxt, parseJsonNumber, maxDigits)
  )
}

function parseBooleanToken(bool: boolean, fail: GenParse): GenParse {
  return (cxt) => {
    const {gen, data} = cxt
    tryParseToken(
      cxt,
      `${bool}`,
      () => fail(cxt),
      () => gen.assign(data, bool)
    )
  }
}

function parseRef(cxt: ParseCxt): void {
  const {gen, self, definitions, schema, schemaEnv} = cxt
  const {ref} = schema
  const refSchema = definitions[ref]
  if (!refSchema) throw new MissingRefError(self.opts.uriResolver, "", ref, `No definition ${ref}`)
  if (!hasRef(refSchema)) return parseCode({...cxt, schema: refSchema})
  const {root} = schemaEnv
  const sch = compileParser.call(self, new SchemaEnv({schema: refSchema, root}), definitions)
  partialParse(cxt, getParser(gen, sch), true)
}

function getParser(gen: CodeGen, sch: SchemaEnv): Code {
  return sch.parse
    ? gen.scopeValue("parse", {ref: sch.parse})
    : _`${gen.scopeValue("wrapper", {ref: sch})}.parse`
}

function parseEmpty(cxt: ParseCxt): void {
  parseWith(cxt, parseJson)
}

function parseWith(cxt: ParseCxt, parseFunc: {code: string}, args?: SafeExpr): void {
  partialParse(cxt, useFunc(cxt.gen, parseFunc), args)
}

function partialParse(cxt: ParseCxt, parseFunc: Name, args?: SafeExpr): void {
  const {gen, data} = cxt
  gen.assign(data, _`${parseFunc}(${N.json}, ${N.jsonPos}${args ? _`, ${args}` : nil})`)
  gen.assign(N.jsonPos, _`${parseFunc}.position`)
  gen.if(_`${data} === undefined`, () => parsingError(cxt, _`${parseFunc}.message`))
}

function parseToken(cxt: ParseCxt, tok: string): void {
  tryParseToken(cxt, tok, jsonSyntaxError)
}

function tryParseToken(cxt: ParseCxt, tok: string, fail: GenParse, success?: GenParse): void {
  const {gen} = cxt
  const n = tok.length
  skipWhitespace(cxt)
  gen.if(
    _`${jsonSlice(n)} === ${tok}`,
    () => {
      gen.add(N.jsonPos, n)
      success?.(cxt)
    },
    () => fail(cxt)
  )
}

function skipWhitespace({gen, char: c}: ParseCxt): void {
  gen.code(
    _`while((${c}=${N.json}[${N.jsonPos}],${c}===" "||${c}==="\\n"||${c}==="\\r"||${c}==="\\t"))${N.jsonPos}++;`
  )
}

function jsonSlice(len: number | Name): Code {
  return len === 1
    ? _`${N.json}[${N.jsonPos}]`
    : _`${N.json}.slice(${N.jsonPos}, ${N.jsonPos}+${len})`
}

function jsonSyntaxError(cxt: ParseCxt): void {
  parsingError(cxt, _`"unexpected token " + ${N.json}[${N.jsonPos}]`)
}

function parsingError({gen, parseName}: ParseCxt, msg: Code): void {
  gen.assign(_`${parseName}.message`, msg)
  gen.assign(_`${parseName}.position`, N.jsonPos)
  gen.return(undef)
}


code:
import type Ajv from "../../core"
import type {SchemaObject} from "../../types"
import {jtdForms, JTDForm, SchemaObjectMap} from "./types"
import {SchemaEnv, getCompilingSchema} from ".."
import {_, str, and, getProperty, CodeGen, Code, Name} from "../codegen"
import MissingRefError from "../ref_error"
import N from "../names"
import {isOwnProperty} from "../../vocabularies/code"
import {hasRef} from "../../vocabularies/jtd/ref"
import {useFunc} from "../util"
import quote from "../../runtime/quote"

const genSerialize: {[F in JTDForm]: (cxt: SerializeCxt) => void} = {
  elements: serializeElements,
  values: serializeValues,
  discriminator: serializeDiscriminator,
  properties: serializeProperties,
  optionalProperties: serializeProperties,
  enum: serializeString,
  type: serializeType,
  ref: serializeRef,
}

interface SerializeCxt {
  readonly gen: CodeGen
  readonly self: Ajv // current Ajv instance
  readonly schemaEnv: SchemaEnv
  readonly definitions: SchemaObjectMap
  schema: SchemaObject
  data: Code
}

export default function compileSerializer(
  this: Ajv,
  sch: SchemaEnv,
  definitions: SchemaObjectMap
): SchemaEnv {
  const _sch = getCompilingSchema.call(this, sch)
  if (_sch) return _sch
  const {es5, lines} = this.opts.code
  const {ownProperties} = this.opts
  const gen = new CodeGen(this.scope, {es5, lines, ownProperties})
  const serializeName = gen.scopeName("serialize")
  const cxt: SerializeCxt = {
    self: this,
    gen,
    schema: sch.schema as SchemaObject,
    schemaEnv: sch,
    definitions,
    data: N.data,
  }

  let sourceCode: string | undefined
  try {
    this._compilations.add(sch)
    sch.serializeName = serializeName
    gen.func(serializeName, N.data, false, () => {
      gen.let(N.json, str``)
      serializeCode(cxt)
      gen.return(N.json)
    })
    gen.optimize(this.opts.code.optimize)
    const serializeFuncCode = gen.toString()
    sourceCode = `${gen.scopeRefs(N.scope)}return ${serializeFuncCode}`
    const makeSerialize = new Function(`${N.scope}`, sourceCode)
    const serialize: (data: unknown) => string = makeSerialize(this.scope.get())
    this.scope.value(serializeName, {ref: serialize})
    sch.serialize = serialize
  } catch (e) {
    if (sourceCode) this.logger.error("Error compiling serializer, function code:", sourceCode)
    delete sch.serialize
    delete sch.serializeName
    throw e
  } finally {
    this._compilations.delete(sch)
  }
  return sch
}

function serializeCode(cxt: SerializeCxt): void {
  let form: JTDForm | undefined
  for (const key of jtdForms) {
    if (key in cxt.schema) {
      form = key
      break
    }
  }
  serializeNullable(cxt, form ? genSerialize[form] : serializeEmpty)
}

function serializeNullable(cxt: SerializeCxt, serializeForm: (_cxt: SerializeCxt) => void): void {
  const {gen, schema, data} = cxt
  if (!schema.nullable) return serializeForm(cxt)
  gen.if(
    _`${data} === undefined || ${data} === null`,
    () => gen.add(N.json, _`"null"`),
    () => serializeForm(cxt)
  )
}

function serializeElements(cxt: SerializeCxt): void {
  const {gen, schema, data} = cxt
  gen.add(N.json, str`[`)
  const first = gen.let("first", true)
  gen.forOf("el", data, (el) => {
    addComma(cxt, first)
    serializeCode({...cxt, schema: schema.elements, data: el})
  })
  gen.add(N.json, str`]`)
}

function serializeValues(cxt: SerializeCxt): void {
  const {gen, schema, data} = cxt
  gen.add(N.json, str`{`)
  const first = gen.let("first", true)
  gen.forIn("key", data, (key) => serializeKeyValue(cxt, key, schema.values, first))
  gen.add(N.json, str`}`)
}

function serializeKeyValue(cxt: SerializeCxt, key: Name, schema: SchemaObject, first?: Name): void {
  const {gen, data} = cxt
  addComma(cxt, first)
  serializeString({...cxt, data: key})
  gen.add(N.json, str`:`)
  const value = gen.const("value", _`${data}${getProperty(key)}`)
  serializeCode({...cxt, schema, data: value})
}

function serializeDiscriminator(cxt: SerializeCxt): void {
  const {gen, schema, data} = cxt
  const {discriminator} = schema
  gen.add(N.json, str`{${JSON.stringify(discriminator)}:`)
  const tag = gen.const("tag", _`${data}${getProperty(discriminator)}`)
  serializeString({...cxt, data: tag})
  gen.if(false)
  for (const tagValue in schema.mapping) {
    gen.elseIf(_`${tag} === ${tagValue}`)
    const sch = schema.mapping[tagValue]
    serializeSchemaProperties({...cxt, schema: sch}, discriminator)
  }
  gen.endIf()
  gen.add(N.json, str`}`)
}

function serializeProperties(cxt: SerializeCxt): void {
  const {gen} = cxt
  gen.add(N.json, str`{`)
  serializeSchemaProperties(cxt)
  gen.add(N.json, str`}`)
}

function serializeSchemaProperties(cxt: SerializeCxt, discriminator?: string): void {
  const {gen, schema, data} = cxt
  const {properties, optionalProperties} = schema
  const props = keys(properties)
  const optProps = keys(optionalProperties)
  const allProps = allProperties(props.concat(optProps))
  let first = !discriminator
  let firstProp: Name | undefined

  for (const key of props) {
    if (first) first = false
    else gen.add(N.json, str`,`)
    serializeProperty(key, properties[key], keyValue(key))
  }
  if (first) firstProp = gen.let("first", true)
  for (const key of optProps) {
    const value = keyValue(key)
    gen.if(and(_`${value} !== undefined`, isOwnProperty(gen, data, key)), () => {
      addComma(cxt, firstProp)
      serializeProperty(key, optionalProperties[key], value)
    })
  }
  if (schema.additionalProperties) {
    gen.forIn("key", data, (key) =>
      gen.if(isAdditional(key, allProps), () => serializeKeyValue(cxt, key, {}, firstProp))
    )
  }

  function keys(ps?: SchemaObjectMap): string[] {
    return ps ? Object.keys(ps) : []
  }

  function allProperties(ps: string[]): string[] {
    if (discriminator) ps.push(discriminator)
    if (new Set(ps).size !== ps.length) {
      throw new Error("JTD: properties/optionalProperties/disciminator overlap")
    }
    return ps
  }

  function keyValue(key: string): Name {
    return gen.const("value", _`${data}${getProperty(key)}`)
  }

  function serializeProperty(key: string, propSchema: SchemaObject, value: Name): void {
    gen.add(N.json, str`${JSON.stringify(key)}:`)
    serializeCode({...cxt, schema: propSchema, data: value})
  }

  function isAdditional(key: Name, ps: string[]): Code | true {
    return ps.length ? and(...ps.map((p) => _`${key} !== ${p}`)) : true
  }
}

function serializeType(cxt: SerializeCxt): void {
  const {gen, schema, data} = cxt
  switch (schema.type) {
    case "boolean":
      gen.add(N.json, _`${data} ? "true" : "false"`)
      break
    case "string":
      serializeString(cxt)
      break
    case "timestamp":
      gen.if(
        _`${data} instanceof Date`,
        () => gen.add(N.json, _`'"' + ${data}.toISOString() + '"'`),
        () => serializeString(cxt)
      )
      break
    default:
      serializeNumber(cxt)
  }
}

function serializeString({gen, data}: SerializeCxt): void {
  gen.add(N.json, _`${useFunc(gen, quote)}(${data})`)
}

function serializeNumber({gen, data}: SerializeCxt): void {
  gen.add(N.json, _`"" + ${data}`)
}

function serializeRef(cxt: SerializeCxt): void {
  const {gen, self, data, definitions, schema, schemaEnv} = cxt
  const {ref} = schema
  const refSchema = definitions[ref]
  if (!refSchema) throw new MissingRefError(self.opts.uriResolver, "", ref, `No definition ${ref}`)
  if (!hasRef(refSchema)) return serializeCode({...cxt, schema: refSchema})
  const {root} = schemaEnv
  const sch = compileSerializer.call(self, new SchemaEnv({schema: refSchema, root}), definitions)
  gen.add(N.json, _`${getSerialize(gen, sch)}(${data})`)
}

function getSerialize(gen: CodeGen, sch: SchemaEnv): Code {
  return sch.serialize
    ? gen.scopeValue("serialize", {ref: sch.serialize})
    : _`${gen.scopeValue("wrapper", {ref: sch})}.serialize`
}

function serializeEmpty({gen, data}: SerializeCxt): void {
  gen.add(N.json, _`JSON.stringify(${data})`)
}

function addComma({gen}: SerializeCxt, first?: Name): void {
  if (first) {
    gen.if(
      first,
      () => gen.assign(first, false),
      () => gen.add(N.json, str`,`)
    )
  } else {
    gen.add(N.json, str`,`)
  }
}


code:
import type {SchemaObject} from "../../types"

export type SchemaObjectMap = {[Ref in string]?: SchemaObject}

export const jtdForms = [
  "elements",
  "values",
  "discriminator",
  "properties",
  "optionalProperties",
  "enum",
  "type",
  "ref",
] as const

export type JTDForm = (typeof jtdForms)[number]


code:
import {Name} from "./codegen"

const names = {
  // validation function arguments
  data: new Name("data"), // data passed to validation function
  // args passed from referencing schema
  valCxt: new Name("valCxt"), // validation/data context - should not be used directly, it is destructured to the names below
  instancePath: new Name("instancePath"),
  parentData: new Name("parentData"),
  parentDataProperty: new Name("parentDataProperty"),
  rootData: new Name("rootData"), // root data - same as the data passed to the first/top validation function
  dynamicAnchors: new Name("dynamicAnchors"), // used to support recursiveRef and dynamicRef
  // function scoped variables
  vErrors: new Name("vErrors"), // null or array of validation errors
  errors: new Name("errors"), // counter of validation errors
  this: new Name("this"),
  // "globals"
  self: new Name("self"),
  scope: new Name("scope"),
  // JTD serialize/parse name for JSON string and position
  json: new Name("json"),
  jsonPos: new Name("jsonPos"),
  jsonLen: new Name("jsonLen"),
  jsonPart: new Name("jsonPart"),
}

export default names


code:
import {resolveUrl, normalizeId, getFullPath} from "./resolve"
import type {UriResolver} from "../types"

export default class MissingRefError extends Error {
  readonly missingRef: string
  readonly missingSchema: string

  constructor(resolver: UriResolver, baseId: string, ref: string, msg?: string) {
    super(msg || `can't resolve reference ${ref} from id ${baseId}`)
    this.missingRef = resolveUrl(resolver, baseId, ref)
    this.missingSchema = normalizeId(getFullPath(resolver, this.missingRef))
  }
}