Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions lib/phronesis.ex
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ defmodule Phronesis do
iex> Phronesis.parse("CONST x = 42")
{:ok, [{:const, "x", {:literal, :integer, 42}}]}

iex> Phronesis.parse("INVALID SYNTAX")
{:error, {:parse_error, "expected POLICY, IMPORT, or CONST", 1, 1}}
iex> {:error, {:parse_error, msg, 1, 1, _details}} = Phronesis.parse("INVALID SYNTAX")
iex> msg =~ "expected"
true
"""
@spec parse(String.t()) :: {:ok, list()} | {:error, term()}
def parse(source) when is_binary(source) do
Expand Down
30 changes: 30 additions & 0 deletions lib/phronesis/ast.ex
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,36 @@ defmodule Phronesis.AST do
{:literal, type, value}
end

@doc """
Create an interpolated string node (v0.2.x).

Parts is a list of either:
- `{:string, "literal text"}`
- `{:expr, [tokens]}`
"""
@spec interpolated_string([{:string, String.t()} | {:expr, list()}]) :: expression()
def interpolated_string(parts) do
{:interpolated_string, parts}
end

@doc """
Create a field access node (record.field).
"""
@spec field_access(expression(), name()) :: expression()
def field_access(base, field) do
{:field_access, base, field}
end

@doc """
Create an optional access node (v0.2.x: record?.field).

Returns null if base is null, otherwise accesses the field.
"""
@spec optional_access(expression(), name()) :: expression()
def optional_access(base, field) do
{:optional_access, base, field}
end

@doc """
Create an execute action node.
"""
Expand Down
78 changes: 78 additions & 0 deletions lib/phronesis/interpreter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,84 @@ defmodule Phronesis.Interpreter do
end
end

# Field access: record.field (v0.2.x)
defp eval_expr({:field_access, base, field}, state) do
with {:ok, base_value, state} <- eval_expr(base, state) do
case base_value do
%{} = map ->
field_atom = String.to_existing_atom(field)
{:ok, Map.get(map, field_atom, Map.get(map, field)), state}

_ ->
{:error, {:type_error, "cannot access field '#{field}' on non-map value"}}
end
end
rescue
ArgumentError ->
{:ok, base_value, state} = eval_expr(base, state)
case base_value do
%{} = map -> {:ok, Map.get(map, field), state}
_ -> {:error, {:type_error, "cannot access field '#{field}' on non-map value"}}
end
end

# Optional access: record?.field - returns nil if base is nil (v0.2.x)
defp eval_expr({:optional_access, base, field}, state) do
with {:ok, base_value, state} <- eval_expr(base, state) do
case base_value do
nil ->
{:ok, nil, state}

%{} = map ->
field_atom =
try do
String.to_existing_atom(field)
rescue
ArgumentError -> nil
end

value = if field_atom, do: Map.get(map, field_atom, Map.get(map, field)), else: Map.get(map, field)
{:ok, value, state}

_ ->
{:error, {:type_error, "cannot access field '#{field}' on non-map value"}}
end
end
end

# Interpolated string (v0.2.x)
defp eval_expr({:interpolated_string, parts}, state) do
eval_interpolated_parts(parts, state, [])
end

defp eval_interpolated_parts([], state, acc) do
{:ok, IO.iodata_to_binary(Enum.reverse(acc)), state}
end

defp eval_interpolated_parts([{:string, s} | rest], state, acc) do
eval_interpolated_parts(rest, state, [s | acc])
end

defp eval_interpolated_parts([{:expr, tokens} | rest], state, acc) do
# Parse and evaluate the tokens as an expression
case Phronesis.Parser.parse(tokens ++ [{:eof, nil, 0, 0}]) do
{:ok, [expr]} ->
case eval_expr(expr, state) do
{:ok, value, state} ->
eval_interpolated_parts(rest, state, [to_string(value) | acc])

{:error, _} = err ->
err
end

{:ok, _} ->
{:error, {:interpolation_error, "interpolation must be a single expression"}}

{:error, _} = err ->
err
end
end

defp eval_args(args, state) do
Enum.reduce_while(args, {:ok, [], state}, fn
{:named_arg, name, expr}, {:ok, acc, state} ->
Expand Down
Loading
Loading