# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [4.0.0] - 2026-08-08

### Added

- **Statement grammar.** `Predicator.parse_program/2` and
  `Predicator.Parser.parse_program/2` parse
  `program := statement (";" statement)* [";"]`, where a statement is either an
  assignment (`location "=" expression`, the left side an identifier optionally
  followed by `.name` and `[key]` accessors) or an ordinary expression. Programs
  parse to `{:program, [statement], position}`, assignments to
  `{:assignment, lhs, rhs, position}`; both carry positions or spans like every
  other node, and both round-trip through `Predicator.decompile/2`. The
  expression grammar is otherwise unchanged, and `Predicator.parse/2` still
  returns a bare expression AST. Compiling and running a program is the
  `store` and `pop` opcodes and `Predicator.execute/2`, below.

- **The `store` and `pop` opcodes; compiling and running a program.** ISA v3
  gains two tier-6 opcodes: `["store", n]` pops a value and the `n` location
  segments beneath it and writes `path -> value` into the evaluator's
  context - the only opcode that does - and `["pop"]` discards the stack top.
  `docs/isa.md` §5 has the full stack discipline and error shapes. **No
  existing instruction list changes meaning**: this is additive on top of a
  retirement (`and`, `or`) that v3 has never shipped, so every instruction
  list valid before this change stays valid and means the same thing after
  it. `Compiler.to_instructions/2` and `to_instructions_with_positions/2` now
  accept a `Parser.program()` as well as an expression AST, compiling
  `{:program, ...}` and `{:assignment, ...}` per `docs/reference/ast.md`'s
  "Statement nodes" section. `Predicator.compile_program/1` and
  `compile_program_with_positions/1` are the program-shaped echoes of
  `compile/1` and `compile_with_positions/1`. `Predicator.execute/1,2,3` is
  the statement-mode entry point: `execute(program_or_source, context \\ %{},
  opts \\ [])` accepts source, an instruction list, or a `%Compiled{}`, and
  returns `{:ok, %Context{}} | {:error, error, %Context{}}`. On error the
  third element is the context as of the last statement that completed
  successfully - prior writes survive a later failure, and whether to keep or
  discard them is the caller's policy, not the engine's: a caller wanting
  all-or-nothing drops the third element and keeps the context it already
  had. `Predicator.Evaluator.run_state/1`, the state-preserving runner
  statement mode needs, is now public. `Predicator.execute/1,2,3` has a
  sibling, `execute_value/1,2,3`, below, for a caller who also wants the
  program's last expression statement's value.

- **`Predicator.execute_value/1,2,3`.** The sibling of `execute/1,2,3` for a
  caller who also wants the program's last expression statement's value:
  it returns `{:ok, value, %Context{}} | {:error, error, %Context{}}`, where
  `value` is that statement's value, or `:undefined` when the program has no
  expression statement (an assignments-only program, for instance).
  `Predicator.execute/1,2,3` is unchanged and still returns
  `{:ok, %Context{}}`. The value comes from `Predicator.Evaluator.last_value/1`,
  a new accessor over a new `last_value` field on `%Evaluator{}` that the
  machine fills in as it runs. No compiled program changes and the ISA does
  not move - the value is retained by the machine, not encoded by the
  compiler, so an instruction list compiled before this change runs
  identically and reports the same value under `execute_value/2`.

- **Source spans.** A point position tells an editor where to put a caret; a
  span tells it what to underline. `Predicator.Parser.parse/2`,
  `Predicator.parse/2`, and `Predicator.evaluate/3` (string input only) take
  `spans: true`, under which every AST node's existing trailing slot carries a
  `t:Predicator.Types.span/0` - `{{start_line, start_col}, {end_line, end_col}}`
  with an **exclusive** end, matching LSP ranges - instead of a
  `{line, column}`. For `a * true` the `arithmetic` node spans the whole
  expression rather than naming column 3. A parenthesized expression's span
  widens to include its parentheses, so `(a + b)` spans `(a + b)` rather than
  `a + b`, and this composes upward: `(a + b) * c` gives the `multiply` node a
  span slicing to the whole source string. Nesting composes to the outermost
  pair, so `((a))` spans `((a))`.

- `t:Predicator.Types.span/0` and `t:Predicator.Types.span_table/0`.

- `Predicator.compile_with_spans/1`, the span-mode sibling of
  `compile_with_positions/1`. Returns `{:ok, %Predicator.Compiled{}}` whose
  `positions` holds a span table mapping each instruction index to a span;
  `instructions` is byte-identical to `compile/1`'s output. Pass the struct
  straight to `evaluate/3`, which threads the table itself.

- **`Predicator.Compiled`**, a two-field struct pairing an instruction list
  (`instructions`) with its source-location table (`positions`), plus a
  doctested `new/2` for a caller who stored the two separately and wants them
  back as one value. Returned by `compile_with_positions/1` and
  `compile_with_spans/1` and accepted directly by `evaluate/3`. It is an
  in-memory Elixir value, not a wire format - see ADR-0009.

- `:span` on `Predicator.Errors.EvaluationError`,
  `Predicator.Errors.TypeMismatchError`, and
  `Predicator.Errors.UndefinedVariableError`, defaulting to `nil`.

- `Predicator.Errors.put_position/2` accepts a span: it sets `:span` to the span
  and `:position` to the span's start, so a caller reading only `:position`
  still gets a usable caret under `spans: true`.

- `Predicator.Evaluator.unbound_loads_with_locations/1`, returning each unbound
  load paired with the source location of the instruction that read it.
  `unbound_loads/1` is unchanged.

- **ISA version stamping.** `Predicator.isa_version/0` returns the integer ISA
  version this build emits and can run, currently `2`, independent of the
  library's semantic version (ADR-0003). `Predicator.Instructions.required_isa/1`
  takes a compiled instruction list and returns the minimum ISA version it
  needs, computed by scanning its opcode names against the table in
  `docs/isa.md`: `{:ok, integer}`, or `{:error, %Predicator.Errors.EvaluationError{}}`
  for an unknown opcode or a malformed element. Together they let a consumer
  holding a stored artifact, or a sibling implementation handed an instruction
  list, refuse it up front instead of failing partway through a run. **No
  instruction changed**: the wire format is still a bare list, no opcode was
  added or altered, and every instruction list valid before this release is
  valid after it.

- `Predicator.Instructions.opcodes/0`, returning the full opcode table -
  every opcode mapped to the ISA version that introduced it and its
  conformance tier - and `Predicator.Instructions.tier/1`, returning a single
  opcode's tier as `{:ok, integer}` or the same `"unknown_opcode"`
  `{:error, %Predicator.Errors.EvaluationError{}}` `required_isa/1` returns.
  Tier is a conformance-corpus grouping (`px-35i.4`), a function of opcode
  only, per `docs/isa.md` section 4.

- **The conformance corpus** (`px-35i.4`). `conformance/` in the repository -
  deliberately not shipped in the hex package, since nothing an application
  does at runtime reads it. The tooling that maintains it is excluded for the
  same reason and is **not public API**: the `mix corpus.*` tasks and the
  `Predicator.Conformance.*` modules they call exist only in a git checkout.
  Authored cases live in `conformance/cases/*.json`;
  the generated, checked-in corpus in `conformance/corpus/tier-*.json` (one
  case per line, sorted by id); and `conformance/manifest.json` (ISA version, a
  `corpus_hash` sha256 over the corpus content, and the tier/opcode/case-count
  table). `mix corpus.generate` regenerates both from the authored cases by
  running each through the real compiler and evaluator; `mix corpus.generate
  --check` regenerates in memory and exits non-zero on drift without writing,
  for CI. `test/predicator/conformance/corpus_freshness_test.exs` does the
  same in-process and fails the suite naming the affected case ids, so a
  semantic change nobody meant to make turns the gate red instead of silently
  shipping a stale corpus.

- **Conformance corpus breadth, schemas, and the runner contract** (`px-35i.4`
  Phase 5). Cases now cover every opcode except the two documented exclusions
  (`relative_date`, clock-dependent; `object_set` on a non-map, unspecified),
  a rule `test/predicator/conformance/opcode_coverage_test.exs` enforces and
  binds to `conformance/README.md`'s own exclusion list. `conformance/schema/`
  gains `corpus.json`, `manifest.json`, and `report.json` (JSON Schema, draft
  2020-12) alongside the existing `case.json`; every generated artifact is
  validated against its own schema. `conformance/README.md` is the runner
  contract a sibling implementer reads first: the two surfaces (evaluator and
  compiler), the tagged-value encoding normatively, the never-skip rule and
  why `schema/report.json`'s `result` enum has no skip value, how to add a
  case without an Elixir toolchain, and the known-uncovered list. `docs/isa.md`
  now points at `conformance/README.md` as the spec's executable form.

