# `Predicator.ContextLocation`
[🔗](https://github.com/riddler/predicator-ex/blob/v9.4.1/lib/predicator/context_location.ex#L1)

Resolves location paths for assignment operations in SCXML datamodel expressions.

This module takes parsed AST nodes and extracts location paths that can be used
for assignment operations. It validates that expressions represent assignable
locations (l-values) rather than computed values.

## Location Path Format

Location paths are returned as lists of keys/indices that represent the path
to a specific location in the context data structure:

- `["user"]` - top-level variable `user`
- `["user", "name"]` - property access `user.name`
- `["items", 0]` - array access `items[0]`
- `["user", "profile", "settings", "theme"]` - nested access `user.profile.settings.theme`

## Assignable vs Non-Assignable

**Assignable (valid locations):**
- Simple identifiers: `user`
- Property access: `user.name`, `obj.prop`
- Bracket access: `items[0]`, `obj["key"]`
- Mixed notation: `user.items[0].name`, `data["users"][0]["profile"]`

**Non-Assignable (invalid locations):**
- Literals: `42`, `"string"`, `true`
- Function calls: `len(items)`, `upper(name)`
- Arithmetic expressions: `user.age + 1`, `items[i + 1]`
- Any computed values that can't be used as assignment targets

## Assignment

`put/3` writes a value at a resolved location path, creating any missing
intermediate containers along the way (auto-vivification):

- A segment whose current value is missing, `nil`, or `:undefined` is created:
  a `%{}` when the next segment is a string key, a `[]` when it is an integer
  index.
- Existing data is never destroyed to make room. A path that traverses a
  scalar returns a `:not_a_container` error, as does a string segment against
  an existing list.
- Integer indices past the end of a list pad the gap with `:undefined`;
  negative indices return `:invalid_index`.
- The leaf is always overwritten, whatever it currently holds.

## Examples

    iex> alias Predicator.{ContextLocation, Lexer, Parser}
    iex> {:ok, tokens} = Lexer.tokenize("user.name")
    iex> {:ok, ast} = Parser.parse(tokens)
    iex> ContextLocation.resolve(ast, %{"user" => %{"name" => "John"}})
    {:ok, ["user", "name"]}

    iex> alias Predicator.{ContextLocation, Lexer, Parser}
    iex> {:ok, tokens} = Lexer.tokenize("items[0]")
    iex> {:ok, ast} = Parser.parse(tokens)
    iex> ContextLocation.resolve(ast, %{"items" => [1, 2, 3]})
    {:ok, ["items", 0]}

# `location_path`

```elixir
@type location_path() :: [binary() | integer()]
```

A location path representing the sequence of keys/indices to reach a location in the context.

String keys represent object properties, integer keys represent array indices.

# `location_result`

```elixir
@type location_result() ::
  {:ok, location_path()} | {:error, Predicator.Errors.LocationError.t()}
```

Result of resolving a location expression.

Returns either a successful path or a structured error explaining why the location is invalid.

# `put_result`

```elixir
@type put_result() ::
  {:ok, Predicator.Types.context()}
  | {:error, Predicator.Errors.LocationError.t()}
```

Result of writing a value at a location path.

Returns either the updated context or a structured error explaining why the
write could not be performed.

# `put`

```elixir
@spec put(Predicator.Types.context(), location_path(), term()) :: put_result()
```

Writes `value` into `context` at `path`, creating missing intermediate containers.

Auto-vivification is ECMAScript-like: a missing (or `nil`/`:undefined`) segment
is created as a map when the next segment is a string key, and as a list when
the next segment is an integer index. Existing data is never destroyed - a path
that traverses a scalar returns a `:not_a_container` error. The leaf is always
overwritten.

Integer indices past the end of an existing list pad the gap with `:undefined`.
Negative indices are rejected with `:invalid_index`. An integer segment against
an existing *map* is allowed and writes with the integer key, because refusing
would destroy data the caller put there.

> #### String and integer keys only {: .info}
>
> `put/3` consults string and integer keys, never atom keys. Writing
> `["user", "name"]` into a context holding `%{user: %{}}` vivifies a new
> `"user"` map beside the atom key rather than descending into it. A caller
> reaching `put/3` via `Predicator.Context.assign/3` never has atom keys to
> worry about in the first place, since `Context.new/2`/`bind/3` already
> normalize them away deeply and eagerly. This note matters only for a
> caller invoking `put/3` directly on a hand-built map.

## Parameters

- `context` - The context map to write into
- `path` - A location path as returned by `resolve/2`
- `value` - The value to write at the leaf

## Returns

- `{:ok, context}` - The updated context
- `{:error, %LocationError{}}` - An error explaining why the write failed

## Examples

    iex> Predicator.ContextLocation.put(%{}, ["user", "profile", "name"], "Ada")
    {:ok, %{"user" => %{"profile" => %{"name" => "Ada"}}}}

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

    iex> {:error, error} = Predicator.ContextLocation.put(%{"user" => 5}, ["user", "name"], "Ada")
    iex> error.type
    :not_a_container

# `resolve`

```elixir
@spec resolve(term(), Predicator.Types.context()) :: location_result()
```

Resolves an AST node to a location path for assignment operations.

Takes a parsed AST node and attempts to extract a valid location path.
Validates that the expression represents an assignable location.

## Parameters

- `ast_node` - The parsed AST node to resolve
- `context` - The evaluation context (used for validating array bounds, etc.)

## Returns

- `{:ok, path}` - A valid location path
- `{:error, %LocationError{}}` - An error explaining why the location is invalid

## Examples

    # Simple identifier
    resolve({:identifier, "user", nil}, %{})
    #=> {:ok, ["user"]}

    # Property access
    resolve({:property_access, {:identifier, "user", nil}, "name", nil}, %{})
    #=> {:ok, ["user", "name"]}

    # Bracket access
    resolve({:bracket_access, {:identifier, "items", nil}, {:literal, 0, nil}, nil}, %{})
    #=> {:ok, ["items", 0]}

    # Invalid: literal value
    resolve({:literal, 42, nil}, %{})
    #=> {:error, %LocationError{type: :not_assignable, message: "Cannot assign to literal value"}}

# `resolve_expression`

```elixir
@spec resolve_expression(binary(), Predicator.Types.context()) ::
  location_result() | {:error, Predicator.Errors.ParseError.t()}
```

Tokenizes, parses, and resolves a location expression in one step.

Equivalent to parsing `expression` and calling `resolve/2` on the result,
except parse and tokenize errors are wrapped as `Predicator.Errors.ParseError`
the same way `Predicator.context_location/3` does - this *is* that
function's implementation, extracted so `Predicator.Context.assign/3` can
share it without depending on the `Predicator` module.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
