Repository: eemeli/yaml
Pull request #248 (2021-03-27 13:31:43)

Make anchor & alias resolution lazier
Previously, the `source` of an alias node was a JS reference to the actual node object that it referred to, and `doc.anchors` provided separately from that the string identifiers used by YAML anchors.

After this change, scalar and collection nodes get a new `anchor?: string` property, and the `source` of an alias is a string identifier. The `doc.anchors` instance is completely dropped. The `createAlias()` method is moved to the document instance, and its handling of corner cases is changed a bit.

As a consequence of these changes, assigning the same anchor to multiple nodes will no longer automatically rename an earlier anchor with the same name.

Node creation options are somewhat clarified and streamlined, and the `replacer` is moved from the options object to be an optional second argument of the `createNode()` method, matching the `new Document()` interface.


Patch:
diff --git a/README.md b/README.md
index 7b62d9e..49e71d1 100644
--- a/README.md
+++ b/README.md
@@ -67,7 +67,7 @@ const YAML = require('yaml')
 - [`new Scalar(value)`](https://eemeli.org/yaml/#scalar-values)
 - [`new YAMLMap()`](https://eemeli.org/yaml/#collections)
 - [`new YAMLSeq()`](https://eemeli.org/yaml/#collections)
-- [`doc.anchors.createAlias(node, name?): Alias`](https://eemeli.org/yaml/#working-with-anchors)
+- [`doc.createAlias(node, name?): Alias`](https://eemeli.org/yaml/#working-with-anchors)
 - [`doc.createNode(value, options?): Node`](https://eemeli.org/yaml/#creating-nodes)
 - [`doc.createPair(key, value): Pair`](https://eemeli.org/yaml/#creating-nodes)
 - [`visit(node, visitor)`](https://eemeli.org/yaml/#modifying-nodes)
diff --git a/docs/01_intro.md b/docs/01_intro.md
index a1eda10..713b284 100644
--- a/docs/01_intro.md
+++ b/docs/01_intro.md
@@ -85,7 +85,7 @@ import {
 - [`new Scalar(value)`](#scalar-values)
 - [`new YAMLMap()`](#collections)
 - [`new YAMLSeq()`](#collections)
-- [`doc.anchors.createAlias(node, name?): Alias`](#working-with-anchors)
+- [`doc.createAlias(node, name?): Alias`](#working-with-anchors)
 - [`doc.createNode(value, options?): Node`](#creating-nodes)
 - [`doc.createPair(key, value): Pair`](#creating-nodes)
 - [`visit(node, visitor)`](#modifying-nodes)
diff --git a/docs/03_options.md b/docs/03_options.md
index 923515a..8ee4411 100644
--- a/docs/03_options.md
+++ b/docs/03_options.md
@@ -37,12 +37,10 @@ Document options are relevant for operations on the `Document` object, which mak
 
 Used by: `parse()`, `parseDocument()`, `parseAllDocuments()`, `stringify()`, `new Composer()`, and `new Document()`
 
-| Name          | Type                            | Default  | Description                                                                                                                                |
-| ------------- | ------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
-| anchorPrefix  | `string`                        | `'a'`    | Default prefix for anchors, resulting in anchors `a1`, `a2`, ... by default.                                                               |
-| keepUndefined | `boolean`                       | `false`  | Keep `undefined` object values when creating mappings and return a Scalar node when stringifying `undefined`.                              |
-| logLevel      | `'warn' ⎮ 'error' ⎮` `'silent'` | `'warn'` | Control the verbosity of `parse()`. Set to `'error'` to silence warnings, and to `'silent'` to also silence most errors (not recommended). |
-| version       | `'1.1' ⎮ '1.2'`                 | `'1.2'`  | The YAML version used by documents without a `%YAML` directive.                                                                            |
+| Name     | Type                            | Default  | Description                                                                                                                                |
+| -------- | ------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
+| logLevel | `'warn' ⎮ 'error' ⎮` `'silent'` | `'warn'` | Control the verbosity of `parse()`. Set to `'error'` to silence warnings, and to `'silent'` to also silence most errors (not recommended). |
+| version  | `'1.1' ⎮ '1.2'`                 | `'1.2'`  | The YAML version used by documents without a `%YAML` directive.                                                                            |
 
 By default, the library will emit warnings as required by the YAML spec during parsing.
 If you'd like to silence these, set the `logLevel` option to `'error'`.
@@ -95,6 +93,17 @@ mergeResult.target
 **Merge** keys are a [YAML 1.1 feature](http://yaml.org/type/merge.html) that is not a part of the 1.2 spec.
 To use a merge key, assign an alias node or an array of alias nodes as the value of a `<<` key in a mapping.
 
+## CreateNode Options
+
+Used by: `stringify()`, `new Document()`, `doc.createNode()`, and `doc.createPair()`
+
+| Name          | Type      | Default | Description                                                                                                                                      |
+| ------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
+| anchorPrefix  | `string`  | `'a'`   | Default prefix for anchors, resulting in anchors `a1`, `a2`, ... by default.                                                                     |
+| flow          | `boolean` | `false` | Force the top-level collection node to use flow style.                                                                                           |
+| keepUndefined | `boolean` | `false` | Keep `undefined` object values when creating mappings and return a Scalar node when stringifying `undefined`.                                    |
+| tag           | `string`  |         | Specify the top-level collection type, e.g. `"!!omap"`. Note that this requires the corresponding tag to be available in this document's schema. |
+
 ## ToJS Options
 
 ```js
diff --git a/docs/04_documents.md b/docs/04_documents.md
index 45793d1..d5b6e99 100644
--- a/docs/04_documents.md
+++ b/docs/04_documents.md
@@ -75,7 +75,6 @@ See [Options](#options) for more information on the last argument.
 
 | Member        | Type                               | Description                                                                                                                                                         |
 | ------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| anchors       | [`Anchors`](#anchors)              | Anchors associated with the document's nodes; also provides alias & merge node creators.                                                                            |
 | commentBefore | `string?`                          | A comment at the very beginning of the document. If not empty, separated from the rest of the document by a blank line or the doc-start indicator when stringified. |
 | comment       | `string?`                          | A comment at the end of the document. If not empty, separated from the rest of the document by a blank line when stringified.                                       |
 | contents      | [`Node`](#content-nodes) `⎮ any`   | The document contents.                                                                                                                                              |
@@ -106,6 +105,7 @@ Although `parseDocument()` and `parseAllDocuments()` will leave it with `YAMLMap
 
 | Method                                     | Returns  | Description                                                                                                                       |
 | ------------------------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------- |
+| createAlias(node: Node, name?: string)     | `Alias`  | Create a new `Alias` node, adding the required anchor for `node`. If `name` is empty, a new anchor name will be generated.        |
 | createNode(value,&nbsp;options?)           | `Node`   | Recursively wrap any input with appropriate `Node` containers. See [Creating Nodes](#creating-nodes) for more information.        |
 | createPair(key,&nbsp;value,&nbsp;options?) | `Pair`   | Recursively wrap `key` and `value` into a `Pair` object. See [Creating Nodes](#creating-nodes) for more information.              |
 | setSchema(version,&nbsp;options?)          | `void`   | Change the YAML version and schema used by the document. `version` must be either `'1.1'` or `'1.2'`; accepts all Schema options. |
@@ -176,73 +176,3 @@ See the section on [custom tags](#writing-custom-tags) for more on this topic.
 
 `doc.contents.yaml` determines if an explicit `%YAML` directive should be included in the output, and what version it should use.
 If changing the version after the document's creation, you'll probably want to use `doc.setSchema()` as it will also update the schema accordingly.
-
-## Working with Anchors
-
-A description of [alias and merge nodes](#alias-nodes) is included in the next section.
-
-<br/>
-
-#### `Document#anchors`
-
-| Method                                 | Returns    | Description                                                                                                                |
-| -------------------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------- |
-| createAlias(node: Node, name?: string) | `Alias`    | Create a new `Alias` node, adding the required anchor for `node`. If `name` is empty, a new anchor name will be generated. |
-| createMergePair(...Node)               | `Pair`     | Create a new merge pair with the given source nodes. Non-`Alias` sources will be automatically wrapped.                    |
-| getName(node: Node)                    | `string?`  | The anchor name associated with `node`, if set.                                                                            |
-| getNames()                             | `string[]` | List of all defined anchor names.                                                                                          |
-| getNode(name: string)                  | `Node?`    | The node associated with the anchor `name`, if set.                                                                        |
-| newName(prefix: string)                | `string`   | Find an available anchor name with the given `prefix` and a numerical suffix.                                              |
-| setAnchor(node?: Node, name?: string)  | `string?`  | Associate an anchor with `node`. If `name` is empty, a new name will be generated.                                         |
-
-```js
-const src = '[{ a: A }, { b: B }]'
-const doc = parseDocument(src)
-doc.anchors.setAnchor(doc.getIn([0, 'a'], true)) // 'a1'
-doc.anchors.setAnchor(doc.getIn([1, 'b'], true)) // 'a2'
-doc.anchors.setAnchor(null, 'a1') // 'a1'
-doc.anchors.getNode('a2')
-// { value: 'B', range: [ 16, 18 ], type: 'PLAIN' }
-String(doc)
-// [ { a: A }, { b: &a2 B } ]
-
-const alias = doc.anchors.createAlias(doc.get(0, true), 'AA')
-// Alias { source: YAMLMap { items: [ [Pair] ] } }
-doc.add(alias)
-doc.toJS()
-// [ { a: 'A' }, { b: 'B' }, { a: 'A' } ]
-String(doc)
-// [ &AA { a: A }, { b: &a2 B }, *AA ]
-
-doc.setSchema('1.2', { merge: true })
-const merge = doc.anchors.createMergePair(alias)
-// Pair {
-//   key: Scalar { value: '<<' },
-//   value: Alias { source: YAMLMap { ... } } }
-doc.addIn([1], merge)
-doc.toJS()
-// [ { a: 'A' }, { b: 'B', a: 'A' }, { a: 'A' } ]
-String(doc)
-// [ &AA { a: A }, { b: &a2 B, <<: *AA }, *AA ]
-
-// This creates a circular reference
-merge.value = doc.anchors.createAlias(doc.get(1, true))
-doc.toJS() // [RangeError: Maximum call stack size exceeded]
-String(doc)
-// [
-//   &AA { a: A },
-//   &a1 { b: &a2 B, <<: *a1 },
-//   *AA
-// ]
-```
-
-You should make sure to only add alias and merge nodes to the document after the nodes to which they refer, or the document's YAML stringification will fail.
-
-It is valid to have an anchor associated with a node even if it has no aliases.
-`yaml` will not allow you to associate the same name with more than one node, even though this is allowed by the YAML spec (all but the last instance will have numerical suffixes added).
-To add or reassign an anchor, use **`setAnchor(node, name)`**.
-The second parameter is optional, and if left out either the pre-existing anchor name of the node will be used, or a new one generated.
-To remove an anchor, use `setAnchor(null, name)`.
-The function will return the new anchor's name, or `null` if both of its arguments are `null`.
-
-While the `merge` option needs to be true to parse merge pairs as such, this is not required during stringification.
diff --git a/docs/05_content_nodes.md b/docs/05_content_nodes.md
index 4417dee..afc9b5a 100644
--- a/docs/05_content_nodes.md
+++ b/docs/05_content_nodes.md
@@ -2,19 +2,23 @@
 
 After parsing, the `contents` value of each `YAML.Document` is the root of an [Abstract Syntax Tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree) of nodes representing the document (or `null` for an empty document).
 
+Both scalar and collection values may have an `anchor` associated with them; this is rendered in the string representation with a `&` prefix, so e.g. in `foo: &aa bar`, the value `bar` has the anchor `aa`.
+Anchors are used by [Alias nodes](#alias-nodes) to allow for the same value to be used in multiple places in the document.
+It is valid to have an anchor associated with a node even if it has no aliases.
+
 ## Scalar Values
 
 ```js
 class NodeBase {
-  comment?: string,   // a comment on or immediately after this
-  commentBefore?: string, // a comment before this
-  range?: [number, number],
+  comment?: string        // a comment on or immediately after this
+  commentBefore?: string  // a comment before this
+  range?: [number, number]
       // the [start, end] range of characters of the source parsed
       // into this node (undefined for pairs or if not parsed)
-  spaceBefore?: boolean,
+  spaceBefore?: boolean
       // a blank line before this node and its commentBefore
-  tag?: string,       // a fully qualified tag, if required
-  toJSON(): any       // a plain JS or JSON representation of this node
+  tag?: string   // a fully qualified tag, if required
+  toJSON(): any  // a plain JS or JSON representation of this node
 }
 ```
 
@@ -22,12 +26,13 @@ For scalar values, the `tag` will not be set unless it was explicitly defined in
 
 ```js
 class Scalar<T = unknown> extends NodeBase {
-  format?: 'BIN' | 'HEX' | 'OCT' | 'TIME' | undefined,
+  anchor?: string  // an anchor associated with this node
+  format?: 'BIN' | 'HEX' | 'OCT' | 'TIME' | undefined
       // By default (undefined), numbers use decimal notation.
       // The YAML 1.2 core schema only supports 'HEX' and 'OCT'.
   type?:
     'BLOCK_FOLDED' | 'BLOCK_LITERAL' | 'PLAIN' |
-    'QUOTE_DOUBLE' | 'QUOTE_SINGLE' | undefined,
+    'QUOTE_DOUBLE' | 'QUOTE_SINGLE' | undefined
   value: T
 }
 ```
@@ -42,12 +47,13 @@ On the other hand, `!!int` and `!!float` stringifiers will take `format` into ac
 
 ```js
 class Pair<K = unknown, V = unknown> extends NodeBase {
-  key: K,   // When parsed, key and value are always
+  key: K    // When parsed, key and value are always
   value: V  // Node or null, but can be set to anything
 }
 
 class Collection extends NodeBase {
-  flow?: boolean // use flow style when stringifying this
+  anchor?: string  // an anchor associated with this node
+  flow?: boolean   // use flow style when stringifying this
   schema?: Schema
   addIn(path: Iterable<unknown>, value: unknown): void
   deleteIn(path: Iterable<unknown>): boolean
@@ -131,7 +137,8 @@ Note that for `addIn` the path argument points to the collection rather than the
 <!-- prettier-ignore -->
 ```js
 class Alias extends NodeBase {
-  source: Scalar | YAMLMap | YAMLSeq
+  source: string
+  resolve(doc: Document): Scalar | YAMLMap | YAMLSeq | undefined
 }
 
 const obj = YAML.parse('[ &x { X: 42 }, Y, *x ]')
@@ -146,7 +153,9 @@ YAML.stringify(obj)
   // - *a1
 ```
 
-`Alias` nodes provide a way to include a single node in multiple places in a document; the `source` of an alias node must be a preceding node in the document. Circular references are fully supported, and where possible the JS representation of alias nodes will be the actual source object.
+`Alias` nodes provide a way to include a single node in multiple places in a document; the `source` of an alias node must be a preceding anchor in the document.
+Circular references are fully supported, and where possible the JS representation of alias nodes will be the actual source object.
+For ease of use, alias nodes also provide a `resolve(doc)` method to dreference its source node.
 
 When nodes are constructed from JS structures (e.g. during `YAML.stringify()`), multiple references to the same object will result in including an autogenerated anchor at its first instance, and alias nodes to that anchor at later references.
 
@@ -176,27 +185,43 @@ String(doc)
 // - balloons: 99
 ```
 
-#### `YAML.Document#createNode(value, options?): Node`
+#### `doc.createNode(value, replacer?, options?): Node`
 
 To create a new node, use the `createNode(value, options?)` document method.
 This will recursively wrap any input with appropriate `Node` containers.
 Generic JS `Object` values as well as `Map` and its descendants become mappings, while arrays and other iterable objects result in sequences.
 With `Object`, entries that have an `undefined` value are dropped.
 
-To force flow styling on a collection, use `options.flow = true`
-Use `options.replacer` to apply a replacer array or function, following the [JSON implementation][replacer].
-To specify the collection type, set `options.tag` to its identifying string, e.g. `"!!omap"`.
-Note that this requires the corresponding tag to be available in the document's schema.
+Use a `replacer` to apply a replacer array or function, following the [JSON implementation][replacer].
+To force flow styling on a collection, use the `flow: true` option.
+For all available options, see the [CreateNode Options](#createnode-options) section.
 
 [replacer]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter
 
-As a possible side effect, this method may add entries to the document's [`anchors`](#working-with-anchors).
-
 The primary purpose of this method is to enable attaching comments or other metadata to a value, or to otherwise exert more fine-grained control over the stringified output.
 To that end, you'll need to assign its return value to the `contents` of a document (or somewhere within said contents), as the document's schema is required for YAML string output.
 If you're not interested in working with such metadata, document `contents` may also include non-`Node` values at any level.
 
-<h4 style="clear:both"><code>new YAMLMap(), new YAMLSeq(), doc.createPair(key, value)</code></h4>
+<h4 style="clear:both"><code>doc.createAlias(node, name?): Alias</code></h4>
+
+```js
+const alias = doc.createAlias(doc.get(1, true), 'foo')
+doc.add(alias)
+String(doc)
+// - some # A commented item
+// - &foo values
+// - balloons: 99
+// - *foo
+```
+
+Create a new `Alias` node, ensuring that the target `node` has the required anchor.
+If `node` already has an anchor, `name` is ignored.
+Otherwise, the `node.anchor` value will be set to `name`, or if an anchor with that name is already present in the document, `name` will be used as a prefix for a new unique anchor.
+If `name` is undefined, the generated anchor will use 'a' as a prefix.
+
+You should make sure to only add alias nodes to the document after the nodes to which they refer, or the document's YAML stringification will fail.
+
+<h4 style="clear:both"><code>new YAMLMap(), new YAMLSeq(), doc.createPair(key, value): Pair</code></h4>
 
 ```js
 import { Document, YAMLSeq } from 'yaml'
diff --git a/src/compose/compose-collection.ts b/src/compose/compose-collection.ts
index ffd1043..d2ba853 100644
--- a/src/compose/compose-collection.ts
+++ b/src/compose/compose-collection.ts
@@ -1,4 +1,3 @@
-import type { Document } from '../doc/Document.js'
 import { isMap, isNode, ParsedNode } from '../nodes/Node.js'
 import { Scalar } from '../nodes/Scalar.js'
 import type { YAMLMap } from '../nodes/YAMLMap.js'
@@ -9,31 +8,30 @@ import type {
   FlowCollection
 } from '../parse/tokens.js'
 import { CollectionTag } from '../schema/types.js'
-import type { ComposeNode } from './compose-node.js'
+import type { ComposeContext, ComposeNode } from './compose-node.js'
 import { resolveBlockMap } from './resolve-block-map.js'
 import { resolveBlockSeq } from './resolve-block-seq.js'
 import { resolveFlowCollection } from './resolve-flow-collection.js'
 
 export function composeCollection(
   CN: ComposeNode,
-  doc: Document.Parsed,
+  ctx: ComposeContext,
   token: BlockMap | BlockSequence | FlowCollection,
-  anchor: string | null,
   tagName: string | null,
   onError: (offset: number, message: string, warning?: boolean) => void
 ) {
   let coll: YAMLMap.Parsed | YAMLSeq.Parsed
   switch (token.type) {
     case 'block-map': {
-      coll = resolveBlockMap(CN, doc, token, anchor, onError)
+      coll = resolveBlockMap(CN, ctx, token, onError)
       break
     }
     case 'block-seq': {
-      coll = resolveBlockSeq(CN, doc, token, anchor, onError)
+      coll = resolveBlockSeq(CN, ctx, token, onError)
       break
     }
     case 'flow-collection': {
-      coll = resolveFlowCollection(CN, doc, token, anchor, onError)
+      coll = resolveFlowCollection(CN, ctx, token, onError)
       break
     }
   }
@@ -48,13 +46,13 @@ export function composeCollection(
   }
 
   const expType = isMap(coll) ? 'map' : 'seq'
-  let tag = doc.schema.tags.find(
+  let tag = ctx.schema.tags.find(
     t => t.collection === expType && t.tag === tagName
   ) as CollectionTag | undefined
   if (!tag) {
-    const kt = doc.schema.knownTags[tagName]
+    const kt = ctx.schema.knownTags[tagName]
     if (kt && kt.collection === expType) {
-      doc.schema.tags.push(Object.assign({}, kt, { default: false }))
+      ctx.schema.tags.push(Object.assign({}, kt, { default: false }))
       tag = kt
     } else {
       onError(coll.range[0], `Unresolved tag: ${tagName}`, true)
@@ -63,15 +61,12 @@ export function composeCollection(
     }
   }
 
-  const res = tag.resolve(coll, msg => onError(coll.range[0], msg), doc.options)
+  const res = tag.resolve(coll, msg => onError(coll.range[0], msg), ctx.options)
   const node = isNode(res)
     ? (res as ParsedNode)
     : (new Scalar(res) as Scalar.Parsed)
   node.range = coll.range
   node.tag = tagName
   if (tag?.format) (node as Scalar).format = tag.format
-  if (anchor && node !== coll) {
-    // FIXME: handle anchor for non-failsafe collections
-  }
   return node
 }
diff --git a/src/compose/compose-doc.ts b/src/compose/compose-doc.ts
index 9b80ce1..2264676 100644
--- a/src/compose/compose-doc.ts
+++ b/src/compose/compose-doc.ts
@@ -1,24 +1,37 @@
 import type { Directives } from '../doc/directives.js'
 import { Document } from '../doc/Document.js'
-import type { Options } from '../options.js'
+import type {
+  DocumentOptions,
+  ParseOptions,
+  SchemaOptions
+} from '../options.js'
 import type * as Tokens from '../parse/tokens.js'
-import { composeEmptyNode, composeNode } from './compose-node.js'
+import {
+  ComposeContext,
+  composeEmptyNode,
+  composeNode
+} from './compose-node.js'
 import { resolveEnd } from './resolve-end.js'
 import { resolveProps } from './resolve-props.js'
 
 export function composeDoc(
-  options: Options,
+  options: ParseOptions & DocumentOptions & SchemaOptions,
   directives: Directives,
   { offset, start, value, end }: Tokens.Document,
   onError: (offset: number, message: string, warning?: boolean) => void
 ) {
   const opts = Object.assign({ directives }, options)
   const doc = new Document(undefined, opts) as Document.Parsed
-  const props = resolveProps(doc, start, true, 'doc-start', offset, onError)
+  const ctx: ComposeContext = {
+    directives: doc.directives,
+    options: doc.options,
+    schema: doc.schema
+  }
+  const props = resolveProps(ctx, start, true, 'doc-start', offset, onError)
   if (props.found) doc.directives.marker = true
   doc.contents = value
-    ? composeNode(doc, value, props, onError)
-    : composeEmptyNode(doc, offset + props.length, start, null, props, onError)
+    ? composeNode(ctx, value, props, onError)
+    : composeEmptyNode(ctx, offset + props.length, start, null, props, onError)
 
   const re = resolveEnd(end, doc.contents.range[1], false, onError)
   if (re.comment) doc.comment = re.comment
diff --git a/src/compose/compose-node.ts b/src/compose/compose-node.ts
index d40ec0c..33f1a90 100644
--- a/src/compose/compose-node.ts
+++ b/src/compose/compose-node.ts
@@ -1,12 +1,20 @@
-import type { Document } from '../doc/Document.js'
+import type { Directives } from '../doc/directives.js'
 import { Alias } from '../nodes/Alias.js'
-import type { Node, ParsedNode } from '../nodes/Node.js'
+import type { ParsedNode } from '../nodes/Node.js'
+import type { ParseOptions } from '../options.js'
 import type { FlowScalar, Token } from '../parse/tokens.js'
+import type { Schema } from '../schema/Schema.js'
 import { composeCollection } from './compose-collection.js'
 import { composeScalar } from './compose-scalar.js'
 import { resolveEnd } from './resolve-end.js'
 import { emptyScalarPosition } from './util-empty-scalar-position.js'
 
+export interface ComposeContext {
+  directives: Directives
+  options: Readonly<Required<Omit<ParseOptions, 'lineCounter'>>>
+  schema: Readonly<Schema>
+}
+
 export interface Props {
   spaceBefore: boolean
   comment: string
@@ -18,7 +26,7 @@ const CN = { composeNode, composeEmptyNode }
 export type ComposeNode = typeof CN
 
 export function composeNode(
-  doc: Document.Parsed,
+  ctx: ComposeContext,
   token: Token,
   props: Props,
   onError: (offset: number, message: string, warning?: boolean) => void
@@ -27,7 +35,7 @@ export function composeNode(
   let node: ParsedNode
   switch (token.type) {
     case 'alias':
-      node = composeAlias(doc, token, onError)
+      node = composeAlias(ctx, token, onError)
       if (anchor || tagName)
         onError(token.offset, 'An alias node must not specify any properties')
       break
@@ -35,12 +43,18 @@ export function composeNode(
     case 'single-quoted-scalar':
     case 'double-quoted-scalar':
     case 'block-scalar':
-      node = composeScalar(doc, token, anchor, tagName, onError)
+      node = composeScalar(ctx, token, tagName, onError)
+      if (anchor) {
+        node.anchor = anchor
+      }
       break
     case 'block-map':
     case 'block-seq':
     case 'flow-collection':
-      node = composeCollection(CN, doc, token, anchor, tagName, onError)
+      node = composeCollection(CN, ctx, token, tagName, onError)
+      if (anchor) {
+        node.anchor = anchor
+      }
       break
     default:
       console.log(token)
@@ -55,7 +69,7 @@ export function composeNode(
 }
 
 export function composeEmptyNode(
-  doc: Document.Parsed,
+  ctx: ComposeContext,
   offset: number,
   before: Token[] | undefined,
   pos: number | null,
@@ -68,26 +82,25 @@ export function composeEmptyNode(
     indent: -1,
     source: ''
   }
-  const node = composeScalar(doc, token, anchor, tagName, onError)
+  const node = composeScalar(ctx, token, tagName, onError)
+  if (anchor) {
+    node.anchor = anchor
+  }
   if (spaceBefore) node.spaceBefore = true
   if (comment) node.comment = comment
   return node
 }
 
 function composeAlias(
-  doc: Document.Parsed,
+  { options }: ComposeContext,
   { offset, source, end }: FlowScalar,
   onError: (offset: number, message: string, warning?: boolean) => void
 ) {
-  const name = source.substring(1)
-  const src = doc.anchors.getNode(name)
-  if (!src) onError(offset, `Aliased anchor not found: ${name}`)
-  const alias = new Alias(src as Node)
-
+  const alias = new Alias(source.substring(1))
   const re = resolveEnd(
     end,
     offset + source.length,
-    doc.options.strict,
+    options.strict,
     onError
   )
   alias.range = [offset, re.offset]
diff --git a/src/compose/compose-scalar.ts b/src/compose/compose-scalar.ts
index 3b0b4a0..7991a93 100644
--- a/src/compose/compose-scalar.ts
+++ b/src/compose/compose-scalar.ts
@@ -1,32 +1,31 @@
-import type { Document } from '../doc/Document.js'
-import type { Schema } from '../schema/Schema.js'
 import { isScalar, SCALAR } from '../nodes/Node.js'
 import { Scalar } from '../nodes/Scalar.js'
 import type { BlockScalar, FlowScalar } from '../parse/tokens.js'
+import type { Schema } from '../schema/Schema.js'
 import type { ScalarTag } from '../schema/types.js'
 import { resolveBlockScalar } from './resolve-block-scalar.js'
 import { resolveFlowScalar } from './resolve-flow-scalar.js'
+import type { ComposeContext } from './compose-node.js'
 
 export function composeScalar(
-  doc: Document.Parsed,
+  ctx: ComposeContext,
   token: FlowScalar | BlockScalar,
-  anchor: string | null,
   tagName: string | null,
   onError: (offset: number, message: string) => void
 ) {
   const { offset } = token
   const { value, type, comment, length } =
     token.type === 'block-scalar'
-      ? resolveBlockScalar(token, doc.options.strict, onError)
-      : resolveFlowScalar(token, doc.options.strict, onError)
+      ? resolveBlockScalar(token, ctx.options.strict, onError)
+      : resolveFlowScalar(token, ctx.options.strict, onError)
 
   const tag = tagName
-    ? findScalarTagByName(doc.schema, value, tagName, onError)
-    : findScalarTagByTest(doc.schema, value, token.type === 'scalar')
+    ? findScalarTagByName(ctx.schema, value, tagName, onError)
+    : findScalarTagByTest(ctx.schema, value, token.type === 'scalar')
 
   let scalar: Scalar
   try {
-    const res = tag.resolve(value, msg => onError(offset, msg), doc.options)
+    const res = tag.resolve(value, msg => onError(offset, msg), ctx.options)
     scalar = isScalar(res) ? res : new Scalar(res)
   } catch (error) {
     onError(offset, error.message)
@@ -39,7 +38,6 @@ export function composeScalar(
   if (tag.format) scalar.format = tag.format
   if (comment) scalar.comment = comment
 
-  if (anchor) doc.anchors.setAnchor(scalar, anchor)
   return scalar as Scalar.Parsed
 }
 
diff --git a/src/compose/composer.ts b/src/compose/composer.ts
index 5590353..8175bf8 100644
--- a/src/compose/composer.ts
+++ b/src/compose/composer.ts
@@ -2,7 +2,12 @@ import { Directives } from '../doc/directives.js'
 import { Document } from '../doc/Document.js'
 import { YAMLParseError, YAMLWarning } from '../errors.js'
 import { isCollection } from '../nodes/Node.js'
-import { defaultOptions, Options } from '../options.js'
+import {
+  defaultOptions,
+  DocumentOptions,
+  ParseOptions,
+  SchemaOptions
+} from '../options.js'
 import type { Token } from '../parse/tokens.js'
 import { composeDoc } from './compose-doc.js'
 import { resolveEnd } from './resolve-end.js'
@@ -38,7 +43,7 @@ function parsePrelude(prelude: string[]) {
  * Compose a stream of CST nodes into a stream of YAML Documents.
  *
  * ```ts
- * const options: Options = { ... }
+ * const options = { ... }
  * const docs: Document.Parsed[] = []
  * const composer = new Composer(doc => docs.push(doc), options)
  * const parser = new Parser(composer.next)
@@ -50,13 +55,16 @@ export class Composer {
   private directives: Directives
   private doc: Document.Parsed | null = null
   private onDocument: (doc: Document.Parsed) => void
-  private options: Options
+  private options: ParseOptions & DocumentOptions & SchemaOptions
   private atDirectives = false
   private prelude: string[] = []
   private errors: YAMLParseError[] = []
   private warnings: YAMLWarning[] = []
 
-  constructor(onDocument: Composer['onDocument'], options: Options = {}) {
+  constructor(
+    onDocument: Composer['onDocument'],
+    options: ParseOptions & DocumentOptions & SchemaOptions = {}
+  ) {
     this.directives = new Directives({
       version: options?.version || defaultOptions.version
     })
diff --git a/src/compose/resolve-block-map.ts b/src/compose/resolve-block-map.ts
index 8be9bc1..feeb1ba 100644
--- a/src/compose/resolve-block-map.ts
+++ b/src/compose/resolve-block-map.ts
@@ -1,8 +1,7 @@
-import type { Document } from '../doc/Document.js'
 import { Pair } from '../nodes/Pair.js'
 import { YAMLMap } from '../nodes/YAMLMap.js'
 import type { BlockMap } from '../parse/tokens.js'
-import type { ComposeNode } from './compose-node.js'
+import type { ComposeContext, ComposeNode } from './compose-node.js'
 import { resolveProps } from './resolve-props.js'
 import { containsNewline } from './util-contains-newline.js'
 
@@ -10,19 +9,17 @@ const startColMsg = 'All mapping items must start at the same column'
 
 export function resolveBlockMap(
   { composeNode, composeEmptyNode }: ComposeNode,
-  doc: Document.Parsed,
+  ctx: ComposeContext,
   { indent, items, offset }: BlockMap,
-  anchor: string | null,
   onError: (offset: number, message: string, warning?: boolean) => void
 ) {
   const start = offset
-  const map = new YAMLMap(doc.schema)
-  if (anchor) doc.anchors.setAnchor(map, anchor)
+  const map = new YAMLMap(ctx.schema)
 
   for (const { start, key, sep, value } of items) {
     // key properties
     const keyProps = resolveProps(
-      doc,
+      ctx,
       start,
       true,
       'explicit-key-ind',
@@ -56,13 +53,13 @@ export function resolveBlockMap(
     // key value
     const keyStart = offset
     const keyNode = key
-      ? composeNode(doc, key, keyProps, onError)
-      : composeEmptyNode(doc, offset, start, null, keyProps, onError)
+      ? composeNode(ctx, key, keyProps, onError)
+      : composeEmptyNode(ctx, offset, start, null, keyProps, onError)
     offset = keyNode.range[1]
 
     // value properties
     const valueProps = resolveProps(
-      doc,
+      ctx,
       sep || [],
       !key || key.type === 'block-scalar',
       'map-value-ind',
@@ -76,7 +73,7 @@ export function resolveBlockMap(
         if (value?.type === 'block-map' && !valueProps.hasNewline)
           onError(offset, 'Nested mappings are not allowed in compact mappings')
         if (
-          doc.options.strict &&
+          ctx.options.strict &&
           keyProps.start < valueProps.found.offset - 1024
         )
           onError(
@@ -86,8 +83,8 @@ export function resolveBlockMap(
       }
       // value value
       const valueNode = value
-        ? composeNode(doc, value, valueProps, onError)
-        : composeEmptyNode(doc, offset, sep, null, valueProps, onError)
+        ? composeNode(ctx, value, valueProps, onError)
+        : composeEmptyNode(ctx, offset, sep, null, valueProps, onError)
       offset = valueNode.range[1]
       map.items.push(new Pair(keyNode, valueNode))
     } else {
diff --git a/src/compose/resolve-block-seq.ts b/src/compose/resolve-block-seq.ts
index 8832e2f..ce7dc70 100644
--- a/src/compose/resolve-block-seq.ts
+++ b/src/compose/resolve-block-seq.ts
@@ -1,22 +1,19 @@
-import type { Document } from '../doc/Document.js'
 import { YAMLSeq } from '../nodes/YAMLSeq.js'
 import type { BlockSequence } from '../parse/tokens.js'
-import type { ComposeNode } from './compose-node.js'
+import type { ComposeContext, ComposeNode } from './compose-node.js'
 import { resolveProps } from './resolve-props.js'
 
 export function resolveBlockSeq(
   { composeNode, composeEmptyNode }: ComposeNode,
-  doc: Document.Parsed,
+  ctx: ComposeContext,
   { items, offset }: BlockSequence,
-  anchor: string | null,
   onError: (offset: number, message: string, warning?: boolean) => void
 ) {
   const start = offset
-  const seq = new YAMLSeq(doc.schema)
-  if (anchor) doc.anchors.setAnchor(seq, anchor)
+  const seq = new YAMLSeq(ctx.schema)
   for (const { start, value } of items) {
     const props = resolveProps(
-      doc,
+      ctx,
       start,
       true,
       'seq-item-ind',
@@ -38,8 +35,8 @@ export function resolveBlockSeq(
       }
     }
     const node = value
-      ? composeNode(doc, value, props, onError)
-      : composeEmptyNode(doc, offset, start, null, props, onError)
+      ? composeNode(ctx, value, props, onError)
+      : composeEmptyNode(ctx, offset, start, null, props, onError)
     offset = node.range[1]
     seq.items.push(node)
   }
diff --git a/src/compose/resolve-flow-collection.ts b/src/compose/resolve-flow-collection.ts
index f683ce2..bee16f2 100644
--- a/src/compose/resolve-flow-collection.ts
+++ b/src/compose/resolve-flow-collection.ts
@@ -1,24 +1,21 @@
-import type { Document } from '../doc/Document.js'
 import { isNode, isPair, ParsedNode } from '../nodes/Node.js'
 import { Pair } from '../nodes/Pair.js'
 import { YAMLMap } from '../nodes/YAMLMap.js'
 import { YAMLSeq } from '../nodes/YAMLSeq.js'
 import type { FlowCollection, SourceToken, Token } from '../parse/tokens.js'
-import type { ComposeNode } from './compose-node.js'
+import type { ComposeContext, ComposeNode } from './compose-node.js'
 import { resolveEnd } from './resolve-end.js'
 import { containsNewline } from './util-contains-newline.js'
 
 export function resolveFlowCollection(
   { composeNode, composeEmptyNode }: ComposeNode,
-  doc: Document.Parsed,
+  ctx: ComposeContext,
   fc: FlowCollection,
-  _anchor: string | null,
   onError: (offset: number, message: string, warning?: boolean) => void
 ) {
   const isMap = fc.start.source === '{'
-  const coll = isMap ? new YAMLMap(doc.schema) : new YAMLSeq(doc.schema)
+  const coll = isMap ? new YAMLMap(ctx.schema) : new YAMLSeq(ctx.schema)
   coll.flow = true
-  if (_anchor) doc.anchors.setAnchor(coll, _anchor)
 
   let key: ParsedNode | null = null
   let value: ParsedNode | null = null
@@ -55,14 +52,14 @@ export function resolveFlowCollection(
     if (value) {
       if (hasComment) value.comment = comment
     } else {
-      value = composeEmptyNode(doc, offset, fc.items, pos, getProps(), onError)
+      value = composeEmptyNode(ctx, offset, fc.items, pos, getProps(), onError)
     }
     if (isMap || atExplicitKey) {
       coll.items.push(key ? new Pair(key, value) : new Pair(value))
     } else {
       const seq = coll as YAMLSeq
       if (key) {
-        const map = new YAMLMap(doc.schema)
+        const map = new YAMLMap(ctx.schema)
         map.flow = true
         map.items.push(new Pair(key, value))
         seq.items.push(map)
@@ -78,7 +75,7 @@ export function resolveFlowCollection(
         hasSpace = true
         break
       case 'comment': {
-        if (doc.options.strict && !hasSpace)
+        if (ctx.options.strict && !hasSpace)
           onError(
             offset,
             'Comments must be separated from other tokens by white space characters'
@@ -120,7 +117,7 @@ export function resolveFlowCollection(
         break
       case 'tag': {
         if (tagName) onError(offset, 'A node can have at most one tag')
-        const tn = doc.directives.tagName(token.source, m => onError(offset, m))
+        const tn = ctx.directives.tagName(token.source, m => onError(offset, m))
         if (tn) tagName = tn
         atLineStart = false
         atValueEnd = false
@@ -139,7 +136,7 @@ export function resolveFlowCollection(
         if (key) {
           if (value) {
             onError(offset, 'Missing {} around pair used as mapping key')
-            const map = new YAMLMap(doc.schema)
+            const map = new YAMLMap(ctx.schema)
             map.flow = true
             map.items.push(new Pair(key, value))
             map.range = [key.range[0], value.range[1]]
@@ -147,7 +144,7 @@ export function resolveFlowCollection(
             value = null
           } // else explicit key
         } else if (value) {
-          if (doc.options.strict) {
+          if (ctx.options.strict) {
             const slMsg =
               'Implicit keys of flow sequence pairs need to be on a single line'
             if (nlAfterValueInSeq) onError(offset, slMsg)
@@ -165,7 +162,7 @@ export function resolveFlowCollection(
           key = value
           value = null
         } else {
-          key = composeEmptyNode(doc, offset, fc.items, i, getProps(), onError)
+          key = composeEmptyNode(ctx, offset, fc.items, i, getProps(), onError)
         }
         if (hasComment) {
           key.comment = comment
@@ -199,7 +196,7 @@ export function resolveFlowCollection(
       default: {
         if (value) onError(offset, 'Missing , between flow collection items')
         if (!isMap && !key && !atExplicitKey) seqKeyToken = token
-        value = composeNode(doc, token, getProps(), onError)
+        value = composeNode(ctx, token, getProps(), onError)
         offset = value.range[1]
         atLineStart = false
         isSourceToken = false
@@ -220,7 +217,7 @@ export function resolveFlowCollection(
   }
   if (ce) offset += ce.source.length
   if (ee.length > 0) {
-    const end = resolveEnd(ee, offset, doc.options.strict, onError)
+    const end = resolveEnd(ee, offset, ctx.options.strict, onError)
     if (end.comment) coll.comment = comment
     offset = end.offset
   }
diff --git a/src/compose/resolve-props.ts b/src/compose/resolve-props.ts
index 33e7dc3..3006b42 100644
--- a/src/compose/resolve-props.ts
+++ b/src/compose/resolve-props.ts
@@ -1,8 +1,8 @@
-import type { Document } from '../doc/Document.js'
 import type { SourceToken } from '../parse/tokens.js'
+import type { ComposeContext } from './compose-node.js'
 
 export function resolveProps(
-  doc: Document.Parsed,
+  ctx: ComposeContext,
   tokens: SourceToken[],
   startOnNewline: boolean,
   indicator:
@@ -35,7 +35,7 @@ export function resolveProps(
         hasSpace = true
         break
       case 'comment': {
-        if (doc.options.strict && !hasSpace)
+        if (ctx.options.strict && !hasSpace)
           onError(
             offset + length,
             'Comments must be separated from other tokens by white space characters'
@@ -64,7 +64,7 @@ export function resolveProps(
         break
       case 'tag': {
         if (tagName) onError(offset + length, 'A node can have at most one tag')
-        const tn = doc.directives.tagName(token.source, msg =>
+        const tn = ctx.directives.tagName(token.source, msg =>
           onError(offset, msg)
         )
         if (tn) tagName = tn
diff --git a/src/doc/Anchors.ts b/src/doc/Anchors.ts
deleted file mode 100644
index 1ab6e1e..0000000
--- a/src/doc/Anchors.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-import { Alias } from '../nodes/Alias.js'
-import { isAlias, isCollection, isMap, isScalar, Node } from '../nodes/Node.js'
-import { Pair } from '../nodes/Pair.js'
-import { Scalar } from '../nodes/Scalar.js'
-import { YAMLSeq } from '../nodes/YAMLSeq.js'
-
-export class Anchors {
-  map: Record<string, Node> = Object.create(null)
-  private prefix: string
-
-  constructor(prefix: string) {
-    this.prefix = prefix
-  }
-
-  /**
-   * Create a new `Alias` node, adding the required anchor for `node`.
-   * If `name` is empty, a new anchor name will be generated.
-   */
-  createAlias(node: Node, name?: string) {
-    this.setAnchor(node, name)
-    return new Alias(node)
-  }
-
-  /**
-   * Create a new merge `Pair` with the given source nodes.
-   * Non-`Alias` sources will be automatically wrapped.
-   */
-  createMergePair(...sources: Node[]) {
-    const key = new Scalar('<<')
-    const items = sources.map(s => {
-      if (isAlias(s)) {
-        if (isMap(s.source)) return s
-      } else if (isMap(s)) {
-        return this.createAlias(s)
-      }
-      throw new Error('Merge sources must be Map nodes or their Aliases')
-    })
-    if (items.length === 1) return new Pair(key, items[0])
-    const seq = new YAMLSeq<Alias>()
-    seq.items = items
-    return new Pair(key, seq)
-  }
-
-  /** The anchor name associated with `node`, if set. */
-  getName(node: Node) {
-    return Object.keys(this.map).find(a => this.map[a] === node)
-  }
-
-  /** List of all defined anchor names. */
-  getNames() {
-    return Object.keys(this.map)
-  }
-
-  /** The node associated with the anchor `name`, if set. */
-  getNode(name: string) {
-    return this.map[name]
-  }
-
-  /**
-   * Find an available anchor name with the given `prefix` and a
-   * numerical suffix.
-   */
-  newName(prefix?: string) {
-    if (!prefix) prefix = this.prefix
-    const names = Object.keys(this.map)
-    for (let i = 1; true; ++i) {
-      const name = `${prefix}${i}`
-      if (!names.includes(name)) return name
-    }
-  }
-
-  /**
-   * Associate an anchor with `node`. If `name` is empty, a new name will be generated.
-   * To remove an anchor, use `setAnchor(null, name)`.
-   */
-  setAnchor(node: Node | null, name?: string) {
-    const { map } = this
-    if (!node) {
-      if (!name) return null
-      delete map[name]
-      return name
-    }
-
-    if (!isScalar(node) && !isCollection(node))
-      throw new Error('Anchors may only be set for Scalar, Seq and Map nodes')
-    if (name) {
-      if (/[\x00-\x19\s,[\]{}]/.test(name))
-        throw new Error(
-          'Anchor names must not contain whitespace or control characters'
-        )
-      const prevNode = map[name]
-      if (prevNode && prevNode !== node) map[this.newName(name)] = prevNode
-    }
-
-    const prevName = Object.keys(map).find(a => map[a] === node)
-    if (prevName) {
-      if (!name || prevName === name) return prevName
-      delete map[prevName]
-    } else if (!name) name = this.newName()
-    map[name] = node
-    return name
-  }
-}
diff --git a/src/doc/Document.ts b/src/doc/Document.ts
index f180643..ed3cc23 100644
--- a/src/doc/Document.ts
+++ b/src/doc/Document.ts
@@ -10,14 +10,14 @@ import {
   ParsedNode
 } from '../nodes/Node.js'
 import { Pair } from '../nodes/Pair.js'
-import { toJS, ToJSAnchorValue, ToJSContext } from '../nodes/toJS.js'
+import type { Scalar } from '../nodes/Scalar.js'
+import { toJS, ToJSContext } from '../nodes/toJS.js'
 import type { YAMLMap } from '../nodes/YAMLMap.js'
 import type { YAMLSeq } from '../nodes/YAMLSeq.js'
 import {
   CreateNodeOptions,
   defaultOptions,
   DocumentOptions,
-  Options,
   ParseOptions,
   SchemaOptions,
   ToJSOptions,
@@ -26,7 +26,7 @@ import {
 import { Schema } from '../schema/Schema.js'
 import { stringify } from '../stringify/stringify.js'
 import { stringifyDocument } from '../stringify/stringifyDocument.js'
-import { Anchors } from './Anchors.js'
+import { anchorNames, createNodeAnchors, findNewAnchor } from './anchors.js'
 import { applyReviver } from './applyReviver.js'
 import { createNode, CreateNodeContext } from './createNode.js'
 import { Directives } from './directives.js'
@@ -44,12 +44,6 @@ export declare namespace Document {
 export class Document<T = unknown> {
   readonly [NODE_TYPE]: symbol
 
-  /**
-   * Anchors associated with the document's nodes;
-   * also provides alias & merge node creators.
-   */
-  anchors: Anchors
-
   /** A comment before this Document */
   commentBefore: string | null = null
 
@@ -82,15 +76,25 @@ export class Document<T = unknown> {
    * @param value - The initial value for the document, which will be wrapped
    *   in a Node container.
    */
-  constructor(value?: any, options?: Options)
-  constructor(value: any, replacer: null | Replacer, options?: Options)
+  constructor(
+    value?: any,
+    options?: DocumentOptions & SchemaOptions & ParseOptions & CreateNodeOptions
+  )
+  constructor(
+    value: any,
+    replacer: null | Replacer,
+    options?: DocumentOptions & SchemaOptions & ParseOptions & CreateNodeOptions
+  )
   constructor(
     value?: unknown,
-    replacer?: Replacer | Options | null,
-    options?: Options
+    replacer?:
+      | Replacer
+      | (DocumentOptions & SchemaOptions & ParseOptions & CreateNodeOptions)
+      | null,
+    options?: DocumentOptions & SchemaOptions & ParseOptions & CreateNodeOptions
   ) {
     Object.defineProperty(this, NODE_TYPE, { value: DOC })
-    let _replacer: Replacer | undefined = undefined
+    let _replacer: Replacer | null = null
     if (typeof replacer === 'function' || Array.isArray(replacer)) {
       _replacer = replacer
     } else if (options === undefined && replacer) {
@@ -100,7 +104,6 @@ export class Document<T = unknown> {
 
     const opt = Object.assign({}, defaultOptions, options)
     this.options = opt
-    this.anchors = new Anchors(this.options.anchorPrefix)
     let { version } = opt
     if (options?.directives) {
       this.directives = options.directives.atDocument()
@@ -108,10 +111,14 @@ export class Document<T = unknown> {
     } else this.directives = new Directives({ version })
     this.setSchema(version, options)
 
-    this.contents =
-      value === undefined
-        ? null
-        : ((this.createNode(value, { replacer: _replacer }) as unknown) as T)
+    if (value === undefined) this.contents = null
+    else {
+      this.contents = (this.createNode(
+        value,
+        _replacer,
+        options
+      ) as unknown) as T
+    }
   }
 
   /** Adds a value to the document. */
@@ -124,53 +131,70 @@ export class Document<T = unknown> {
     if (assertCollection(this.contents)) this.contents.addIn(path, value)
   }
 
+  /**
+   * Create a new `Alias` node, ensuring that the target `node` has the required anchor.
+   *
+   * If `node` already has an anchor, `name` is ignored.
+   * Otherwise, the `node.anchor` value will be set to `name`,
+   * or if an anchor with that name is already present in the document,
+   * `name` will be used as a prefix for a new unique anchor.
+   * If `name` is undefined, the generated anchor will use 'a' as a prefix.
+   */
+  createAlias(node: Scalar | YAMLMap | YAMLSeq, name?: string): Alias {
+    if (!node.anchor) {
+      const prev = anchorNames(this)
+      node.anchor =
+        !name || prev.has(name) ? findNewAnchor(name || 'a', prev) : name
+    }
+    return new Alias(node.anchor)
+  }
+
   /**
    * Convert any value into a `Node` using the current schema, recursively
    * turning objects into collections.
    */
+  createNode(value: unknown, options?: CreateNodeOptions): Node
   createNode(
     value: unknown,
-    { flow, keepUndefined, onTagObj, replacer, tag }: CreateNodeOptions = {}
+    replacer: Replacer | CreateNodeOptions | null,
+    options?: CreateNodeOptions
+  ): Node
+  createNode(
+    value: unknown,
+    replacer?: Replacer | CreateNodeOptions | null,
+    options?: CreateNodeOptions
   ): Node {
-    if (typeof replacer === 'function')
+    let _replacer: Replacer | undefined = undefined
+    if (typeof replacer === 'function') {
       value = replacer.call({ '': value }, '', value)
-    else if (Array.isArray(replacer)) {
+      _replacer = replacer
+    } else if (Array.isArray(replacer)) {
       const keyToStr = (v: unknown) =>
         typeof v === 'number' || v instanceof String || v instanceof Number
       const asStr = replacer.filter(keyToStr).map(String)
       if (asStr.length > 0) replacer = replacer.concat(asStr)
+      _replacer = replacer
+    } else if (options === undefined && replacer) {
+      options = replacer
+      replacer = undefined
     }
-    if (typeof keepUndefined !== 'boolean')
-      keepUndefined = !!this.options.keepUndefined
-    const aliasNodes: Alias[] = []
+
+    const { anchorPrefix, flow, keepUndefined, onTagObj, tag } = options || {}
+    const { onAnchor, setAnchors, sourceObjects } = createNodeAnchors(
+      this,
+      anchorPrefix || 'a'
+    )
     const ctx: CreateNodeContext = {
-      keepUndefined,
-      onAlias(source) {
-        // These get fixed later in createNode()
-        const alias = new Alias((source as unknown) as Node)
-        aliasNodes.push(alias)
-        return alias
-      },
+      keepUndefined: keepUndefined ?? false,
+      onAnchor,
       onTagObj,
-      prevObjects: new Map(),
-      replacer,
-      schema: this.schema
+      replacer: _replacer,
+      schema: this.schema,
+      sourceObjects
     }
-
     const node = createNode(value, tag, ctx)
-    for (const alias of aliasNodes) {
-      // With circular references, the source node is only resolved after all of
-      // its child nodes are. This is why anchors are set only after all of the
-      // nodes have been created.
-      alias.source = (alias.source as any).node as Node
-      let name = this.anchors.getName(alias.source)
-      if (!name) {
-        name = this.anchors.newName()
-        this.anchors.map[name] = alias.source
-      }
-    }
     if (flow && isCollection(node)) node.flow = true
-
+    setAnchors()
     return node
   }
 
@@ -183,8 +207,8 @@ export class Document<T = unknown> {
     value: unknown,
     options: CreateNodeOptions = {}
   ) {
-    const k = this.createNode(key, options) as K
-    const v = this.createNode(value, options) as V
+    const k = this.createNode(key, null, options) as K
+    const v = this.createNode(value, null, options) as V
     return new Pair(k, v)
   }
 
@@ -327,16 +351,8 @@ export class Document<T = unknown> {
     onAnchor,
     reviver
   }: ToJSOptions & { json?: boolean; jsonArg?: string | null } = {}) {
-    const anchorNodes = Object.values(this.anchors.map).map(
-      node =>
-        [node, { alias: [], aliasCount: 0, count: 1 }] as [
-          Node,
-          ToJSAnchorValue
-        ]
-    )
-    const anchors = anchorNodes.length > 0 ? new Map(anchorNodes) : null
     const ctx: ToJSContext = {
-      anchors,
+      anchors: new Map(),
       doc: this,
       keep: !json,
       mapAsMap: mapAsMap === true,
@@ -345,8 +361,8 @@ export class Document<T = unknown> {
       stringify
     }
     const res = toJS(this.contents, jsonArg || '', ctx)
-    if (typeof onAnchor === 'function' && anchors)
-      for (const { count, res } of anchors.values()) onAnchor(res, count)
+    if (typeof onAnchor === 'function')
+      for (const { count, res } of ctx.anchors.values()) onAnchor(res, count)
     return typeof reviver === 'function'
       ? applyReviver(reviver, { '': res }, '', res)
       : res
diff --git a/src/doc/anchors.ts b/src/doc/anchors.ts
new file mode 100644
index 0000000..0c6bd40
--- /dev/null
+++ b/src/doc/anchors.ts
@@ -0,0 +1,80 @@
+import { isCollection, isScalar, Node } from '../nodes/Node.js'
+import type { Scalar } from '../nodes/Scalar.js'
+import type { YAMLMap } from '../nodes/YAMLMap.js'
+import type { YAMLSeq } from '../nodes/YAMLSeq.js'
+import { visit } from '../visit.js'
+import { CreateNodeContext } from './createNode.js'
+import type { Document } from './Document.js'
+
+/**
+ * Verify that the input string is a valid anchor.
+ *
+ * Will throw on errors.
+ */
+export function anchorIsValid(anchor: string): true {
+  if (/[\x00-\x19\s,[\]{}]/.test(anchor)) {
+    const sa = JSON.stringify(anchor)
+    const msg = `Anchor must not contain whitespace or control characters: ${sa}`
+    throw new Error(msg)
+  }
+  return true
+}
+
+export function anchorNames(root: Document | Node) {
+  const anchors = new Set<string>()
+  const addNode = (_key: unknown, node: Scalar | YAMLMap | YAMLSeq) => {
+    if (node.anchor) anchors.add(node.anchor)
+  }
+  visit(root, { Scalar: addNode, Map: addNode, Seq: addNode })
+  return anchors
+}
+
+/** Find a new anchor name with the given `prefix` and a one-indexed suffix. */
+export function findNewAnchor(prefix: string, exclude: Set<string>) {
+  for (let i = 1; true; ++i) {
+    const name = `${prefix}${i}`
+    if (!exclude.has(name)) return name
+  }
+}
+
+export function createNodeAnchors(doc: Document, prefix: string) {
+  const aliasObjects: unknown[] = []
+  const sourceObjects: CreateNodeContext['sourceObjects'] = new Map()
+  let prevAnchors: Set<string> | null = null
+
+  return {
+    onAnchor(source: unknown) {
+      aliasObjects.push(source)
+      if (!prevAnchors) prevAnchors = anchorNames(doc)
+      const anchor = findNewAnchor(prefix, prevAnchors)
+      prevAnchors.add(anchor)
+      return anchor
+    },
+
+    /**
+     * With circular references, the source node is only resolved after all
+     * of its child nodes are. This is why anchors are set only after all of
+     * the nodes have been created.
+     */
+    setAnchors() {
+      for (const source of aliasObjects) {
+        const ref = sourceObjects.get(source)
+        if (
+          typeof ref === 'object' &&
+          ref.anchor &&
+          (isScalar(ref.node) || isCollection(ref.node))
+        ) {
+          ref.node.anchor = ref.anchor
+        } else {
+          const error = new Error(
+            'Failed to resolve repeated object (this should not happen)'
+          ) as Error & { source: unknown }
+          error.source = source
+          throw error
+        }
+      }
+    },
+
+    sourceObjects
+  }
+}
diff --git a/src/doc/createNode.ts b/src/doc/createNode.ts
index dd100cd..b2a0f2b 100644
--- a/src/doc/createNode.ts
+++ b/src/doc/createNode.ts
@@ -1,4 +1,4 @@
-import type { Alias } from '../nodes/Alias.js'
+import { Alias } from '../nodes/Alias.js'
 import { isNode, isPair, MAP, Node, SEQ } from '../nodes/Node.js'
 import { Scalar } from '../nodes/Scalar.js'
 import type { YAMLMap } from '../nodes/YAMLMap.js'
@@ -8,16 +8,11 @@ import type { Replacer } from './Document.js'
 
 const defaultTagPrefix = 'tag:yaml.org,2002:'
 
-export interface CreateNodeAliasRef {
-  node: Node | undefined
-  value: unknown
-}
-
 export interface CreateNodeContext {
   keepUndefined?: boolean
-  onAlias(source: CreateNodeAliasRef): Alias
+  onAnchor(source: unknown): string
   onTagObj?: (tagObj: ScalarTag | CollectionTag) => void
-  prevObjects: Map<unknown, CreateNodeAliasRef>
+  sourceObjects: Map<unknown, { anchor: string | null; node: Node | null }>
   replacer?: Replacer
   schema: Schema
 }
@@ -57,7 +52,22 @@ export function createNode(
     value = value.valueOf()
   }
 
-  const { onAlias, onTagObj, prevObjects, schema } = ctx
+  const { onAnchor, onTagObj, schema, sourceObjects } = ctx
+
+  // Detect duplicate references to the same object & use Alias nodes for all
+  // after first. The `ref` wrapper allows for circular references to resolve.
+  let ref: { anchor: string | null; node: Node | null } | undefined = undefined
+  if (value && typeof value === 'object') {
+    ref = sourceObjects.get(value)
+    if (ref) {
+      if (!ref.anchor) ref.anchor = onAnchor(value)
+      return new Alias(ref.anchor)
+    } else {
+      ref = { anchor: null, node: null }
+      sourceObjects.set(value, ref)
+    }
+  }
+
   if (tagName && tagName.startsWith('!!'))
     tagName = defaultTagPrefix + tagName.slice(2)
 
@@ -78,21 +88,11 @@ export function createNode(
     delete ctx.onTagObj
   }
 
-  // Detect duplicate references to the same object & use Alias nodes for all
-  // after first. The `ref` wrapper allows for circular references to resolve.
-  const ref: CreateNodeAliasRef = { value: undefined, node: undefined }
-  if (value && typeof value === 'object') {
-    const prev = prevObjects.get(value)
-    if (prev) return onAlias(prev)
-    ref.value = value
-    prevObjects.set(value, ref)
-  }
-
   const node = tagObj?.createNode
     ? tagObj.createNode(ctx.schema, value, ctx)
     : new Scalar(value)
   if (tagName) node.tag = tagName
-  ref.node = node
 
+  if (ref) ref.node = node
   return node
 }
diff --git a/src/index.ts b/src/index.ts
index 92111ca..dbb9c04 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -26,7 +26,8 @@ export { YAMLSeq } from './nodes/YAMLSeq.js'
 export {
   CreateNodeOptions,
   defaultOptions,
-  Options,
+  DocumentOptions,
+  ParseOptions,
   SchemaOptions,
   ToJSOptions,
   ToStringOptions
diff --git a/src/nodes/Alias.ts b/src/nodes/Alias.ts
index 130b9ba..9814f95 100644
--- a/src/nodes/Alias.ts
+++ b/src/nodes/Alias.ts
@@ -1,6 +1,12 @@
-import { StringifyContext } from '../stringify/stringify.js'
+import { anchorIsValid } from '../doc/anchors'
+import type { Document } from '../doc/Document'
+import type { StringifyContext } from '../stringify/stringify.js'
+import { visit } from '../visit'
 import { ALIAS, isAlias, isCollection, isPair, Node, NodeBase } from './Node.js'
-import { toJS, ToJSContext } from './toJS.js'
+import type { Scalar } from './Scalar'
+import type { ToJSContext } from './toJS.js'
+import type { YAMLMap } from './YAMLMap'
+import type { YAMLSeq } from './YAMLSeq'
 
 export declare namespace Alias {
   interface Parsed extends Alias {
@@ -8,10 +14,12 @@ export declare namespace Alias {
   }
 }
 
-export class Alias<T extends Node = Node> extends NodeBase {
-  source: T
+export class Alias extends NodeBase {
+  source: string
 
-  constructor(source: T) {
+  declare anchor?: never
+
+  constructor(source: string) {
     super(ALIAS)
     this.source = source
     Object.defineProperty(this, 'tag', {
@@ -21,61 +29,84 @@ export class Alias<T extends Node = Node> extends NodeBase {
     })
   }
 
-  toJSON(arg?: unknown, ctx?: ToJSContext) {
-    if (!ctx)
-      return toJS(this.source, typeof arg === 'string' ? arg : null, ctx)
-    const { anchors, maxAliasCount } = ctx
-    const anchor = anchors && anchors.get(this.source)
+  /**
+   * Resolve the value of this alias within `doc`, finding the last
+   * instance of the `source` anchor before this node.
+   */
+  resolve(doc: Document): Scalar | YAMLMap | YAMLSeq | undefined {
+    let found: Scalar | YAMLMap | YAMLSeq | undefined = undefined
+    const find = (_key: unknown, node: Node) => {
+      if (node === this) return visit.BREAK
+      if (node.anchor === this.source) found = node
+    }
+    visit(doc, { Alias: find, Scalar: find, Map: find, Seq: find })
+    return found
+  }
+
+  toJSON(_arg?: unknown, ctx?: ToJSContext) {
+    if (!ctx) return { source: this.source }
+    const { anchors, doc, maxAliasCount } = ctx
+    const source = this.resolve(doc)
+    if (!source) {
+      const msg = `Unresolved alias (the anchor must be set before the alias): ${this.source}`
+      throw new ReferenceError(msg)
+    }
+    const data = anchors.get(source)
     /* istanbul ignore if */
-    if (!anchor || anchor.res === undefined) {
+    if (!data || data.res === undefined) {
       const msg = 'This should not happen: Alias anchor was not resolved?'
       throw new ReferenceError(msg)
     }
     if (maxAliasCount >= 0) {
-      anchor.count += 1
-      if (anchor.aliasCount === 0)
-        anchor.aliasCount = getAliasCount(this.source, anchors)
-      if (anchor.count * anchor.aliasCount > maxAliasCount) {
+      data.count += 1
+      if (data.aliasCount === 0)
+        data.aliasCount = getAliasCount(doc, source, anchors)
+      if (data.count * data.aliasCount > maxAliasCount) {
         const msg =
           'Excessive alias count indicates a resource exhaustion attack'
         throw new ReferenceError(msg)
       }
     }
-    return anchor.res
+    return data.res
   }
 
-  // Only called when stringifying an alias mapping key while constructing
-  // Object output.
   toString(
-    { anchors, doc, implicitKey, inStringifyKey }: StringifyContext,
+    ctx?: StringifyContext,
     _onComment?: () => void,
     _onChompKeep?: () => void
   ) {
-    let anchor = Object.keys(anchors).find(a => anchors[a] === this.source)
-    if (!anchor && inStringifyKey)
-      anchor = doc.anchors.getName(this.source) || doc.anchors.newName()
-    if (anchor) return `*${anchor}${implicitKey ? ' ' : ''}`
-    const msg = doc.anchors.getName(this.source)
-      ? 'Alias node must be after source node'
-      : 'Source node not found for alias node'
-    throw new Error(`${msg} [${this.range}]`)
+    const src = `*${this.source}`
+    if (ctx) {
+      anchorIsValid(this.source)
+      if (ctx.options.verifyAliasOrder && !ctx.anchors.has(this.source)) {
+        const msg = `Unresolved alias (the anchor must be set before the alias): ${this.source}`
+        throw new Error(msg)
+      }
+      if (ctx.implicitKey) return `${src} `
+    }
+    return src
   }
 }
 
-function getAliasCount(node: unknown, anchors: ToJSContext['anchors']): number {
+function getAliasCount(
+  doc: Document,
+  node: unknown,
+  anchors: ToJSContext['anchors']
+): number {
   if (isAlias(node)) {
-    const anchor = anchors && anchors.get(node.source)
+    const source = node.resolve(doc)
+    const anchor = anchors && source && anchors.get(source)
     return anchor ? anchor.count * anchor.aliasCount : 0
   } else if (isCollection(node)) {
     let count = 0
     for (const item of node.items) {
-      const c = getAliasCount(item, anchors)
+      const c = getAliasCount(doc, item, anchors)
       if (c > count) count = c
     }
     return count
   } else if (isPair(node)) {
-    const kc = getAliasCount(node.key, anchors)
-    const vc = getAliasCount(node.value, anchors)
+    const kc = getAliasCount(doc, node.key, anchors)
+    const vc = getAliasCount(doc, node.value, anchors)
     return Math.max(kc, vc)
   }
   return 1
diff --git a/src/nodes/Collection.ts b/src/nodes/Collection.ts
index 7ce5389..b40547e 100644
--- a/src/nodes/Collection.ts
+++ b/src/nodes/Collection.ts
@@ -27,11 +27,11 @@ export function collectionFromPath(
     }
   }
   return createNode(v, undefined, {
-    onAlias() {
+    onAnchor() {
       throw new Error('Repeated objects are not supported here')
     },
-    prevObjects: new Map(),
-    schema
+    schema,
+    sourceObjects: new Map()
   })
 }
 
@@ -49,6 +49,9 @@ export abstract class Collection extends NodeBase {
 
   declare items: unknown[]
 
+  /** An optional anchor on this node. Used by alias nodes. */
+  declare anchor?: string
+
   /**
    * If true, stringify this and all child nodes using flow rather than
    * block styles.
diff --git a/src/nodes/Node.ts b/src/nodes/Node.ts
index e5d72a5..e47d367 100644
--- a/src/nodes/Node.ts
+++ b/src/nodes/Node.ts
@@ -62,6 +62,9 @@ export function isNode(node: any): node is Node {
   return false
 }
 
+export const hasAnchor = (node: unknown): node is Scalar | YAMLMap | YAMLSeq =>
+  (isScalar(node) || isCollection(node)) && !!node.anchor
+
 export abstract class NodeBase {
   readonly [NODE_TYPE]: symbol
 
diff --git a/src/nodes/Scalar.ts b/src/nodes/Scalar.ts
index e731c30..1f97801 100644
--- a/src/nodes/Scalar.ts
+++ b/src/nodes/Scalar.ts
@@ -28,7 +28,8 @@ export class Scalar<T = unknown> extends NodeBase {
 
   value: T
 
-  declare type?: Scalar.Type
+  /** An optional anchor on this node. Used by alias nodes. */
+  declare anchor?: string
 
   /**
    * By default (undefined), numbers use decimal notation.
@@ -43,6 +44,9 @@ export class Scalar<T = unknown> extends NodeBase {
   /** Set during parsing to the source string value */
   declare source?: string
 
+  /** The scalar style used for the node's string representation */
+  declare type?: Scalar.Type
+
   constructor(value: T) {
     super(SCALAR)
     this.value = value
diff --git a/src/nodes/addPairToJSMap.ts b/src/nodes/addPairToJSMap.ts
index c2a25a0..ed6ccee 100644
--- a/src/nodes/addPairToJSMap.ts
+++ b/src/nodes/addPairToJSMap.ts
@@ -63,9 +63,9 @@ function mergeToJSMap(
     | Record<string | number | symbol, unknown>,
   value: unknown
 ) {
-  if (!isAlias(value) || !isMap(value.source))
-    throw new Error('Merge sources must be map aliases')
-  const srcMap = value.source.toJSON(null, ctx, Map)
+  const source = ctx && isAlias(value) ? value.resolve(ctx.doc) : null
+  if (!isMap(source)) throw new Error('Merge sources must be map aliases')
+  const srcMap = source.toJSON(null, ctx, Map)
   for (const [key, value] of srcMap) {
     if (map instanceof Map) {
       if (!map.has(key)) map.set(key, value)
@@ -92,6 +92,9 @@ function stringifyKey(
   if (typeof jsKey !== 'object') return String(jsKey)
   if (isNode(key) && ctx && ctx.doc) {
     const strCtx = createStringifyContext(ctx.doc, {})
+    strCtx.anchors = new Set()
+    for (const node of ctx.anchors.keys())
+      strCtx.anchors.add(node.anchor as 'string')
     strCtx.inFlow = true
     strCtx.inStringifyKey = true
     const strKey = key.toString(strCtx)
diff --git a/src/nodes/toJS.ts b/src/nodes/toJS.ts
index ce368d1..c519b15 100644
--- a/src/nodes/toJS.ts
+++ b/src/nodes/toJS.ts
@@ -1,16 +1,15 @@
 import type { Document } from '../doc/Document.js'
 import type { stringify } from '../stringify/stringify.js'
-import type { Node } from './Node.js'
+import { hasAnchor, Node } from './Node.js'
 
-export interface ToJSAnchorValue {
-  alias: string[]
+export interface AnchorData {
   aliasCount: number
   count: number
-  res?: unknown
+  res: unknown
 }
 
 export interface ToJSContext {
-  anchors: Map<Node, ToJSAnchorValue> | null
+  anchors: Map<Node, AnchorData>
   doc: Document
   keep: boolean
   mapAsMap: boolean
@@ -35,17 +34,17 @@ export interface ToJSContext {
 export function toJS(value: any, arg: string | null, ctx?: ToJSContext): any {
   if (Array.isArray(value)) return value.map((v, i) => toJS(v, String(i), ctx))
   if (value && typeof value.toJSON === 'function') {
-    if (!ctx) return value.toJSON(arg)
-    const anchor = ctx.anchors && ctx.anchors.get(value)
-    if (anchor)
-      ctx.onCreate = res => {
-        anchor.res = res
-        delete ctx.onCreate
-      }
+    if (!ctx || !hasAnchor(value)) return value.toJSON(arg, ctx)
+    const data: AnchorData = { aliasCount: 0, count: 1, res: undefined }
+    ctx.anchors.set(value, data)
+    ctx.onCreate = res => {
+      data.res = res
+      delete ctx.onCreate
+    }
     const res = value.toJSON(arg, ctx)
-    if (anchor && ctx.onCreate) ctx.onCreate(res)
+    if (ctx.onCreate) ctx.onCreate(res)
     return res
   }
-  if (!(ctx && ctx.keep) && typeof value === 'bigint') return Number(value)
+  if (typeof value === 'bigint' && !(ctx && ctx.keep)) return Number(value)
   return value
 }
diff --git a/src/options.ts b/src/options.ts
index c24ce1e..d30de96 100644
--- a/src/options.ts
+++ b/src/options.ts
@@ -1,6 +1,5 @@
 import type { Reviver } from './doc/applyReviver.js'
 import type { Directives } from './doc/directives.js'
-import type { Replacer } from './doc/Document.js'
 import type { LogLevelId } from './log.js'
 import type { Pair } from './nodes/Pair.js'
 import type { Scalar } from './nodes/Scalar.js'
@@ -42,27 +41,12 @@ export type ParseOptions = {
 }
 
 export type DocumentOptions = {
-  /**
-   * Default prefix for anchors.
-   *
-   * Default: `'a'`, resulting in anchors `a1`, `a2`, etc.
-   */
-  anchorPrefix?: string
-
   /**
    * Used internally by Composer. If set and includes an explicit version,
    * that overrides the `version` option.
    */
   directives?: Directives
 
-  /**
-   * Keep `undefined` object values when creating mappings and return a Scalar
-   * node when calling `YAML.stringify(undefined)`, rather than `undefined`.
-   *
-   * Default: `false`
-   */
-  keepUndefined?: boolean
-
   /**
    * Control the logging level during parsing
    *
@@ -119,6 +103,13 @@ export type SchemaOptions = {
 }
 
 export type CreateNodeOptions = {
+  /**
+   * Default prefix for anchors.
+   *
+   * Default: `'a'`, resulting in anchors `a1`, `a2`, etc.
+   */
+  anchorPrefix?: string
+
   /** Force the top-level collection node to use flow style. */
   flow?: boolean
 
@@ -133,15 +124,8 @@ export type CreateNodeOptions = {
   onTagObj?: (tagObj: ScalarTag | CollectionTag) => void
 
   /**
-   * Filter or modify values while creating a node.
-   *
-   * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter
-   */
-  replacer?: Replacer
-
-  /**
-   * Specify the collection type, e.g. `"!!omap"`. Note that this requires the
-   * corresponding tag to be available in this document's schema.
+   * Specify the top-level collection type, e.g. `"!!omap"`. Note that this
+   * requires the corresponding tag to be available in this document's schema.
    */
   tag?: string
 }
@@ -295,9 +279,16 @@ export type ToStringOptions = {
    * Default: `'true'`
    */
   trueStr?: string
-}
 
-export type Options = ParseOptions & DocumentOptions & SchemaOptions
+  /**
+   * The anchor used by an alias must be defined before the alias node. As it's
+   * possible for the document to be modified manually, the order may be
+   * verified during stringification.
+   *
+   * Default: `'true'`
+   */
+  verifyAliasOrder?: boolean
+}
 
 /**
  * `yaml` defines document-specific options in three places: as an argument of
@@ -309,9 +300,7 @@ export type Options = ParseOptions & DocumentOptions & SchemaOptions
 export const defaultOptions: Required<
   Omit<ParseOptions, 'lineCounter'> & Omit<DocumentOptions, 'directives'>
 > = {
-  anchorPrefix: 'a',
   intAsBigInt: false,
-  keepUndefined: false,
   logLevel: 'warn',
   prettyErrors: true,
   strict: true,
diff --git a/src/public-api.ts b/src/public-api.ts
index 1e6d8d1..946cee8 100644
--- a/src/public-api.ts
+++ b/src/public-api.ts
@@ -4,7 +4,14 @@ import { Document, Replacer } from './doc/Document.js'
 import { prettifyError, YAMLParseError } from './errors.js'
 import { warn } from './log.js'
 import type { ParsedNode } from './nodes/Node.js'
-import type { Options, ToJSOptions, ToStringOptions } from './options.js'
+import type {
+  CreateNodeOptions,
+  DocumentOptions,
+  ParseOptions,
+  SchemaOptions,
+  ToJSOptions,
+  ToStringOptions
+} from './options.js'
 import { LineCounter } from './parse/line-counter.js'
 import { Parser } from './parse/parser.js'
 
@@ -14,7 +21,7 @@ export interface EmptyStream
   empty: true
 }
 
-function parseOptions(options: Options | undefined) {
+function parseOptions(options: ParseOptions | undefined) {
   const prettyErrors = !options || options.prettyErrors !== false
   const lineCounter =
     (options && options.lineCounter) ||
@@ -34,7 +41,7 @@ function parseOptions(options: Options | undefined) {
  */
 export function parseAllDocuments<T extends ParsedNode = ParsedNode>(
   source: string,
-  options?: Options
+  options?: ParseOptions & DocumentOptions & SchemaOptions
 ): Document.Parsed<T>[] | EmptyStream {
   const { lineCounter, prettyErrors } = parseOptions(options)
 
@@ -64,7 +71,7 @@ export function parseAllDocuments<T extends ParsedNode = ParsedNode>(
 /** Parse an input string into a single YAML.Document */
 export function parseDocument<T extends ParsedNode = ParsedNode>(
   source: string,
-  options?: Options
+  options?: ParseOptions & DocumentOptions & SchemaOptions
 ) {
   const { lineCounter, prettyErrors } = parseOptions(options)
 
@@ -102,17 +109,22 @@ export function parseDocument<T extends ParsedNode = ParsedNode>(
  *   document, so Maps become objects, Sequences arrays, and scalars result in
  *   nulls, booleans, numbers and strings.
  */
-export function parse(src: string, options?: Options & ToJSOptions): any
+export function parse(
+  src: string,
+  options?: ParseOptions & DocumentOptions & SchemaOptions & ToJSOptions
+): any
 export function parse(
   src: string,
   reviver: Reviver,
-  options?: Options & ToJSOptions
+  options?: ParseOptions & DocumentOptions & SchemaOptions & ToJSOptions
 ): any
 
 export function parse(
   src: string,
-  reviver?: Reviver | (Options & ToJSOptions),
-  options?: Options & ToJSOptions
+  reviver?:
+    | Reviver
+    | (ParseOptions & DocumentOptions & SchemaOptions & ToJSOptions),
+  options?: ParseOptions & DocumentOptions & SchemaOptions & ToJSOptions
 ) {
   let _reviver: Reviver | undefined = undefined
   if (typeof reviver === 'function') {
@@ -139,17 +151,43 @@ export function parse(
  */
 export function stringify(
   value: any,
-  options?: Options & ToStringOptions
+  options?: DocumentOptions &
+    SchemaOptions &
+    ParseOptions &
+    CreateNodeOptions &
+    ToStringOptions
 ): string
 export function stringify(
   value: any,
   replacer?: Replacer | null,
-  options?: string | number | (Options & ToStringOptions)
+  options?:
+    | string
+    | number
+    | (DocumentOptions &
+        SchemaOptions &
+        ParseOptions &
+        CreateNodeOptions &
+        ToStringOptions)
 ): string
+
 export function stringify(
   value: any,
-  replacer?: Replacer | (Options & ToStringOptions) | null,
-  options?: string | number | (Options & ToStringOptions)
+  replacer?:
+    | Replacer
+    | (DocumentOptions &
+        SchemaOptions &
+        ParseOptions &
+        CreateNodeOptions &
+        ToStringOptions)
+    | null,
+  options?:
+    | string
+    | number
+    | (DocumentOptions &
+        SchemaOptions &
+        ParseOptions &
+        CreateNodeOptions &
+        ToStringOptions)
 ) {
   let _replacer: Replacer | null = null
   if (typeof replacer === 'function' || Array.isArray(replacer)) {
@@ -164,8 +202,7 @@ export function stringify(
     options = indent < 1 ? undefined : indent > 8 ? { indent: 8 } : { indent }
   }
   if (value === undefined) {
-    const { keepUndefined } =
-      options || (replacer as Options & ToStringOptions) || {}
+    const { keepUndefined } = options || (replacer as CreateNodeOptions) || {}
     if (!keepUndefined) return undefined
   }
   return new Document(value, _replacer, options).toString(options)
diff --git a/src/stringify/stringify.ts b/src/stringify/stringify.ts
index a23d621..8a96827 100644
--- a/src/stringify/stringify.ts
+++ b/src/stringify/stringify.ts
@@ -1,5 +1,13 @@
+import { anchorIsValid } from '../doc/anchors.js'
 import type { Document } from '../doc/Document.js'
-import { isAlias, isNode, isPair, isScalar, Node } from '../nodes/Node.js'
+import {
+  isAlias,
+  isCollection,
+  isNode,
+  isPair,
+  isScalar,
+  Node
+} from '../nodes/Node.js'
 import type { Scalar } from '../nodes/Scalar.js'
 import type { ToStringOptions } from '../options.js'
 import type { CollectionTag, ScalarTag } from '../schema/types.js'
@@ -8,7 +16,7 @@ import { stringifyString } from './stringifyString.js'
 export type StringifyContext = {
   actualString?: boolean
   allNullValues?: boolean
-  anchors: Record<string, Node>
+  anchors: Set<string>
   doc: Document
   forceBlockIndent?: boolean
   implicitKey?: boolean
@@ -24,7 +32,7 @@ export const createStringifyContext = (
   doc: Document,
   options: ToStringOptions
 ): StringifyContext => ({
-  anchors: Object.create(null),
+  anchors: new Set(),
   doc,
   indent: '',
   indentStep:
@@ -43,7 +51,8 @@ export const createStringifyContext = (
       nullStr: 'null',
       simpleKeys: false,
       singleQuote: false,
-      trueStr: 'true'
+      trueStr: 'true',
+      verifyAliasOrder: true
     },
     options
   )
@@ -83,9 +92,9 @@ function stringifyProps(
   { anchors, doc }: StringifyContext
 ) {
   const props = []
-  const anchor = doc.anchors.getName(node)
-  if (anchor) {
-    anchors[anchor] = node
+  const anchor = (isScalar(node) || isCollection(node)) && node.anchor
+  if (anchor && anchorIsValid(anchor)) {
+    anchors.add(anchor)
     props.push(`&${anchor}`)
   }
   if (node.tag) {
diff --git a/src/stringify/stringifyPair.ts b/src/stringify/stringifyPair.ts
index 1c9d7c9..49b14a4 100644
--- a/src/stringify/stringifyPair.ts
+++ b/src/stringify/stringifyPair.ts
@@ -105,7 +105,7 @@ export function stringifyPair(
     isSeq(value) &&
     !value.flow &&
     !value.tag &&
-    !doc.anchors.getName(value)
+    !value.anchor
   ) {
     // If indentSeq === false, consider '- ' as part of indentation where possible
     ctx.indent = ctx.indent.substr(2)
diff --git a/src/util.ts b/src/util.ts
index 3bbcf56..6ba8320 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -1,6 +1,6 @@
 export { debug, LogLevelId, warn } from './log.js'
 export { findPair } from './nodes/YAMLMap.js'
-export { toJS, ToJSAnchorValue, ToJSContext } from './nodes/toJS.js'
+export { toJS, ToJSContext } from './nodes/toJS.js'
 export { foldFlowLines } from './stringify/foldFlowLines'
 export { stringifyNumber } from './stringify/stringifyNumber.js'
 export { stringifyString } from './stringify/stringifyString.js'


Test patch:
diff --git a/src/test-events.ts b/src/test-events.ts
index df816a3..49f8c38 100644
--- a/src/test-events.ts
+++ b/src/test-events.ts
@@ -1,16 +1,19 @@
 import { Document } from './doc/Document.js'
 import {
   isAlias,
+  isCollection,
   isMap,
   isNode,
   isPair,
+  isScalar,
   isSeq,
+  Node,
   ParsedNode
 } from './nodes/Node.js'
-import { Pair } from './nodes/Pair.js'
-import { Scalar } from './nodes/Scalar.js'
-import type { Options } from './options.js'
+import type { Pair } from './nodes/Pair.js'
+import type { Scalar } from './nodes/Scalar.js'
 import { parseAllDocuments } from './public-api.js'
+import { visit } from './visit.js'
 
 const scalarChar: Record<string, string> = {
   BLOCK_FOLDED: '>',
@@ -20,10 +23,21 @@ const scalarChar: Record<string, string> = {
   QUOTE_SINGLE: "'"
 }
 
+function anchorExists(doc: Document, anchor: string): boolean {
+  let found = false
+  const find = (_key: unknown, node: Node) => {
+    if (node.anchor === anchor) {
+      found = true
+      return visit.BREAK
+    }
+  }
+  visit(doc, { Scalar: find, Map: find, Seq: find })
+  return found
+}
+
 // test harness for yaml-test-suite event tests
-export function testEvents(src: string, options?: Options) {
-  const opt = Object.assign({ version: '1.2' }, options)
-  const docs = parseAllDocuments(src, opt)
+export function testEvents(src: string) {
+  const docs = parseAllDocuments(src)
   const errDoc = docs.find(doc => doc.errors.length > 0)
   const error = errDoc ? errDoc.errors[0].message : null
   const events = ['+STR']
@@ -70,11 +84,11 @@ function addEvents(
   if (errPos !== -1 && isNode(node) && node.range[0] >= errPos)
     throw new Error()
   let props = ''
-  let anchor = isNode(node) ? doc.anchors.getName(node) : undefined
+  let anchor = isScalar(node) || isCollection(node) ? node.anchor : undefined
   if (anchor) {
     if (/\d$/.test(anchor)) {
       const alt = anchor.replace(/\d$/, '')
-      if (doc.anchors.getNode(alt)) anchor = alt
+      if (anchorExists(doc, alt)) anchor = alt
     }
     props = ` &${anchor}`
   }
@@ -99,10 +113,10 @@ function addEvents(
     addEvents(events, doc, errPos, node.value)
     events.push('-MAP')
   } else if (isAlias(node)) {
-    let alias = doc.anchors.getName(node.source)
+    let alias = node.source
     if (alias && /\d$/.test(alias)) {
       const alt = alias.replace(/\d$/, '')
-      if (doc.anchors.getNode(alt)) alias = alt
+      if (anchorExists(doc, alt)) alias = alt
     }
     events.push(`=ALI${props} *${alias}`)
   } else {
diff --git a/tests/doc/anchors.js b/tests/doc/anchors.js
index 729ee75..dcc0487 100644
--- a/tests/doc/anchors.js
+++ b/tests/doc/anchors.js
@@ -4,9 +4,10 @@ test('basic', () => {
   const src = `- &a 1\n- *a\n`
   const doc = YAML.parseDocument(src)
   expect(doc.errors).toHaveLength(0)
-  const { items } = doc.contents
-  expect(items).toMatchObject([{ value: 1 }, { source: { value: 1 } }])
-  expect(items[1].source).toBe(items[0])
+  expect(doc.contents.items).toMatchObject([
+    { anchor: 'a', value: 1 },
+    { source: 'a' }
+  ])
   expect(String(doc)).toBe(src)
 })
 
@@ -14,14 +15,13 @@ test('re-defined anchor', () => {
   const src = '- &a 1\n- &a 2\n- *a\n'
   const doc = YAML.parseDocument(src)
   expect(doc.errors).toHaveLength(0)
-  const { items } = doc.contents
-  expect(items).toMatchObject([
-    { value: 1 },
-    { value: 2 },
-    { source: { value: 2 } }
+  expect(doc.contents.items).toMatchObject([
+    { anchor: 'a', value: 1 },
+    { anchor: 'a', value: 2 },
+    { source: 'a' }
   ])
-  expect(items[2].source).toBe(items[1])
-  expect(String(doc)).toBe('- &a1 1\n- &a 2\n- *a\n')
+  expect(doc.toJS()).toMatchObject([1, 2, 2])
+  expect(String(doc)).toBe('- &a 1\n- &a 2\n- *a\n')
 })
 
 test('circular reference', () => {
@@ -29,58 +29,80 @@ test('circular reference', () => {
   const doc = YAML.parseDocument(src)
   expect(doc.errors).toHaveLength(0)
   expect(doc.warnings).toHaveLength(0)
-  const { items } = doc.contents
-  expect(items).toHaveLength(2)
-  expect(items[1].source).toBe(doc.contents)
+  expect(doc.contents).toMatchObject({
+    anchor: 'a',
+    items: [{ value: 1 }, { source: 'a' }]
+  })
   const res = doc.toJS()
   expect(res[1]).toBe(res)
   expect(String(doc)).toBe(src)
 })
 
+describe('anchor on tagged collection', () => {
+  test('!!set', () => {
+    const res = YAML.parse('- &a !!set { 1, 2 }\n- *a\n')
+    expect(Array.from(res[0])).toMatchObject([1, 2])
+    expect(res[1]).toBe(res[0])
+  })
+
+  test('!!omap', () => {
+    const res = YAML.parse('- &a !!omap [ 1: 1, 2: 2 ]\n- *a\n')
+    expect(Array.from(res[0])).toMatchObject([
+      [1, 1],
+      [2, 2]
+    ])
+    expect(res[1]).toBe(res[0])
+  })
+})
+
 describe('create', () => {
-  test('doc.anchors.setAnchor', () => {
+  test('node.anchor', () => {
     const doc = YAML.parseDocument('[{ a: A }, { b: B }]')
-    const [a, b] = doc.contents.items
-    expect(doc.anchors.setAnchor(null, null)).toBe(null)
-    expect(doc.anchors.setAnchor(a, 'XX')).toBe('XX')
-    expect(doc.anchors.setAnchor(a, 'AA')).toBe('AA')
-    expect(doc.anchors.setAnchor(a, 'AA')).toBe('AA')
-    expect(doc.anchors.setAnchor(a)).toBe('AA')
-    expect(doc.anchors.setAnchor(a.items[0].value)).toBe('a1')
-    expect(doc.anchors.setAnchor(b.items[0].value)).toBe('a2')
-    expect(doc.anchors.setAnchor(null, 'a1')).toBe('a1')
-    expect(doc.anchors.getName(a)).toBe('AA')
-    expect(doc.anchors.getNode('a2').value).toBe('B')
-    expect(String(doc)).toBe('[ &AA { a: A }, { b: &a2 B } ]\n')
-    expect(() => doc.anchors.setAnchor(a.items[0])).toThrow(
-      'Anchors may only be set for Scalar, Seq and Map nodes'
-    )
-    expect(() => doc.anchors.setAnchor(a, 'A A')).toThrow(
-      'Anchor names must not contain whitespace or control characters'
-    )
-    expect(doc.anchors.setAnchor(a.items[0].value, 'AA')).toBe('AA')
-    expect(String(doc)).toBe('[ &AA1 { a: &AA A }, { b: &a2 B } ]\n')
-    expect(doc.anchors.getNames()).toMatchObject(['AA', 'a2', 'AA1'])
+    doc.get(0).anchor = 'AA'
+    doc.getIn([0, 'a'], true).anchor = 'a'
+    doc.getIn([0, 'a'], true).anchor = ''
+    doc.getIn([1, 'b'], true).anchor = 'BB'
+    expect(String(doc)).toBe('[ &AA { a: A }, { b: &BB B } ]\n')
   })
 
-  test('doc.anchors.createAlias', () => {
+  test('doc.createAlias', () => {
     const doc = YAML.parseDocument('[{ a: A }, { b: B }]')
-    const alias = doc.anchors.createAlias(doc.contents.items[0], 'AA')
+    const alias = doc.createAlias(doc.get(0), 'AA')
     doc.contents.items.push(alias)
     expect(doc.toJS()).toMatchObject([{ a: 'A' }, { b: 'B' }, { a: 'A' }])
-    expect(String(doc)).toMatch('[ &AA { a: A }, { b: B }, *AA ]\n')
+    const alias2 = doc.createAlias(doc.get(1), 'AA')
+    expect(doc.get(1).anchor).toBe('AA1')
+    expect(alias2.source).toBe('AA1')
+    expect(String(doc)).toMatch('[ &AA { a: A }, &AA1 { b: B }, *AA ]\n')
   })
+})
 
-  test('errors', () => {
+describe('errors', () => {
+  test('invalid anchor characters', () => {
+    const doc = YAML.parseDocument('[{ a: A }, { b: B }]')
+    doc.get(0).anchor = 'A A'
+    expect(() => String(doc)).toThrow(
+      'Anchor must not contain whitespace or control characters: "A A"'
+    )
+  })
+
+  test('set tag on alias', () => {
     const doc = YAML.parseDocument('[{ a: A }, { b: B }]')
     const node = doc.contents.items[0]
-    const alias = doc.anchors.createAlias(node, 'AA')
-    doc.contents.items.unshift(alias)
-    expect(() => String(doc)).toThrow('Alias node must be after source node')
+    const alias = doc.createAlias(node, 'AA')
     expect(() => {
       alias.tag = 'tag:yaml.org,2002:alias'
     }).toThrow('Alias nodes cannot have tags')
   })
+
+  test('alias before anchor', () => {
+    const doc = YAML.parseDocument('[{ a: A }, { b: B }]')
+    const alias = doc.createAlias(doc.get(0), 'AA')
+    doc.contents.items.unshift(alias)
+    expect(() => String(doc)).toThrow(
+      'Unresolved alias (the anchor must be set before the alias): AA'
+    )
+  })
 })
 
 describe('__proto__ as anchor name', () => {
@@ -88,15 +110,17 @@ describe('__proto__ as anchor name', () => {
     const src = `- &__proto__ 1\n- *__proto__\n`
     const doc = YAML.parseDocument(src)
     expect(doc.errors).toHaveLength(0)
-    const { items } = doc.contents
-    expect(items).toMatchObject([{ value: 1 }, { source: { value: 1 } }])
-    expect(items[1].source).toBe(items[0])
+    expect(doc.contents.items).toMatchObject([
+      { anchor: '__proto__', value: 1 },
+      { source: '__proto__' }
+    ])
+    expect(doc.toJS()).toMatchObject([1, 1])
     expect(String(doc)).toBe(src)
   })
 
   test('create/stringify', () => {
     const doc = YAML.parseDocument('[{ a: A }, { b: B }]')
-    const alias = doc.anchors.createAlias(doc.contents.items[0], '__proto__')
+    const alias = doc.createAlias(doc.contents.items[0], '__proto__')
     doc.contents.items.push(alias)
     expect(doc.toJSON()).toMatchObject([{ a: 'A' }, { b: 'B' }, { a: 'A' }])
     expect(String(doc)).toMatch(
@@ -150,31 +174,22 @@ describe('merge <<', () => {
     }
   })
 
-  test('YAML.parseAllDocuments', () => {
+  test('YAML.parseDocument', () => {
     const doc = YAML.parseDocument(src, { merge: true })
-    expect(doc.contents.items).toHaveLength(8)
-    expect(Object.keys(doc.anchors.map)).toMatchObject([
-      'CENTER',
-      'LEFT',
-      'BIG',
-      'SMALL'
+    expect(
+      doc.contents.items.slice(5).map(it => it.items[0].value)
+    ).toMatchObject([
+      { source: 'CENTER' },
+      { items: [{ source: 'CENTER' }, { source: 'BIG' }] },
+      { items: [{ source: 'BIG' }, { source: 'LEFT' }, { source: 'SMALL' }] }
     ])
-    doc.contents.items.slice(5).forEach(({ items }) => {
-      const merge = items[0]
-      expect(YAML.isPair(merge)).toBe(true)
-      if (YAML.isSeq(merge.value))
-        merge.value.items.forEach(({ source }) => {
-          expect(source).toBeInstanceOf(YAML.YAMLMap)
-        })
-      else expect(merge.value.source).toBeInstanceOf(YAML.YAMLMap)
-    })
   })
 
-  describe('doc.anchors.createMergePair', () => {
+  describe('create', () => {
     test('simple case', () => {
       const doc = YAML.parseDocument('[{ a: A }, { b: B }]', { merge: true })
       const [a, b] = doc.contents.items
-      const merge = doc.anchors.createMergePair(a)
+      const merge = doc.createPair('<<', doc.createAlias(a))
       b.items.push(merge)
       expect(doc.toJS()).toMatchObject([{ a: 'A' }, { a: 'A', b: 'B' }])
       expect(String(doc)).toBe('[ &a1 { a: A }, { b: B, <<: *a1 } ]\n')
@@ -183,21 +198,19 @@ describe('merge <<', () => {
     test('merge pair of an alias', () => {
       const doc = YAML.parseDocument('[{ a: A }, { b: B }]', { merge: true })
       const [a, b] = doc.contents.items
-      const alias = doc.anchors.createAlias(a, 'AA')
-      const merge = doc.anchors.createMergePair(alias)
+      const alias = doc.createAlias(a, 'AA')
+      const merge = doc.createPair('<<', alias)
       b.items.push(merge)
       expect(doc.toJS()).toMatchObject([{ a: 'A' }, { a: 'A', b: 'B' }])
       expect(String(doc)).toBe('[ &AA { a: A }, { b: B, <<: *AA } ]\n')
     })
 
     test('require map node', () => {
-      const exp = 'Merge sources must be Map nodes or their Aliases'
       const doc = YAML.parseDocument('[{ a: A }, { b: B }]', { merge: true })
-      const [a] = doc.contents.items
-      const merge = doc.anchors.createMergePair(a)
-      expect(() => doc.anchors.createMergePair(merge)).toThrow(exp)
-      const alias = doc.anchors.createAlias(a.items[0].value)
-      expect(() => doc.anchors.createMergePair(alias)).toThrow(exp)
+      const alias = doc.createAlias(doc.getIn([0, 'a'], true))
+      doc.addIn([1], doc.createPair('<<', alias))
+      expect(String(doc)).toBe('[ { a: &a1 A }, { b: B, <<: *a1 } ]\n')
+      expect(() => doc.toJS()).toThrow('Merge sources must be map aliases')
     })
   })
 
diff --git a/tests/doc/createNode.js b/tests/doc/createNode.js
index 46dd4a7..6989be9 100644
--- a/tests/doc/createNode.js
+++ b/tests/doc/createNode.js
@@ -1,4 +1,5 @@
 import { Document, Scalar, YAMLMap, YAMLSeq } from 'yaml'
+import { source } from '../_utils'
 
 let doc
 beforeEach(() => {
@@ -300,57 +301,64 @@ describe('circular references', () => {
   test('parent at root', () => {
     const map = { foo: 'bar' }
     map.map = map
-    const doc = new Document(null)
-    expect(doc.createNode(map)).toMatchObject({
+    const doc = new Document(map)
+    expect(doc.contents).toMatchObject({
+      anchor: 'a1',
       items: [
         { key: { value: 'foo' }, value: { value: 'bar' } },
         {
           key: { value: 'map' },
-          value: {
-            source: {
-              items: [{ key: { value: 'foo' } }, { key: { value: 'map' } }]
-            }
-          }
+          value: { source: 'a1' }
         }
       ]
     })
-    expect(doc.anchors.map).toMatchObject({
-      a1: { items: [{ key: { value: 'foo' } }, { key: { value: 'map' } }] }
-    })
+    expect(doc.toString()).toBe(source`
+      &a1
+      foo: bar
+      map: *a1
+    `)
   })
 
   test('ancestor at root', () => {
     const baz = {}
     const map = { foo: { bar: { baz } } }
     baz.map = map
-    const doc = new Document(null)
-    const node = doc.createNode(map)
-    expect(node.getIn(['foo', 'bar', 'baz', 'map'])).toMatchObject({
-      source: { items: [{ key: { value: 'foo' } }] }
-    })
-    expect(doc.anchors.map).toMatchObject({
-      a1: { items: [{ key: { value: 'foo' } }] }
+    const doc = new Document(map)
+    expect(doc.getIn(['foo', 'bar', 'baz', 'map'])).toMatchObject({
+      source: 'a1'
     })
+    expect(doc.toString()).toBe(source`
+      &a1
+      foo:
+        bar:
+          baz:
+            map: *a1
+    `)
   })
 
   test('sibling sequences', () => {
     const one = ['one']
     const two = ['two']
     const seq = [one, two, one, one, two]
-    const doc = new Document(null)
-    expect(doc.createNode(seq)).toMatchObject({
+    const doc = new Document(seq)
+    expect(doc.contents).toMatchObject({
       items: [
         { items: [{ value: 'one' }] },
         { items: [{ value: 'two' }] },
-        { source: { items: [{ value: 'one' }] } },
-        { source: { items: [{ value: 'one' }] } },
-        { source: { items: [{ value: 'two' }] } }
+        { source: 'a1' },
+        { source: 'a1' },
+        { source: 'a2' }
       ]
     })
-    expect(doc.anchors.map).toMatchObject({
-      a1: { items: [{ value: 'one' }] },
-      a2: { items: [{ value: 'two' }] }
-    })
+    expect(doc.toString()).toBe(source`
+      - &a1
+        - one
+      - &a2
+        - two
+      - *a1
+      - *a1
+      - *a2
+    `)
   })
 
   test('further relatives', () => {
@@ -363,6 +371,6 @@ describe('circular references', () => {
     expect(source).toMatchObject({
       items: [{ key: { value: 'a' }, value: { value: 1 } }]
     })
-    expect(alias.source).toBe(source)
+    expect(alias.source).toBe(source.anchor)
   })
 })
diff --git a/tests/doc/errors.js b/tests/doc/errors.js
index bc61c81..16f01ac 100644
--- a/tests/doc/errors.js
+++ b/tests/doc/errors.js
@@ -288,7 +288,6 @@ describe('invalid options', () => {
 test('broken document with comment before first node', () => {
   const doc = YAML.parseDocument('#c\n*x\nfoo\n', { prettyErrors: false })
   expect(doc.errors).toMatchObject([
-    { message: 'Aliased anchor not found: x' },
     { message: 'Unexpected scalar at node end' }
   ])
 })
diff --git a/tests/doc/stringify.js b/tests/doc/stringify.js
index 4c6e798..14f988b 100644
--- a/tests/doc/stringify.js
+++ b/tests/doc/stringify.js
@@ -942,10 +942,10 @@ describe('replacer', () => {
   })
 
   test('createNode, !!set', () => {
-    const replacer = jest.fn((key, value) => value)
+    const replacer = jest.fn((_key, value) => value)
     const doc = new YAML.Document(null, { customTags: ['set'] })
     const set = new Set(['a', 'b', 1, [2]])
-    doc.createNode(set, { replacer })
+    doc.createNode(set, replacer)
     expect(replacer.mock.calls).toMatchObject([
       ['', set],
       ['a', 'a'],
@@ -965,13 +965,13 @@ describe('replacer', () => {
   })
 
   test('createNode, !!omap', () => {
-    const replacer = jest.fn((key, value) => value)
+    const replacer = jest.fn((_key, value) => value)
     const doc = new YAML.Document(null, { customTags: ['omap'] })
     const omap = [
       ['a', 1],
       [1, 'a']
     ]
-    doc.createNode(omap, { replacer, tag: '!!omap' })
+    doc.createNode(omap, replacer, { tag: '!!omap' })
     expect(replacer.mock.calls).toMatchObject([
       ['', omap],
       ['0', omap[0]],
diff --git a/tests/doc/types.js b/tests/doc/types.js
index ef12680..fde297f 100644
--- a/tests/doc/types.js
+++ b/tests/doc/types.js
@@ -585,8 +585,8 @@ date (00:00:00Z): 2002-12-14\n`)
     test('explicit creation', () => {
       const src = '- { a: A, b: B }\n- { b: X }\n'
       const doc = YAML.parseDocument(src, { version: '1.1' })
-      const alias = doc.anchors.createAlias(doc.get(0), 'a')
-      doc.addIn([1], doc.anchors.createMergePair(alias))
+      const alias = doc.createAlias(doc.get(0), 'a')
+      doc.addIn([1], doc.createPair('<<', alias))
       expect(doc.toString()).toBe('- &a { a: A, b: B }\n- { b: X, <<: *a }\n')
       expect(doc.toJS()).toMatchObject([
         { a: 'A', b: 'B' },
@@ -597,7 +597,7 @@ date (00:00:00Z): 2002-12-14\n`)
     test('creation by duck typing', () => {
       const src = '- { a: A, b: B }\n- { b: X }\n'
       const doc = YAML.parseDocument(src, { version: '1.1' })
-      const alias = doc.anchors.createAlias(doc.get(0), 'a')
+      const alias = doc.createAlias(doc.get(0), 'a')
       doc.addIn([1], doc.createPair('<<', alias))
       expect(doc.toString()).toBe('- &a { a: A, b: B }\n- { b: X, <<: *a }\n')
       expect(doc.toJS()).toMatchObject([
diff --git a/tests/visit.ts b/tests/visit.ts
index 5083732..a0b7046 100644
--- a/tests/visit.ts
+++ b/tests/visit.ts
@@ -33,8 +33,8 @@ test('Alias', () => {
   visit(doc, { Map: fn, Pair: fn, Seq: fn, Alias: fn, Scalar: fn })
   expect(fn.mock.calls).toMatchObject([
     [null, coll, [{}]],
-    [0, { type: 'PLAIN', value: 1 }, [{}, {}]],
-    [1, { source: { value: 1 } }, [{}, {}]]
+    [0, { type: 'PLAIN', value: 1, anchor: 'a' }, [{}, {}]],
+    [1, { source: 'a' }, [{}, {}]]
   ])
 })