- **`mix corpus.coverage`** (`px-35i.4` Phase 6). The corpus is authored, not
  extracted from the existing ExUnit suite, so nothing else tells an author
  what that suite exercises that the corpus does not. This dev-only task
  statically scans `test/**/*.exs` (excluding the corpus's own test suite)
  for literal `Predicator.evaluate/2,3` and `Predicator.compile/1` sources,
  compiles each through the real compiler, and diffs the resulting opcode/
  operand patterns against the shipped corpus's own instructions, printing a
  checklist of gaps grouped by tier. `Predicator.Conformance.Coverage` holds
  the diffing logic; report only, it never writes a case and never fails the
  gate. Function gaps are classified against the builtin registry: a name no
  builtin module registers is suite-local test scaffolding and prints under a
  trailing "Not corpus candidates" heading, and the non-deterministic
  exclusions carry an inline note instead of a bare `corpus: 0` row.

- **`conformance/RATCHET.md`: the sibling conformance ratchet format**
  (`px-35i.8`). Specifies the registry a sibling implementation (`impl/rb`,
  `impl/ts`, or any future port) writes to record which corpus cases it
  passes, on which surface, against which corpus version - this repo
  publishes the format only, and ships no registry and no runner itself. A
  registry entry keys on `(case_id, surface)`, not `case_id` alone, because a
  `source: null` case is absent from the compiler's case set rather than
  present-and-skipped, and a flat id list cannot express that a `compiler`
  claim on such a case is meaningless. Rule 1: an entry whose
  `(case_id, surface)` pair (or `tier`) disagrees with the pinned corpus fails
  the run, never silently drops. Rule 2: entries are sorted by
  `(surface, tier, case_id)` and encoded one per line with no indentation, so
  ratcheting a case in is a one-line diff rather than a reflowed array nobody
  reviews. Rule 3: the registry is grown only by verify-then-add - written
  solely from a runner report, refusing to record a failing case and never
  removing an existing entry - so every line in the file is a claim a run
  actually observed. The whole registry pins to a single
  `corpus_hash` from `conformance/manifest.json`; a mismatch is a hard
  failure, not an auto-refresh. `conformance/schema/registry.json` is the
  schema and `conformance/examples/registry.example.json` the worked example,
  both validated against the shipped corpus by
  `test/predicator/conformance/ratchet_registry_test.exs`. RATCHET.md also
  carries language-neutral pseudocode for the reference runner and the CI-side
  check step, so a sibling with no Elixir toolchain can implement both without
  guessing. Nothing here enters the hex package: `conformance/` is excluded
  from `mix.exs`'s `files:` list, same as the rest of the corpus tree.

- **Opcode retirement mechanics** (`px-t2v`). `Predicator.Instructions.in_isa?/2`
  answers whether an opcode's table entry is in a given ISA version's set;
  `opcode_set/1` returns the full set of opcode names a given ISA version
  comprises; `retired_in/1` returns the ISA version that retired an opcode, or
  `{:ok, nil}` for one still live, mirroring `tier/1`'s error shape for an
  unknown name. `opcodes/0`'s value shape widens to carry an optional
  `:removed_in` key alongside the existing `:isa` and `:tier` - no opcode
  carries it yet, so every existing return value is unchanged.

- **Two new guides** (`px-ycj`). [Porting Predicator](docs/guides/porting.md)
  is the path a sibling implementation follows: what an ISA version obliges it
  to implement, the two conformance surfaces and which to lead with, how to
  run the tiered corpus, and what "conformant at tier N" claims.
  [Embedding compiled programs](docs/guides/embedding.md) is the
  compile-once/store/check lifecycle: `compile/1` versus
  `compile_with_spans/1`, what is safe to store, the `required_isa/1` check
  against `isa_version/0`, and what a major version does to a stored
  artifact. Both are published as hexdocs extras and linked from the README.

### Documentation

- **Contributor how-tos move out of the published docs** (`px-7jd.3`).
  `docs/architecture.md`'s Development, Common Tasks, Code Standards,
  Performance Considerations, and Troubleshooting sections - the quality-check
  commands, the "Adding New Operators" and "Adding New Data Types" checklists,
  and debugging notes - move to the new `docs/contributing.md`, which is not
  listed in `mix.exs`'s hexdocs extras and so is neither published nor shipped
  in the hex package. `docs/architecture.md` now reads as architecture; the
  README's Development section points at the new file. **No behavior
  changed**; this only moves where contributor instructions live.

- **`docs/reference/language.md` documents `:undefined` and sparse-data
  semantics.** A new "Undefined and Sparse Data" section covers where
  `:undefined` comes from (unbound roots, missing nested paths), mismatched
  non-strict comparisons, `AND`/`OR` falsiness and its ECMAScript-style
  asymmetry, a per-operator reject-vs-propagate table, and the `on_unbound`
  option, including the API-layer rule that reports a genuinely unbound root
  as `UndefinedVariableError` even under the default policy. The Arithmetic
  Operators table's `/` row is corrected: it previously read "Division
  (integer)" for every case, when a float operand actually produces float
  division. **No behavior changed**; this is a documentation-only addition.

- **`docs/isa.md` reserves `pop` and specifies the statement-mode halt
  contract.** `["pop"]` joins `["store"]` as a reserved tier-6 name for the
  future 4.0 statement layer - not implemented, not accepted by any current
  evaluator clause, distinct from the live `jump_if_falsy_or_pop` /
  `jump_if_true_or_pop` opcodes despite the shared word. Section 2 now
  specifies two execution modes, distinguished by entry point rather than by
  anything in the instruction list: expression mode, where the result is the
  stack top at halt; and statement mode, where the result is the context at
  halt, with an empty stack at halt by design. `empty_stack` is now documented
  as an expression-mode rule only - a statement program halting with an empty
  stack is a normal halt, not an error. A statement program that halts on an
  error has no result; whether the host keeps or discards the partial context
  from statements that already completed is the host's policy, not the VM's.
  **No instruction-set behavior changed**: no opcode is added, removed, or
  resemanticized, and the ISA version stays v2.

- **`docs/isa.md`: the ISA reference.** The single specification of
  predicator's instruction set - one table row per opcode naming its arity,
  operand types, stack effect, error semantics, ISA version, and conformance-
  corpus tier, plus the cross-cutting rules that previously existed only as
  prose in ADR-0001: what "falsy" means at a jump (`false` or `:undefined`),
  that jumps are relative and forward-only, that opcodes validate rather than
  coerce, and that a malformed operand is an unknown instruction rather than a
  bad one. It also records ADR-0003's versioning scheme (integer ISA versions
  independent of this library's semver) rather than re-arguing it. The
  `Predicator.Evaluator` moduledoc and `t:Predicator.Types.instruction/0` no
  longer carry their own opcode lists - both now point at `docs/isa.md`
  instead. **No instruction-set behavior changed**: this is a documentation
  addition that consolidates specification already true of the evaluator, not
  a change to what any opcode does.

- **ADR-0003: the Elixir implementation leads the ISA.** Amends ADR-0001's
  consequences (not its decision): sibling parity in Ruby and JavaScript is a
  downstream obligation, not a gate on ISA changes made here, and the ISA is
  versioned so a sibling behind the current version is an expected, documented
  state rather than a defect. Stored-artifact compatibility remains the
  stronger, separate guarantee. The ADR also settles three rules the ISA moves
  under: an opcode's semantics never change under its own name, ISA versions are
  integers independent of this library's version (additive versions ship in a
  minor release, opcode retirement in a major one), and each sibling publishes
  its own supported version rather than being tracked in a matrix here. This
  does not change the instruction set - no opcode is added, removed, or
  resemanticized. `README.md`'s "Cross-Language Siblings" section and the
  equivalent section in `docs/architecture.md` are reworded to match.

### Fixed

- `Predicator.decompile/2` under the default `parentheses: :minimal` now adds
  parentheses when a child subexpression binds looser than its parent, or
  ties with it in a position where left-associativity would otherwise regroup
  it. Previously `:minimal` added no parentheses at all, so
  `{:arithmetic, :multiply, {:arithmetic, :add, 1, 2}, 3}` rendered as
  `"1 + 2 * 3"`, which re-parses as `1 + (2 * 3)` - a different AST and a
  different value than the one decompiled. `parentheses: :explicit` and
  `parentheses: :none` are unchanged.

