View Source Dequel.Semantic.Analyzer (Dequel v0.6.0)

Semantic analyzer that transforms parser AST into typed AST.

The parser produces syntax-only AST with :block nodes for relation filtering. This analyzer walks the AST and resolves :block nodes to their semantic equivalents based on schema information:

  • has_many relations → :exists with [cardinality: :many]
  • belongs_to / has_one relations → :join with [cardinality: :one]
  • embeds_many:embedded with [cardinality: :many]
  • embeds_one:embedded with [cardinality: :one]

Additionally, field values are coerced to their proper types based on schema info.

Example

# Parser output (untyped AST)
{:block, [], [:items, {:==, [], [:name, "foo"]}]}

# After semantic analysis with has_many relation
{:exists, [cardinality: :many], [:items, {:==, [], [:name, "foo"]}]}

# After semantic analysis with belongs_to relation
{:join, [cardinality: :one], [:author, {:==, [], [:name, "foo"]}]}

# Type coercion
{:==, [], [:age, "25"]}    {:==, [], [:age, 25]}

The semantic layer also enables LSP support by providing type information that can be used for validation, completion, and hover information.

Schema Resolver

The analyzer accepts a schema resolver function that returns field/relation info:

resolver = fn field ->
  case field do
    :items -> %{kind: :has_many, resolver: nested_resolver}
    :author -> %{kind: :belongs_to, resolver: nested_resolver}
    :address -> %{kind: :embeds_one, resolver: nested_resolver}
    :age -> %{kind: :field, type: :integer}
    _ -> nil
  end
end

Analyzer.analyze(ast, resolver)

For Ecto schemas, use Analyzer.ecto_resolver/1 to create a resolver.

Summary

Functions

Analyzes the AST and resolves block nodes to their typed equivalents.

Creates a resolver function for an Ecto schema module.

Types

@type ast() :: tuple() | atom()
@type field_info() :: %{kind: :field, type: atom()}
@type relation_info() :: %{kind: relation_kind(), resolver: resolver()}
@type relation_kind() ::
  :has_many | :has_one | :belongs_to | :embeds_one | :embeds_many
@type resolver() :: (atom() -> field_info() | relation_info() | nil) | nil

Functions

Link to this function

analyze(ast, resolver \\ nil)

View Source
@spec analyze(ast(), resolver()) :: ast()

Analyzes the AST and resolves block nodes to their typed equivalents.

Takes an optional resolver function that maps field names to field/relation info. The resolver should return one of:

  • %{kind: :has_many | :has_one | :belongs_to | :embeds_one | :embeds_many, resolver: fn} for relations

  • %{kind: :field, type: atom} for typed fields
  • nil for unknown fields

If no resolver is provided, block nodes are left unchanged and no type coercion occurs.

Examples

Block nodes are transformed based on relation type:

iex> ast = {:block, [], [:items, {:==, [], [:name, "foo"]}]}
iex> resolver = fn :items -> %{kind: :has_many, resolver: nil}; _ -> nil end
iex> Dequel.Semantic.Analyzer.analyze(ast, resolver)
{:exists, [cardinality: :many], [:items, {:==, [], [:name, "foo"]}]}

iex> ast = {:block, [], [:author, {:==, [], [:name, "Tolkien"]}]}
iex> resolver = fn :author -> %{kind: :belongs_to, resolver: nil}; _ -> nil end
iex> Dequel.Semantic.Analyzer.analyze(ast, resolver)
{:join, [cardinality: :one], [:author, {:==, [], [:name, "Tolkien"]}]}

iex> ast = {:block, [], [:address, {:==, [], [:city, "NYC"]}]}
iex> resolver = fn :address -> %{kind: :embeds_one, resolver: nil}; _ -> nil end
iex> Dequel.Semantic.Analyzer.analyze(ast, resolver)
{:embedded, [cardinality: :one], [:address, {:==, [], [:city, "NYC"]}]}

Field values are coerced based on type:

iex> ast = {:==, [], [:age, "25"]}
iex> resolver = fn :age -> %{kind: :field, type: :integer}; _ -> nil end
iex> Dequel.Semantic.Analyzer.analyze(ast, resolver)
{:==, [], [:age, 25]}

iex> ast = {:==, [], [:active, "true"]}
iex> resolver = fn :active -> %{kind: :field, type: :boolean}; _ -> nil end
iex> Dequel.Semantic.Analyzer.analyze(ast, resolver)
{:==, [], [:active, true]}

Comparison operators are coerced based on field type:

iex> ast = {:>, [], [:age, "18"]}
iex> resolver = fn :age -> %{kind: :field, type: :integer}; _ -> nil end
iex> Dequel.Semantic.Analyzer.analyze(ast, resolver)
{:>, [], [:age, 18]}

iex> ast = {:between, [], [:price, "10", "50"]}
iex> resolver = fn :price -> %{kind: :field, type: :integer}; _ -> nil end
iex> Dequel.Semantic.Analyzer.analyze(ast, resolver)
{:between, [], [:price, 10, 50]}

Without a resolver, AST passes through unchanged:

iex> Dequel.Semantic.Analyzer.analyze({:==, [], [:name, "foo"]}, nil)
{:==, [], [:name, "foo"]}

iex> Dequel.Semantic.Analyzer.analyze({:block, [], [:items, {:==, [], [:name, "foo"]}]}, nil)
{:block, [], [:items, {:==, [], [:name, "foo"]}]}
@spec ecto_resolver(module()) :: resolver()

Creates a resolver function for an Ecto schema module.

The schema must have __schema__/2 support for associations, embeds, and types.

Example

resolver = Analyzer.ecto_resolver(MyApp.ItemSchema)
Analyzer.analyze(ast, resolver)