Skip to content

Commit 743ce64

Browse files
johnnytclaude
andauthored
Adds strict equality operators (=== and !==) (#34)
Implements strict equality and strict inequality operators with proper tokenization, parsing, evaluation, and decompilation support. Operators work across all data types including undefined values and preserve round-trip accuracy. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7588e24 commit 743ce64

9 files changed

Lines changed: 347 additions & 34 deletions

File tree

.github/workflows/ci.yml

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -158,32 +158,47 @@ jobs:
158158
dialyzer:
159159
name: Static Type Analysis (Dialyzer)
160160
runs-on: ubuntu-latest
161-
steps:
162-
- name: Checkout code
163-
uses: actions/checkout@v4
164-
165-
- name: Set up Elixir
166-
uses: erlef/setup-beam@v1
167-
with:
168-
elixir-version: '1.17'
169-
otp-version: '27'
170-
171-
- name: Cache dependencies and PLTs
172-
uses: actions/cache@v4
173-
with:
174-
path: |
175-
deps
176-
_build
177-
priv/plts
178-
key: deps-plt-${{ runner.os }}-27-1.17-${{ hashFiles('**/mix.lock') }}
179-
restore-keys: |
180-
deps-plt-${{ runner.os }}-27-1.17-
161+
needs: compile
181162

182-
- name: Install dependencies
183-
run: mix deps.get
184-
185-
- name: Run Dialyzer
186-
run: mix dialyzer
163+
steps:
164+
- name: Checkout code
165+
uses: actions/checkout@v4
166+
167+
- name: Set up Elixir
168+
uses: erlef/setup-beam@v1
169+
with:
170+
elixir-version: '1.18.3'
171+
otp-version: '27.3'
172+
173+
- name: Cache deps
174+
uses: actions/cache@v4
175+
with:
176+
path: |
177+
deps
178+
_build
179+
key: deps-${{ runner.os }}-27.3-1.18.3-${{ hashFiles('**/mix.lock') }}
180+
restore-keys: |
181+
deps-${{ runner.os }}-27.3-1.18.3-
182+
183+
- name: Cache Dialyzer PLTs
184+
uses: actions/cache@v4
185+
with:
186+
path: priv/plts
187+
key: dialyzer-${{ runner.os }}-27.3-1.18.3-${{ hashFiles('**/mix.lock') }}
188+
restore-keys: |
189+
dialyzer-${{ runner.os }}-27.3-1.18.3-
190+
191+
- name: Install dependencies
192+
run: mix deps.get
193+
194+
- name: Compile dependencies
195+
run: mix deps.compile
196+
197+
- name: Compile project
198+
run: mix compile
199+
200+
- name: Run Dialyzer
201+
run: mix dialyzer
187202

188203
quality:
189204
name: Quality Gate

CLAUDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,3 +416,9 @@ test/predicator/
416416
- Elixir ~> 1.11 required
417417
- All dependencies in development/test only
418418
- No runtime dependencies for core functionality
419+
420+
- When creating git commit messages:
421+
- be concise but informative, and highlight the functional changes
422+
- no need to mention code quality improvements as they are expected (unless the functional change is about code quality improvements)
423+
- commit titles should be less than 50 characters and be in the simple present tense (active voice)
424+
- commit descriptions should wrap at about 72 characters and also be in the simple present tense (active voice)

lib/predicator/evaluator.ex

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ defmodule Predicator.Evaluator do
226226

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

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

364364
@spec compare_values(Types.value(), Types.value(), binary()) :: Types.value()
365-
defp compare_values(:undefined, _right, _operator), do: :undefined
366-
defp compare_values(_left, :undefined, _operator), do: :undefined
365+
# Handle undefined values for non-strict operators
366+
defp compare_values(:undefined, _right, operator)
367+
when operator not in ["STRICT_EQ", "STRICT_NE"],
368+
do: :undefined
369+
370+
defp compare_values(_left, :undefined, operator)
371+
when operator not in ["STRICT_EQ", "STRICT_NE"],
372+
do: :undefined
373+
374+
# Strict equality works on all types, including :undefined
375+
defp compare_values(left, right, "STRICT_EQ"), do: left === right
376+
defp compare_values(left, right, "STRICT_NE"), do: left !== right
367377

368378
defp compare_values(left, right, operator) when types_match(left, right) do
369379
case operator do

lib/predicator/lexer.ex

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ defmodule Predicator.Lexer do
5252
| {:eq, pos_integer(), pos_integer(), pos_integer(), binary()}
5353
| {:ne, pos_integer(), pos_integer(), pos_integer(), binary()}
5454
| {:equal_equal, pos_integer(), pos_integer(), pos_integer(), binary()}
55+
| {:strict_equal, pos_integer(), pos_integer(), pos_integer(), binary()}
56+
| {:strict_ne, pos_integer(), pos_integer(), pos_integer(), binary()}
5557
| {:plus, pos_integer(), pos_integer(), pos_integer(), binary()}
5658
| {:minus, pos_integer(), pos_integer(), pos_integer(), binary()}
5759
| {:multiply, pos_integer(), pos_integer(), pos_integer(), binary()}
@@ -251,6 +253,10 @@ defmodule Predicator.Lexer do
251253

252254
?! ->
253255
case rest do
256+
[?=, ?= | rest3] ->
257+
token = {:strict_ne, line, col, 3, "!=="}
258+
tokenize_chars(rest3, line, col + 3, [token | tokens])
259+
254260
[?= | rest2] ->
255261
token = {:ne, line, col, 2, "!="}
256262
tokenize_chars(rest2, line, col + 2, [token | tokens])
@@ -262,6 +268,10 @@ defmodule Predicator.Lexer do
262268

263269
?= ->
264270
case rest do
271+
[?=, ?= | rest3] ->
272+
token = {:strict_equal, line, col, 3, "==="}
273+
tokenize_chars(rest3, line, col + 3, [token | tokens])
274+
265275
[?= | rest2] ->
266276
token = {:equal_equal, line, col, 2, "=="}
267277
tokenize_chars(rest2, line, col + 2, [token | tokens])

lib/predicator/parser.ex

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ defmodule Predicator.Parser do
100100
@typedoc """
101101
Comparison operators in the AST.
102102
"""
103-
@type comparison_op :: :gt | :lt | :gte | :lte | :eq | :ne
103+
@type comparison_op ::
104+
:gt | :lt | :gte | :lte | :eq | :equal_equal | :ne | :strict_eq | :strict_ne
104105

105106
@typedoc """
106107
Arithmetic operators in the AST.
@@ -330,13 +331,30 @@ defmodule Predicator.Parser do
330331
case peek_token(new_state) do
331332
# Comparison operators (including equality)
332333
{op_type, _line, _col, _len, _value}
333-
when op_type in [:gt, :lt, :gte, :lte, :eq, :equal_equal, :ne] ->
334+
when op_type in [
335+
:gt,
336+
:lt,
337+
:gte,
338+
:lte,
339+
:eq,
340+
:equal_equal,
341+
:ne,
342+
:strict_equal,
343+
:strict_ne
344+
] ->
334345
op_state = advance(new_state)
335346

336347
case parse_addition(op_state) do
337348
{:ok, right, final_state} ->
338-
# Map == to :eq for consistency, != stays as :ne
339-
normalized_op = if op_type == :equal_equal, do: :eq, else: op_type
349+
# Map tokens to AST operators
350+
normalized_op =
351+
case op_type do
352+
:equal_equal -> :equal_equal
353+
:strict_equal -> :strict_eq
354+
:strict_ne -> :strict_ne
355+
_other_op_type -> op_type
356+
end
357+
340358
ast = {:comparison, normalized_op, left, right}
341359
{:ok, ast, final_state}
342360

@@ -724,6 +742,9 @@ defmodule Predicator.Parser do
724742
defp format_token(:lte, _value), do: "'<='"
725743
defp format_token(:eq, _value), do: "'='"
726744
defp format_token(:ne, _value), do: "'!='"
745+
defp format_token(:equal_equal, _value), do: "'=='"
746+
defp format_token(:strict_equal, _value), do: "'==='"
747+
defp format_token(:strict_ne, _value), do: "'!=='"
727748
defp format_token(:and_op, _value), do: "'AND'"
728749
defp format_token(:or_op, _value), do: "'OR'"
729750
defp format_token(:not_op, _value), do: "'NOT'"
@@ -742,7 +763,6 @@ defmodule Predicator.Parser do
742763
defp format_token(:multiply, _value), do: "'*'"
743764
defp format_token(:divide, _value), do: "'/'"
744765
defp format_token(:modulo, _value), do: "'%'"
745-
defp format_token(:equal_equal, _value), do: "'=='"
746766
defp format_token(:and_and, _value), do: "'&&'"
747767
defp format_token(:or_or, _value), do: "'||'"
748768
defp format_token(:bang, _value), do: "'!'"

lib/predicator/visitors/instructions_visitor.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,10 @@ defmodule Predicator.Visitors.InstructionsVisitor do
193193
defp map_comparison_op(:gte), do: "GTE"
194194
defp map_comparison_op(:lte), do: "LTE"
195195
defp map_comparison_op(:eq), do: "EQ"
196+
defp map_comparison_op(:equal_equal), do: "EQ"
196197
defp map_comparison_op(:ne), do: "NE"
198+
defp map_comparison_op(:strict_eq), do: "STRICT_EQ"
199+
defp map_comparison_op(:strict_ne), do: "STRICT_NE"
197200

198201
# Helper function to map AST arithmetic operators to instruction format
199202
@spec map_arithmetic_op(Parser.arithmetic_op()) :: binary()

lib/predicator/visitors/string_visitor.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,10 @@ defmodule Predicator.Visitors.StringVisitor do
241241
defp format_operator(:gte), do: ">="
242242
defp format_operator(:lte), do: "<="
243243
defp format_operator(:eq), do: "="
244+
defp format_operator(:equal_equal), do: "=="
244245
defp format_operator(:ne), do: "!="
246+
defp format_operator(:strict_eq), do: "==="
247+
defp format_operator(:strict_ne), do: "!=="
245248
defp format_operator(:add), do: "+"
246249
defp format_operator(:subtract), do: "-"
247250
defp format_operator(:multiply), do: "*"

test/predicator/parser_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1065,7 +1065,7 @@ defmodule Predicator.ParserTest do
10651065
result = Parser.parse(tokens)
10661066

10671067
expected_ast =
1068-
{:comparison, :eq, {:arithmetic, :add, {:identifier, "a"}, {:identifier, "b"}},
1068+
{:comparison, :equal_equal, {:arithmetic, :add, {:identifier, "a"}, {:identifier, "b"}},
10691069
{:arithmetic, :multiply, {:identifier, "c"}, {:identifier, "d"}}}
10701070

10711071
assert {:ok, ^expected_ast} = result

0 commit comments

Comments
 (0)