- `Predicator.Errors.UndefinedVariableError` now carries a `:position` (and a
  `:span` under `spans: true`) on every path. The evaluator records each unbound
  load's source location alongside its name, so the error `Predicator.evaluate/3`
  builds after the run - for a bare unbound root, and for the `px-8um.7` rewrite
  of a `TypeMismatchError` that rejected an unbound root's `:undefined` - points
  at the variable's own token. It was the one runtime error type whose
  `:position` was always `nil`. An instruction-list caller who passes no
  `positions:` still sees `nil`.

- **`bracket_access` on a list with a non-integer key no longer crashes.**
  `xs[flag]` against a list target with a boolean (or any other non-integer)
  key raised `FunctionClauseError` from ordinary user-authored source instead
  of returning an error value; it now returns `{:error,
  %Predicator.Errors.TypeMismatchError{}}` with `expected: :integer`. The same
  crash via `.property` (the `access` opcode) against a list target now pushes
  `:undefined`, matching that opcode's existing "never an error" contract.

- **`docs/isa.md`'s `bracket_access` bullet corrected**: a boolean key against
  a map target has always been an accepted key, not a `TypeMismatchError` -
  the bullet previously left a reader to guess whether a boolean fell on the
  atom side or the rejected side of that line. This is a documentation
  correction, not a behavior change: no ISA version change and no existing
  instruction list changes meaning.

- **A store failure blames the location, not the `=`.** In point-position mode
  the `["store", n]` instruction is now annotated with the lhs root segment's
  position rather than the assignment node's operator token, so
  `Predicator.execute("a = 1; a.b = 2", %{})` reports `position: {1, 8}` (the
  `a` being written) instead of `{1, 12}` (the `=`). Span mode is unchanged -
  the assignment's span already started at the lhs root, and this makes the two
  modes agree. Every emitted instruction list is byte-identical; no ISA version,
  error type, reason, or `{:error, error, context}` shape moves.

- **The store segment-type message names both accepted types.** An out-of-domain
  location segment now reports `Store requires a string or an integer, got true
  (boolean)` rather than `Store requires a string`, which was false about what
  `store` accepts - integer segments index lists. The normative
  `expected: :string` field is unchanged, matching how `docs/isa.md` states
  `bracket_access`'s key rule. `Predicator.Errors.TypeMismatchError.unary/5` is
  the new constructor that separates the message text from the `expected` atom.

- **A store failure blames the exact failing location segment.** Building on
  the fix above, the compiler now emits a per-store side table mapping each
  `["store", n]` instruction's index to one source annotation per location
  segment, and the evaluator joins it with the failing segment's path index.
  `Predicator.execute(~s(a = {"b": 1}; a.b.c = 2), %{})` reports
  `position: {1, 17}` - the property `b`, which held a scalar - instead of
  `{1, 15}`, the location's root;
  `Predicator.execute("a[true] = 1", %{"a" => %{}})` reports `{1, 3}`, the
  offending key, instead of `{1, 1}`. Under `spans: true` the
  underline narrows to the failing segment (`a.b`) instead of covering the whole
  statement; the caret is unchanged, because a chain node's span already started
  at the chain root. `Predicator.Compiled` gains a `segment_positions` field and
  `Predicator.Compiled.new/3`, `Predicator.Compiler` gains
  `to_instructions_with_segment_positions/2`, and
  `Predicator.Evaluator.evaluate/3` accepts a `:segment_positions` option - all
  additive. A run without the table (a bare instruction list, a stored program)
  positions a store failure exactly as it did before, at the location's root.
  Every emitted instruction list is byte-identical; no ISA version, error type,
  reason, `expected`, or `{:error, error, context}` shape moves.

### Changed

- **Property and bracket access blame the accessed thing, not the accessor.**
  A `{:property_access, ...}` node's point position is now the property-name
  token rather than the `.`, and a `{:bracket_access, ...}` node's is the
  first token of the key expression rather than the `[`.
  `Predicator.parse("user.name")` reports `{1, 6}` instead of `{1, 5}`; the
  position table entry for an `["access", ...]` or `["bracket_access"]`
  instruction moves with it, and so does any error stamped from one. Spans are
  unchanged: `spans: true` still runs a chain node from the chain root to the
  accessor's end. No instruction list, opcode, ISA version, error type, or
  reason moves.

- `Predicator.compile_with_positions/1` now returns `{:ok, %Predicator.Compiled{}}`
  instead of `{:ok, instructions, position_table}`. The envelope carries the
  instruction list and its source-location table as one value, so the table
  cannot be silently dropped between compilation and evaluation;
  `Predicator.evaluate/3` accepts a `%Predicator.Compiled{}` directly and
  threads the table itself. Read `compiled.instructions` and
  `compiled.positions` for the old tuple elements; `evaluate/3`'s `:positions`
  option still works for a bare instruction list. `Predicator.compile/1` and
  `Predicator.compile!/1` are unchanged and still return a bare instruction
  list, which remains what a consumer serializes and stores. No instruction
  changed and the ISA stays at version 3, so stored artifacts need no
  migration. See [ADR-0009](docs/adr/0009-the-compiled-envelope-carries-the-position-table.md).

- `Predicator.decompile/2` renders a `{:comparison, :eq, ...}` node as `==`
  rather than `=`, so decompiled output always re-parses under the 4.0 grammar.
  The node's meaning and compiled instructions are unchanged.

- `docs/isa.md` now specifies opcode retirement mechanics: retiring an opcode
  mints the next ISA version, a version's opcode set is a half-open interval
  so a retired opcode keeps its table row instead of being deleted, and the
  conformance corpus freezes a retired case's expectation rather than
  recomputing it through an evaluator clause that no longer exists.

- `Predicator.Instructions.required_isa/1`'s `unknown_opcode` error message now
  names the ISA version this build supports, not just the offending opcode:
  `Unknown opcode "store"; this build supports ISA v2` instead of
  `Unknown opcode: "store"`. The error struct is unchanged - reason
  `"unknown_opcode"`, operation `:required_isa`.

- **BREAKING: the minimum Elixir version is now 1.18.** `mix.exs` previously
  declared `~> 1.11`, but CI has tested only 1.17 and 1.18 for a long time, so
  the declaration promised support that was neither verified nor known to work.
  1.18 is required for the built-in `JSON` module; consumers on 1.17 or earlier
  must stay on 3.x.

- `object_set` on a non-map target is now specified behavior. The evaluator
  returns `%Predicator.Errors.EvaluationError{}` with reason
  `"invalid_stack_value"` and operation `:object_set` instead of raising a
  `FunctionClauseError`, matching how `relative_date` reports a non-duration on
  the stack. `docs/isa.md` section 5 states it normatively rather than calling
  it unspecified, and the conformance corpus covers it in the errors group - so
  a sibling implementation must now produce this error to claim tier 4. The
  shape is reachable only from a hand-built instruction list; the compiler
  always emits `object_new` immediately before `object_set`, so nothing
  compiled from source changes.

### Removed

- **`=` as an equality operator.** `==` and `===` are the only equality
  operators. `=` is assignment, valid only at the start of a statement and only
  with an assignable left side; a bare `=` in expression position - through
  `Predicator.parse/2`, `Predicator.evaluate/3`, or nested inside a statement -
  is a parse error naming `==` as the fix, never a silent reinterpretation. The
  3.7.0 deprecation warning was the notice period; migrate to `==` before
  upgrading. **The instruction set is unaffected**: `=` and `==` always compiled
  to `["compare", "EQ"]`, `{:comparison, :eq, ...}` remains a fully supported
  AST node, and no stored instruction list is invalidated. See
  [ADR-0002](docs/adr/0002-the-equals-grammar-break.md).

- The `config :predicator, deprecation_warnings` setting, which existed only to
  silence that warning. It is now inert and has been deleted; remove it from
  your config.

- **Breaking:** `:line` and `:column` on `Predicator.Errors.ParseError`. The
  struct now stores the location once, in `:position`, as the
  `{line, column}` tuple typed `t:Predicator.Types.position/0` that
  `EvaluationError`, `TypeMismatchError`, and `UndefinedVariableError` already
  carry. Code reading `error.line` reads `elem(error.position, 0)` instead, or
  better, matches `%ParseError{position: {line, column}}`.
  `ParseError.new/3` keeps its `(message, line, column)` signature, and error
  message text is unchanged.

