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
65 changes: 40 additions & 25 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,32 +158,47 @@ jobs:
dialyzer:
name: Static Type Analysis (Dialyzer)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Elixir
uses: erlef/setup-beam@v1
with:
elixir-version: '1.17'
otp-version: '27'

- name: Cache dependencies and PLTs
uses: actions/cache@v4
with:
path: |
deps
_build
priv/plts
key: deps-plt-${{ runner.os }}-27-1.17-${{ hashFiles('**/mix.lock') }}
restore-keys: |
deps-plt-${{ runner.os }}-27-1.17-
needs: compile

- name: Install dependencies
run: mix deps.get

- name: Run Dialyzer
run: mix dialyzer
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Elixir
uses: erlef/setup-beam@v1
with:
elixir-version: '1.18.3'
otp-version: '27.3'

- name: Cache deps
uses: actions/cache@v4
with:
path: |
deps
_build
key: deps-${{ runner.os }}-27.3-1.18.3-${{ hashFiles('**/mix.lock') }}
restore-keys: |
deps-${{ runner.os }}-27.3-1.18.3-

- name: Cache Dialyzer PLTs
uses: actions/cache@v4
with:
path: priv/plts
key: dialyzer-${{ runner.os }}-27.3-1.18.3-${{ hashFiles('**/mix.lock') }}
restore-keys: |
dialyzer-${{ runner.os }}-27.3-1.18.3-

- name: Install dependencies
run: mix deps.get

- name: Compile dependencies
run: mix deps.compile

- name: Compile project
run: mix compile

- name: Run Dialyzer
run: mix dialyzer

quality:
name: Quality Gate
Expand Down
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,3 +416,9 @@ test/predicator/
- Elixir ~> 1.11 required
- All dependencies in development/test only
- No runtime dependencies for core functionality

- When creating git commit messages:
- be concise but informative, and highlight the functional changes
- no need to mention code quality improvements as they are expected (unless the functional change is about code quality improvements)
- commit titles should be less than 50 characters and be in the simple present tense (active voice)
- commit descriptions should wrap at about 72 characters and also be in the simple present tense (active voice)
16 changes: 13 additions & 3 deletions lib/predicator/evaluator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ defmodule Predicator.Evaluator do

# Comparison instruction
defp execute_instruction(%__MODULE__{} = evaluator, ["compare", operator])
when operator in ["GT", "LT", "EQ", "GTE", "LTE", "NE"] do
when operator in ["GT", "LT", "EQ", "GTE", "LTE", "NE", "STRICT_EQ", "STRICT_NE"] do
execute_compare(evaluator, operator)
end

Expand Down Expand Up @@ -362,8 +362,18 @@ defmodule Predicator.Evaluator do
(is_map(a) and is_map(b) and not is_struct(a) and not is_struct(b))

@spec compare_values(Types.value(), Types.value(), binary()) :: Types.value()
defp compare_values(:undefined, _right, _operator), do: :undefined
defp compare_values(_left, :undefined, _operator), do: :undefined
# Handle undefined values for non-strict operators
defp compare_values(:undefined, _right, operator)
when operator not in ["STRICT_EQ", "STRICT_NE"],
do: :undefined

defp compare_values(_left, :undefined, operator)
when operator not in ["STRICT_EQ", "STRICT_NE"],
do: :undefined

# Strict equality works on all types, including :undefined
defp compare_values(left, right, "STRICT_EQ"), do: left === right
defp compare_values(left, right, "STRICT_NE"), do: left !== right

defp compare_values(left, right, operator) when types_match(left, right) do
case operator do
Expand Down
10 changes: 10 additions & 0 deletions lib/predicator/lexer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ defmodule Predicator.Lexer do
| {:eq, pos_integer(), pos_integer(), pos_integer(), binary()}
| {:ne, pos_integer(), pos_integer(), pos_integer(), binary()}
| {:equal_equal, pos_integer(), pos_integer(), pos_integer(), binary()}
| {:strict_equal, pos_integer(), pos_integer(), pos_integer(), binary()}
| {:strict_ne, pos_integer(), pos_integer(), pos_integer(), binary()}
| {:plus, pos_integer(), pos_integer(), pos_integer(), binary()}
| {:minus, pos_integer(), pos_integer(), pos_integer(), binary()}
| {:multiply, pos_integer(), pos_integer(), pos_integer(), binary()}
Expand Down Expand Up @@ -251,6 +253,10 @@ defmodule Predicator.Lexer do

?! ->
case rest do
[?=, ?= | rest3] ->
token = {:strict_ne, line, col, 3, "!=="}
tokenize_chars(rest3, line, col + 3, [token | tokens])

[?= | rest2] ->
token = {:ne, line, col, 2, "!="}
tokenize_chars(rest2, line, col + 2, [token | tokens])
Expand All @@ -262,6 +268,10 @@ defmodule Predicator.Lexer do

