diff --git a/lib/spitfire.ex b/lib/spitfire.ex index c561f75..4ac3eee 100644 --- a/lib/spitfire.ex +++ b/lib/spitfire.ex @@ -319,7 +319,6 @@ defmodule Spitfire do defp do_parse_expression(parser, {associativity, precedence}, is_list, is_map, is_top) do stop_before_stab_op? = Map.get(parser, :stop_before_stab_op?, false) stop_before_map_op? = Map.get(parser, :stop_before_map_op?, false) - inside_map_update_pairs? = Map.get(parser, :inside_map_update_pairs, false) prefix = case current_token_type(parser) do @@ -398,8 +397,13 @@ defmodule Spitfire do :when_op -> parse_infix_expression(next_token(parser), left) - :pipe_op when is_map and not inside_map_update_pairs? -> - parse_pipe_op_in_map(next_token(parser), left) + :pipe_op when is_map -> + # When already inside map pairs (not first), treat | as infix operator + if Map.get(parser, :inside_map_update_pairs, false) or Map.get(parser, :in_map_pairs, false) do + parse_infix_expression(next_token(parser), left) + else + parse_pipe_op_in_map(next_token(parser), left) + end :pipe_op -> parse_infix_expression(next_token(parser), left) @@ -919,6 +923,9 @@ defmodule Spitfire do # backtrack later Process.put(:comma_list_parsers, [parser]) + # After first expression in map, subsequent | should be infix not map update + parser = if is_map, do: Map.put(parser, :in_map_pairs, true), else: parser + {items, parser} = while2 peek_token(parser) == :"," <- parser do parser = next_token(parser) @@ -938,6 +945,7 @@ defmodule Spitfire do end end + parser = if is_map, do: Map.delete(parser, :in_map_pairs), else: parser {[front | items], parser} end end @@ -1037,34 +1045,14 @@ defmodule Spitfire do defp unmatched_expr?(_), do: false - defp parse_prefix_lone_identifer(parser) do - trace "parse_prefix_lone_identifer", trace_meta(parser) do + defp parse_struct_type_prefix(parser) do + trace "parse_struct_type_prefix", trace_meta(parser) do token = current_token(parser) meta = current_meta(parser) - parser = parser |> next_token() |> eat_eoe() - - {rhs, parser} = - case current_token_type(parser) do - :"(" -> - parse_grouped_expression(parser) - - _ -> - {ident, parser} = parse_lone_identifier(parser) - {associativity, precedence} = @lowest - - while peek_token(parser) in [:., :"["] && - calc_prec(parser, associativity, precedence) <- {ident, parser} do - case peek_token(parser) do - :. -> parse_dot_for_struct_type(next_token(parser), ident) - :"[" -> parse_access_expression(next_token(parser), ident) - end - end - end - - ast = {token, meta, [rhs]} - - {ast, parser} + parser = Map.delete(parser, :inside_map_update_pairs) + {rhs, parser} = parse_struct_type(parser) + {{token, meta, [rhs]}, parser} end end @@ -1126,7 +1114,12 @@ defmodule Spitfire do trace "parse_stab_expression", trace_meta(parser) do token = current_token(parser) meta = current_meta(parser) - newlines = get_newlines(parser) + + newlines = + case current_newlines(parser) do + nil -> get_newlines(parser) + nl -> [newlines: nl] + end parser = eat_at(parser, [:eol, :";"], 1) old_nesting = parser.nesting @@ -1188,8 +1181,35 @@ defmodule Spitfire do :-> -> token = current_token(parser) meta = current_meta(parser) - newlines = get_newlines(parser) - has_leading_semicolon = peek_token(parser) == :";" + + newlines = + case current_newlines(parser) do + nil -> get_newlines(parser) + nl -> [newlines: nl] + end + + # Check if we have a semicolon right after -> (possibly after eol) + # Check if there's a leading semicolon right after -> + # Handles both ";expr" and "\n;expr" patterns + has_leading_semicolon = + case peek_token(parser) do + :";" -> + true + + :eol -> + # Peek at the next token after eol without consuming + # We need to manually check the token sequence + with {:eol, _} <- parser.current_token, + # Look at the tokens list to find what comes after eol + [{:";", _} | _] <- parser.tokens do + true + else + _ -> false + end + + _ -> + false + end parser = eat_eoe_at(parser, 1) @@ -1200,7 +1220,14 @@ defmodule Spitfire do while2 not stab_state_set?(parser) and peek_token(parser) not in [:eof, :end, :")", :block_identifier, :->] <- parser do parser = next_token(parser) - {ast, parser} = parse_stab_aware_expression(parser) + + # If we encounter a semicolon, it represents a nil expression + {ast, parser} = + if current_token_type(parser) == :";" do + {nil, eat_eoe(parser)} + else + parse_stab_aware_expression(parser) + end if stab_state_set?(parser) do {:filter, {nil, next_token(parser)}} @@ -1362,9 +1389,15 @@ defmodule Spitfire do # e.g., `() when bar 1, 2, 3 -> foo()` should parse `bar 1, 2, 3` as the guard {rhs, parser} = if token == :when do + # Check if when has simple LHS (empty block or comma args). + # If so and we're in fn context, use lower precedence to allow <- in guard. + in_fn_context = Map.get(parser, :stop_before_stab_op?, false) + simple_lhs = match?({:__block__, _, []}, lhs) or match?({:comma, _, _}, lhs) + when_precedence = if in_fn_context and simple_lhs, do: @list_comma, else: effective_precedence + {rhs, parser} = with_context(parser, %{stop_before_stab_op?: true}, fn parser -> - parse_expression(parser, effective_precedence, false, false, false) + parse_expression(parser, when_precedence, false, false, false) end) parser = Map.delete(parser, :stab_state) @@ -1825,7 +1858,23 @@ defmodule Spitfire do if current_token_type(parser) == :stab_op do parse_stab_expression(parser) else - parse_expression(parser, @lowest, false, false, true) + # Use stab-aware expression parsing to properly handle stabs in do-block contexts + {ast, parser} = parse_stab_aware_expression(parser, @lowest, true) + + case Map.get(parser, :stab_state) do + %{ast: lhs} -> + parser = + if current_token(parser) != :-> do + next_token(Map.delete(parser, :stab_state)) + else + Map.delete(parser, :stab_state) + end + + parse_stab_expression(parser, lhs) + + nil -> + {ast, parser} + end end end @@ -1863,8 +1912,10 @@ defmodule Spitfire do {put_error(parser, {do_meta, "missing `end` for do block"}), do_meta} end + exprs = exprs ++ extra_exprs + exprs = - case exprs ++ extra_exprs do + case exprs do [] -> [{type, []}] exprs -> exprs end @@ -2663,30 +2714,117 @@ defmodule Spitfire do prefix = case current_token_type(parser) do - :identifier -> parse_lone_identifier(parser) - :paren_identifier -> parse_paren_identifier(parser) - :alias -> parse_alias(parser) - :at_op -> parse_lone_module_attr(parser) - :unary_op -> parse_prefix_lone_identifer(parser) - :dual_op -> parse_prefix_lone_identifer(parser) - :ternary_op -> parse_ternary_prefix_lone_identifier(parser) - :ellipsis_op -> parse_ellipsis_lone_identifier(parser) - :list_string -> parse_string(parser) - :bin_string -> parse_string(parser) - :int -> parse_int(parser) - nil -> parse_nil_literal(parser) - :atom -> parse_atom(parser) - :"(" -> parse_grouped_expression(parser) - _ -> nil + :identifier -> + parse_lone_identifier(parser) + + :bracket_identifier -> + parse_lone_identifier(parser) + + :paren_identifier -> + parse_paren_identifier(parser) + + :alias -> + parse_alias(parser) + + :at_op -> + parse_lone_module_attr(parser) + + :unary_op -> + parse_struct_type_prefix(parser) + + :dual_op -> + parse_struct_type_prefix(parser) + + :capture_op -> + parse_struct_type_prefix(parser) + + :capture_int -> + parse_capture_int(parser) + + :ternary_op -> + parse_ternary_prefix_lone_identifier(parser) + + :ellipsis_op -> + parse_ellipsis_lone_identifier(parser) + + :range_op -> + parse_range_expression(parser) + + :sigil -> + parse_sigil(parser) + + :list_string -> + parse_string(parser) + + :bin_string -> + parse_string(parser) + + :int -> + parse_int(parser) + + :flt -> + parse_float(parser) + + :char -> + parse_char(parser) + + true -> + parse_boolean(parser) + + false -> + parse_boolean(parser) + + nil -> + parse_nil_literal(parser) + + :atom -> + parse_atom(parser) + + :atom_quoted -> + parse_atom(parser) + + :"(" -> + parse_grouped_expression(parser) + + _ -> + nil end case prefix do {left, parser} -> - while peek_token(parser) in [:., :"["] && + while peek_token(parser) in [:., :dot_call_op, :"[", :"("] && calc_prec(parser, associativity, precedence) <- {left, parser} do case peek_token(parser) do - :. -> parse_dot_for_struct_type(next_token(parser), left) - :"[" -> parse_access_expression(next_token(parser), left) + token when token in [:., :dot_call_op] -> + {new_left, parser} = parse_dot_for_struct_type(next_token(parser), left) + # Check if next token is ( for zero-arity calls + if peek_token(parser) == :"(" do + parser = next_token(parser) + + if current_token(parser) == :")" do + closing = current_meta(parser) + new_left = {new_left, [{:closing, closing}, {:line, 1}, {:column, 3}], []} + {new_left, next_token(parser)} + else + {new_left, parser} + end + else + {new_left, parser} + end + + :"[" -> + parse_access_expression(next_token(parser), left) + + :"(" -> + # Handle () after a dot expression + parser = next_token(parser) + + if current_token(parser) == :")" do + closing = current_meta(parser) + {{:., current_meta(parser), [left]}, [{:closing, closing}], []} + else + {left, parser} + end end end @@ -2716,31 +2854,56 @@ defmodule Spitfire do token = current_token(parser) meta = current_meta(parser) - case peek_token_type(parser) do - :alias -> - parser = next_token(parser) - {{:__aliases__, ameta, aliases}, parser} = parse_alias(parser) - last = ameta[:last] - {{:__aliases__, [{:last, last} | meta], [lhs | aliases]}, parser} + # For dot_call_op, we need to handle the () specially + current_type = current_token_type(parser) - type when type in [:identifier, :do_identifier, :op_identifier] -> - parser = next_token(parser) - %{current_token: {_, token_meta, rhs_name}} = parser + if current_type == :dot_call_op do + # dot_call_op means . followed by () + # Just produce the dot with lhs, the () will be handled by the while loop + parser = next_token(parser) + {{:., meta, [lhs]}, parser} + else + case peek_token_type(parser) do + :alias -> + parser = next_token(parser) + {{:__aliases__, ameta, aliases}, parser} = parse_alias(parser) + last = ameta[:last] + {{:__aliases__, [{:last, last} | meta], [lhs | aliases]}, parser} - ident_meta = - parser - |> current_meta() - |> push_delimiter(token_meta) + type when type in [:identifier, :do_identifier, :op_identifier] -> + parser = next_token(parser) + %{current_token: {_, token_meta, rhs_name}} = parser - ast = {{token, meta, [lhs, rhs_name]}, [no_parens: true] ++ ident_meta, []} - {ast, parser} + ident_meta = + parser + |> current_meta() + |> push_delimiter(token_meta) - _ -> - parser = next_token(parser) - next_meta = current_meta(parser) - {rhs, parser} = parse_expression(parser, @lowest, false, false, false) - ast = {{token, meta, [lhs, rhs]}, next_meta, []} - {ast, parser} + # Check if there's a () after the identifier for zero-arity calls + if peek_token(parser) == :"(" do + parser = next_token(parser) + + if current_token(parser) == :")" do + closing = current_meta(parser) + ast = {{token, meta, [lhs, rhs_name]}, [{:closing, closing}] ++ ident_meta, []} + {ast, next_token(parser)} + else + # Shouldn't happen in valid syntax, but handle gracefully + ast = {{token, meta, [lhs, rhs_name]}, [no_parens: true] ++ ident_meta, []} + {ast, parser} + end + else + ast = {{token, meta, [lhs, rhs_name]}, [no_parens: true] ++ ident_meta, []} + {ast, parser} + end + + _ -> + parser = next_token(parser) + next_meta = current_meta(parser) + {rhs, parser} = parse_expression(parser, @lowest, false, false, false) + ast = {{token, meta, [lhs, rhs]}, next_meta, []} + {ast, parser} + end end end end @@ -2824,9 +2987,9 @@ defmodule Spitfire do valid_type? = type != {:__block__, [], []} struct_name = format_struct_type(type) - case peek_token(parser) do + case peek_token_eat_eoe(parser) do :"{" -> - parser = next_token(parser) + parser = eat_eol(next_token(parser)) brace_meta = current_meta(parser) parser = next_token(parser) @@ -3301,11 +3464,47 @@ defmodule Spitfire do {rhs, parser} = case current_token_type(parser) do - :"[" -> parse_list_literal(parser) - :int -> parse_int(parser) - :dual_op -> parse_prefix_lone_identifer(parser) - :unary_op -> parse_prefix_lone_identifer(parser) - _ -> parse_lone_identifier(parser) + :"[" -> + parse_list_literal(parser) + + :int -> + parse_int(parser) + + :flt -> + parse_float(parser) + + :char -> + parse_char(parser) + + :list_string -> + parse_string(parser) + + :bin_string -> + parse_string(parser) + + :capture_op -> + parse_struct_type_prefix(parser) + + :capture_int -> + parse_capture_int(parser) + + :dual_op -> + parse_struct_type_prefix(parser) + + :unary_op -> + parse_struct_type_prefix(parser) + + :atom -> + parse_atom(parser) + + :alias -> + parse_alias(parser) + + :at_op -> + parse_lone_module_attr(parser) + + _ -> + parse_lone_identifier(parser) end {{token, meta, [rhs]}, parser} diff --git a/mix.exs b/mix.exs index d2faf74..be1e09b 100644 --- a/mix.exs +++ b/mix.exs @@ -34,10 +34,8 @@ defmodule Spitfire.MixProject do {:ex_doc, ">= 0.0.0", only: :dev}, {:styler, "~> 0.11", only: [:dev, :test]}, {:credo, "~> 1.7", only: :dev}, - {:dialyxir, "~> 1.0", only: :dev} - - # {:dep_from_hexpm, "~> 0.3.0"}, - # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} + {:dialyxir, "~> 1.0", only: :dev}, + {:stream_data, "~> 1.0", only: [:dev, :test]} ] end diff --git a/mix.lock b/mix.lock index eef14fc..a61ad71 100644 --- a/mix.lock +++ b/mix.lock @@ -11,5 +11,6 @@ "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"}, "styler": {:hex, :styler, "0.11.9", "2595393b94e660cd6e8b582876337cc50ff047d184ccbed42fdad2bfd5d78af5", [:mix], [], "hexpm", "8b7806ba1fdc94d0a75127c56875f91db89b75117fcc67572661010c13e1f259"}, } diff --git a/test/property/char_property_test.exs b/test/property/char_property_test.exs new file mode 100644 index 0000000..4315914 --- /dev/null +++ b/test/property/char_property_test.exs @@ -0,0 +1,1801 @@ +defmodule Spitfire.CharPropertyTest do + @moduledoc """ + Property tests for ascii strings in various Elixir contexts. + """ + use ExUnit.Case, + async: false, + parameterize: [ + %{mode: :strict}, + %{mode: :tolerant} + ] + + use ExUnitProperties + + setup %{mode: mode} do + Process.put(:spitfire_test_mode, mode) + :ok + end + + # Options used by both elixir and Spitfire for consistency + @elixir_opts [ + columns: true, + token_metadata: true, + emit_warnings: false, + existing_atoms_only: false + ] + + # Character set for generating random code fragments + @char_set [ + # minimal set to cover all keywords, operators, brackets, separators, numbers, aliases and identifiers + ?0, + ?1, + ?b, + ?x, + ?d, + ?o, + ?e, + ?n, + ?d, + ?c, + ?a, + ?t, + ?c, + ?h, + ?r, + ?e, + ?s, + ?c, + ?u, + ?e, + ?a, + ?f, + ?t, + ?e, + ?r, + ?e, + ?l, + ?s, + ?e, + ?f, + ?n, + ?w, + ?h, + ?e, + ?n, + ?a, + ?n, + ?d, + ?o, + ?r, + ?n, + ?o, + ?t, + ?i, + ?n, + ?t, + ?r, + ?u, + ?e, + ?f, + ?a, + ?l, + ?s, + ?e, + ?n, + ?i, + ?l, + ?A, + ?!, + ?@, + ?^, + ?&, + ?*, + ?(, + ?), + ?-, + ?+, + ?[, + ?], + ?{, + ?}, + ?;, + ?:, + ?', + ?", + ?\\, + ?|, + ?~, + ?<, + ?>, + ?,, + ?., + ?/, + ??, + ?$, + ?%, + ?_, + ?=, + ?\s, + # excluded for now - create too many comments + # ?#, + # excluded for now + ?\n + ] + + # =========================================================================== + # Code Fragment Generator + # =========================================================================== + + defp code_fragment_gen(opts \\ []) do + min_length = Keyword.get(opts, :min_length, 0) + max_length = Keyword.get(opts, :max_length, 16) + StreamData.string(@char_set, min_length: min_length, max_length: max_length) + end + + defp nonempty_code_fragment_gen(opts \\ []) do + opts + |> Keyword.put_new(:min_length, 1) + |> code_fragment_gen() + end + + # =========================================================================== + # Context Generators - each returns {context_name, full_code} + # =========================================================================== + + # Beginning of string (code as standalone expression) + defp context_standalone do + StreamData.bind(code_fragment_gen(), fn code -> + StreamData.constant({"standalone", code}) + end) + end + + # Inside bitstring: <> + defp context_bitstring do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "< code <> " >>" + StreamData.constant({"bitstring", full_code}) + end) + end + + # bitstring -> open_bit container_args close_bit + # container_args -> container_args_base ',' kw_data + # Bitstring with positional segment then kw_data tail: <> + defp context_bitstring_positional_then_kw_data do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "< code <> ">>" + StreamData.constant({"bitstring_positional_then_kw_data", full_code}) + end) + end + + # Before do block: CODE do :ok end + defp context_before_do do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = code <> " do :ok end" + StreamData.constant({"before_do", full_code}) + end) + end + + # After do block: foo do :ok end CODE + defp context_after_do do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "foo do :ok end " <> code + StreamData.constant({"after_do", full_code}) + end) + end + + # Inside fn - various positions + defp context_fn_arg do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "fn " <> code <> " -> :ok end" + StreamData.constant({"fn_arg", full_code}) + end) + end + + # stab_expr -> empty_paren stab_op_eol_and_expr + # Empty-paren stab: fn () -> CODE end + defp context_fn_empty_paren_stab do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "fn () -> " <> code <> " end" + StreamData.constant({"fn_empty_paren_stab", full_code}) + end) + end + + # stab_expr -> empty_paren when_op expr stab_op_eol_and_expr + # Empty-paren stab with guard: fn () when CODE -> :ok end + defp context_fn_empty_paren_when do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "fn () when " <> code <> " -> :ok end" + StreamData.constant({"fn_empty_paren_when", full_code}) + end) + end + + # stab_parens_many -> open_paren call_args_no_parens_many close_paren + # Paren-wrapped patterns: fn (a, CODE) -> :ok end + defp context_fn_parens_many_lhs do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "fn (a, " <> code <> ") -> :ok end" + StreamData.constant({"fn_parens_many_lhs", full_code}) + end) + end + + # stab_expr -> stab_parens_many when_op expr stab_op_eol_and_expr + # Paren-wrapped patterns with guard: fn (a, b) when CODE -> :ok end + defp context_fn_parens_many_when do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "fn (a, b) when " <> code <> " -> :ok end" + StreamData.constant({"fn_parens_many_when", full_code}) + end) + end + + defp context_fn_body do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "fn 1 -> " <> code <> " end" + StreamData.constant({"fn_body", full_code}) + end) + end + + defp context_fn_no_arrow do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "fn " <> code <> " end" + StreamData.constant({"fn_no_arrow", full_code}) + end) + end + + defp context_fn_multi_arg do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "fn a, " <> code <> " end" + StreamData.constant({"fn_multi_arg", full_code}) + end) + end + + defp context_fn_multi_arg_with_arrow do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "fn a, " <> code <> " -> :ok end" + StreamData.constant({"fn_multi_arg_arrow", full_code}) + end) + end + + # Inside do block: foo do CODE end + defp context_inside_do do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "foo do " <> code <> " end" + StreamData.constant({"inside_do", full_code}) + end) + end + + # Inside parens call: foo(CODE) + defp context_parens_call do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "foo(" <> code <> ")" + StreamData.constant({"parens_call", full_code}) + end) + end + + # call_args_parens -> open_paren no_parens_expr close_paren + # Parens call with a single no-parens expression argument: foo(f CODE) + defp context_parens_call_single_no_parens_expr_arg do + [min_length: 1] + |> code_fragment_gen() + |> StreamData.filter(fn code -> + not String.contains?(code, ",") and + not String.contains?(code, ")") and + not String.contains?(code, "\n") + end) + |> StreamData.bind(fn code -> + full_code = "foo(f " <> code <> ")" + StreamData.constant({"parens_call_single_no_parens_expr_arg", full_code}) + end) + end + + # call_args_parens -> open_paren call_args_parens_base ',' kw_call close_paren + # Parens call with positional args and trailing kw_call: foo(1, a: CODE) + defp context_parens_call_args_then_kw_call do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "foo(1, a: " <> code <> ")" + StreamData.constant({"parens_call_args_then_kw_call", full_code}) + end) + end + + # kw_call -> kw_base ',' matched_expr + # Parens call that begins as kw_call then has a follow-up expr: foo(a: 1, CODE) + defp context_parens_call_kw_call_follow_up do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "foo(a: 1, " <> code <> ")" + StreamData.constant({"parens_call_kw_call_follow_up", full_code}) + end) + end + + # Inside no parens call: foo CODE + defp context_no_parens_call do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "foo " <> code + StreamData.constant({"no_parens_call", full_code}) + end) + end + + # Inside bracket access: foo[CODE] + defp context_bracket_access do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "foo[" <> code <> "]" + StreamData.constant({"bracket_access", full_code}) + end) + end + + # bracket_arg -> open_bracket container_expr ',' close_bracket + # Inside bracket access with trailing comma: foo[CODE,] + defp context_bracket_access_trailing_comma do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "foo[" <> code <> ",]" + StreamData.constant({"bracket_access_trailing_comma", full_code}) + end) + end + + # bracket_arg -> open_bracket kw_data close_bracket + # Inside bracket access using kw_data: foo[a: CODE] + defp context_bracket_access_kw_data_value do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "foo[a: " <> code <> "]" + StreamData.constant({"bracket_access_kw_data_value", full_code}) + end) + end + + # bracket_at_expr -> at_op_eol access_expr bracket_arg + # Access syntax under @: @foo[CODE] + defp context_bracket_at_access do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "@foo[" <> code <> "]" + StreamData.constant({"bracket_at_access", full_code}) + end) + end + + # Access syntax under @ with kw_data: @foo[a: CODE] + defp context_bracket_at_access_kw_data_value do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "@foo[a: " <> code <> "]" + StreamData.constant({"bracket_at_access_kw_data_value", full_code}) + end) + end + + # Inside map: %{CODE} + defp context_map do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "%{" <> code <> "}" + StreamData.constant({"map", full_code}) + end) + end + + # map_close -> assoc_base ',' kw_data close_curly + # Hybrid map with assoc (=>) and kw_data: %{K1 => 1, a: V2} + defp context_map_assoc_then_kw_data do + StreamData.bind(StreamData.tuple({code_fragment_gen(), code_fragment_gen()}), fn {k1, v2} -> + full_code = "%{" <> k1 <> " => 1, a: " <> v2 <> "}" + StreamData.constant({"map_assoc_then_kw_data", full_code}) + end) + end + + # Hybrid map with assoc after a kw_data key: %{a: V1, K2 => 1} + defp context_map_kw_data_then_assoc do + StreamData.bind(StreamData.tuple({code_fragment_gen(), code_fragment_gen()}), fn {v1, k2} -> + full_code = "%{a: " <> v1 <> ", " <> k2 <> " => 1}" + StreamData.constant({"map_kw_data_then_assoc", full_code}) + end) + end + + # map_args -> open_curly assoc_update_kw close_curly + # Map update using kw_data on RHS: %{x | a: CODE} + defp context_map_update_kw_data_value do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "%{x | a: " <> code <> "}" + StreamData.constant({"map_update_kw_data_value", full_code}) + end) + end + + # map_args -> open_curly assoc_update close_curly + # Map update using assoc_expr on RHS: %{x | CODE => 1} + defp context_map_update_assoc_key do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "%{x | " <> code <> " => 1}" + StreamData.constant({"map_update_assoc_key", full_code}) + end) + end + + # Inside struct: %Foo{CODE} + defp context_struct do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "%Foo{" <> code <> "}" + StreamData.constant({"struct", full_code}) + end) + end + + # Inside tuple: {CODE} + defp context_tuple do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "{" <> code <> "}" + StreamData.constant({"tuple", full_code}) + end) + end + + # tuple -> open_curly container_args close_curly + # container_args -> container_args_base ',' kw_data + # Tuple with positional element(s) then kw_data tail: {x, a: CODE} + defp context_tuple_positional_then_kw_data do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "{x, a: " <> code <> "}" + StreamData.constant({"tuple_positional_then_kw_data", full_code}) + end) + end + + # Inside list: [CODE] + defp context_list do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "[" <> code <> "]" + StreamData.constant({"list", full_code}) + end) + end + + # list_args -> container_args_base ',' kw_data + # List with positional element(s) then kw_data: [x, a: CODE] + defp context_list_positional_then_kw_data do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "[x, a: " <> code <> "]" + StreamData.constant({"list_positional_then_kw_data", full_code}) + end) + end + + # kw_data -> kw_base ',' matched_expr + # List that begins as keyword data then has a follow-up expr: [a: 1, CODE] + defp context_list_kw_data_follow_up do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "[a: 1, " <> code <> "]" + StreamData.constant({"list_kw_data_follow_up", full_code}) + end) + end + + # Inside parens: (CODE) + defp context_parens do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "(" <> code <> ")" + StreamData.constant({"parens", full_code}) + end) + end + + # Inside string interpolation: "#{CODE}" + defp context_interpolation do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "\"" <> "\#{" <> code <> "}" <> "\"" + StreamData.constant({"interpolation", full_code}) + end) + end + + # After pipe: :ok |> CODE + defp context_after_pipe do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = ":ok |> " <> code + StreamData.constant({"after_pipe", full_code}) + end) + end + + # After assignment: x = CODE + defp context_after_assignment do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "x = " <> code + StreamData.constant({"after_assignment", full_code}) + end) + end + + # Inside struct arg: %CODE{} + defp context_struct_arg do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "%" <> code <> "{}" + StreamData.constant({"struct_arg", full_code}) + end) + end + + # Between do blocks: foo do :ok end CODE do :error end + defp context_between_do_blocks do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "foo do :ok end " <> code <> " do :error end" + StreamData.constant({"between_do_blocks", full_code}) + end) + end + + # Inside ternary range - first position: CODE..x//y + defp context_ternary_first do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = code <> "..x//y" + StreamData.constant({"ternary_first", full_code}) + end) + end + + # Inside ternary range - second position: x..CODE//y + defp context_ternary_second do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "x.." <> code <> "//y" + StreamData.constant({"ternary_second", full_code}) + end) + end + + # Inside ternary range - third position: x..y//CODE + defp context_ternary_third do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "x..y//" <> code + StreamData.constant({"ternary_third", full_code}) + end) + end + + # Inside map update - updated expression: %{CODE | x: y} + defp context_map_update_expr do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "%{" <> code <> " | x: y}" + StreamData.constant({"map_update_expr", full_code}) + end) + end + + # Inside map update - key/value part: %{x | CODE} + defp context_map_update_kv do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "%{x | " <> code <> "}" + StreamData.constant({"map_update_kv", full_code}) + end) + end + + # Inside map update - value: %{x | foo: CODE} + defp context_map_update_value do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "%{x | foo: " <> code <> "}" + StreamData.constant({"map_update_value", full_code}) + end) + end + + # After parens call: foo()CODE + defp context_after_parens_call do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "foo()" <> code + StreamData.constant({"after_parens_call", full_code}) + end) + end + + # Inside no parens call with two args: foo CODE bar + defp context_no_parens_call_middle do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "foo " <> code <> " bar" + StreamData.constant({"no_parens_call_middle", full_code}) + end) + end + + # Inside dot - before dot call: CODE.foo() + defp context_before_dot_call do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = code <> ".foo()" + StreamData.constant({"before_dot_call", full_code}) + end) + end + + # Inside dot - middle of chain: A.CODE.foo() + defp context_dot_chain_middle do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "A." <> code <> ".foo()" + StreamData.constant({"dot_chain_middle", full_code}) + end) + end + + # Inside dot - with tuple: A.CODE.{} + defp context_dot_tuple do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "A." <> code <> ".{}" + StreamData.constant({"dot_tuple", full_code}) + end) + end + + # After dot: foo.CODE + defp context_after_dot do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "foo." <> code + StreamData.constant({"after_dot", full_code}) + end) + end + + # dot_call_identifier -> matched_expr dot_call_op + # Function call via `.(...)` with empty args: (CODE).() + defp context_dot_call_empty_args do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "(" <> code <> ").()" + StreamData.constant({"dot_call_empty_args", full_code}) + end) + end + + # Function call via `.(...)` with one argument: (LHS).(CODE) + defp context_dot_call_one_arg do + StreamData.bind(StreamData.tuple({code_fragment_gen(), code_fragment_gen()}), fn {lhs, arg} -> + full_code = "(" <> lhs <> ").(" <> arg <> ")" + StreamData.constant({"dot_call_one_arg", full_code}) + end) + end + + # Function call via `.(...)` with kw_call: (LHS).(a: CODE) + defp context_dot_call_kw_call do + StreamData.bind(StreamData.tuple({code_fragment_gen(), code_fragment_gen()}), fn {lhs, value} -> + full_code = "(" <> lhs <> ").(a: " <> value <> ")" + StreamData.constant({"dot_call_kw_call", full_code}) + end) + end + + # access_expr -> open_paren stab_eoe ')' + # Parenthesized stab expression: (x -> CODE) + defp context_paren_stab_single do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "(x -> " <> code <> ")" + StreamData.constant({"paren_stab_single", full_code}) + end) + end + + # Parenthesized multi-clause stab: (x -> :ok; y -> CODE) + defp context_paren_stab_multi do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "(x -> :ok; y -> " <> code <> ")" + StreamData.constant({"paren_stab_multi", full_code}) + end) + end + + # access_expr -> open_paren ';' stab_eoe ')' + # Semicolon-prefixed parenthesized stab: (; x -> CODE) + defp context_paren_stab_semicolon_single do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "(; x -> " <> code <> ")" + StreamData.constant({"paren_stab_semicolon_single", full_code}) + end) + end + + # Semicolon-prefixed multi-clause stab: (; x -> :ok; y -> CODE) + defp context_paren_stab_semicolon_multi do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "(; x -> :ok; y -> " <> code <> ")" + StreamData.constant({"paren_stab_semicolon_multi", full_code}) + end) + end + + # access_expr -> open_paren ';' close_paren + # Empty paren-stab form: (;) + defp context_empty_paren_semicolon do + StreamData.constant({"empty_paren_semicolon", "(;)"}) + end + + # bracket_expr -> access_expr bracket_arg + # Bracket access on a parenthesized expr: (CODE)[x] + defp context_bracket_on_parens_expr do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "(" <> code <> ")[x]" + StreamData.constant({"bracket_on_parens_expr", full_code}) + end) + end + + # Between operators: x + CODE * y + defp context_between_operators do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "x + " <> code <> " * y" + StreamData.constant({"between_operators", full_code}) + end) + end + + # After unary &: &CODE + defp context_after_capture do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "&" <> code + StreamData.constant({"after_capture", full_code}) + end) + end + + # After unary ^: ^CODE + defp context_after_pin do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "^" <> code + StreamData.constant({"after_pin", full_code}) + end) + end + + # After unary +: +CODE + defp context_after_unary_plus do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "+" <> code + StreamData.constant({"after_unary_plus", full_code}) + end) + end + + # After unary -: -CODE + defp context_after_unary_minus do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "-" <> code + StreamData.constant({"after_unary_minus", full_code}) + end) + end + + # After unary @: @CODE + defp context_after_at do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "@" <> code + StreamData.constant({"after_at", full_code}) + end) + end + + # After unary !: !CODE + defp context_after_bang do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "!" <> code + StreamData.constant({"after_bang", full_code}) + end) + end + + # After unary not: not CODE + defp context_after_not do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "not " <> code + StreamData.constant({"after_not", full_code}) + end) + end + + # Inside interpolated atom: :"foo#{CODE}" + defp context_interpolated_atom do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = ":\"foo\#{" <> code <> "}\"" + StreamData.constant({"interpolated_atom", full_code}) + end) + end + + # Inside interpolated keyword key: ["foo#{CODE}": 1] + defp context_interpolated_keyword do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "[\"foo\#{" <> code <> "}\": 1]" + StreamData.constant({"interpolated_keyword", full_code}) + end) + end + + # Inside charlist interpolation: 'foo#{CODE}' + defp context_charlist_interpolation do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "'foo\#{" <> code <> "}'" + StreamData.constant({"charlist_interpolation", full_code}) + end) + end + + # Inside string heredoc interpolation: """ + # foo#{CODE} + # """ + defp context_string_heredoc_interpolation do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "\"\"\"\nfoo\#{" <> code <> "}\n\"\"\"" + StreamData.constant({"string_heredoc_interpolation", full_code}) + end) + end + + # Inside charlist heredoc interpolation: ''' + # foo#{CODE} + # ''' + defp context_charlist_heredoc_interpolation do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "'''\nfoo\#{" <> code <> "}\n'''" + StreamData.constant({"charlist_heredoc_interpolation", full_code}) + end) + end + + # Inside sigil interpolation: ~s/foo#{CODE}/ + defp context_sigil_interpolation do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "~s/foo\#{" <> code <> "}/" + StreamData.constant({"sigil_interpolation", full_code}) + end) + end + + # Inside sigil heredoc interpolation: ~s""" + # foo#{CODE} + # """ + defp context_sigil_heredoc_interpolation do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "~s\"\"\"\nfoo\#{" <> code <> "}\n\"\"\"" + StreamData.constant({"sigil_heredoc_interpolation", full_code}) + end) + end + + # Inside when expr in fn: fn x when CODE -> 1 end + defp context_fn_when do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "fn x when " <> code <> " -> 1 end" + StreamData.constant({"fn_when", full_code}) + end) + end + + # Inside def with parens args: def foo(CODE) do :ok end + defp context_def_parens_arg do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "def foo(" <> code <> ") do :ok end" + StreamData.constant({"def_parens_arg", full_code}) + end) + end + + # Inside def with no parens args: def foo CODE do 1 end + defp context_def_no_parens_arg do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "def foo " <> code <> " do 1 end" + StreamData.constant({"def_no_parens_arg", full_code}) + end) + end + + # Inside def when guard: def foo() when CODE do 1 end + defp context_def_when do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "def foo() when " <> code <> " do 1 end" + StreamData.constant({"def_when", full_code}) + end) + end + + # Inside keyword list value: [a: CODE] + defp context_keyword_list_value do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "[a: " <> code <> "]" + StreamData.constant({"keyword_list_value", full_code}) + end) + end + + # Inside map kv value: %{a: CODE} + defp context_map_kv_value do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "%{a: " <> code <> "}" + StreamData.constant({"map_kv_value", full_code}) + end) + end + + # Inside map rocket key: %{CODE => 1} + defp context_map_rocket_key do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "%{" <> code <> " => 1}" + StreamData.constant({"map_rocket_key", full_code}) + end) + end + + # Inside struct kv value: %Foo{a: CODE} + defp context_struct_kv_value do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "%Foo{a: " <> code <> "}" + StreamData.constant({"struct_kv_value", full_code}) + end) + end + + # Inside parens call with multiple args: foo(1, CODE, 2) + defp context_parens_call_multi_args_middle do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "foo(1, " <> code <> ", 2)" + StreamData.constant({"parens_call_multi_args_middle", full_code}) + end) + end + + # Inside parens call keyword arg value: foo(a: CODE) + defp context_parens_call_kw_value do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "foo(a: " <> code <> ")" + StreamData.constant({"parens_call_kw_value", full_code}) + end) + end + + # Inside no-parens call keyword arg value: foo a: CODE + defp context_no_parens_call_kw_value do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "foo a: " <> code + StreamData.constant({"no_parens_call_kw_value", full_code}) + end) + end + + # Inside no-parens call with multiple args: foo 1, CODE + defp context_no_parens_call_multi_args do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "foo 1, " <> code + StreamData.constant({"no_parens_call_multi_args", full_code}) + end) + end + + # Nested no-parens call ambiguity: f g CODE, h + defp context_nested_no_parens_call do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "f g " <> code <> ", h" + StreamData.constant({"nested_no_parens_call", full_code}) + end) + end + + # Do-block attached to a parens call: foo(CODE) do :ok end + defp context_parens_call_with_do do + StreamData.bind(nonempty_code_fragment_gen(), fn code -> + full_code = "foo(" <> code <> ") do :ok end" + StreamData.constant({"parens_call_with_do", full_code}) + end) + end + + # Do-block attached to a no-parens call: foo CODE do :ok end + defp context_no_parens_call_with_do do + StreamData.bind(nonempty_code_fragment_gen(), fn code -> + full_code = "foo " <> code <> " do :ok end" + StreamData.constant({"no_parens_call_with_do", full_code}) + end) + end + + # If condition: if CODE do :ok end + defp context_if_condition do + StreamData.bind(nonempty_code_fragment_gen(), fn code -> + full_code = "if " <> code <> " do :ok end" + StreamData.constant({"if_condition", full_code}) + end) + end + + # If else body: if true do :ok else CODE end + defp context_if_else_body do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "if true do :ok else " <> code <> " end" + StreamData.constant({"if_else_body", full_code}) + end) + end + + # Case clause pattern (stab lhs): case x do CODE -> :ok end + defp context_case_clause_lhs do + StreamData.bind(nonempty_code_fragment_gen(), fn code -> + full_code = "case x do " <> code <> " -> :ok end" + StreamData.constant({"case_clause_lhs", full_code}) + end) + end + + # Case clause body (stab rhs): case x do 1 -> CODE end + defp context_case_clause_rhs do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "case x do 1 -> " <> code <> " end" + StreamData.constant({"case_clause_rhs", full_code}) + end) + end + + # Case second clause pattern: case x do 1 -> :ok; CODE -> :error end + defp context_case_second_clause_lhs do + StreamData.bind(nonempty_code_fragment_gen(), fn code -> + full_code = "case x do 1 -> :ok; " <> code <> " -> :error end" + StreamData.constant({"case_second_clause_lhs", full_code}) + end) + end + + # Cond clause condition (stab lhs): cond do CODE -> :ok end + defp context_cond_clause_lhs do + StreamData.bind(nonempty_code_fragment_gen(), fn code -> + full_code = "cond do " <> code <> " -> :ok end" + StreamData.constant({"cond_clause_lhs", full_code}) + end) + end + + # Multi-clause fn (stab lhs): fn 1 -> :ok; CODE -> :error end + defp context_fn_second_clause_lhs do + StreamData.bind(nonempty_code_fragment_gen(), fn code -> + full_code = "fn 1 -> :ok; " <> code <> " -> :error end" + StreamData.constant({"fn_second_clause_lhs", full_code}) + end) + end + + # With generator RHS: with x <- CODE do x end + defp context_with_generator_rhs do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "with x <- " <> code <> " do x end" + StreamData.constant({"with_generator_rhs", full_code}) + end) + end + + # With generator LHS (pattern): with CODE <- x do x end + defp context_with_generator_lhs do + StreamData.bind(nonempty_code_fragment_gen(), fn code -> + full_code = "with " <> code <> " <- x do x end" + StreamData.constant({"with_generator_lhs", full_code}) + end) + end + + # With else clause body: with x <- 1 do :ok else _ -> CODE end + defp context_with_else_body do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "with x <- 1 do :ok else _ -> " <> code <> " end" + StreamData.constant({"with_else_body", full_code}) + end) + end + + # For generator RHS: for x <- CODE, do: x + defp context_for_generator_rhs do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "for x <- " <> code <> ", do: x" + StreamData.constant({"for_generator_rhs", full_code}) + end) + end + + # For filter expr: for x <- [1], CODE, do: x + defp context_for_filter do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "for x <- [1], " <> code <> ", do: x" + StreamData.constant({"for_filter", full_code}) + end) + end + + # Try body: try do CODE after :ok end + defp context_try_body do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "try do " <> code <> " after :ok end" + StreamData.constant({"try_body", full_code}) + end) + end + + # Try rescue clause body: try do :ok rescue _ -> CODE end + defp context_try_rescue_body do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "try do :ok rescue _ -> " <> code <> " end" + StreamData.constant({"try_rescue_body", full_code}) + end) + end + + # Receive clause pattern (stab lhs): receive do CODE -> :ok after 0 -> :timeout end + defp context_receive_clause_lhs do + StreamData.bind(nonempty_code_fragment_gen(), fn code -> + full_code = "receive do " <> code <> " -> :ok after 0 -> :timeout end" + StreamData.constant({"receive_clause_lhs", full_code}) + end) + end + + # Bitstring segment spec: <> + defp context_bitstring_segment_spec do + StreamData.bind(code_fragment_gen(), fn code -> + full_code = "< code <> ">>" + StreamData.constant({"bitstring_segment_spec", full_code}) + end) + end + + # Combined generator that picks one context randomly + defp all_contexts_gen do + StreamData.one_of([ + context_standalone(), + context_bitstring(), + context_bitstring_positional_then_kw_data(), + context_before_do(), + context_after_do(), + context_fn_arg(), + context_fn_empty_paren_stab(), + context_fn_empty_paren_when(), + context_fn_parens_many_lhs(), + context_fn_parens_many_when(), + context_fn_body(), + context_fn_no_arrow(), + context_fn_multi_arg(), + context_fn_multi_arg_with_arrow(), + context_inside_do(), + context_parens_call(), + context_parens_call_single_no_parens_expr_arg(), + context_parens_call_args_then_kw_call(), + context_parens_call_kw_call_follow_up(), + context_no_parens_call(), + context_bracket_access(), + context_bracket_access_trailing_comma(), + context_bracket_access_kw_data_value(), + context_bracket_at_access(), + context_bracket_at_access_kw_data_value(), + context_map(), + context_map_assoc_then_kw_data(), + context_map_kw_data_then_assoc(), + context_struct(), + context_tuple(), + context_tuple_positional_then_kw_data(), + context_list(), + context_list_positional_then_kw_data(), + context_list_kw_data_follow_up(), + context_parens(), + context_interpolation(), + context_after_pipe(), + context_after_assignment(), + # New contexts + context_struct_arg(), + context_between_do_blocks(), + context_ternary_first(), + context_ternary_second(), + context_ternary_third(), + context_map_update_expr(), + context_map_update_kv(), + context_map_update_value(), + context_after_parens_call(), + context_no_parens_call_middle(), + context_before_dot_call(), + context_dot_chain_middle(), + context_dot_tuple(), + context_after_dot(), + context_dot_call_empty_args(), + context_dot_call_one_arg(), + context_dot_call_kw_call(), + context_paren_stab_single(), + context_paren_stab_multi(), + context_paren_stab_semicolon_single(), + context_paren_stab_semicolon_multi(), + context_empty_paren_semicolon(), + context_bracket_on_parens_expr(), + context_between_operators(), + context_after_capture(), + context_after_pin(), + context_after_unary_plus(), + context_after_unary_minus(), + context_after_at(), + context_after_bang(), + context_after_not(), + context_interpolated_atom(), + context_interpolated_keyword(), + # Interpolation contexts + context_charlist_interpolation(), + context_string_heredoc_interpolation(), + context_charlist_heredoc_interpolation(), + context_sigil_interpolation(), + context_sigil_heredoc_interpolation(), + # When and def contexts + context_fn_when(), + context_def_parens_arg(), + context_def_no_parens_arg(), + context_def_when(), + # Keyword/kv_data + call-args variants + context_keyword_list_value(), + context_map_kv_value(), + context_map_rocket_key(), + context_map_update_kw_data_value(), + context_map_update_assoc_key(), + context_struct_kv_value(), + context_parens_call_multi_args_middle(), + context_parens_call_kw_value(), + context_no_parens_call_kw_value(), + context_no_parens_call_multi_args(), + context_nested_no_parens_call(), + # Do-block attachment variants + control flow / stabs + context_parens_call_with_do(), + context_no_parens_call_with_do(), + context_if_condition(), + context_if_else_body(), + context_case_clause_lhs(), + context_case_clause_rhs(), + context_case_second_clause_lhs(), + context_cond_clause_lhs(), + context_fn_second_clause_lhs(), + context_with_generator_rhs(), + context_with_generator_lhs(), + context_with_else_body(), + context_for_generator_rhs(), + context_for_filter(), + context_try_body(), + context_try_rescue_body(), + context_receive_clause_lhs(), + context_bitstring_segment_spec() + ]) + end + + # =========================================================================== + # Property Tests + # =========================================================================== + + describe "ascii in contexts" do + @tag :property + @tag timeout: 120_000 + property "grammar trees round-trip through Spitfire in all contexts" do + check all( + {context, code} <- all_contexts_gen(), + max_runs: 1000, + max_shrinking_steps: 50 + ) do + run_comparison(context, code, current_mode()) + end + end + end + + # Individual context tests for targeted debugging + describe "ascii standalone" do + @tag :property + @tag timeout: 120_000 + property "standalone expressions" do + check all( + {context, code} <- context_standalone(), + max_runs: 1000, + max_shrinking_steps: 50 + ) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii bitstring" do + @tag :property + @tag timeout: 120_000 + property "inside bitstring" do + check all( + {context, code} <- + StreamData.one_of([ + context_bitstring(), + context_bitstring_positional_then_kw_data() + ]), + max_runs: 1000, + max_shrinking_steps: 50 + ) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii before_do" do + @tag :property + @tag timeout: 120_000 + property "before do block" do + check all( + {context, code} <- context_before_do(), + max_runs: 1000, + max_shrinking_steps: 50 + ) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii after_do" do + @tag :property + @tag timeout: 120_000 + property "after do block" do + check all({context, code} <- context_after_do(), max_runs: 1000, max_shrinking_steps: 50) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii fn" do + @tag :property + @tag timeout: 120_000 + property "inside fn expressions" do + fn_contexts = + StreamData.one_of([ + context_fn_arg(), + context_fn_empty_paren_stab(), + context_fn_empty_paren_when(), + context_fn_parens_many_lhs(), + context_fn_parens_many_when(), + context_fn_body(), + context_fn_no_arrow(), + context_fn_multi_arg(), + context_fn_multi_arg_with_arrow() + ]) + + check all({context, code} <- fn_contexts, max_runs: 1000, max_shrinking_steps: 50) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii inside_do" do + @tag :property + @tag timeout: 120_000 + property "inside do block" do + check all( + {context, code} <- context_inside_do(), + max_runs: 1000, + max_shrinking_steps: 50 + ) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii calls" do + @tag :property + @tag timeout: 120_000 + property "inside function calls" do + call_contexts = + StreamData.one_of([ + context_parens_call(), + context_parens_call_args_then_kw_call(), + context_parens_call_kw_call_follow_up(), + context_no_parens_call() + ]) + + check all({context, code} <- call_contexts, max_runs: 1000, max_shrinking_steps: 50) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii bracket_access" do + @tag :property + @tag timeout: 120_000 + property "inside bracket access" do + check all( + {context, code} <- + StreamData.one_of([ + context_bracket_access(), + context_bracket_access_trailing_comma(), + context_bracket_access_kw_data_value(), + context_bracket_at_access(), + context_bracket_at_access_kw_data_value() + ]), + max_runs: 1000, + max_shrinking_steps: 50 + ) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii containers" do + @tag :property + @tag timeout: 120_000 + property "inside containers (map, tuple, list, struct)" do + container_contexts = + StreamData.one_of([ + context_map(), + context_map_assoc_then_kw_data(), + context_map_kw_data_then_assoc(), + context_struct(), + context_tuple(), + context_tuple_positional_then_kw_data(), + context_list(), + context_list_positional_then_kw_data(), + context_list_kw_data_follow_up(), + context_parens() + ]) + + check all( + {context, code} <- container_contexts, + max_runs: 1000, + max_shrinking_steps: 50 + ) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii interpolation" do + @tag :property + @tag timeout: 120_000 + property "inside string interpolation" do + check all( + {context, code} <- context_interpolation(), + max_runs: 1000, + max_shrinking_steps: 50 + ) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii operators" do + @tag :property + @tag timeout: 120_000 + property "after operators (pipe, assignment)" do + op_contexts = + StreamData.one_of([ + context_after_pipe(), + context_after_assignment() + ]) + + check all({context, code} <- op_contexts, max_runs: 1000, max_shrinking_steps: 50) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii struct_arg" do + @tag :property + @tag timeout: 120_000 + property "inside struct arg" do + check all( + {context, code} <- context_struct_arg(), + max_runs: 1000, + max_shrinking_steps: 50 + ) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii between_do_blocks" do + @tag :property + @tag timeout: 120_000 + property "between do blocks" do + check all( + {context, code} <- context_between_do_blocks(), + max_runs: 1000, + max_shrinking_steps: 50 + ) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii ternary" do + @tag :property + @tag timeout: 120_000 + property "inside ternary range expressions" do + ternary_contexts = + StreamData.one_of([ + context_ternary_first(), + context_ternary_second(), + context_ternary_third() + ]) + + check all({context, code} <- ternary_contexts, max_runs: 1000, max_shrinking_steps: 50) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii map_update" do + @tag :property + @tag timeout: 120_000 + property "inside map update expressions" do + map_update_contexts = + StreamData.one_of([ + context_map_update_expr(), + context_map_update_kv(), + context_map_update_value() + ]) + + check all( + {context, code} <- map_update_contexts, + max_runs: 1000, + max_shrinking_steps: 50 + ) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii after_parens_call" do + @tag :property + @tag timeout: 120_000 + property "after parens call" do + check all( + {context, code} <- context_after_parens_call(), + max_runs: 1000, + max_shrinking_steps: 50 + ) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii no_parens_call_middle" do + @tag :property + @tag timeout: 120_000 + property "inside no parens call middle" do + check all( + {context, code} <- context_no_parens_call_middle(), + max_runs: 1000, + max_shrinking_steps: 50 + ) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii dot" do + @tag :property + @tag timeout: 120_000 + property "inside dot expressions" do + dot_contexts = + StreamData.one_of([ + context_before_dot_call(), + context_dot_chain_middle(), + context_dot_tuple(), + context_after_dot() + ]) + + check all({context, code} <- dot_contexts, max_runs: 1000, max_shrinking_steps: 50) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii between_operators" do + @tag :property + @tag timeout: 120_000 + property "between operators" do + check all( + {context, code} <- context_between_operators(), + max_runs: 1000, + max_shrinking_steps: 50 + ) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii unary" do + @tag :property + @tag timeout: 120_000 + property "after unary operators" do + unary_contexts = + StreamData.one_of([ + context_after_capture(), + context_after_pin(), + context_after_unary_plus(), + context_after_unary_minus(), + context_after_at(), + context_after_bang(), + context_after_not() + ]) + + check all({context, code} <- unary_contexts, max_runs: 1000, max_shrinking_steps: 50) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii interpolated_atom_keyword" do + @tag :property + @tag timeout: 120_000 + property "inside interpolated atoms and keywords" do + interp_contexts = + StreamData.one_of([ + context_interpolated_atom(), + context_interpolated_keyword() + ]) + + check all({context, code} <- interp_contexts, max_runs: 1000, max_shrinking_steps: 50) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii charlist_interpolation" do + @tag :property + @tag timeout: 120_000 + property "inside charlist interpolation" do + check all( + {context, code} <- context_charlist_interpolation(), + max_runs: 1000, + max_shrinking_steps: 50 + ) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii heredoc_interpolation" do + @tag :property + @tag timeout: 120_000 + property "inside heredoc interpolation" do + heredoc_contexts = + StreamData.one_of([ + context_string_heredoc_interpolation(), + context_charlist_heredoc_interpolation() + ]) + + check all( + {context, code} <- heredoc_contexts, + max_runs: 1000, + max_shrinking_steps: 50 + ) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii sigil_interpolation" do + @tag :property + @tag timeout: 120_000 + property "inside sigil interpolation" do + sigil_contexts = + StreamData.one_of([ + context_sigil_interpolation(), + context_sigil_heredoc_interpolation() + ]) + + check all({context, code} <- sigil_contexts, max_runs: 1000, max_shrinking_steps: 50) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii fn_when" do + @tag :property + @tag timeout: 120_000 + property "inside fn when guard" do + check all({context, code} <- context_fn_when(), max_runs: 1000, max_shrinking_steps: 50) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii def" do + @tag :property + @tag timeout: 120_000 + property "inside def expressions" do + def_contexts = + StreamData.one_of([ + context_def_parens_arg(), + context_def_no_parens_arg(), + context_def_when() + ]) + + check all({context, code} <- def_contexts, max_runs: 1000, max_shrinking_steps: 50) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii keyword/kv_data" do + @tag :property + @tag timeout: 120_000 + property "inside keyword lists, maps, structs and keyword args" do + kv_contexts = + StreamData.one_of([ + context_keyword_list_value(), + context_map_kv_value(), + context_map_rocket_key(), + context_struct_kv_value(), + context_parens_call_kw_value(), + context_no_parens_call_kw_value() + ]) + + check all({context, code} <- kv_contexts, max_runs: 1000, max_shrinking_steps: 50) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii stabs/control-flow" do + @tag :property + @tag timeout: 120_000 + property "inside case/cond/fn clauses and do/else/after" do + stab_contexts = + StreamData.one_of([ + context_case_clause_lhs(), + context_case_clause_rhs(), + context_case_second_clause_lhs(), + context_cond_clause_lhs(), + context_fn_second_clause_lhs(), + context_if_condition(), + context_if_else_body(), + context_parens_call_with_do(), + context_no_parens_call_with_do(), + context_try_body(), + context_try_rescue_body(), + context_receive_clause_lhs() + ]) + + check all({context, code} <- stab_contexts, max_runs: 1000, max_shrinking_steps: 50) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii call_args variants" do + @tag :property + @tag timeout: 120_000 + property "inside multi-arg calls and nested no-parens calls" do + args_contexts = + StreamData.one_of([ + context_parens_call_single_no_parens_expr_arg(), + context_parens_call_multi_args_middle(), + context_no_parens_call_multi_args(), + context_nested_no_parens_call() + ]) + + check all({context, code} <- args_contexts, max_runs: 1000, max_shrinking_steps: 50) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii comprehensions/with" do + @tag :property + @tag timeout: 120_000 + property "inside for/with generators and filters/else" do + comp_contexts = + StreamData.one_of([ + context_for_generator_rhs(), + context_for_filter(), + context_with_generator_rhs(), + context_with_generator_lhs(), + context_with_else_body() + ]) + + check all({context, code} <- comp_contexts, max_runs: 1000, max_shrinking_steps: 50) do + run_comparison(context, code, current_mode()) + end + end + end + + describe "ascii bitstring specs" do + @tag :property + @tag timeout: 120_000 + property "inside bitstring segment spec" do + check all( + {context, code} <- context_bitstring_segment_spec(), + max_runs: 1000, + max_shrinking_steps: 50 + ) do + run_comparison(context, code, current_mode()) + end + end + end + + # =========================================================================== + # Comparison Helper + # =========================================================================== + + defp run_comparison(context, code, mode) do + elixir_result = elixir_parse(code) + + case {mode, elixir_result} do + # Strict mode: if elixir errors/crashes, skip (assume passed) + {:strict, {:error, _}} -> + :ok + + {:strict, :crashed} -> + :ok + + # Tolerant mode: if elixir errors/crashes, spitfire must not crash + {:tolerant, {:error, _}} -> + # Spitfire must not crash - just call it and ensure no exception + _ = spitfire_parse(code) + :ok + + {:tolerant, :crashed} -> + # Spitfire must not crash - just call it and ensure no exception + _ = spitfire_parse(code) + :ok + + # Both modes: if elixir returns ok, spitfire must return exactly the same AST + {_, {:ok, {:__block__, _, []}}} -> + # Empty block - skip detailed comparison + :ok + + {_, {:ok, elixir_ast}} -> + spitfire_result = spitfire_parse(code) + + case spitfire_result do + {:ok, spitfire_ast} -> + assert elixir_ast == spitfire_ast, + """ + AST mismatch in context #{context} for code: #{inspect(code)} + + Elixir: + #{inspect(elixir_ast, pretty: true)} + + Spitfire: + #{inspect(spitfire_ast, pretty: true)} + """ + + {:error, _spitfire_ast, _errors} -> + flunk(""" + Spitfire returned error when elixir succeeded in context #{context} for code: #{inspect(code)} + + Elixir AST: + #{inspect(elixir_ast, pretty: true)} + """) + + {:error, :no_fuel_remaining} -> + flunk(""" + Spitfire ran out of fuel in context #{context} for code: #{inspect(code)} + + Elixir AST: + #{inspect(elixir_ast, pretty: true)} + """) + + :crashed -> + flunk(""" + Spitfire crashed when elixir succeeded in context #{context} for code: #{inspect(code)} + + Elixir AST: + #{inspect(elixir_ast, pretty: true)} + """) + end + end + end + + defp elixir_parse(code) do + Code.string_to_quoted(code, @elixir_opts) + rescue + _ -> :crashed + end + + defp spitfire_parse(code) do + Spitfire.parse(code) + rescue + _ -> :crashed + end + + defp current_mode do + Process.get(:spitfire_test_mode, :strict) + end +end diff --git a/test/spitfire_test.exs b/test/spitfire_test.exs index 2865eab..0dda3f0 100644 --- a/test/spitfire_test.exs +++ b/test/spitfire_test.exs @@ -1465,6 +1465,16 @@ defmodule SpitfireTest do else _ -> :error end + ''', + ~s''' + fn () when o<-c -> + :ok + end + ''', + ~s''' + fn x when c<-c -> + 1 + end ''' ] @@ -2237,6 +2247,52 @@ defmodule SpitfireTest do assert Spitfire.parse(code) == s2q(code) end + + # These were found by property tests but were not triggering reliably + # We have them here to make sure they don't regress + test "property test regression cases" do + # Prefix operators in struct types + assert Spitfire.parse("%?0{}") == s2q("%?0{}") + assert Spitfire.parse("%^@_{}") == s2q("%^@_{}") + assert Spitfire.parse("%-..{}") == s2q("%-..{}") + assert Spitfire.parse("%!:c{}") == s2q("%!:c{}") + assert Spitfire.parse("%~a<>{}") == s2q("%~a<>{}") + assert Spitfire.parse("%!A{}") == s2q("%!A{}") + assert Spitfire.parse("%@A{}") == s2q("%@A{}") + assert Spitfire.parse("%@:rd{}") == s2q("%@:rd{}") + assert Spitfire.parse("%!0{}") == s2q("%!0{}") + + # Nested module attributes + assert Spitfire.parse("%@@u{}") == s2q("%@@u{}") + + # Quoted atoms + assert Spitfire.parse(~s(%:""{})) == s2q(~s(%:""{})) + + # Range operator in struct types + assert Spitfire.parse("%..{}") == s2q("%..{}") + + # Char tokens after module attributes in struct types + assert Spitfire.parse("%@?w{}") == s2q("%@?w{}") + + # Empty char list after module attributes in struct types + assert Spitfire.parse("%@''{}") == s2q(~s(%@''{})) + + # Float in struct types + assert Spitfire.parse("%0.0{}") == s2q("%0.0{}") + + # Bin strings after module attributes in struct types + assert Spitfire.parse(~s(%@"foo"{})) == s2q(~s(%@"foo"{})) + + # Capture operator in struct types + assert Spitfire.parse("%&0{}") == s2q("%&0{}") + + # Boolean literals in struct types + assert Spitfire.parse("%false{}") == s2q("%false{}") + assert Spitfire.parse("%true{}") == s2q("%true{}") + + # In-match operator (<-) in map keys - should be part of key, not wrap it + assert Spitfire.parse("%{s\\\\r => 1}") == s2q("%{s\\\\r => 1}") + end end describe "code with errors" do