- **BREAKING: the pre-4.0 AST shape acceptance.**
  `Predicator.Parser.strip_positions/1` and
  `Predicator.Parser.ensure_positions/1` are gone, along with the
  `t:Predicator.Parser.bare_ast/0` and `t:Predicator.Parser.bare_object_key/0`
  types. The AST has one shape: every node carries a trailing slot holding a
  position, a span, or `nil`.

  `Predicator.decompile/2`, `Predicator.Compiler.to_instructions/2`,
  `Predicator.Compiler.to_string/2`, and `Predicator.ContextLocation.resolve/2`
  no longer accept the position-free shape Predicator 3.6 produced. A caller
  building an AST by hand adds the slot:
  `{:comparison, :gt, {:identifier, "score", nil}, {:literal, 85, nil}, nil}`.
  A caller that was calling `strip_positions/1` to compare two ASTs while
  ignoring positions writes that traversal itself; it is a few lines and its
  exact semantics - whether object-key style is significant, for one - are the
  caller's to choose.

  The instruction set is unchanged, so stored compiled artifacts and the
  cross-language interchange format are unaffected (ADR-0001).

- **Breaking: the legacy `and`/`or` opcodes are retired from the evaluator.**
  ISA v2 becomes v3 (`Predicator.isa_version/0` now returns `3`). This only
  affects a consumer holding an instruction list that was compiled before
  3.7.0 and stored somewhere; nothing compiled by 3.7.0 or later contains
  these opcodes, since the compiler stopped emitting them then, so
  recompiling from source and every surface `AND` / `OR` expression are
  unaffected. Running such a stored list now returns an `EvaluationError` with
  reason `"retired_opcode"`, naming ISA v3 and the upgrade path, instead of
  being silently mis-run. The migration is
  `Predicator.Instructions.upgrade/1`, run once over stored artifacts to
  rewrite them into jump form (identity on anything containing neither
  opcode); the upgraded list short-circuits and follows the
  ECMAScript-aligned `:undefined` rules that ADR-0001 documented for 3.7.0,
  so a right operand that errored or was `:undefined` can now produce a
  value where it previously produced a `TypeMismatchError`. Because jumps are
  ISA v2 opcodes, upgrading also raises the list's `required_isa/1` answer
  from `1` to `2`, so an artifact shared with an implementation still on ISA
  v1 - which both siblings are - should be upgraded in step with those
  consumers rather than ahead of them. `and` and `or` keep their rows in the
  ISA table - `required_isa/1` and `tier/1` still answer for them - and the
  conformance corpus still carries all five legacy cases.

- **Breaking: the `jason` runtime dependency.**
  `Predicator.Functions.JSONFunctions` now uses Elixir 1.18's built-in `JSON`
  module, so **predicator has no runtime dependencies at all**. The error text
  from `JSON.parse` on malformed input changes wording - it now reads e.g.
  `Invalid JSON: unexpected byte 0x6F at position 1` - because the built-in
  decoder reports failures differently. `JSON.stringify` behavior, including
  the `inspect/1` fallback for values that cannot be encoded, is unchanged.

### Unchanged

Stated explicitly, because this release adds a second location representation
and nothing about the first one moves:

- Point positions remain the default at every entry point. `Predicator.parse/1`,
  `Predicator.compile/1`, `Predicator.compile_with_positions/1`, and
  `Predicator.evaluate/3` without the option behave exactly as in 3.8.0.
- `t:Predicator.Types.position/0` is untouched and still means a point.
- No AST node gained or lost an element; spans reuse the trailing slot.
- Every rendered error `message` string is identical with and without spans.
- The instruction list produced by `compile/1` is byte-identical, so stored
  compiled artifacts and cross-language interchange with the Ruby and
  JavaScript siblings are unaffected (ADR-0001).
- A parenthesized expression's span excludes its parentheses, which build no
  AST node.

## [3.8.0] - 2026-08-05

### Changed