?= ->
case rest do
[?=, ?= | rest3] ->
token = {:strict_equal, line, col, 3, "==="}
tokenize_chars(rest3, line, col + 3, [token | tokens])

[?= | rest2] ->
token = {:equal_equal, line, col, 2, "=="}
tokenize_chars(rest2, line, col + 2, [token | tokens])
Expand Down
30 changes: 25 additions & 5 deletions lib/predicator/parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ defmodule Predicator.Parser do
@typedoc """
Comparison operators in the AST.
"""
@type comparison_op :: :gt | :lt | :gte | :lte | :eq | :ne
@type comparison_op ::
:gt | :lt | :gte | :lte | :eq | :equal_equal | :ne | :strict_eq | :strict_ne

@typedoc """
Arithmetic operators in the AST.
Expand Down Expand Up @@ -330,13 +331,30 @@ defmodule Predicator.Parser do
case peek_token(new_state) do
# Comparison operators (including equality)
{op_type, _line, _col, _len, _value}
when op_type in [:gt, :lt, :gte, :lte, :eq, :equal_equal, :ne] ->
when op_type in [
:gt,
:lt,
:gte,
:lte,
:eq,
:equal_equal,
:ne,
:strict_equal,
:strict_ne
] ->
op_state = advance(new_state)

case parse_addition(op_state) do
{:ok, right, final_state} ->
# Map == to :eq for consistency, != stays as :ne
normalized_op = if op_type == :equal_equal, do: :eq, else: op_type
# Map tokens to AST operators
normalized_op =
case op_type do
:equal_equal -> :equal_equal
:strict_equal -> :strict_eq
:strict_ne -> :strict_ne
_other_op_type -> op_type
end

ast = {:comparison, normalized_op, left, right}
{:ok, ast, final_state}

Expand Down Expand Up @@ -724,6 +742,9 @@ defmodule Predicator.Parser do
defp format_token(:lte, _value), do: "'<='"
defp format_token(:eq, _value), do: "'='"
defp format_token(:ne, _value), do: "'!='"
defp format_token(:equal_equal, _value), do: "'=='"
defp format_token(:strict_equal, _value), do: "'==='"
defp format_token(:strict_ne, _value), do: "'!=='"
defp format_token(:and_op, _value), do: "'AND'"
defp format_token(:or_op, _value), do: "'OR'"
defp format_token(:not_op, _value), do: "'NOT'"
Expand All @@ -742,7 +763,6 @@ defmodule Predicator.Parser do
defp format_token(:multiply, _value), do: "'*'"
defp format_token(:divide, _value), do: "'/'"
defp format_token(:modulo, _value), do: "'%'"
defp format_token(:equal_equal, _value), do: "'=='"
defp format_token(:and_and, _value), do: "'&&'"
defp format_token(:or_or, _value), do: "'||'"
defp format_token(:bang, _value), do: "'!'"
Expand Down
3 changes: 3 additions & 0 deletions lib/predicator/visitors/instructions_visitor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,10 @@ defmodule Predicator.Visitors.InstructionsVisitor do
defp map_comparison_op(:gte), do: "GTE"
defp map_comparison_op(:lte), do: "LTE"
defp map_comparison_op(:eq), do: "EQ"
defp map_comparison_op(:equal_equal), do: "EQ"
defp map_comparison_op(:ne), do: "NE"
defp map_comparison_op(:strict_eq), do: "STRICT_EQ"
defp map_comparison_op(:strict_ne), do: "STRICT_NE"

# Helper function to map AST arithmetic operators to instruction format
@spec map_arithmetic_op(Parser.arithmetic_op()) :: binary()
Expand Down
3 changes: 3 additions & 0 deletions lib/predicator/visitors/string_visitor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,10 @@ defmodule Predicator.Visitors.StringVisitor do
defp format_operator(:gte), do: ">="
defp format_operator(:lte), do: "<="
defp format_operator(:eq), do: "="
defp format_operator(:equal_equal), do: "=="
defp format_operator(:ne), do: "!="
defp format_operator(:strict_eq), do: "==="
defp format_operator(:strict_ne), do: "!=="
defp format_operator(:add), do: "+"
defp format_operator(:subtract), do: "-"
defp format_operator(:multiply), do: "*"
Expand Down
2 changes: 1 addition & 1 deletion test/predicator/parser_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1065,7 +1065,7 @@ defmodule Predicator.ParserTest do
result = Parser.parse(tokens)

expected_ast =
{:comparison, :eq, {:arithmetic, :add, {:identifier, "a"}, {:identifier, "b"}},
{:comparison, :equal_equal, {:arithmetic, :add, {:identifier, "a"}, {:identifier, "b"}},
{:arithmetic, :multiply, {:identifier, "c"}, {:identifier, "d"}}}

assert {:ok, ^expected_ast} = result
Expand Down
Loading