- The Hex package tarball no longer bundles the markdown doc sources under
  `docs/`. Every guide, the language and architecture references, and the
  ADRs are still published in full at
  [hexdocs.pm/predicator](https://hexdocs.pm/predicator) and still live in
  the GitHub repository; only the copy that `mix deps.get` unpacked into
  `deps/predicator/docs/` is gone. Read them online or from a repo checkout
  instead.

- `Predicator.Evaluator.run_prepared/1` returns `{:error, error, evaluator}`
  instead of `{:error, error}`, so the final evaluator state - and with it
  `unbound_loads/1` - is available on the error path as well as the success
  path. `run/1`, `evaluate/3`, `evaluate!/3`, `evaluate_prepared/1`, and
  `Predicator.run_evaluator/1` are unchanged.

- **Context keys and `nil` values are now normalized eagerly and deeply.**
  `Predicator.Context.new/2` and `bind/3` convert atom keys to string keys
  (string key wins on collision) and `nil` values to `:undefined`, recursing
  through nested maps and lists, before evaluation ever sees the data. This
  is the one edge where atom keys and `nil` are accepted; the two
  `String.to_existing_atom/1` read-time fallbacks that used to paper over
  their absence - in `Predicator.Evaluator.load_from_context/2` (variable
  load) and `access_value/2` (property/bracket access) - are deleted, since a
  context reaching them through `Context.new/2`/`bind/3` never has atom keys
  left to fall back to. Ordinary `Predicator.evaluate/3`/`evaluate!/3`
  callers passing a bare map are unaffected - atom-keyed and `nil`-bearing
  contexts keep working exactly as before, now via the edge instead of the
  read-time fallback. The low-level `Predicator.Evaluator.evaluate/3`/
  `evaluate!/3` and `Predicator.evaluator/2` APIs, which construct an
  evaluator directly and bypass `Context.new/2`, no longer accept atom keys:
  this is better-defined behavior for that narrow surface, not a removal - a
  caller who wants atom-key or `nil` normalization goes through
  `Predicator.Context` or `Predicator.evaluate/3` instead.

  One narrowing follows from "deep and total": a duration value
  (`t:Predicator.Types.duration/0`) is a plain atom-keyed map, not a struct,
  so a pre-built `Predicator.Duration.new/1` result *bound into a context*
  now has its keys stringified like any other map and is no longer recognized
  as a duration by date arithmetic. Durations built the documented way - by a
  `duration(...)` or `3d8h` literal in the expression, during evaluation -
  never pass through this normalization and are unaffected.

- **Object keys are now `{:object_key, value, style, pos}`** rather than
  `{:identifier, name, pos}` / `{:string_literal, value, pos}`, where `style`
  is `:identifier`, `:double`, or `:single` and records how the key was
  written. Keys no longer reuse the expression node tags, so nothing tells a
  key from an expression by tuple arity. Callers pattern-matching a parsed
  object entry's key update their patterns to the new tag;
  `Predicator.Parser.strip_positions/1` still returns the 3.6 shape and
  `Predicator.Parser.ensure_positions/1` still accepts every earlier key
  shape, so a hand-built AST passed to `Predicator.decompile/2` or
  `Predicator.Compiler.to_instructions/2` is unaffected, and the instruction
  list is byte-identical.
- `Predicator.decompile/2` now renders a single-quoted object key with single
  quotes instead of rewriting it to double quotes, and escapes a quote
  character inside a key. A key containing the quote character previously
  decompiled to syntactically invalid source.

### Added

- `on_unbound: :error` on `Predicator.Context.new/2` (and as an option to
  `Predicator.evaluate/3` and `Predicator.Evaluator.evaluate/3`): a load of an
  unbound root variable returns
  `{:error, %Predicator.Errors.UndefinedVariableError{}}` instead of the
  `:undefined` sentinel. Roots only - a missing key on a bound map stays
  `:undefined` under either policy - and a load a short-circuit skipped never
  fires it. The default, `:undefined`, is unchanged behavior.

- `Predicator.Errors.ParseError` gains a `:position` field - `{line, column}`,
  derived from the existing `:line` and `:column` fields, which stay
  populated unchanged. Generic error-reporting code can now read `:position`
  uniformly across `ParseError`, `EvaluationError`, `TypeMismatchError`, and
  `UndefinedVariableError` instead of special-casing `ParseError`. Additive
  and non-breaking - no existing caller matching on `:line`/`:column` needs
  to change.

### Fixed

- **An unbound variable is no longer hidden behind a nameless type mismatch.**
  `Predicator.evaluate/3` reported `TypeMismatchError "Logical NOT requires a
  boolean, got :undefined"` for `not unbound`, naming no variable, while
  `unbound > 5` correctly returned `UndefinedVariableError`. Every opcode that
  rejects an `:undefined` operand - `not`, `unary_minus`, `unary_bang`, `add`,
  `subtract`, `multiply`, `divide`, `modulo`, and the legacy `["and"]`/`["or"]`
  instructions - now reports the unbound root instead, when the operand came
  from a variable the run loaded and did not find bound. A key *bound* to
  `:undefined` (`%{"b" => :undefined}`) and a missing nested path on a bound
  root (`user.nope`) still produce a `TypeMismatchError`: those are genuine
  type mismatches on data the caller supplied. Evaluation semantics are
  unchanged - `:undefined` still errors in these positions - and the low-level
  `Predicator.Evaluator.evaluate/3` still returns the bare `TypeMismatchError`.

- The Hex package `files:` list named a bare `docs` entry, which swept the
  whole `docs/` tree - including `docs/plans/*.md` and `docs/design/*.md`,
  the agent workflow's internal per-bead planning documents. It now names the
  published doc subtrees explicitly (`docs/reference`, `docs/guides`,
  `docs/adr`, `docs/architecture.md`), matching what the `docs()` extras list
  already publishes to hexdocs.

## [3.7.0] - 2026-08-05

### Changed

- **`AND` and `OR` now short-circuit.** Previously the compiler evaluated both
  sides of every `AND`/`OR` unconditionally, so an unbound variable or a
  runtime error on the side that should have been skipped surfaced as an
  error - `false AND score > 5` with `score` unbound raised
  `TypeMismatchError`, and `true OR (1 / 0) > 1` raised a division-by-zero
  error. Both now evaluate to `false` and `true` respectively, matching every
  mainstream language's `AND`/`OR` semantics and this library's own graceful
  undefined-handling documentation. **This is an observable behavior change**:
  expressions that previously returned `{:error, _}` now return `{:ok, _}`. A
  consumer relying on the error was relying on the bug. `:undefined`
  propagation is ECMAScript-aligned rather than symmetric - see
  `docs/architecture.md`'s "Short-Circuit Evaluation" section for the exact
  rule. Old compiled artifacts using `["and"]`/`["or"]` directly are
  unaffected; the evaluator still accepts both opcodes.
- `Predicator.parse/1` now returns positioned AST nodes, so every node has one
  more trailing element than it did in 3.6. Callers that pattern-match on node
  shape either wrap the result in `Predicator.Parser.strip_positions/1` to get
  the old shape back, or add a trailing `_position` to their patterns.
  `Predicator.decompile/2` and `Predicator.Compiler.to_instructions/2` still
  accept a hand-built 3.6-shaped AST unchanged, and the instruction list
  `Predicator.compile/1` produces is byte-identical, so stored compiled
  artifacts and cross-language interchange are unaffected.
- Documentation restructured: the README is now a slim entry point, with the
  language reference, nested data access, custom functions, and location
  expressions moved to `docs/reference/` and `docs/guides/` and published to
  hexdocs. All documentation examples are now executed by the test suite.

### Added

- **ISA v2** (ADR-0001): the instruction set is Predicator's cross-language
  interchange format, and both entries below are new opcodes the Ruby and
  JavaScript siblings need to add for parity with this release.
  - `["make_list", n]` instruction: pops n values from the stack and pushes
    them as a list, in source order.
  - `["jump_if_falsy_or_pop", offset]` and `["jump_if_true_or_pop", offset]`
    instructions: relative, forward-only conditional jumps used to
    short-circuit `AND`/`OR`.
- `Predicator.Context`: a bound evaluation context built once with `new/2`
  (merging builtin and custom functions a single time), rebound cheaply with
  `bind/3` and `assign/3`, and evaluated against many times via
  `Predicator.evaluate/3`, which now accepts either a `%Context{}` or a bare
  map
- `Predicator.Undefined`: the one public module that owns the `:undefined`
  sentinel - `value/0`, `undefined?/1`, and `to_nil/1`/`from_nil/1`
  normalizers for a JSON-shaped boundary. `Predicator.Types.undefined?/1`
  now delegates to it.
- `Predicator.Context.bound?/2`: answers whether a root variable is bound in
  a context's data, checking both string and atom keys.
- `starts_with(s, prefix)`, `ends_with(s, suffix)`, `substring(s, start[, len])`,
  and `index_of(s, sub)` builtin string functions
- `concat(list1, list2)` builtin function: concatenates two lists.
- `+` now concatenates two lists (`[1, 2] + [3]` -> `[1, 2, 3]`), alongside
  its existing numeric and string coercions.
- `Predicator.Evaluator.run_prepared/1` (result plus final evaluator state),
  `Predicator.Evaluator.unbound_loads/1`, and
  `Predicator.Evaluator.resolve_key/2`.
- Source positions on every AST node: each node carries a trailing
  `{line, column}` naming the token that defines it (the operator token for
  binary and unary operators, the opening bracket for lists and objects, the
  name token for function calls).
- `Predicator.Parser.strip_positions/1` and
  `Predicator.Parser.ensure_positions/1`: total, idempotent normalizers between
  the positioned AST and the position-free shape Predicator 3.6 produced.
- `Predicator.Compiler.to_instructions_with_positions/2` and
  `Predicator.Visitors.InstructionsVisitor.visit_with_positions/2`: compile to
  the usual instruction list plus a side table mapping each instruction's
  0-based index to the `{line, column}` of the AST node that emitted it. The
  table is an Elixir-side companion value and never enters the instruction
  format itself.
- `Predicator.compile_with_positions/1`: compiles a string expression to the
  instruction list `compile/1` returns plus that side table.
- An optional `:position` field on `Predicator.Errors.EvaluationError`,
  `Predicator.Errors.TypeMismatchError`, and
  `Predicator.Errors.UndefinedVariableError`, holding the `{line, column}` of
  the source token behind the failing instruction, or `nil` when no side table
  was available.
- `Predicator.Errors.put_position/2`: attaches a position to any error value,
  returning it unchanged when the position is `nil` or the value has no
  `:position` field.
- A `:positions` option on `Predicator.evaluate/3` and
  `Predicator.Evaluator.evaluate/3`, seeding the side table used to populate
  `:position` on runtime errors. Evaluating a string expression threads its own
  table automatically; an instruction-list caller who omits the option sees
  `position: nil` and no other change.

### Fixed

- `Predicator.decompile/2` and `Predicator.Compiler.to_string/2` no longer
  raise `FunctionClauseError` on ASTs containing dotted property access
  (`user.name`). `Predicator.Visitors.StringVisitor` was missing the
  `:property_access` clause; it now renders `object.property`, including
  chains (`user.profile.email`) and mixes with bracket access.
- Duration units now parse in source order: `3d8h` produces
  `[{3, "d"}, {8, "h"}]` instead of the reversed `[{8, "h"}, {3, "d"}]`, and
  multi-unit durations round-trip through the string visitor unchanged
- Comparing a `Date` against a `DateTime` now returns a boolean instead of
  silently evaluating to `:undefined`. The `Date` is coerced to `00:00:00`
  UTC of that day, matching the coercion mixed date subtraction already
  performs. This covers ordering, `==`/`!=`, and `in`/`contains`, and it
  makes every relative date (`3d ago`, `2w from now`, `next 1mo`, `last 1y`,
  all of which produce a `DateTime`) usable against a `Date` context value.
  Strict equality (`===`/`!==`) stays type-strict and never crosses the
  boundary.
- `Date` and `DateTime` ordering (`<`, `>`, `<=`, `>=`) is now chronological.
  The evaluator previously dispatched ordering comparisons to Erlang's `<`/`>`
  after confirming both sides were the same struct type, but Erlang orders
  structs by sorted map key, not by field meaning - `Date`'s keys sort
  `day, month, year`, so `#2026-08-14# < #2030-01-01#` compared day 14 against
  day 1 and returned `false`. `DateTime` was worse, sorting `microsecond`
  ahead of `month`. Ordering now goes through `Date.compare/2` and
  `DateTime.compare/2`, and `EQ`/`NE`/list-membership on `DateTime` now agree
  with `DateTime.compare/2` rather than structural equality, so two `DateTime`
  values denoting the same instant in different time zones compare equal.
- `Predicator.evaluate/3` now correctly reports `UndefinedVariableError` for
  any unbound root variable, not just a bare `variable_name` expression. The
  old check only matched a single-instruction `[["load", _]]` program, so an
  unbound variable inside a larger expression (`"missing > 5"`) silently
  returned `{:ok, :undefined}` instead of an error.
- Unbound-variable reporting now reflects the loads a run actually executed
  rather than the loads the compiled program contains. With short-circuiting
  `AND`/`OR`, a load inside a skipped branch is never read, but the previous
  check scanned the whole instruction list and could name it -
  `(false AND missing) OR unbound_b` reported `missing` instead of
  `unbound_b`.
- List literals with non-literal elements (`[x + 1, y]`) now compile and
  evaluate. Previously the compiler raised
  `"Non-literal list elements are not yet supported"`, the one place the
  errors-are-values convention was broken; errors from such expressions are now
  returned as `{:error, _}` values like every other failure.

### Deprecated

#### `=` as an equality operator

- Parsing an expression that uses `=` as an equality operator now emits a
  deprecation warning naming `==` as the replacement
- Behavior is otherwise unchanged: `=` still parses and still compiles to
  `["compare", "EQ"]`
- **Predicator 4.0 will make expression-position `=` a parse error.** Migrate
  to `==` before upgrading
- The warning is emitted once per parse and can be silenced with
  `config :predicator, deprecation_warnings: false`

```elixir
# Deprecated - warns, still works in 3.x
Predicator.evaluate("status = 'active'", context)

# Preferred
Predicator.evaluate("status == 'active'", context)
```

## [3.6.0] - 2026-08-04

### Added

#### Auto-vivifying path assignment for SCXML location expressions

- `Predicator.ContextLocation.put/3` writes a value at a resolved location path,
  creating missing intermediate maps and lists
- `Predicator.context_assign/4` resolves a location expression and writes in one call
- Integer path segments index lists and pad gaps with `:undefined`
- Assigning through an existing scalar returns a `:not_a_container` error rather than
  destroying data; negative indices return `:invalid_index`

#### Examples

```elixir
Predicator.context_assign(%{}, "user.profile.name", "Ada")
# {:ok, %{"user" => %{"profile" => %{"name" => "Ada"}}}}

Predicator.context_assign(%{"items" => [1]}, "items[2]", "x")
# {:ok, %{"items" => [1, :undefined, "x"]}}

Predicator.ContextLocation.put(%{}, ["data", "users", 0, "name"], "Ada")
# {:ok, %{"data" => %{"users" => [%{"name" => "Ada"}]}}}
```

### Changed

#### Replaces the hand-rolled quality gate with ex_quality

- `mix quality` is now [ex_quality](https://hex.pm/packages/ex_quality), configured
  in `.quality.exs`; the vendored `lib/mix/tasks/quality.ex` has been removed
- The gate runs format, compile (warnings as errors), Credo `--strict`, Dialyzer,
  an unused-dependency and security audit, and the full suite with the existing
  90% coverage minimum - stages run in parallel and report `file:line` findings
- `mix quality --profile loop` replaces `--skip-dialyzer`: it skips Dialyzer and
  coverage and runs only the tests covering changed code
- `mix quality --format json` emits a machine-readable report
- `mix quality.check` and `mix test --watch` are gone; the former no longer
  existed as a task and the latter's `mix_test_watch` dependency was undeclared

## [3.5.0] - 2025-09-09

### Added

#### Adds milliseconds support to duration system

- New 'ms' unit support in lexer, parser, and evaluator
- Duration.to_milliseconds/1 function for high-precision calculations
- Pattern matching guards for automatic precision selection
- Smart DateTime arithmetic (millisecond vs second precision)
- Comprehensive test coverage with 89 new tests
- Refactors evaluator to use Duration module functions (DRY)

#### Examples

- 500ms ago, 2s750ms from now
- #2024-01-15T10:30:00.000Z# + 1s500ms
- Automatic precision: ms > 0 triggers millisecond precision

## [3.4.0] - 2025-09-09

### Added

#### Durations and relative date/time arithmetic

- New duration literals and relative date expressions (e.g., `3d ago`, `2w from now`, `next 1mo`, `last 1y`)
- Date and DateTime arithmetic using durations (e.g., `#2024-01-10# + 5d`, `#2024-01-15T10:30:00Z# - 2h`)
- Grammar additions: `duration` and `relative_date` productions
- Full pipeline support (lexer, parser, compiler, evaluator, string visitor) with tests

#### Examples

```elixir
Predicator.evaluate("created_at > 3d ago", %{"created_at" => ~U[2024-01-20 00:00:00Z]})
Predicator.evaluate("due_at < 2w from now", %{"due_at" => Date.add(Date.utc_today(), 10)})
Predicator.evaluate("#2024-01-10# + 5d = #2024-01-15#", %{})
Predicator.evaluate("#2024-01-15T10:30:00Z# - 2h < #2024-01-15T10:30:00Z#", %{})
```

### Documentation

- Updated EBNF grammar in docs
- Added AGENTS.md with model-agnostic agent guidance; `CLAUDE.md` now references the same content

## [3.3.0] - 2025-08-31

### Added

- Depends on Jason library

## [3.2.0] - 2025-08-31

### Added

#### Strict Equality Operators

- **New Operators**: Added `===` (strict equality) and `!==` (strict inequality) operators
- **Type-Safe Comparisons**: Strict operators compare both value and type, unlike loose equality
- **Round-Trip Preservation**: Operators maintain their exact form during parse/decompile cycles
- **Complete Pipeline Support**: Full lexer, parser, evaluator, and visitor implementation
- **Comprehensive Testing**: 23 tests covering all aspects of strict equality functionality

#### Examples

```elixir
# Strict equality - same type and value required
Predicator.evaluate("5 === 5", %{})      # {:ok, true}
Predicator.evaluate("5 === '5'", %{})    # {:ok, false} - different types

# Strict inequality - true when type or value differs
Predicator.evaluate("5 !== '5'", %{})    # {:ok, true} - different types
Predicator.evaluate("1 !== true", %{})   # {:ok, true} - different types

# Operator distinction preserved
Predicator.parse("x = y") |> elem(1) |> Predicator.decompile()   # "x = y"
Predicator.parse("x == y") |> elem(1) |> Predicator.decompile()  # "x == y"  
Predicator.parse("x === y") |> elem(1) |> Predicator.decompile() # "x === y"
```

#### Technical Implementation

- **Lexer**: Added `:strict_equal` and `:strict_ne` token types with proper precedence
- **Parser**: Extended comparison grammar to support strict operators
- **Evaluator**: Added `STRICT_EQ` and `STRICT_NE` instruction handlers
- **StringVisitor**: Added decompilation support for round-trip accuracy
- **Type Safety**: Works with all data types including `:undefined` values

## [3.1.0] - 2025-08-30

### Added

#### JavaScript-Style Object Literals (Complete Implementation)

- **Object Literal Syntax**: Full support for JavaScript-style object notation with `{key: value}` syntax
- **Multiple Key Types**: Both identifier keys (`name: "John"`) and string keys (`"first name": "John"`)
- **Nested Objects**: Unlimited nesting depth for complex data structures
- **All Value Types**: Objects support all Predicator value types (strings, numbers, booleans, dates, lists, expressions)
- **Object Comparisons**: Full equality and inequality operations between objects
- **Integration**: Seamless compatibility with all existing features (functions, operators, property access)

#### Object Literal Examples

```elixir
# Basic object creation
Predicator.evaluate("{}", %{})                                    # {:ok, %{}}
Predicator.evaluate("{name: \"John\", age: 30}", %{})            # {:ok, %{"name" => "John", "age" => 30}}

# Variable references and expressions
Predicator.evaluate("{user: name, total: price + tax}", %{"name" => "Alice", "price" => 100, "tax" => 10})
# {:ok, %{"user" => "Alice", "total" => 110}}

# Nested objects
Predicator.evaluate("{user: {name: \"Bob\", role: \"admin\"}, active: true}", %{})
# {:ok, %{"user" => %{"name" => "Bob", "role" => "admin"}, "active" => true}}

# String keys for complex property names
Predicator.evaluate("{\"first name\": \"John\", \"last-name\": \"Doe\"}", %{})
# {:ok, %{"first name" => "John", "last-name" => "Doe"}}

# Object comparisons
Predicator.evaluate("{score: 85} == user_data", %{"user_data" => %{"score" => 85}})
# {:ok, true}
```

#### Complete Pipeline Support

- **Lexer**: Added `{`, `}`, `:` token recognition
- **Parser**: Full object grammar with proper precedence and error handling
- **Instructions**: Stack-based `object_new` and `object_set` instruction execution
- **Evaluator**: Efficient object construction and comparison operations
- **String Visitor**: Bidirectional transformation support (AST ↔ string representation)
- **Type System**: Enhanced type matching for object equality comparisons

#### Integration Features

- **Function Integration**: Objects work as function parameters and return values
- **Property Access**: Objects integrate with dot notation (`obj.property`) and bracket access (`obj["key"]`)
- **Boolean Logic**: Objects support all logical operations (AND, OR, NOT)
- **Arithmetic**: Object properties can contain arithmetic expressions and results
- **Date Support**: Objects can contain date/datetime literals and date function results
- **Custom Functions**: Objects work seamlessly with user-defined functions

#### Quality and Testing

- **886 Total Tests**: Comprehensive test coverage including edge cases and integration scenarios
- **91.8% Coverage**: High test coverage across all components
- **Parser Error Handling**: Robust error recovery for malformed object syntax
- **Performance Tested**: Validated with large objects and repeated evaluations
- **Production Ready**: Full quality assurance (formatting, linting, type checking)

## [3.0.0] - 2025-08-25

### Added

#### Location Expressions for SCXML Assignment Operations (Phase 2 Complete)

- **SCXML Location Expressions**: Complete implementation of location path resolution for SCXML `<assign>` operations
- **New API Function**: `Predicator.context_location/3` - resolves assignable location paths from expressions
- **Location Path Resolution**: Returns navigation paths like `["user", "name"]`, `["items", 0, "property"]` for SCXML assignment targets
- **Assignment Validation**: Distinguishes valid assignment targets (l-values) from computed expressions (r-values)
- **Core Module**: `Predicator.ContextLocation` with comprehensive location resolution logic and error handling
- **Structured Error Handling**: `Predicator.Errors.LocationError` with detailed error types and context information

#### Location Expression Examples

```elixir
# Valid assignment targets resolve to location paths
Predicator.context_location("user.profile.name", %{})                    # {:ok, ["user", "profile", "name"]}
Predicator.context_location("items[0]", %{})                             # {:ok, ["items", 0]}
Predicator.context_location("data['users'][index]['profile']", %{"index" => 2})  # {:ok, ["data", "users", 2, "profile"]}

# Invalid assignment targets return structured errors
Predicator.context_location("len(name)", %{})                            # {:error, %LocationError{type: :not_assignable}}
Predicator.context_location("42", %{})                                   # {:error, %LocationError{type: :not_assignable}}
Predicator.context_location("score + 1", %{})                            # {:error, %LocationError{type: :not_assignable}}
```

#### Error Types and Validation

- **`:not_assignable`**: Expression cannot be used as assignment target (literals, functions, computed expressions)
- **`:invalid_node`**: Unknown or unsupported AST node type encountered during resolution
- **`:undefined_variable`**: Variable referenced in bracket key is not defined in evaluation context
- **`:invalid_key`**: Bracket key is not a valid string or integer type
- **`:computed_key`**: Computed expressions cannot be used as assignment target keys

#### Assignable vs Non-Assignable Classifications

- **✅ Valid Assignment Targets**: Simple identifiers, property access, bracket access, mixed notation
  - `user`, `score`, `config.database.host`
  - `items[0]`, `user['profile']`, `data["settings"]`
  - `user.settings['theme']`, `data['users'][0].profile`
- **❌ Invalid Assignment Targets**: Literals, function calls, computed expressions
  - `42`, `"hello"`, `true`, `#2024-01-15#`
  - `len(name)`, `upper(role)`, `max(a, b)`
  - `score + 1`, `items[i + 1]`, `score > 85`

#### Technical Implementation

- **Full Location Resolution**: Recursive resolution of nested property access and bracket access
- **Mixed Notation Support**: Complete support for expressions like `user.settings['theme']` and `data['users'][0].name`
- **Variable Key Resolution**: Bracket keys can reference context variables for dynamic access patterns
- **Context Integration**: Uses existing evaluation context for variable key resolution
- **Comprehensive Testing**: 49 comprehensive tests covering all location resolution scenarios and error cases

#### Type Coercion and Float Support

- **Float Literal Support**: Extended lexer to parse floating-point numbers (e.g., `3.14`, `0.5`)
- **Float Token Type**: Added `:float` token type to distinguish from integers
- **Parser Float Handling**: Updated parser to handle float tokens and create appropriate AST nodes
- **Arithmetic with Floats**: All arithmetic operations now support both integers and floats
  - Addition, subtraction, multiplication work seamlessly with mixed numeric types
  - Division returns float when needed, integer when evenly divisible
  - Modulo remains integer-only as per mathematical conventions
- **String Concatenation with `+` Operator**: Implemented JavaScript-like type coercion
  - `"Hello" + "World"` → `"HelloWorld"` (string concatenation)
  - `"Count: " + 5` → `"Count: 5"` (string + number coercion)
  - `42 + " items"` → `"42 items"` (number + string coercion)
- **Type Coercion Rules**:
  - Number + Number → Numeric addition (supports mixed int/float)
  - String + String → String concatenation
  - String + Number → String concatenation (number converted to string)
  - Number + String → String concatenation (number converted to string)
- **Comparison Enhancements**: Numbers of different types (int/float) can be compared
- **Unary Minus for Floats**: Unary minus operator now works with floating-point numbers
- **Error Message Updates**: Updated error messages from "integer" to "number" where appropriate
- **Comprehensive Testing**: Added 28 new tests covering all type coercion scenarios

### Changed

#### Property Access Parsing Architecture Overhaul (Breaking Changes)

- **Complete Dot Notation Reimplementation**: Transformed from dotted identifiers to proper property access AST nodes
- **Lexer Breaking Change**: Dots removed from valid identifier characters, now parsed as separate tokens
- **Parser Grammar Enhancement**: Added property access grammar `postfix → primary ( "[" expression "]" | "." IDENTIFIER )*`
- **New AST Structure**: Expressions like `user.email` now parsed as `{:property_access, {:identifier, "user"}, "email"}`
- **Instruction Pipeline**: Evaluation generates separate `load` and `access` instructions instead of single `load` with dotted name
- **Mixed Notation Support**: Enables complex expressions like `user.settings['theme']` and `data['users'][0].profile`

### Breaking Changes

#### v3.0.0 - Property Access Parsing Overhaul

This is a **major breaking change** affecting how dot notation is parsed and evaluated:

**⚠️ Context Key Impact**: Context keys containing dots (e.g., `"user.email"`) will no longer match dot notation expressions (`user.email`). The expression `user.email` is now parsed as property access requiring nested structure `%{"user" => %{"email" => "..."}}`

**Migration Required**:

```elixir
# BEFORE (v2.2.0 and earlier) - WILL NO LONGER WORK
context = %{"user.email" => "john@example.com"}
Predicator.evaluate("user.email = 'john@example.com'", context)  # No longer matches

# AFTER (v3.0.0+) - Use proper nested structures
context = %{"user" => %{"email" => "john@example.com"}}
Predicator.evaluate("user.email = 'john@example.com'", context)  # Works correctly
```

**Technical Changes**:

- **Lexer**: Dots no longer valid in identifier characters, parsed as separate `:dot` tokens
- **Parser**: New property access AST nodes `{:property_access, left_node, property}`
- **Evaluator**: New `access` instruction handler, removed dotted identifier support from `load_from_context`
- **Instructions**: `user.email` generates `[["load", "user"], ["access", "email"]]` instead of `[["load", "user.email"]]`

**Benefits**:

- Enables mixed notation: `user.settings['theme']`, `data['users'][0].name`
- Supports SCXML location expressions for assignment operations
- Proper property access semantics for complex data structures
- Foundation for advanced SCXML datamodel integration

## [2.2.0] - 2025-08-24

### Added

#### Bracket Access and Property Access Enhancement

- **Complete Bracket Notation Support**: Implemented full bracket access functionality (`obj['key']`, `arr[0]`, `obj[variable]`)
- **Parser Extensions**: Added postfix parsing for bracket access with recursive chaining support
- **Grammar Enhancement**: Updated grammar with postfix operations: `unary → postfix`, `postfix → primary ( "[" expression "]" )*`
- **New AST Node Type**: Added `{:bracket_access, object, key}` AST node for bracket access expressions
- **Evaluator Support**: Implemented `["bracket_access"]` instruction with comprehensive evaluation logic
- **Mixed Access Patterns**: Full support for chained access like `data['users'][0]['name']`
- **Array Indexing**: Complete array access with bounds checking (`items[0]`, `scores[index]`)
- **Dynamic Key Access**: Support for variable and expression-based keys (`obj[key]`, `items[i + 1]`)
- **Type Safety**: Comprehensive error handling for invalid key types with structured error messages
- **String Visitor Support**: Added round-trip string conversion for bracket access expressions
- **Comprehensive Testing**: Added 12 new parser tests covering all bracket access scenarios

#### Error Handling Architecture Refactoring

- **Modular Error Structure**: Refactored monolithic error handling into individual error modules under `lib/predicator/errors/`
- **Shared Error Utilities**: Created `Predicator.Errors` module with common utility functions for consistent error formatting
- **Individual Error Modules**: Split error handling into focused modules:
  - `Predicator.Errors.TypeMismatchError` - Type validation and mismatch errors
  - `Predicator.Errors.EvaluationError` - Runtime evaluation errors (division by zero, insufficient operands)
  - `Predicator.Errors.UndefinedVariableError` - Variable access errors
  - `Predicator.Errors.ParseError` - Expression parsing and syntax errors
- **Consistent Error Messages**: Unified error message formatting across all error types
- **Code Quality Improvements**: Resolved all credo issues with proper module aliasing and organization

## [2.1.0] - 2025-08-24

### Added

#### Arithmetic and Unary Operations (Complete Implementation)

- **Full Arithmetic Support**: Complete parsing and evaluation pipeline for arithmetic expressions
  - **Binary operations**: `+` (addition), `-` (subtraction), `*` (multiplication), `/` (division), `%` (modulo)
  - **Unary operations**: `-` (unary minus), `!` (unary bang/logical NOT)
- **Proper Precedence**: Mathematical precedence handling (unary → multiplication → addition → equality → comparison)
- **Instruction Execution**: Stack-based evaluator with 7 new instruction handlers
- **Error Handling**: Division by zero protection, type checking, comprehensive error messages
- **Pattern Matching**: Idiomatic Elixir implementation using pattern matching for clean code

## [2.0.0] - 2025-08-21

### Changed

#### Custom Function Architecture Overhaul

- **Breaking Change**: Removed global function registry system in favor of evaluation-time function parameters
- **New API**: Custom functions now passed via `functions:` option in `Predicator.evaluate/3` calls
- **Function Format**: Custom functions use `%{name => {arity, function}}` format where function takes `[args], context` and returns `{:ok, result}` or `{:error, message}`
- **Thread Safety**: Eliminated global state for improved concurrency and thread safety
- **Function Merging**: SystemFunctions always available with custom functions merged in, allowing overrides
- **Simplified Startup**: No application-level function registry initialization required

#### Examples

```elixir
# Old registry-based approach (removed)
Predicator.register_function("double", 1, fn [n], _context -> {:ok, n * 2} end)
Predicator.evaluate("double(21)", %{})

# New evaluation-time approach
custom_functions = %{"double" => {1, fn [n], _context -> {:ok, n * 2} end}}
Predicator.evaluate("double(21)", %{}, functions: custom_functions)

# Custom functions can override built-ins
custom_len = %{"len" => {1, fn [_], _context -> {:ok, "custom_result"} end}}
Predicator.evaluate("len('anything')", %{}, functions: custom_len)  # {:ok, "custom_result"}
```

#### Removed APIs

- `Predicator.register_function/3` - Use `functions:` option instead
- `Predicator.clear_custom_functions/0` - No longer needed
- `Predicator.list_custom_functions/0` - No longer needed
- `Predicator.Functions.Registry` module - Entire registry system removed

### Breaking Changes

#### v2.0.0 - Custom Function Architecture Overhaul

- **Removed**: Global function registry system (`Predicator.Functions.Registry` module)
- **Removed**: `Predicator.register_function/3`, `Predicator.clear_custom_functions/0`, `Predicator.list_custom_functions/0`
- **Changed**: Custom functions now passed via `functions:` option in `evaluate/3` calls instead of global registration
- **Benefit**: Thread-safe, no global state, per-evaluation function scoping
- **Migration**: Replace registry calls with function maps passed to `evaluate/3`

## [1.1.0] - 2025-08-20

### Added

#### Nested Data Structure Access

- **Dot Notation Support**: Access deeply nested data structures using dot notation syntax
- **Enhanced Lexer**: Extended identifier tokenization to include dots (`.`) as valid characters
- **Recursive Context Loading**: Added `load_nested_value/2` function for traversing nested maps
- **Mixed Key Type Support**: Works seamlessly with string keys, atom keys, or mixed key types
- **Graceful Error Handling**: Returns `:undefined` for missing paths or non-map intermediate values
- **Unlimited Nesting Depth**: Support for arbitrarily deep nested structures

#### Single Quote String Support

- **Dual Quote Types**: Added support for single-quoted strings (`'hello'`) alongside double-quoted strings (`"hello"`)
- **Quote Type Preservation**: Round-trip parsing and decompilation preserves original quote type
- **Enhanced Lexer**: Extended string tokenization to handle both quote types with proper escaping
- **AST Enhancement**: New `{:string_literal, value, quote_type}` AST node for quote-aware string handling
- **Escape Sequences**: Full escape sequence support in both quote types (`\'`, `\"`, `\n`, `\t`, etc.)

### Breaking Changes

#### v1.1.0 - Nested Access Parsing

- **Changed**: Variables containing dots (e.g., `"user.email"`) now parsed as nested access paths
- **Impact**: Context keys like `"user.profile.name"` will no longer match identifier `user.profile.name`
- **Solution**: Use proper nested data structures instead of flat keys with dots

## [1.0.1] - 2025-08-20

### Documentation

- Fixes main page for Hex docs

## [1.0.0] - 2025-08-19

### Added

#### Core Language Features

- **Comparison Operators**: Full support for `>`, `<`, `>=`, `<=`, `=`, `!=` with proper type handling
- **Logical Operators**: Case-insensitive `AND`/`and`, `OR`/`or`, `NOT`/`not` with correct precedence
- **Data Types**:
  - Numbers (integers): `42`, `-17`
  - Strings (double-quoted): `"hello"`, `"world"`
  - Booleans: `true`, `false`
  - Date literals: `#2024-01-15#` (ISO 8601 format)
  - DateTime literals: `#2024-01-15T10:30:00Z#` (ISO 8601 with timezone)
  - List literals: `[1, 2, 3]`, `["admin", "manager"]`
  - Identifiers: `score`, `user_name`, `is_active`

#### Advanced Operations

- **Membership Operators**:
  - `in` for element-in-collection testing (`role in ["admin", "manager"]`)
  - `contains` for collection-contains-element testing (`[1, 2, 3] contains 2`)
- **Parenthesized Expressions**: Full support with proper precedence handling
- **Plain Boolean Expressions**: Support for bare identifiers (`active`, `expired`) without explicit `= true`

#### Function System

- **Built-in System Functions**:
  - **String functions**: `len(string)`, `upper(string)`, `lower(string)`, `trim(string)`
  - **Numeric functions**: `abs(number)`, `max(a, b)`, `min(a, b)`
  - **Date functions**: `year(date)`, `month(date)`, `day(date)`
- **Custom Function Registration**: Register anonymous functions with `Predicator.register_function/3`
- **Function Registry**: ETS-based registry with automatic arity validation and error handling
- **Context-Aware Functions**: Functions receive evaluation context for dynamic behavior

#### Architecture & Performance

- **Multi-Stage Compilation Pipeline**: Expression → Lexer → Parser → Compiler → Instructions → Evaluator
- **Compile-Once, Evaluate-Many**: Pre-compile expressions for repeated evaluation
- **Stack-Based Evaluator**: Efficient instruction execution with minimal overhead
- **Comprehensive Error Handling**: Detailed error messages with line/column positioning

#### Developer Experience

- **String Decompilation**: Convert AST back to readable expressions with formatting options
- **Multiple Evaluation APIs**:
  - `evaluate/2` - Returns `{:ok, result}` or `{:error, message}`
  - `evaluate!/2` - Returns result directly or raises exception
  - `compile/1` - Pre-compile expressions to instructions
  - `parse/1` - Parse expressions to AST for inspection
- **Formatting Options**: Configurable spacing (`:normal`, `:compact`, `:verbose`) and parentheses (`:minimal`, `:explicit`, `:none`)

### Breaking Changes

#### ⚠️ COMPLETE LIBRARY REWRITE ⚠️

Version 1.0.0 is a **complete rewrite** of the Predicator library with entirely new:

- API design and function signatures
- Expression syntax and grammar
- Internal architecture and data structures
- Feature set and capabilities

#### Migration Guide

**Migration from versions < 1.0.0 has NOT been tested and is NOT guaranteed to work.**

If you are upgrading from a pre-1.0.0 version:

1. **Treat this as a new library adoption**, not an upgrade
2. **Review all documentation** - APIs have completely changed
3. **Test thoroughly** in development environments
4. **Expect to rewrite** all integration code
5. **Plan for significant refactoring** of existing expressions

Future 1.x.x versions will maintain backwards compatibility and include proper migration guides.

---

For detailed information about upcoming features and development roadmap, see the project README.
