diff --git a/compiler-core/src/erlang.rs b/compiler-core/src/erlang.rs index f271509e06c..febb64d5e55 100644 --- a/compiler-core/src/erlang.rs +++ b/compiler-core/src/erlang.rs @@ -46,6 +46,7 @@ struct Env<'a> { module: &'a str, function: &'a str, line_numbers: &'a LineNumbers, + echo_used: bool, current_scope_vars: im::HashMap, erl_function_scope_vars: im::HashMap, } @@ -56,6 +57,7 @@ impl<'env> Env<'env> { Self { current_scope_vars: vars.clone(), erl_function_scope_vars: vars, + echo_used: false, line_numbers, function, module, @@ -217,21 +219,35 @@ fn module_document<'a>( let src_path = EcoString::from(module.type_info.src_path.as_str()); - let statements = join( - module - .definitions - .iter() - .flat_map(|s| module_statement(s, &module.name, line_numbers, &src_path)), + let mut echo_used = false; + let mut statements = Vec::with_capacity(module.definitions.len()); + for definition in module.definitions.iter() { + if let Some((statement_document, env)) = + module_statement(definition, &module.name, line_numbers, &src_path) + { + echo_used = echo_used || env.echo_used; + statements.push(statement_document); + } + } + + let module = docvec![ + header, + "-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).", lines(2), - ); + exports, + type_defs, + join(statements, lines(2)), + ]; - Ok(header - .append("-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).") - .append(lines(2)) - .append(exports) - .append(type_defs) - .append(statements) - .append(line())) + let module = if echo_used { + module + .append(lines(2)) + .append(std::include_str!("../templates/echo.erl").to_doc()) + } else { + module + }; + + Ok(module.append(line())) } fn register_imports( @@ -354,7 +370,7 @@ fn module_statement<'a>( module: &'a str, line_numbers: &'a LineNumbers, src_path: &EcoString, -) -> Option> { +) -> Option<(Document<'a>, Env<'a>)> { match statement { Definition::TypeAlias(TypeAlias { .. }) | Definition::CustomType(CustomType { .. }) @@ -372,7 +388,7 @@ fn module_function<'a>( module: &'a str, line_numbers: &'a LineNumbers, src_path: EcoString, -) -> Option> { +) -> Option<(Document<'a>, Env<'a>)> { // Private external functions don't need to render anything, the underlying // Erlang implementation is used directly at the call site. if function.external_erlang.is_some() && function.publicity.is_private() { @@ -424,16 +440,19 @@ fn module_function<'a>( }) .unwrap_or_else(|| statement_sequence(&function.body, &mut env)); - Some(docvec![ - file_attribute, - line(), - spec, - atom_string(escape_erlang_existing_name(function_name).to_string()), - arguments, - " ->", - line().append(body).nest(INDENT).group(), - ".", - ]) + Some(( + docvec![ + file_attribute, + line(), + spec, + atom_string(escape_erlang_existing_name(function_name).to_string()), + arguments, + " ->", + line().append(body).nest(INDENT).group(), + ".", + ], + env, + )) } fn file_attribute<'a>( @@ -1738,7 +1757,8 @@ fn panic<'a>(location: SrcSpan, message: Option<&'a TypedExpr>, env: &mut Env<'a } fn echo<'a>(body: Document<'a>, env: &mut Env<'a>) -> Document<'a> { - module_fn_with_args("mmmh", "echo", vec![body], env) + env.echo_used = true; + "echo".to_doc().append(wrap_args(vec![body])) } fn erlang_error<'a>( diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__echo_in_a_pipeline.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__echo_in_a_pipeline.snap index c82511d95dc..0e149cb510b 100644 --- a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__echo_in_a_pipeline.snap +++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__echo_in_a_pipeline.snap @@ -16,5 +16,177 @@ wibble(N) -> -spec main() -> list(integer()). main() -> _pipe = [1, 2, 3], - mmmh:echo(_pipe), + echo(_pipe), wibble(_pipe). + +-define(is_lowercase_char(X), (X > 96 andalso X < 123)). +-define(is_underscore_char(X), (X == 95)). +-define(is_digit_char(X), (X > 47 andalso X < 58)). +-define(could_be_record(Tuple), + is_tuple(Tuple) andalso + is_atom(element(1, Tuple)) andalso + element(1, Tuple) =/= false andalso + element(1, Tuple) =/= true andalso + element(1, Tuple) =/= nil +). +-define(is_atom_char(C), + (?is_lowercase_char(C) orelse + ?is_underscore_char(C) orelse + ?is_digit_char(C)) +). + +echo(Value) -> + String = echo@inspect(Value), + io:put_chars(standard_error, [String, $\n]), + Value. + +echo@inspect(Value) -> + case Value of + nil -> "Nil"; + true -> "True"; + false -> "False"; + Int when is_integer(Int) -> erlang:integer_to_list(Int); + Float when is_float(Float) -> io_lib_format:fwrite_g(Float); + Bits when is_bitstring(Bits) -> inspect@bit_array(Bits); + Binary when is_binary(Binary) -> inspect@binary(Binary); + Atom when is_atom(Atom) -> inspect@atom(Atom); + List when is_list(List) -> inspect@list(List); + Map when is_map(Map) -> inspect@map(Map); + Record when ?could_be_record(Record) -> inspect@record(Record); + Tuple when is_tuple(Tuple) -> inspect@tuple(Tuple); + Function when is_function(Function) -> inspect@function(Function); + Any -> ["//erl(", io_lib:format("~p", [Any]), ")"] + end. + +inspect@bit_array(Bits) -> + Pieces = inspect@bit_array_pieces(Bits, []), + Inner = string:join(lists:reverse(Pieces), <<", ">>), + <<"<<", Inner/binary, ">>">>. + +inspect@bit_array_pieces(Bits, Acc) -> + case Bits of + <<>> -> + Acc; + <> -> + inspect@bit_array_pieces(Rest, [integer_to_binary(Byte) | Acc]); + _ -> + Size = bit_size(Bits), + <> = Bits, + SizeString = <<":size(", (integer_to_binary(Size)), ")">>, + Piece = <<(integer_to_binary(RemainingBits)), SizeString>>, + inspect@bit_array_pieces(<<>>, [Piece | Acc]) + end. + +inspect@binary(Binary) -> + case inspect@maybe_utf8_string(Binary, <<>>) of + {ok, InspectedUtf8String} -> + InspectedUtf8String; + {error, not_a_utf8_string} -> + Segments = [integer_to_list(X) || <> <= Binary], + ["<<", lists:join(", ", Segments), ">>"] + end. + +inspect@atom(Atom) -> + Binary = erlang:atom_to_binary(Atom), + case inspect@maybe_gleam_atom(Binary, none, <<>>) of + {ok, Inspected} -> Inspected; + {error, _} -> ["atom.create_from_string(\"", Binary, "\")"] + end. + +inspect@list(List) -> + case inspect@proper_or_improper_list(List) of + {proper, Elements} -> ["[", Elements, "]"]; + {improper, Elements} -> ["//erl([", Elements, "])"] + end. + +inspect@map(Map) -> + Fields = [ + [<<"#(">>, echo@inspect(Key), <<", ">>, echo@inspect(Value), <<")">>] + || {Key, Value} <- maps:to_list(Map) + ], + ["dict.from_list([", lists:join(", ", Fields), "])"]. + +inspect@record(Record) -> + [Atom | ArgsList] = erlang:tuple_to_list(Record), + Args = lists:join(", ", lists:map(fun echo@inspect/1, ArgsList)), + [echo@inspect(Atom), "(", Args, ")"]. + +inspect@tuple(Tuple) -> + Elements = lists:map(fun echo@inspect/1, erlang:tuple_to_list(Tuple)), + ["#(", lists:join(", ", Elements), ")"]. + +inspect@function(Function) -> + {arity, Arity} = erlang:fun_info(Function, arity), + ArgsAsciiCodes = lists:seq($a, $a + Arity - 1), + Args = lists:join(", ", lists:map(fun(Arg) -> <> end, ArgsAsciiCodes)), + ["//fn(", Args, ") { ... }"]. + +inspect@maybe_utf8_string(Binary, Acc) -> + case Binary of + <<>> -> + {ok, <<$", Acc/binary, $">>}; + <> -> + Escaped = inspect@escape_grapheme(First), + inspect@maybe_utf8_string(Rest, <>); + _ -> + {error, not_a_utf8_string} + end. + +inspect@escape_grapheme(Char) -> + case Char of + $" -> <<$\\, $">>; + $\\ -> <<$\\, $\\>>; + $\r -> <<$\\, $r>>; + $\n -> <<$\\, $n>>; + $\t -> <<$\\, $t>>; + $\f -> <<$\\, $f>>; + X when X > 126, X < 160 -> inspect@convert_to_u(X); + X when X < 32 -> inspect@convert_to_u(X); + Other -> <> + end. + +inspect@convert_to_u(Code) -> + list_to_binary(io_lib:format("\\u{~4.16.0B}", [Code])). + +inspect@proper_or_improper_list(List) -> + case List of + [] -> + {proper, []}; + [First] -> + {proper, [echo@inspect(First)]}; + [First | Rest] when is_list(Rest) -> + {Kind, Inspected} = inspect@list(Rest), + {Kind, [echo@inspect(First), ", " | Inspected]}; + [First | ImproperRest] -> + {improper, [echo@inspect(First), " | ", echo@inspect(ImproperRest)]} + end. + +inspect@maybe_gleam_atom(Atom, PrevChar, Acc) -> + case {Atom, PrevChar} of + {<<>>, none} -> + {error, nil}; + {<>, none} when ?is_digit_char(First) -> + {error, nil}; + {<<"_", _/binary>>, none} -> + {error, nil}; + {<<"_">>, _} -> + {error, nil}; + {<<"_", _/binary>>, $_} -> + {error, nil}; + {<>, _} when not ?is_atom_char(First) -> + {error, nil}; + {<>, none} -> + inspect@maybe_gleam_atom(Rest, First, <>); + {<<"_", Rest/binary>>, _} -> + inspect@maybe_gleam_atom(Rest, $_, Acc); + {<>, $_} -> + inspect@maybe_gleam_atom(Rest, First, <>); + {<>, _} -> + inspect@maybe_gleam_atom(Rest, First, <>); + {<<>>, _} -> + {ok, Acc}; + _ -> + throw({gleam_error, echo, Atom, PrevChar, Acc}) + end. + +inspect@uppercase(X) -> X - 32. diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__echo_with_a_block.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__echo_with_a_block.snap index 26aa8479566..51639cc29e5 100644 --- a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__echo_with_a_block.snap +++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__echo_with_a_block.snap @@ -10,9 +10,181 @@ expression: "\npub fn main() {\n echo {\n Nil\n 1\n }\n}\n" -file("/root/project/test/my/mod.gleam", 2). -spec main() -> integer(). main() -> - mmmh:echo( + echo( begin nil, 1 end ). + +-define(is_lowercase_char(X), (X > 96 andalso X < 123)). +-define(is_underscore_char(X), (X == 95)). +-define(is_digit_char(X), (X > 47 andalso X < 58)). +-define(could_be_record(Tuple), + is_tuple(Tuple) andalso + is_atom(element(1, Tuple)) andalso + element(1, Tuple) =/= false andalso + element(1, Tuple) =/= true andalso + element(1, Tuple) =/= nil +). +-define(is_atom_char(C), + (?is_lowercase_char(C) orelse + ?is_underscore_char(C) orelse + ?is_digit_char(C)) +). + +echo(Value) -> + String = echo@inspect(Value), + io:put_chars(standard_error, [String, $\n]), + Value. + +echo@inspect(Value) -> + case Value of + nil -> "Nil"; + true -> "True"; + false -> "False"; + Int when is_integer(Int) -> erlang:integer_to_list(Int); + Float when is_float(Float) -> io_lib_format:fwrite_g(Float); + Bits when is_bitstring(Bits) -> inspect@bit_array(Bits); + Binary when is_binary(Binary) -> inspect@binary(Binary); + Atom when is_atom(Atom) -> inspect@atom(Atom); + List when is_list(List) -> inspect@list(List); + Map when is_map(Map) -> inspect@map(Map); + Record when ?could_be_record(Record) -> inspect@record(Record); + Tuple when is_tuple(Tuple) -> inspect@tuple(Tuple); + Function when is_function(Function) -> inspect@function(Function); + Any -> ["//erl(", io_lib:format("~p", [Any]), ")"] + end. + +inspect@bit_array(Bits) -> + Pieces = inspect@bit_array_pieces(Bits, []), + Inner = string:join(lists:reverse(Pieces), <<", ">>), + <<"<<", Inner/binary, ">>">>. + +inspect@bit_array_pieces(Bits, Acc) -> + case Bits of + <<>> -> + Acc; + <> -> + inspect@bit_array_pieces(Rest, [integer_to_binary(Byte) | Acc]); + _ -> + Size = bit_size(Bits), + <> = Bits, + SizeString = <<":size(", (integer_to_binary(Size)), ")">>, + Piece = <<(integer_to_binary(RemainingBits)), SizeString>>, + inspect@bit_array_pieces(<<>>, [Piece | Acc]) + end. + +inspect@binary(Binary) -> + case inspect@maybe_utf8_string(Binary, <<>>) of + {ok, InspectedUtf8String} -> + InspectedUtf8String; + {error, not_a_utf8_string} -> + Segments = [integer_to_list(X) || <> <= Binary], + ["<<", lists:join(", ", Segments), ">>"] + end. + +inspect@atom(Atom) -> + Binary = erlang:atom_to_binary(Atom), + case inspect@maybe_gleam_atom(Binary, none, <<>>) of + {ok, Inspected} -> Inspected; + {error, _} -> ["atom.create_from_string(\"", Binary, "\")"] + end. + +inspect@list(List) -> + case inspect@proper_or_improper_list(List) of + {proper, Elements} -> ["[", Elements, "]"]; + {improper, Elements} -> ["//erl([", Elements, "])"] + end. + +inspect@map(Map) -> + Fields = [ + [<<"#(">>, echo@inspect(Key), <<", ">>, echo@inspect(Value), <<")">>] + || {Key, Value} <- maps:to_list(Map) + ], + ["dict.from_list([", lists:join(", ", Fields), "])"]. + +inspect@record(Record) -> + [Atom | ArgsList] = erlang:tuple_to_list(Record), + Args = lists:join(", ", lists:map(fun echo@inspect/1, ArgsList)), + [echo@inspect(Atom), "(", Args, ")"]. + +inspect@tuple(Tuple) -> + Elements = lists:map(fun echo@inspect/1, erlang:tuple_to_list(Tuple)), + ["#(", lists:join(", ", Elements), ")"]. + +inspect@function(Function) -> + {arity, Arity} = erlang:fun_info(Function, arity), + ArgsAsciiCodes = lists:seq($a, $a + Arity - 1), + Args = lists:join(", ", lists:map(fun(Arg) -> <> end, ArgsAsciiCodes)), + ["//fn(", Args, ") { ... }"]. + +inspect@maybe_utf8_string(Binary, Acc) -> + case Binary of + <<>> -> + {ok, <<$", Acc/binary, $">>}; + <> -> + Escaped = inspect@escape_grapheme(First), + inspect@maybe_utf8_string(Rest, <>); + _ -> + {error, not_a_utf8_string} + end. + +inspect@escape_grapheme(Char) -> + case Char of + $" -> <<$\\, $">>; + $\\ -> <<$\\, $\\>>; + $\r -> <<$\\, $r>>; + $\n -> <<$\\, $n>>; + $\t -> <<$\\, $t>>; + $\f -> <<$\\, $f>>; + X when X > 126, X < 160 -> inspect@convert_to_u(X); + X when X < 32 -> inspect@convert_to_u(X); + Other -> <> + end. + +inspect@convert_to_u(Code) -> + list_to_binary(io_lib:format("\\u{~4.16.0B}", [Code])). + +inspect@proper_or_improper_list(List) -> + case List of + [] -> + {proper, []}; + [First] -> + {proper, [echo@inspect(First)]}; + [First | Rest] when is_list(Rest) -> + {Kind, Inspected} = inspect@list(Rest), + {Kind, [echo@inspect(First), ", " | Inspected]}; + [First | ImproperRest] -> + {improper, [echo@inspect(First), " | ", echo@inspect(ImproperRest)]} + end. + +inspect@maybe_gleam_atom(Atom, PrevChar, Acc) -> + case {Atom, PrevChar} of + {<<>>, none} -> + {error, nil}; + {<>, none} when ?is_digit_char(First) -> + {error, nil}; + {<<"_", _/binary>>, none} -> + {error, nil}; + {<<"_">>, _} -> + {error, nil}; + {<<"_", _/binary>>, $_} -> + {error, nil}; + {<>, _} when not ?is_atom_char(First) -> + {error, nil}; + {<>, none} -> + inspect@maybe_gleam_atom(Rest, First, <>); + {<<"_", Rest/binary>>, _} -> + inspect@maybe_gleam_atom(Rest, $_, Acc); + {<>, $_} -> + inspect@maybe_gleam_atom(Rest, First, <>); + {<>, _} -> + inspect@maybe_gleam_atom(Rest, First, <>); + {<<>>, _} -> + {ok, Acc}; + _ -> + throw({gleam_error, echo, Atom, PrevChar, Acc}) + end. + +inspect@uppercase(X) -> X - 32. diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__echo_with_a_case_expression.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__echo_with_a_case_expression.snap index acd4a4ab9fa..3d00129129f 100644 --- a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__echo_with_a_case_expression.snap +++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__echo_with_a_case_expression.snap @@ -10,7 +10,179 @@ expression: "\npub fn main() {\n echo case 1 {\n _ -> 2\n }\n}\n" -file("/root/project/test/my/mod.gleam", 2). -spec main() -> integer(). main() -> - mmmh:echo(case 1 of + echo(case 1 of _ -> 2 end). + +-define(is_lowercase_char(X), (X > 96 andalso X < 123)). +-define(is_underscore_char(X), (X == 95)). +-define(is_digit_char(X), (X > 47 andalso X < 58)). +-define(could_be_record(Tuple), + is_tuple(Tuple) andalso + is_atom(element(1, Tuple)) andalso + element(1, Tuple) =/= false andalso + element(1, Tuple) =/= true andalso + element(1, Tuple) =/= nil +). +-define(is_atom_char(C), + (?is_lowercase_char(C) orelse + ?is_underscore_char(C) orelse + ?is_digit_char(C)) +). + +echo(Value) -> + String = echo@inspect(Value), + io:put_chars(standard_error, [String, $\n]), + Value. + +echo@inspect(Value) -> + case Value of + nil -> "Nil"; + true -> "True"; + false -> "False"; + Int when is_integer(Int) -> erlang:integer_to_list(Int); + Float when is_float(Float) -> io_lib_format:fwrite_g(Float); + Bits when is_bitstring(Bits) -> inspect@bit_array(Bits); + Binary when is_binary(Binary) -> inspect@binary(Binary); + Atom when is_atom(Atom) -> inspect@atom(Atom); + List when is_list(List) -> inspect@list(List); + Map when is_map(Map) -> inspect@map(Map); + Record when ?could_be_record(Record) -> inspect@record(Record); + Tuple when is_tuple(Tuple) -> inspect@tuple(Tuple); + Function when is_function(Function) -> inspect@function(Function); + Any -> ["//erl(", io_lib:format("~p", [Any]), ")"] + end. + +inspect@bit_array(Bits) -> + Pieces = inspect@bit_array_pieces(Bits, []), + Inner = string:join(lists:reverse(Pieces), <<", ">>), + <<"<<", Inner/binary, ">>">>. + +inspect@bit_array_pieces(Bits, Acc) -> + case Bits of + <<>> -> + Acc; + <> -> + inspect@bit_array_pieces(Rest, [integer_to_binary(Byte) | Acc]); + _ -> + Size = bit_size(Bits), + <> = Bits, + SizeString = <<":size(", (integer_to_binary(Size)), ")">>, + Piece = <<(integer_to_binary(RemainingBits)), SizeString>>, + inspect@bit_array_pieces(<<>>, [Piece | Acc]) + end. + +inspect@binary(Binary) -> + case inspect@maybe_utf8_string(Binary, <<>>) of + {ok, InspectedUtf8String} -> + InspectedUtf8String; + {error, not_a_utf8_string} -> + Segments = [integer_to_list(X) || <> <= Binary], + ["<<", lists:join(", ", Segments), ">>"] + end. + +inspect@atom(Atom) -> + Binary = erlang:atom_to_binary(Atom), + case inspect@maybe_gleam_atom(Binary, none, <<>>) of + {ok, Inspected} -> Inspected; + {error, _} -> ["atom.create_from_string(\"", Binary, "\")"] + end. + +inspect@list(List) -> + case inspect@proper_or_improper_list(List) of + {proper, Elements} -> ["[", Elements, "]"]; + {improper, Elements} -> ["//erl([", Elements, "])"] + end. + +inspect@map(Map) -> + Fields = [ + [<<"#(">>, echo@inspect(Key), <<", ">>, echo@inspect(Value), <<")">>] + || {Key, Value} <- maps:to_list(Map) + ], + ["dict.from_list([", lists:join(", ", Fields), "])"]. + +inspect@record(Record) -> + [Atom | ArgsList] = erlang:tuple_to_list(Record), + Args = lists:join(", ", lists:map(fun echo@inspect/1, ArgsList)), + [echo@inspect(Atom), "(", Args, ")"]. + +inspect@tuple(Tuple) -> + Elements = lists:map(fun echo@inspect/1, erlang:tuple_to_list(Tuple)), + ["#(", lists:join(", ", Elements), ")"]. + +inspect@function(Function) -> + {arity, Arity} = erlang:fun_info(Function, arity), + ArgsAsciiCodes = lists:seq($a, $a + Arity - 1), + Args = lists:join(", ", lists:map(fun(Arg) -> <> end, ArgsAsciiCodes)), + ["//fn(", Args, ") { ... }"]. + +inspect@maybe_utf8_string(Binary, Acc) -> + case Binary of + <<>> -> + {ok, <<$", Acc/binary, $">>}; + <> -> + Escaped = inspect@escape_grapheme(First), + inspect@maybe_utf8_string(Rest, <>); + _ -> + {error, not_a_utf8_string} + end. + +inspect@escape_grapheme(Char) -> + case Char of + $" -> <<$\\, $">>; + $\\ -> <<$\\, $\\>>; + $\r -> <<$\\, $r>>; + $\n -> <<$\\, $n>>; + $\t -> <<$\\, $t>>; + $\f -> <<$\\, $f>>; + X when X > 126, X < 160 -> inspect@convert_to_u(X); + X when X < 32 -> inspect@convert_to_u(X); + Other -> <> + end. + +inspect@convert_to_u(Code) -> + list_to_binary(io_lib:format("\\u{~4.16.0B}", [Code])). + +inspect@proper_or_improper_list(List) -> + case List of + [] -> + {proper, []}; + [First] -> + {proper, [echo@inspect(First)]}; + [First | Rest] when is_list(Rest) -> + {Kind, Inspected} = inspect@list(Rest), + {Kind, [echo@inspect(First), ", " | Inspected]}; + [First | ImproperRest] -> + {improper, [echo@inspect(First), " | ", echo@inspect(ImproperRest)]} + end. + +inspect@maybe_gleam_atom(Atom, PrevChar, Acc) -> + case {Atom, PrevChar} of + {<<>>, none} -> + {error, nil}; + {<>, none} when ?is_digit_char(First) -> + {error, nil}; + {<<"_", _/binary>>, none} -> + {error, nil}; + {<<"_">>, _} -> + {error, nil}; + {<<"_", _/binary>>, $_} -> + {error, nil}; + {<>, _} when not ?is_atom_char(First) -> + {error, nil}; + {<>, none} -> + inspect@maybe_gleam_atom(Rest, First, <>); + {<<"_", Rest/binary>>, _} -> + inspect@maybe_gleam_atom(Rest, $_, Acc); + {<>, $_} -> + inspect@maybe_gleam_atom(Rest, First, <>); + {<>, _} -> + inspect@maybe_gleam_atom(Rest, First, <>); + {<<>>, _} -> + {ok, Acc}; + _ -> + throw({gleam_error, echo, Atom, PrevChar, Acc}) + end. + +inspect@uppercase(X) -> X - 32. diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__echo_with_a_function_call.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__echo_with_a_function_call.snap index 8e2fd934f02..48552335f35 100644 --- a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__echo_with_a_function_call.snap +++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__echo_with_a_function_call.snap @@ -15,4 +15,176 @@ wibble(N, M) -> -file("/root/project/test/my/mod.gleam", 2). -spec main() -> integer(). main() -> - mmmh:echo(wibble(1, 2)). + echo(wibble(1, 2)). + +-define(is_lowercase_char(X), (X > 96 andalso X < 123)). +-define(is_underscore_char(X), (X == 95)). +-define(is_digit_char(X), (X > 47 andalso X < 58)). +-define(could_be_record(Tuple), + is_tuple(Tuple) andalso + is_atom(element(1, Tuple)) andalso + element(1, Tuple) =/= false andalso + element(1, Tuple) =/= true andalso + element(1, Tuple) =/= nil +). +-define(is_atom_char(C), + (?is_lowercase_char(C) orelse + ?is_underscore_char(C) orelse + ?is_digit_char(C)) +). + +echo(Value) -> + String = echo@inspect(Value), + io:put_chars(standard_error, [String, $\n]), + Value. + +echo@inspect(Value) -> + case Value of + nil -> "Nil"; + true -> "True"; + false -> "False"; + Int when is_integer(Int) -> erlang:integer_to_list(Int); + Float when is_float(Float) -> io_lib_format:fwrite_g(Float); + Bits when is_bitstring(Bits) -> inspect@bit_array(Bits); + Binary when is_binary(Binary) -> inspect@binary(Binary); + Atom when is_atom(Atom) -> inspect@atom(Atom); + List when is_list(List) -> inspect@list(List); + Map when is_map(Map) -> inspect@map(Map); + Record when ?could_be_record(Record) -> inspect@record(Record); + Tuple when is_tuple(Tuple) -> inspect@tuple(Tuple); + Function when is_function(Function) -> inspect@function(Function); + Any -> ["//erl(", io_lib:format("~p", [Any]), ")"] + end. + +inspect@bit_array(Bits) -> + Pieces = inspect@bit_array_pieces(Bits, []), + Inner = string:join(lists:reverse(Pieces), <<", ">>), + <<"<<", Inner/binary, ">>">>. + +inspect@bit_array_pieces(Bits, Acc) -> + case Bits of + <<>> -> + Acc; + <> -> + inspect@bit_array_pieces(Rest, [integer_to_binary(Byte) | Acc]); + _ -> + Size = bit_size(Bits), + <> = Bits, + SizeString = <<":size(", (integer_to_binary(Size)), ")">>, + Piece = <<(integer_to_binary(RemainingBits)), SizeString>>, + inspect@bit_array_pieces(<<>>, [Piece | Acc]) + end. + +inspect@binary(Binary) -> + case inspect@maybe_utf8_string(Binary, <<>>) of + {ok, InspectedUtf8String} -> + InspectedUtf8String; + {error, not_a_utf8_string} -> + Segments = [integer_to_list(X) || <> <= Binary], + ["<<", lists:join(", ", Segments), ">>"] + end. + +inspect@atom(Atom) -> + Binary = erlang:atom_to_binary(Atom), + case inspect@maybe_gleam_atom(Binary, none, <<>>) of + {ok, Inspected} -> Inspected; + {error, _} -> ["atom.create_from_string(\"", Binary, "\")"] + end. + +inspect@list(List) -> + case inspect@proper_or_improper_list(List) of + {proper, Elements} -> ["[", Elements, "]"]; + {improper, Elements} -> ["//erl([", Elements, "])"] + end. + +inspect@map(Map) -> + Fields = [ + [<<"#(">>, echo@inspect(Key), <<", ">>, echo@inspect(Value), <<")">>] + || {Key, Value} <- maps:to_list(Map) + ], + ["dict.from_list([", lists:join(", ", Fields), "])"]. + +inspect@record(Record) -> + [Atom | ArgsList] = erlang:tuple_to_list(Record), + Args = lists:join(", ", lists:map(fun echo@inspect/1, ArgsList)), + [echo@inspect(Atom), "(", Args, ")"]. + +inspect@tuple(Tuple) -> + Elements = lists:map(fun echo@inspect/1, erlang:tuple_to_list(Tuple)), + ["#(", lists:join(", ", Elements), ")"]. + +inspect@function(Function) -> + {arity, Arity} = erlang:fun_info(Function, arity), + ArgsAsciiCodes = lists:seq($a, $a + Arity - 1), + Args = lists:join(", ", lists:map(fun(Arg) -> <> end, ArgsAsciiCodes)), + ["//fn(", Args, ") { ... }"]. + +inspect@maybe_utf8_string(Binary, Acc) -> + case Binary of + <<>> -> + {ok, <<$", Acc/binary, $">>}; + <> -> + Escaped = inspect@escape_grapheme(First), + inspect@maybe_utf8_string(Rest, <>); + _ -> + {error, not_a_utf8_string} + end. + +inspect@escape_grapheme(Char) -> + case Char of + $" -> <<$\\, $">>; + $\\ -> <<$\\, $\\>>; + $\r -> <<$\\, $r>>; + $\n -> <<$\\, $n>>; + $\t -> <<$\\, $t>>; + $\f -> <<$\\, $f>>; + X when X > 126, X < 160 -> inspect@convert_to_u(X); + X when X < 32 -> inspect@convert_to_u(X); + Other -> <> + end. + +inspect@convert_to_u(Code) -> + list_to_binary(io_lib:format("\\u{~4.16.0B}", [Code])). + +inspect@proper_or_improper_list(List) -> + case List of + [] -> + {proper, []}; + [First] -> + {proper, [echo@inspect(First)]}; + [First | Rest] when is_list(Rest) -> + {Kind, Inspected} = inspect@list(Rest), + {Kind, [echo@inspect(First), ", " | Inspected]}; + [First | ImproperRest] -> + {improper, [echo@inspect(First), " | ", echo@inspect(ImproperRest)]} + end. + +inspect@maybe_gleam_atom(Atom, PrevChar, Acc) -> + case {Atom, PrevChar} of + {<<>>, none} -> + {error, nil}; + {<>, none} when ?is_digit_char(First) -> + {error, nil}; + {<<"_", _/binary>>, none} -> + {error, nil}; + {<<"_">>, _} -> + {error, nil}; + {<<"_", _/binary>>, $_} -> + {error, nil}; + {<>, _} when not ?is_atom_char(First) -> + {error, nil}; + {<>, none} -> + inspect@maybe_gleam_atom(Rest, First, <>); + {<<"_", Rest/binary>>, _} -> + inspect@maybe_gleam_atom(Rest, $_, Acc); + {<>, $_} -> + inspect@maybe_gleam_atom(Rest, First, <>); + {<>, _} -> + inspect@maybe_gleam_atom(Rest, First, <>); + {<<>>, _} -> + {ok, Acc}; + _ -> + throw({gleam_error, echo, Atom, PrevChar, Acc}) + end. + +inspect@uppercase(X) -> X - 32. diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__echo_with_a_panic.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__echo_with_a_panic.snap index f51f1f70957..68a1d174fe9 100644 --- a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__echo_with_a_panic.snap +++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__echo_with_a_panic.snap @@ -10,8 +10,180 @@ expression: "\npub fn main() {\n echo panic\n}\n" -file("/root/project/test/my/mod.gleam", 2). -spec main() -> any(). main() -> - mmmh:echo(erlang:error(#{gleam_error => panic, + echo(erlang:error(#{gleam_error => panic, message => <<"`panic` expression evaluated."/utf8>>, module => <<"my/mod"/utf8>>, function => <<"main"/utf8>>, line => 3})). + +-define(is_lowercase_char(X), (X > 96 andalso X < 123)). +-define(is_underscore_char(X), (X == 95)). +-define(is_digit_char(X), (X > 47 andalso X < 58)). +-define(could_be_record(Tuple), + is_tuple(Tuple) andalso + is_atom(element(1, Tuple)) andalso + element(1, Tuple) =/= false andalso + element(1, Tuple) =/= true andalso + element(1, Tuple) =/= nil +). +-define(is_atom_char(C), + (?is_lowercase_char(C) orelse + ?is_underscore_char(C) orelse + ?is_digit_char(C)) +). + +echo(Value) -> + String = echo@inspect(Value), + io:put_chars(standard_error, [String, $\n]), + Value. + +echo@inspect(Value) -> + case Value of + nil -> "Nil"; + true -> "True"; + false -> "False"; + Int when is_integer(Int) -> erlang:integer_to_list(Int); + Float when is_float(Float) -> io_lib_format:fwrite_g(Float); + Bits when is_bitstring(Bits) -> inspect@bit_array(Bits); + Binary when is_binary(Binary) -> inspect@binary(Binary); + Atom when is_atom(Atom) -> inspect@atom(Atom); + List when is_list(List) -> inspect@list(List); + Map when is_map(Map) -> inspect@map(Map); + Record when ?could_be_record(Record) -> inspect@record(Record); + Tuple when is_tuple(Tuple) -> inspect@tuple(Tuple); + Function when is_function(Function) -> inspect@function(Function); + Any -> ["//erl(", io_lib:format("~p", [Any]), ")"] + end. + +inspect@bit_array(Bits) -> + Pieces = inspect@bit_array_pieces(Bits, []), + Inner = string:join(lists:reverse(Pieces), <<", ">>), + <<"<<", Inner/binary, ">>">>. + +inspect@bit_array_pieces(Bits, Acc) -> + case Bits of + <<>> -> + Acc; + <> -> + inspect@bit_array_pieces(Rest, [integer_to_binary(Byte) | Acc]); + _ -> + Size = bit_size(Bits), + <> = Bits, + SizeString = <<":size(", (integer_to_binary(Size)), ")">>, + Piece = <<(integer_to_binary(RemainingBits)), SizeString>>, + inspect@bit_array_pieces(<<>>, [Piece | Acc]) + end. + +inspect@binary(Binary) -> + case inspect@maybe_utf8_string(Binary, <<>>) of + {ok, InspectedUtf8String} -> + InspectedUtf8String; + {error, not_a_utf8_string} -> + Segments = [integer_to_list(X) || <> <= Binary], + ["<<", lists:join(", ", Segments), ">>"] + end. + +inspect@atom(Atom) -> + Binary = erlang:atom_to_binary(Atom), + case inspect@maybe_gleam_atom(Binary, none, <<>>) of + {ok, Inspected} -> Inspected; + {error, _} -> ["atom.create_from_string(\"", Binary, "\")"] + end. + +inspect@list(List) -> + case inspect@proper_or_improper_list(List) of + {proper, Elements} -> ["[", Elements, "]"]; + {improper, Elements} -> ["//erl([", Elements, "])"] + end. + +inspect@map(Map) -> + Fields = [ + [<<"#(">>, echo@inspect(Key), <<", ">>, echo@inspect(Value), <<")">>] + || {Key, Value} <- maps:to_list(Map) + ], + ["dict.from_list([", lists:join(", ", Fields), "])"]. + +inspect@record(Record) -> + [Atom | ArgsList] = erlang:tuple_to_list(Record), + Args = lists:join(", ", lists:map(fun echo@inspect/1, ArgsList)), + [echo@inspect(Atom), "(", Args, ")"]. + +inspect@tuple(Tuple) -> + Elements = lists:map(fun echo@inspect/1, erlang:tuple_to_list(Tuple)), + ["#(", lists:join(", ", Elements), ")"]. + +inspect@function(Function) -> + {arity, Arity} = erlang:fun_info(Function, arity), + ArgsAsciiCodes = lists:seq($a, $a + Arity - 1), + Args = lists:join(", ", lists:map(fun(Arg) -> <> end, ArgsAsciiCodes)), + ["//fn(", Args, ") { ... }"]. + +inspect@maybe_utf8_string(Binary, Acc) -> + case Binary of + <<>> -> + {ok, <<$", Acc/binary, $">>}; + <> -> + Escaped = inspect@escape_grapheme(First), + inspect@maybe_utf8_string(Rest, <>); + _ -> + {error, not_a_utf8_string} + end. + +inspect@escape_grapheme(Char) -> + case Char of + $" -> <<$\\, $">>; + $\\ -> <<$\\, $\\>>; + $\r -> <<$\\, $r>>; + $\n -> <<$\\, $n>>; + $\t -> <<$\\, $t>>; + $\f -> <<$\\, $f>>; + X when X > 126, X < 160 -> inspect@convert_to_u(X); + X when X < 32 -> inspect@convert_to_u(X); + Other -> <> + end. + +inspect@convert_to_u(Code) -> + list_to_binary(io_lib:format("\\u{~4.16.0B}", [Code])). + +inspect@proper_or_improper_list(List) -> + case List of + [] -> + {proper, []}; + [First] -> + {proper, [echo@inspect(First)]}; + [First | Rest] when is_list(Rest) -> + {Kind, Inspected} = inspect@list(Rest), + {Kind, [echo@inspect(First), ", " | Inspected]}; + [First | ImproperRest] -> + {improper, [echo@inspect(First), " | ", echo@inspect(ImproperRest)]} + end. + +inspect@maybe_gleam_atom(Atom, PrevChar, Acc) -> + case {Atom, PrevChar} of + {<<>>, none} -> + {error, nil}; + {<>, none} when ?is_digit_char(First) -> + {error, nil}; + {<<"_", _/binary>>, none} -> + {error, nil}; + {<<"_">>, _} -> + {error, nil}; + {<<"_", _/binary>>, $_} -> + {error, nil}; + {<>, _} when not ?is_atom_char(First) -> + {error, nil}; + {<>, none} -> + inspect@maybe_gleam_atom(Rest, First, <>); + {<<"_", Rest/binary>>, _} -> + inspect@maybe_gleam_atom(Rest, $_, Acc); + {<>, $_} -> + inspect@maybe_gleam_atom(Rest, First, <>); + {<>, _} -> + inspect@maybe_gleam_atom(Rest, First, <>); + {<<>>, _} -> + {ok, Acc}; + _ -> + throw({gleam_error, echo, Atom, PrevChar, Acc}) + end. + +inspect@uppercase(X) -> X - 32. diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__echo_with_a_simple_expression.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__echo_with_a_simple_expression.snap index 42cbe93a2d8..6662d847a38 100644 --- a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__echo_with_a_simple_expression.snap +++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__echo_with_a_simple_expression.snap @@ -10,4 +10,176 @@ expression: "\npub fn main() {\n echo 1\n}\n" -file("/root/project/test/my/mod.gleam", 2). -spec main() -> integer(). main() -> - mmmh:echo(1). + echo(1). + +-define(is_lowercase_char(X), (X > 96 andalso X < 123)). +-define(is_underscore_char(X), (X == 95)). +-define(is_digit_char(X), (X > 47 andalso X < 58)). +-define(could_be_record(Tuple), + is_tuple(Tuple) andalso + is_atom(element(1, Tuple)) andalso + element(1, Tuple) =/= false andalso + element(1, Tuple) =/= true andalso + element(1, Tuple) =/= nil +). +-define(is_atom_char(C), + (?is_lowercase_char(C) orelse + ?is_underscore_char(C) orelse + ?is_digit_char(C)) +). + +echo(Value) -> + String = echo@inspect(Value), + io:put_chars(standard_error, [String, $\n]), + Value. + +echo@inspect(Value) -> + case Value of + nil -> "Nil"; + true -> "True"; + false -> "False"; + Int when is_integer(Int) -> erlang:integer_to_list(Int); + Float when is_float(Float) -> io_lib_format:fwrite_g(Float); + Bits when is_bitstring(Bits) -> inspect@bit_array(Bits); + Binary when is_binary(Binary) -> inspect@binary(Binary); + Atom when is_atom(Atom) -> inspect@atom(Atom); + List when is_list(List) -> inspect@list(List); + Map when is_map(Map) -> inspect@map(Map); + Record when ?could_be_record(Record) -> inspect@record(Record); + Tuple when is_tuple(Tuple) -> inspect@tuple(Tuple); + Function when is_function(Function) -> inspect@function(Function); + Any -> ["//erl(", io_lib:format("~p", [Any]), ")"] + end. + +inspect@bit_array(Bits) -> + Pieces = inspect@bit_array_pieces(Bits, []), + Inner = string:join(lists:reverse(Pieces), <<", ">>), + <<"<<", Inner/binary, ">>">>. + +inspect@bit_array_pieces(Bits, Acc) -> + case Bits of + <<>> -> + Acc; + <> -> + inspect@bit_array_pieces(Rest, [integer_to_binary(Byte) | Acc]); + _ -> + Size = bit_size(Bits), + <> = Bits, + SizeString = <<":size(", (integer_to_binary(Size)), ")">>, + Piece = <<(integer_to_binary(RemainingBits)), SizeString>>, + inspect@bit_array_pieces(<<>>, [Piece | Acc]) + end. + +inspect@binary(Binary) -> + case inspect@maybe_utf8_string(Binary, <<>>) of + {ok, InspectedUtf8String} -> + InspectedUtf8String; + {error, not_a_utf8_string} -> + Segments = [integer_to_list(X) || <> <= Binary], + ["<<", lists:join(", ", Segments), ">>"] + end. + +inspect@atom(Atom) -> + Binary = erlang:atom_to_binary(Atom), + case inspect@maybe_gleam_atom(Binary, none, <<>>) of + {ok, Inspected} -> Inspected; + {error, _} -> ["atom.create_from_string(\"", Binary, "\")"] + end. + +inspect@list(List) -> + case inspect@proper_or_improper_list(List) of + {proper, Elements} -> ["[", Elements, "]"]; + {improper, Elements} -> ["//erl([", Elements, "])"] + end. + +inspect@map(Map) -> + Fields = [ + [<<"#(">>, echo@inspect(Key), <<", ">>, echo@inspect(Value), <<")">>] + || {Key, Value} <- maps:to_list(Map) + ], + ["dict.from_list([", lists:join(", ", Fields), "])"]. + +inspect@record(Record) -> + [Atom | ArgsList] = erlang:tuple_to_list(Record), + Args = lists:join(", ", lists:map(fun echo@inspect/1, ArgsList)), + [echo@inspect(Atom), "(", Args, ")"]. + +inspect@tuple(Tuple) -> + Elements = lists:map(fun echo@inspect/1, erlang:tuple_to_list(Tuple)), + ["#(", lists:join(", ", Elements), ")"]. + +inspect@function(Function) -> + {arity, Arity} = erlang:fun_info(Function, arity), + ArgsAsciiCodes = lists:seq($a, $a + Arity - 1), + Args = lists:join(", ", lists:map(fun(Arg) -> <> end, ArgsAsciiCodes)), + ["//fn(", Args, ") { ... }"]. + +inspect@maybe_utf8_string(Binary, Acc) -> + case Binary of + <<>> -> + {ok, <<$", Acc/binary, $">>}; + <> -> + Escaped = inspect@escape_grapheme(First), + inspect@maybe_utf8_string(Rest, <>); + _ -> + {error, not_a_utf8_string} + end. + +inspect@escape_grapheme(Char) -> + case Char of + $" -> <<$\\, $">>; + $\\ -> <<$\\, $\\>>; + $\r -> <<$\\, $r>>; + $\n -> <<$\\, $n>>; + $\t -> <<$\\, $t>>; + $\f -> <<$\\, $f>>; + X when X > 126, X < 160 -> inspect@convert_to_u(X); + X when X < 32 -> inspect@convert_to_u(X); + Other -> <> + end. + +inspect@convert_to_u(Code) -> + list_to_binary(io_lib:format("\\u{~4.16.0B}", [Code])). + +inspect@proper_or_improper_list(List) -> + case List of + [] -> + {proper, []}; + [First] -> + {proper, [echo@inspect(First)]}; + [First | Rest] when is_list(Rest) -> + {Kind, Inspected} = inspect@list(Rest), + {Kind, [echo@inspect(First), ", " | Inspected]}; + [First | ImproperRest] -> + {improper, [echo@inspect(First), " | ", echo@inspect(ImproperRest)]} + end. + +inspect@maybe_gleam_atom(Atom, PrevChar, Acc) -> + case {Atom, PrevChar} of + {<<>>, none} -> + {error, nil}; + {<>, none} when ?is_digit_char(First) -> + {error, nil}; + {<<"_", _/binary>>, none} -> + {error, nil}; + {<<"_">>, _} -> + {error, nil}; + {<<"_", _/binary>>, $_} -> + {error, nil}; + {<>, _} when not ?is_atom_char(First) -> + {error, nil}; + {<>, none} -> + inspect@maybe_gleam_atom(Rest, First, <>); + {<<"_", Rest/binary>>, _} -> + inspect@maybe_gleam_atom(Rest, $_, Acc); + {<>, $_} -> + inspect@maybe_gleam_atom(Rest, First, <>); + {<>, _} -> + inspect@maybe_gleam_atom(Rest, First, <>); + {<<>>, _} -> + {ok, Acc}; + _ -> + throw({gleam_error, echo, Atom, PrevChar, Acc}) + end. + +inspect@uppercase(X) -> X - 32. diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__multiple_echos_in_a_pipeline.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__multiple_echos_in_a_pipeline.snap index b9c5a7318b9..54aff514c61 100644 --- a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__multiple_echos_in_a_pipeline.snap +++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__multiple_echos_in_a_pipeline.snap @@ -16,8 +16,180 @@ wibble(N) -> -spec main() -> list(integer()). main() -> _pipe = [1, 2, 3], - mmmh:echo(_pipe), + echo(_pipe), _pipe@1 = wibble(_pipe), - mmmh:echo(_pipe@1), + echo(_pipe@1), _pipe@2 = wibble(_pipe@1), - mmmh:echo(_pipe@2). + echo(_pipe@2). + +-define(is_lowercase_char(X), (X > 96 andalso X < 123)). +-define(is_underscore_char(X), (X == 95)). +-define(is_digit_char(X), (X > 47 andalso X < 58)). +-define(could_be_record(Tuple), + is_tuple(Tuple) andalso + is_atom(element(1, Tuple)) andalso + element(1, Tuple) =/= false andalso + element(1, Tuple) =/= true andalso + element(1, Tuple) =/= nil +). +-define(is_atom_char(C), + (?is_lowercase_char(C) orelse + ?is_underscore_char(C) orelse + ?is_digit_char(C)) +). + +echo(Value) -> + String = echo@inspect(Value), + io:put_chars(standard_error, [String, $\n]), + Value. + +echo@inspect(Value) -> + case Value of + nil -> "Nil"; + true -> "True"; + false -> "False"; + Int when is_integer(Int) -> erlang:integer_to_list(Int); + Float when is_float(Float) -> io_lib_format:fwrite_g(Float); + Bits when is_bitstring(Bits) -> inspect@bit_array(Bits); + Binary when is_binary(Binary) -> inspect@binary(Binary); + Atom when is_atom(Atom) -> inspect@atom(Atom); + List when is_list(List) -> inspect@list(List); + Map when is_map(Map) -> inspect@map(Map); + Record when ?could_be_record(Record) -> inspect@record(Record); + Tuple when is_tuple(Tuple) -> inspect@tuple(Tuple); + Function when is_function(Function) -> inspect@function(Function); + Any -> ["//erl(", io_lib:format("~p", [Any]), ")"] + end. + +inspect@bit_array(Bits) -> + Pieces = inspect@bit_array_pieces(Bits, []), + Inner = string:join(lists:reverse(Pieces), <<", ">>), + <<"<<", Inner/binary, ">>">>. + +inspect@bit_array_pieces(Bits, Acc) -> + case Bits of + <<>> -> + Acc; + <> -> + inspect@bit_array_pieces(Rest, [integer_to_binary(Byte) | Acc]); + _ -> + Size = bit_size(Bits), + <> = Bits, + SizeString = <<":size(", (integer_to_binary(Size)), ")">>, + Piece = <<(integer_to_binary(RemainingBits)), SizeString>>, + inspect@bit_array_pieces(<<>>, [Piece | Acc]) + end. + +inspect@binary(Binary) -> + case inspect@maybe_utf8_string(Binary, <<>>) of + {ok, InspectedUtf8String} -> + InspectedUtf8String; + {error, not_a_utf8_string} -> + Segments = [integer_to_list(X) || <> <= Binary], + ["<<", lists:join(", ", Segments), ">>"] + end. + +inspect@atom(Atom) -> + Binary = erlang:atom_to_binary(Atom), + case inspect@maybe_gleam_atom(Binary, none, <<>>) of + {ok, Inspected} -> Inspected; + {error, _} -> ["atom.create_from_string(\"", Binary, "\")"] + end. + +inspect@list(List) -> + case inspect@proper_or_improper_list(List) of + {proper, Elements} -> ["[", Elements, "]"]; + {improper, Elements} -> ["//erl([", Elements, "])"] + end. + +inspect@map(Map) -> + Fields = [ + [<<"#(">>, echo@inspect(Key), <<", ">>, echo@inspect(Value), <<")">>] + || {Key, Value} <- maps:to_list(Map) + ], + ["dict.from_list([", lists:join(", ", Fields), "])"]. + +inspect@record(Record) -> + [Atom | ArgsList] = erlang:tuple_to_list(Record), + Args = lists:join(", ", lists:map(fun echo@inspect/1, ArgsList)), + [echo@inspect(Atom), "(", Args, ")"]. + +inspect@tuple(Tuple) -> + Elements = lists:map(fun echo@inspect/1, erlang:tuple_to_list(Tuple)), + ["#(", lists:join(", ", Elements), ")"]. + +inspect@function(Function) -> + {arity, Arity} = erlang:fun_info(Function, arity), + ArgsAsciiCodes = lists:seq($a, $a + Arity - 1), + Args = lists:join(", ", lists:map(fun(Arg) -> <> end, ArgsAsciiCodes)), + ["//fn(", Args, ") { ... }"]. + +inspect@maybe_utf8_string(Binary, Acc) -> + case Binary of + <<>> -> + {ok, <<$", Acc/binary, $">>}; + <> -> + Escaped = inspect@escape_grapheme(First), + inspect@maybe_utf8_string(Rest, <>); + _ -> + {error, not_a_utf8_string} + end. + +inspect@escape_grapheme(Char) -> + case Char of + $" -> <<$\\, $">>; + $\\ -> <<$\\, $\\>>; + $\r -> <<$\\, $r>>; + $\n -> <<$\\, $n>>; + $\t -> <<$\\, $t>>; + $\f -> <<$\\, $f>>; + X when X > 126, X < 160 -> inspect@convert_to_u(X); + X when X < 32 -> inspect@convert_to_u(X); + Other -> <> + end. + +inspect@convert_to_u(Code) -> + list_to_binary(io_lib:format("\\u{~4.16.0B}", [Code])). + +inspect@proper_or_improper_list(List) -> + case List of + [] -> + {proper, []}; + [First] -> + {proper, [echo@inspect(First)]}; + [First | Rest] when is_list(Rest) -> + {Kind, Inspected} = inspect@list(Rest), + {Kind, [echo@inspect(First), ", " | Inspected]}; + [First | ImproperRest] -> + {improper, [echo@inspect(First), " | ", echo@inspect(ImproperRest)]} + end. + +inspect@maybe_gleam_atom(Atom, PrevChar, Acc) -> + case {Atom, PrevChar} of + {<<>>, none} -> + {error, nil}; + {<>, none} when ?is_digit_char(First) -> + {error, nil}; + {<<"_", _/binary>>, none} -> + {error, nil}; + {<<"_">>, _} -> + {error, nil}; + {<<"_", _/binary>>, $_} -> + {error, nil}; + {<>, _} when not ?is_atom_char(First) -> + {error, nil}; + {<>, none} -> + inspect@maybe_gleam_atom(Rest, First, <>); + {<<"_", Rest/binary>>, _} -> + inspect@maybe_gleam_atom(Rest, $_, Acc); + {<>, $_} -> + inspect@maybe_gleam_atom(Rest, First, <>); + {<>, _} -> + inspect@maybe_gleam_atom(Rest, First, <>); + {<<>>, _} -> + {ok, Acc}; + _ -> + throw({gleam_error, echo, Atom, PrevChar, Acc}) + end. + +inspect@uppercase(X) -> X - 32. diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__multiple_echos_inside_expression.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__multiple_echos_inside_expression.snap index 669de12ff05..7972eb26230 100644 --- a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__multiple_echos_inside_expression.snap +++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__echo__multiple_echos_inside_expression.snap @@ -10,5 +10,177 @@ expression: "\npub fn main() {\n echo 1\n echo 2\n}\n" -file("/root/project/test/my/mod.gleam", 2). -spec main() -> integer(). main() -> - mmmh:echo(1), - mmmh:echo(2). + echo(1), + echo(2). + +-define(is_lowercase_char(X), (X > 96 andalso X < 123)). +-define(is_underscore_char(X), (X == 95)). +-define(is_digit_char(X), (X > 47 andalso X < 58)). +-define(could_be_record(Tuple), + is_tuple(Tuple) andalso + is_atom(element(1, Tuple)) andalso + element(1, Tuple) =/= false andalso + element(1, Tuple) =/= true andalso + element(1, Tuple) =/= nil +). +-define(is_atom_char(C), + (?is_lowercase_char(C) orelse + ?is_underscore_char(C) orelse + ?is_digit_char(C)) +). + +echo(Value) -> + String = echo@inspect(Value), + io:put_chars(standard_error, [String, $\n]), + Value. + +echo@inspect(Value) -> + case Value of + nil -> "Nil"; + true -> "True"; + false -> "False"; + Int when is_integer(Int) -> erlang:integer_to_list(Int); + Float when is_float(Float) -> io_lib_format:fwrite_g(Float); + Bits when is_bitstring(Bits) -> inspect@bit_array(Bits); + Binary when is_binary(Binary) -> inspect@binary(Binary); + Atom when is_atom(Atom) -> inspect@atom(Atom); + List when is_list(List) -> inspect@list(List); + Map when is_map(Map) -> inspect@map(Map); + Record when ?could_be_record(Record) -> inspect@record(Record); + Tuple when is_tuple(Tuple) -> inspect@tuple(Tuple); + Function when is_function(Function) -> inspect@function(Function); + Any -> ["//erl(", io_lib:format("~p", [Any]), ")"] + end. + +inspect@bit_array(Bits) -> + Pieces = inspect@bit_array_pieces(Bits, []), + Inner = string:join(lists:reverse(Pieces), <<", ">>), + <<"<<", Inner/binary, ">>">>. + +inspect@bit_array_pieces(Bits, Acc) -> + case Bits of + <<>> -> + Acc; + <> -> + inspect@bit_array_pieces(Rest, [integer_to_binary(Byte) | Acc]); + _ -> + Size = bit_size(Bits), + <> = Bits, + SizeString = <<":size(", (integer_to_binary(Size)), ")">>, + Piece = <<(integer_to_binary(RemainingBits)), SizeString>>, + inspect@bit_array_pieces(<<>>, [Piece | Acc]) + end. + +inspect@binary(Binary) -> + case inspect@maybe_utf8_string(Binary, <<>>) of + {ok, InspectedUtf8String} -> + InspectedUtf8String; + {error, not_a_utf8_string} -> + Segments = [integer_to_list(X) || <> <= Binary], + ["<<", lists:join(", ", Segments), ">>"] + end. + +inspect@atom(Atom) -> + Binary = erlang:atom_to_binary(Atom), + case inspect@maybe_gleam_atom(Binary, none, <<>>) of + {ok, Inspected} -> Inspected; + {error, _} -> ["atom.create_from_string(\"", Binary, "\")"] + end. + +inspect@list(List) -> + case inspect@proper_or_improper_list(List) of + {proper, Elements} -> ["[", Elements, "]"]; + {improper, Elements} -> ["//erl([", Elements, "])"] + end. + +inspect@map(Map) -> + Fields = [ + [<<"#(">>, echo@inspect(Key), <<", ">>, echo@inspect(Value), <<")">>] + || {Key, Value} <- maps:to_list(Map) + ], + ["dict.from_list([", lists:join(", ", Fields), "])"]. + +inspect@record(Record) -> + [Atom | ArgsList] = erlang:tuple_to_list(Record), + Args = lists:join(", ", lists:map(fun echo@inspect/1, ArgsList)), + [echo@inspect(Atom), "(", Args, ")"]. + +inspect@tuple(Tuple) -> + Elements = lists:map(fun echo@inspect/1, erlang:tuple_to_list(Tuple)), + ["#(", lists:join(", ", Elements), ")"]. + +inspect@function(Function) -> + {arity, Arity} = erlang:fun_info(Function, arity), + ArgsAsciiCodes = lists:seq($a, $a + Arity - 1), + Args = lists:join(", ", lists:map(fun(Arg) -> <> end, ArgsAsciiCodes)), + ["//fn(", Args, ") { ... }"]. + +inspect@maybe_utf8_string(Binary, Acc) -> + case Binary of + <<>> -> + {ok, <<$", Acc/binary, $">>}; + <> -> + Escaped = inspect@escape_grapheme(First), + inspect@maybe_utf8_string(Rest, <>); + _ -> + {error, not_a_utf8_string} + end. + +inspect@escape_grapheme(Char) -> + case Char of + $" -> <<$\\, $">>; + $\\ -> <<$\\, $\\>>; + $\r -> <<$\\, $r>>; + $\n -> <<$\\, $n>>; + $\t -> <<$\\, $t>>; + $\f -> <<$\\, $f>>; + X when X > 126, X < 160 -> inspect@convert_to_u(X); + X when X < 32 -> inspect@convert_to_u(X); + Other -> <> + end. + +inspect@convert_to_u(Code) -> + list_to_binary(io_lib:format("\\u{~4.16.0B}", [Code])). + +inspect@proper_or_improper_list(List) -> + case List of + [] -> + {proper, []}; + [First] -> + {proper, [echo@inspect(First)]}; + [First | Rest] when is_list(Rest) -> + {Kind, Inspected} = inspect@list(Rest), + {Kind, [echo@inspect(First), ", " | Inspected]}; + [First | ImproperRest] -> + {improper, [echo@inspect(First), " | ", echo@inspect(ImproperRest)]} + end. + +inspect@maybe_gleam_atom(Atom, PrevChar, Acc) -> + case {Atom, PrevChar} of + {<<>>, none} -> + {error, nil}; + {<>, none} when ?is_digit_char(First) -> + {error, nil}; + {<<"_", _/binary>>, none} -> + {error, nil}; + {<<"_">>, _} -> + {error, nil}; + {<<"_", _/binary>>, $_} -> + {error, nil}; + {<>, _} when not ?is_atom_char(First) -> + {error, nil}; + {<>, none} -> + inspect@maybe_gleam_atom(Rest, First, <>); + {<<"_", Rest/binary>>, _} -> + inspect@maybe_gleam_atom(Rest, $_, Acc); + {<>, $_} -> + inspect@maybe_gleam_atom(Rest, First, <>); + {<>, _} -> + inspect@maybe_gleam_atom(Rest, First, <>); + {<<>>, _} -> + {ok, Acc}; + _ -> + throw({gleam_error, echo, Atom, PrevChar, Acc}) + end. + +inspect@uppercase(X) -> X - 32. diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__echo_in_a_pipeline.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__echo_in_a_pipeline.snap index 5d40b4d8341..0f8479939f5 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__echo_in_a_pipeline.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__echo_in_a_pipeline.snap @@ -14,99 +14,103 @@ export function main() { return wibble(_pipe); } -// Debug printing code +function echo(value) { + const string = $inspect(value); + if (typeof process === "object" && process.stderr?.write) { + // If we're in Node.js, use `stderr` + process.stderr.write(string + "\n"); + } else if (typeof Deno === "object") { + // If we're in Deno, use `stderr` + Deno.stderr.writeSync(new TextEncoder().encode(string + "\n")); + } else { + // Otherwise, use `console.log` + console.log(string); + } + + return value; +} -function echo(v) { - function inspectString(str) { - let new_str = '"'; - for (let i = 0; i < str.length; i++) { - let char = str[i]; - if (char == "\n") new_str += "\\n"; - else if (char == "\r") new_str += "\\r"; - else if (char == "\t") new_str += "\\t"; - else if (char == "\f") new_str += "\\f"; - else if (char == "\\") new_str += "\\\\"; - else if (char == '"') new_str += '\\"'; - else if (char < " " || (char > "~" && char < "\u{00A0}")) { - new_str += "\\u{" + char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0") + "}"; - } else { - new_str += char; - } +function $inspectString(str) { + let new_str = '"'; + for (let i = 0; i < str.length; i++) { + let char = str[i]; + if (char == "\n") new_str += "\\n"; + else if (char == "\r") new_str += "\\r"; + else if (char == "\t") new_str += "\\t"; + else if (char == "\f") new_str += "\\f"; + else if (char == "\\") new_str += "\\\\"; + else if (char == '"') new_str += '\\"'; + else if (char < " " || (char > "~" && char < "\u{00A0}")) { + new_str += "\\u{" + char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0") + "}"; + } else { + new_str += char; } - new_str += '"'; - return new_str; } + new_str += '"'; + return new_str; +} - function inspectDict(map) { - let body = "dict.from_list(["; - let first = true; - map.forEach((value, key) => { - if (!first) body = body + ", "; - body = body + "#(" + inspect(key) + ", " + inspect(value) + ")"; - first = false; - }); - return body + "])"; - } +function $inspectDict(map) { + let body = "dict.from_list(["; + let first = true; + map.forEach((value, key) => { + if (!first) body = body + ", "; + body = body + "#(" + $inspect(key) + ", " + $inspect(value) + ")"; + first = false; + }); + return body + "])"; +} - function inspectCustomType(record) { - const props = Object.keys(record) - .map((label) => { - const value = inspect(record[label]); - return isNaN(parseInt(label)) ? `${label}: ${value}` : value; - }) - .join(", "); - return props ? `${record.constructor.name}(${props})` : record.constructor.name; - } +function $inspectCustomType(record) { + const props = Object.keys(record) + .map((label) => { + const value = $inspect(record[label]); + return isNaN(parseInt(label)) ? `${label}: ${value}` : value; + }) + .join(", "); + return props ? `${record.constructor.name}(${props})` : record.constructor.name; +} - function inspectObject(v) { - const name = Object.getPrototypeOf(v)?.constructor?.name || "Object"; - const props = []; - for (const k of Object.keys(v)) { - props.push(`${inspect(k)}: ${inspect(v[k])}`); - } - const body = props.length ? " " + props.join(", ") + " " : ""; - const head = name === "Object" ? "" : name + " "; - return `//js(${head}{${body}})`; +function $inspectObject(v) { + const name = Object.getPrototypeOf(v)?.constructor?.name || "Object"; + const props = []; + for (const k of Object.keys(v)) { + props.push(`${$inspect(k)}: ${$inspect(v[k])}`); } + const body = props.length ? " " + props.join(", ") + " " : ""; + const head = name === "Object" ? "" : name + " "; + return `//js(${head}{${body}})`; +} - function inspect(v) { - const t = typeof v; - if (v === true) return "True"; - if (v === false) return "False"; - if (v === null) return "//js(null)"; - if (v === undefined) return "Nil"; - if (t === "string") return inspectString(v); - if (t === "bigint" || t === "number") return v.toString(); - if (Array.isArray(v)) return `#(${v.map(inspect).join(", ")})`; - if (v instanceof List) return `[${v.toArray().map(inspect).join(", ")}]`; - if (v instanceof UtfCodepoint) return `//utfcodepoint(${String.fromCodePoint(v.value)})`; - if (v instanceof BitArray) return `<<${Array.from(v.buffer).join(", ")}>>`; - if (v instanceof CustomType) return inspectCustomType(v); - if (v instanceof Dict) return inspectDict(v); - if (v instanceof Set) return `//js(Set(${[...v].map(inspect).join(", ")}))`; - if (v instanceof RegExp) return `//js(${v})`; - if (v instanceof Date) return `//js(Date("${v.toISOString()}"))`; - if (v instanceof Function) { - const args = []; - for (const i of Array(v.length).keys()) args.push(String.fromCharCode(i + 97)); - return `//fn(${args.join(", ")}) { ... }`; - } - return inspectObject(v); +function $inspect(v) { + const t = typeof v; + if (v === true) return "True"; + if (v === false) return "False"; + if (v === null) return "//js(null)"; + if (v === undefined) return "Nil"; + if (t === "string") return $inspectString(v); + if (t === "bigint" || t === "number") return v.toString(); + if (Array.isArray(v)) return `#(${v.map($inspect).join(", ")})`; + if (v instanceof List) return `[${v.toArray().map($inspect).join(", ")}]`; + if (v instanceof UtfCodepoint) return `//utfcodepoint(${String.fromCodePoint(v.value)})`; + if (v instanceof BitArray) return `<<${Array.from(v.buffer).join(", ")}>>`; + if (v instanceof CustomType) return $inspectCustomType(v); + if ($isDict(v)) return $inspectDict(v); + if (v instanceof Set) return `//js(Set(${[...v].map($inspect).join(", ")}))`; + if (v instanceof RegExp) return `//js(${v})`; + if (v instanceof Date) return `//js(Date("${v.toISOString()}"))`; + if (v instanceof Function) { + const args = []; + for (const i of Array(v.length).keys()) args.push(String.fromCharCode(i + 97)); + return `//fn(${args.join(", ")}) { ... }`; } + return $inspectObject(v); +} - function print_debug(string) { - if (typeof process === "object" && process.stderr?.write) { - // If we're in Node.js, use `stderr` - process.stderr.write(string + "\n"); - } else if (typeof Deno === "object") { - // If we're in Deno, use `stderr` - Deno.stderr.writeSync(new TextEncoder().encode(string + "\n")); - } else { - // Otherwise, use `console.log` (so that it doesn't look like an error) - console.log(string); - } +function $isDict(value) { + try { + return value instanceof Dict; + } catch { + return false; } - - print_debug(inspect(v)); - return v; } diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__echo_with_a_block.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__echo_with_a_block.snap index 3d93898d71a..cabb650d223 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__echo_with_a_block.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__echo_with_a_block.snap @@ -13,99 +13,103 @@ export function main() { ); } -// Debug printing code +function echo(value) { + const string = $inspect(value); + if (typeof process === "object" && process.stderr?.write) { + // If we're in Node.js, use `stderr` + process.stderr.write(string + "\n"); + } else if (typeof Deno === "object") { + // If we're in Deno, use `stderr` + Deno.stderr.writeSync(new TextEncoder().encode(string + "\n")); + } else { + // Otherwise, use `console.log` + console.log(string); + } + + return value; +} -function echo(v) { - function inspectString(str) { - let new_str = '"'; - for (let i = 0; i < str.length; i++) { - let char = str[i]; - if (char == "\n") new_str += "\\n"; - else if (char == "\r") new_str += "\\r"; - else if (char == "\t") new_str += "\\t"; - else if (char == "\f") new_str += "\\f"; - else if (char == "\\") new_str += "\\\\"; - else if (char == '"') new_str += '\\"'; - else if (char < " " || (char > "~" && char < "\u{00A0}")) { - new_str += "\\u{" + char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0") + "}"; - } else { - new_str += char; - } +function $inspectString(str) { + let new_str = '"'; + for (let i = 0; i < str.length; i++) { + let char = str[i]; + if (char == "\n") new_str += "\\n"; + else if (char == "\r") new_str += "\\r"; + else if (char == "\t") new_str += "\\t"; + else if (char == "\f") new_str += "\\f"; + else if (char == "\\") new_str += "\\\\"; + else if (char == '"') new_str += '\\"'; + else if (char < " " || (char > "~" && char < "\u{00A0}")) { + new_str += "\\u{" + char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0") + "}"; + } else { + new_str += char; } - new_str += '"'; - return new_str; } + new_str += '"'; + return new_str; +} - function inspectDict(map) { - let body = "dict.from_list(["; - let first = true; - map.forEach((value, key) => { - if (!first) body = body + ", "; - body = body + "#(" + inspect(key) + ", " + inspect(value) + ")"; - first = false; - }); - return body + "])"; - } +function $inspectDict(map) { + let body = "dict.from_list(["; + let first = true; + map.forEach((value, key) => { + if (!first) body = body + ", "; + body = body + "#(" + $inspect(key) + ", " + $inspect(value) + ")"; + first = false; + }); + return body + "])"; +} - function inspectCustomType(record) { - const props = Object.keys(record) - .map((label) => { - const value = inspect(record[label]); - return isNaN(parseInt(label)) ? `${label}: ${value}` : value; - }) - .join(", "); - return props ? `${record.constructor.name}(${props})` : record.constructor.name; - } +function $inspectCustomType(record) { + const props = Object.keys(record) + .map((label) => { + const value = $inspect(record[label]); + return isNaN(parseInt(label)) ? `${label}: ${value}` : value; + }) + .join(", "); + return props ? `${record.constructor.name}(${props})` : record.constructor.name; +} - function inspectObject(v) { - const name = Object.getPrototypeOf(v)?.constructor?.name || "Object"; - const props = []; - for (const k of Object.keys(v)) { - props.push(`${inspect(k)}: ${inspect(v[k])}`); - } - const body = props.length ? " " + props.join(", ") + " " : ""; - const head = name === "Object" ? "" : name + " "; - return `//js(${head}{${body}})`; +function $inspectObject(v) { + const name = Object.getPrototypeOf(v)?.constructor?.name || "Object"; + const props = []; + for (const k of Object.keys(v)) { + props.push(`${$inspect(k)}: ${$inspect(v[k])}`); } + const body = props.length ? " " + props.join(", ") + " " : ""; + const head = name === "Object" ? "" : name + " "; + return `//js(${head}{${body}})`; +} - function inspect(v) { - const t = typeof v; - if (v === true) return "True"; - if (v === false) return "False"; - if (v === null) return "//js(null)"; - if (v === undefined) return "Nil"; - if (t === "string") return inspectString(v); - if (t === "bigint" || t === "number") return v.toString(); - if (Array.isArray(v)) return `#(${v.map(inspect).join(", ")})`; - if (v instanceof List) return `[${v.toArray().map(inspect).join(", ")}]`; - if (v instanceof UtfCodepoint) return `//utfcodepoint(${String.fromCodePoint(v.value)})`; - if (v instanceof BitArray) return `<<${Array.from(v.buffer).join(", ")}>>`; - if (v instanceof CustomType) return inspectCustomType(v); - if (v instanceof Dict) return inspectDict(v); - if (v instanceof Set) return `//js(Set(${[...v].map(inspect).join(", ")}))`; - if (v instanceof RegExp) return `//js(${v})`; - if (v instanceof Date) return `//js(Date("${v.toISOString()}"))`; - if (v instanceof Function) { - const args = []; - for (const i of Array(v.length).keys()) args.push(String.fromCharCode(i + 97)); - return `//fn(${args.join(", ")}) { ... }`; - } - return inspectObject(v); +function $inspect(v) { + const t = typeof v; + if (v === true) return "True"; + if (v === false) return "False"; + if (v === null) return "//js(null)"; + if (v === undefined) return "Nil"; + if (t === "string") return $inspectString(v); + if (t === "bigint" || t === "number") return v.toString(); + if (Array.isArray(v)) return `#(${v.map($inspect).join(", ")})`; + if (v instanceof List) return `[${v.toArray().map($inspect).join(", ")}]`; + if (v instanceof UtfCodepoint) return `//utfcodepoint(${String.fromCodePoint(v.value)})`; + if (v instanceof BitArray) return `<<${Array.from(v.buffer).join(", ")}>>`; + if (v instanceof CustomType) return $inspectCustomType(v); + if ($isDict(v)) return $inspectDict(v); + if (v instanceof Set) return `//js(Set(${[...v].map($inspect).join(", ")}))`; + if (v instanceof RegExp) return `//js(${v})`; + if (v instanceof Date) return `//js(Date("${v.toISOString()}"))`; + if (v instanceof Function) { + const args = []; + for (const i of Array(v.length).keys()) args.push(String.fromCharCode(i + 97)); + return `//fn(${args.join(", ")}) { ... }`; } + return $inspectObject(v); +} - function print_debug(string) { - if (typeof process === "object" && process.stderr?.write) { - // If we're in Node.js, use `stderr` - process.stderr.write(string + "\n"); - } else if (typeof Deno === "object") { - // If we're in Deno, use `stderr` - Deno.stderr.writeSync(new TextEncoder().encode(string + "\n")); - } else { - // Otherwise, use `console.log` (so that it doesn't look like an error) - console.log(string); - } +function $isDict(value) { + try { + return value instanceof Dict; + } catch { + return false; } - - print_debug(inspect(v)); - return v; } diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__echo_with_a_case_expression.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__echo_with_a_case_expression.snap index a6e6345c9cc..8ead7153b08 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__echo_with_a_case_expression.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__echo_with_a_case_expression.snap @@ -15,99 +15,103 @@ export function main() { ); } -// Debug printing code +function echo(value) { + const string = $inspect(value); + if (typeof process === "object" && process.stderr?.write) { + // If we're in Node.js, use `stderr` + process.stderr.write(string + "\n"); + } else if (typeof Deno === "object") { + // If we're in Deno, use `stderr` + Deno.stderr.writeSync(new TextEncoder().encode(string + "\n")); + } else { + // Otherwise, use `console.log` + console.log(string); + } -function echo(v) { - function inspectString(str) { - let new_str = '"'; - for (let i = 0; i < str.length; i++) { - let char = str[i]; - if (char == "\n") new_str += "\\n"; - else if (char == "\r") new_str += "\\r"; - else if (char == "\t") new_str += "\\t"; - else if (char == "\f") new_str += "\\f"; - else if (char == "\\") new_str += "\\\\"; - else if (char == '"') new_str += '\\"'; - else if (char < " " || (char > "~" && char < "\u{00A0}")) { - new_str += "\\u{" + char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0") + "}"; - } else { - new_str += char; - } + return value; +} + +function $inspectString(str) { + let new_str = '"'; + for (let i = 0; i < str.length; i++) { + let char = str[i]; + if (char == "\n") new_str += "\\n"; + else if (char == "\r") new_str += "\\r"; + else if (char == "\t") new_str += "\\t"; + else if (char == "\f") new_str += "\\f"; + else if (char == "\\") new_str += "\\\\"; + else if (char == '"') new_str += '\\"'; + else if (char < " " || (char > "~" && char < "\u{00A0}")) { + new_str += "\\u{" + char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0") + "}"; + } else { + new_str += char; } - new_str += '"'; - return new_str; } + new_str += '"'; + return new_str; +} - function inspectDict(map) { - let body = "dict.from_list(["; - let first = true; - map.forEach((value, key) => { - if (!first) body = body + ", "; - body = body + "#(" + inspect(key) + ", " + inspect(value) + ")"; - first = false; - }); - return body + "])"; - } +function $inspectDict(map) { + let body = "dict.from_list(["; + let first = true; + map.forEach((value, key) => { + if (!first) body = body + ", "; + body = body + "#(" + $inspect(key) + ", " + $inspect(value) + ")"; + first = false; + }); + return body + "])"; +} - function inspectCustomType(record) { - const props = Object.keys(record) - .map((label) => { - const value = inspect(record[label]); - return isNaN(parseInt(label)) ? `${label}: ${value}` : value; - }) - .join(", "); - return props ? `${record.constructor.name}(${props})` : record.constructor.name; - } +function $inspectCustomType(record) { + const props = Object.keys(record) + .map((label) => { + const value = $inspect(record[label]); + return isNaN(parseInt(label)) ? `${label}: ${value}` : value; + }) + .join(", "); + return props ? `${record.constructor.name}(${props})` : record.constructor.name; +} - function inspectObject(v) { - const name = Object.getPrototypeOf(v)?.constructor?.name || "Object"; - const props = []; - for (const k of Object.keys(v)) { - props.push(`${inspect(k)}: ${inspect(v[k])}`); - } - const body = props.length ? " " + props.join(", ") + " " : ""; - const head = name === "Object" ? "" : name + " "; - return `//js(${head}{${body}})`; +function $inspectObject(v) { + const name = Object.getPrototypeOf(v)?.constructor?.name || "Object"; + const props = []; + for (const k of Object.keys(v)) { + props.push(`${$inspect(k)}: ${$inspect(v[k])}`); } + const body = props.length ? " " + props.join(", ") + " " : ""; + const head = name === "Object" ? "" : name + " "; + return `//js(${head}{${body}})`; +} - function inspect(v) { - const t = typeof v; - if (v === true) return "True"; - if (v === false) return "False"; - if (v === null) return "//js(null)"; - if (v === undefined) return "Nil"; - if (t === "string") return inspectString(v); - if (t === "bigint" || t === "number") return v.toString(); - if (Array.isArray(v)) return `#(${v.map(inspect).join(", ")})`; - if (v instanceof List) return `[${v.toArray().map(inspect).join(", ")}]`; - if (v instanceof UtfCodepoint) return `//utfcodepoint(${String.fromCodePoint(v.value)})`; - if (v instanceof BitArray) return `<<${Array.from(v.buffer).join(", ")}>>`; - if (v instanceof CustomType) return inspectCustomType(v); - if (v instanceof Dict) return inspectDict(v); - if (v instanceof Set) return `//js(Set(${[...v].map(inspect).join(", ")}))`; - if (v instanceof RegExp) return `//js(${v})`; - if (v instanceof Date) return `//js(Date("${v.toISOString()}"))`; - if (v instanceof Function) { - const args = []; - for (const i of Array(v.length).keys()) args.push(String.fromCharCode(i + 97)); - return `//fn(${args.join(", ")}) { ... }`; - } - return inspectObject(v); +function $inspect(v) { + const t = typeof v; + if (v === true) return "True"; + if (v === false) return "False"; + if (v === null) return "//js(null)"; + if (v === undefined) return "Nil"; + if (t === "string") return $inspectString(v); + if (t === "bigint" || t === "number") return v.toString(); + if (Array.isArray(v)) return `#(${v.map($inspect).join(", ")})`; + if (v instanceof List) return `[${v.toArray().map($inspect).join(", ")}]`; + if (v instanceof UtfCodepoint) return `//utfcodepoint(${String.fromCodePoint(v.value)})`; + if (v instanceof BitArray) return `<<${Array.from(v.buffer).join(", ")}>>`; + if (v instanceof CustomType) return $inspectCustomType(v); + if ($isDict(v)) return $inspectDict(v); + if (v instanceof Set) return `//js(Set(${[...v].map($inspect).join(", ")}))`; + if (v instanceof RegExp) return `//js(${v})`; + if (v instanceof Date) return `//js(Date("${v.toISOString()}"))`; + if (v instanceof Function) { + const args = []; + for (const i of Array(v.length).keys()) args.push(String.fromCharCode(i + 97)); + return `//fn(${args.join(", ")}) { ... }`; } + return $inspectObject(v); +} - function print_debug(string) { - if (typeof process === "object" && process.stderr?.write) { - // If we're in Node.js, use `stderr` - process.stderr.write(string + "\n"); - } else if (typeof Deno === "object") { - // If we're in Deno, use `stderr` - Deno.stderr.writeSync(new TextEncoder().encode(string + "\n")); - } else { - // Otherwise, use `console.log` (so that it doesn't look like an error) - console.log(string); - } +function $isDict(value) { + try { + return value instanceof Dict; + } catch { + return false; } - - print_debug(inspect(v)); - return v; } diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__echo_with_a_function_call.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__echo_with_a_function_call.snap index 35c7c674b50..c95f3bf23ec 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__echo_with_a_function_call.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__echo_with_a_function_call.snap @@ -12,99 +12,103 @@ export function main() { return echo(wibble(1, 2)); } -// Debug printing code +function echo(value) { + const string = $inspect(value); + if (typeof process === "object" && process.stderr?.write) { + // If we're in Node.js, use `stderr` + process.stderr.write(string + "\n"); + } else if (typeof Deno === "object") { + // If we're in Deno, use `stderr` + Deno.stderr.writeSync(new TextEncoder().encode(string + "\n")); + } else { + // Otherwise, use `console.log` + console.log(string); + } + + return value; +} -function echo(v) { - function inspectString(str) { - let new_str = '"'; - for (let i = 0; i < str.length; i++) { - let char = str[i]; - if (char == "\n") new_str += "\\n"; - else if (char == "\r") new_str += "\\r"; - else if (char == "\t") new_str += "\\t"; - else if (char == "\f") new_str += "\\f"; - else if (char == "\\") new_str += "\\\\"; - else if (char == '"') new_str += '\\"'; - else if (char < " " || (char > "~" && char < "\u{00A0}")) { - new_str += "\\u{" + char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0") + "}"; - } else { - new_str += char; - } +function $inspectString(str) { + let new_str = '"'; + for (let i = 0; i < str.length; i++) { + let char = str[i]; + if (char == "\n") new_str += "\\n"; + else if (char == "\r") new_str += "\\r"; + else if (char == "\t") new_str += "\\t"; + else if (char == "\f") new_str += "\\f"; + else if (char == "\\") new_str += "\\\\"; + else if (char == '"') new_str += '\\"'; + else if (char < " " || (char > "~" && char < "\u{00A0}")) { + new_str += "\\u{" + char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0") + "}"; + } else { + new_str += char; } - new_str += '"'; - return new_str; } + new_str += '"'; + return new_str; +} - function inspectDict(map) { - let body = "dict.from_list(["; - let first = true; - map.forEach((value, key) => { - if (!first) body = body + ", "; - body = body + "#(" + inspect(key) + ", " + inspect(value) + ")"; - first = false; - }); - return body + "])"; - } +function $inspectDict(map) { + let body = "dict.from_list(["; + let first = true; + map.forEach((value, key) => { + if (!first) body = body + ", "; + body = body + "#(" + $inspect(key) + ", " + $inspect(value) + ")"; + first = false; + }); + return body + "])"; +} - function inspectCustomType(record) { - const props = Object.keys(record) - .map((label) => { - const value = inspect(record[label]); - return isNaN(parseInt(label)) ? `${label}: ${value}` : value; - }) - .join(", "); - return props ? `${record.constructor.name}(${props})` : record.constructor.name; - } +function $inspectCustomType(record) { + const props = Object.keys(record) + .map((label) => { + const value = $inspect(record[label]); + return isNaN(parseInt(label)) ? `${label}: ${value}` : value; + }) + .join(", "); + return props ? `${record.constructor.name}(${props})` : record.constructor.name; +} - function inspectObject(v) { - const name = Object.getPrototypeOf(v)?.constructor?.name || "Object"; - const props = []; - for (const k of Object.keys(v)) { - props.push(`${inspect(k)}: ${inspect(v[k])}`); - } - const body = props.length ? " " + props.join(", ") + " " : ""; - const head = name === "Object" ? "" : name + " "; - return `//js(${head}{${body}})`; +function $inspectObject(v) { + const name = Object.getPrototypeOf(v)?.constructor?.name || "Object"; + const props = []; + for (const k of Object.keys(v)) { + props.push(`${$inspect(k)}: ${$inspect(v[k])}`); } + const body = props.length ? " " + props.join(", ") + " " : ""; + const head = name === "Object" ? "" : name + " "; + return `//js(${head}{${body}})`; +} - function inspect(v) { - const t = typeof v; - if (v === true) return "True"; - if (v === false) return "False"; - if (v === null) return "//js(null)"; - if (v === undefined) return "Nil"; - if (t === "string") return inspectString(v); - if (t === "bigint" || t === "number") return v.toString(); - if (Array.isArray(v)) return `#(${v.map(inspect).join(", ")})`; - if (v instanceof List) return `[${v.toArray().map(inspect).join(", ")}]`; - if (v instanceof UtfCodepoint) return `//utfcodepoint(${String.fromCodePoint(v.value)})`; - if (v instanceof BitArray) return `<<${Array.from(v.buffer).join(", ")}>>`; - if (v instanceof CustomType) return inspectCustomType(v); - if (v instanceof Dict) return inspectDict(v); - if (v instanceof Set) return `//js(Set(${[...v].map(inspect).join(", ")}))`; - if (v instanceof RegExp) return `//js(${v})`; - if (v instanceof Date) return `//js(Date("${v.toISOString()}"))`; - if (v instanceof Function) { - const args = []; - for (const i of Array(v.length).keys()) args.push(String.fromCharCode(i + 97)); - return `//fn(${args.join(", ")}) { ... }`; - } - return inspectObject(v); +function $inspect(v) { + const t = typeof v; + if (v === true) return "True"; + if (v === false) return "False"; + if (v === null) return "//js(null)"; + if (v === undefined) return "Nil"; + if (t === "string") return $inspectString(v); + if (t === "bigint" || t === "number") return v.toString(); + if (Array.isArray(v)) return `#(${v.map($inspect).join(", ")})`; + if (v instanceof List) return `[${v.toArray().map($inspect).join(", ")}]`; + if (v instanceof UtfCodepoint) return `//utfcodepoint(${String.fromCodePoint(v.value)})`; + if (v instanceof BitArray) return `<<${Array.from(v.buffer).join(", ")}>>`; + if (v instanceof CustomType) return $inspectCustomType(v); + if ($isDict(v)) return $inspectDict(v); + if (v instanceof Set) return `//js(Set(${[...v].map($inspect).join(", ")}))`; + if (v instanceof RegExp) return `//js(${v})`; + if (v instanceof Date) return `//js(Date("${v.toISOString()}"))`; + if (v instanceof Function) { + const args = []; + for (const i of Array(v.length).keys()) args.push(String.fromCharCode(i + 97)); + return `//fn(${args.join(", ")}) { ... }`; } + return $inspectObject(v); +} - function print_debug(string) { - if (typeof process === "object" && process.stderr?.write) { - // If we're in Node.js, use `stderr` - process.stderr.write(string + "\n"); - } else if (typeof Deno === "object") { - // If we're in Deno, use `stderr` - Deno.stderr.writeSync(new TextEncoder().encode(string + "\n")); - } else { - // Otherwise, use `console.log` (so that it doesn't look like an error) - console.log(string); - } +function $isDict(value) { + try { + return value instanceof Dict; + } catch { + return false; } - - print_debug(inspect(v)); - return v; } diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__echo_with_a_panic.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__echo_with_a_panic.snap index d52bf41b5a0..02c9b16031c 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__echo_with_a_panic.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__echo_with_a_panic.snap @@ -19,99 +19,103 @@ export function main() { ); } -// Debug printing code +function echo(value) { + const string = $inspect(value); + if (typeof process === "object" && process.stderr?.write) { + // If we're in Node.js, use `stderr` + process.stderr.write(string + "\n"); + } else if (typeof Deno === "object") { + // If we're in Deno, use `stderr` + Deno.stderr.writeSync(new TextEncoder().encode(string + "\n")); + } else { + // Otherwise, use `console.log` + console.log(string); + } + + return value; +} -function echo(v) { - function inspectString(str) { - let new_str = '"'; - for (let i = 0; i < str.length; i++) { - let char = str[i]; - if (char == "\n") new_str += "\\n"; - else if (char == "\r") new_str += "\\r"; - else if (char == "\t") new_str += "\\t"; - else if (char == "\f") new_str += "\\f"; - else if (char == "\\") new_str += "\\\\"; - else if (char == '"') new_str += '\\"'; - else if (char < " " || (char > "~" && char < "\u{00A0}")) { - new_str += "\\u{" + char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0") + "}"; - } else { - new_str += char; - } +function $inspectString(str) { + let new_str = '"'; + for (let i = 0; i < str.length; i++) { + let char = str[i]; + if (char == "\n") new_str += "\\n"; + else if (char == "\r") new_str += "\\r"; + else if (char == "\t") new_str += "\\t"; + else if (char == "\f") new_str += "\\f"; + else if (char == "\\") new_str += "\\\\"; + else if (char == '"') new_str += '\\"'; + else if (char < " " || (char > "~" && char < "\u{00A0}")) { + new_str += "\\u{" + char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0") + "}"; + } else { + new_str += char; } - new_str += '"'; - return new_str; } + new_str += '"'; + return new_str; +} - function inspectDict(map) { - let body = "dict.from_list(["; - let first = true; - map.forEach((value, key) => { - if (!first) body = body + ", "; - body = body + "#(" + inspect(key) + ", " + inspect(value) + ")"; - first = false; - }); - return body + "])"; - } +function $inspectDict(map) { + let body = "dict.from_list(["; + let first = true; + map.forEach((value, key) => { + if (!first) body = body + ", "; + body = body + "#(" + $inspect(key) + ", " + $inspect(value) + ")"; + first = false; + }); + return body + "])"; +} - function inspectCustomType(record) { - const props = Object.keys(record) - .map((label) => { - const value = inspect(record[label]); - return isNaN(parseInt(label)) ? `${label}: ${value}` : value; - }) - .join(", "); - return props ? `${record.constructor.name}(${props})` : record.constructor.name; - } +function $inspectCustomType(record) { + const props = Object.keys(record) + .map((label) => { + const value = $inspect(record[label]); + return isNaN(parseInt(label)) ? `${label}: ${value}` : value; + }) + .join(", "); + return props ? `${record.constructor.name}(${props})` : record.constructor.name; +} - function inspectObject(v) { - const name = Object.getPrototypeOf(v)?.constructor?.name || "Object"; - const props = []; - for (const k of Object.keys(v)) { - props.push(`${inspect(k)}: ${inspect(v[k])}`); - } - const body = props.length ? " " + props.join(", ") + " " : ""; - const head = name === "Object" ? "" : name + " "; - return `//js(${head}{${body}})`; +function $inspectObject(v) { + const name = Object.getPrototypeOf(v)?.constructor?.name || "Object"; + const props = []; + for (const k of Object.keys(v)) { + props.push(`${$inspect(k)}: ${$inspect(v[k])}`); } + const body = props.length ? " " + props.join(", ") + " " : ""; + const head = name === "Object" ? "" : name + " "; + return `//js(${head}{${body}})`; +} - function inspect(v) { - const t = typeof v; - if (v === true) return "True"; - if (v === false) return "False"; - if (v === null) return "//js(null)"; - if (v === undefined) return "Nil"; - if (t === "string") return inspectString(v); - if (t === "bigint" || t === "number") return v.toString(); - if (Array.isArray(v)) return `#(${v.map(inspect).join(", ")})`; - if (v instanceof List) return `[${v.toArray().map(inspect).join(", ")}]`; - if (v instanceof UtfCodepoint) return `//utfcodepoint(${String.fromCodePoint(v.value)})`; - if (v instanceof BitArray) return `<<${Array.from(v.buffer).join(", ")}>>`; - if (v instanceof CustomType) return inspectCustomType(v); - if (v instanceof Dict) return inspectDict(v); - if (v instanceof Set) return `//js(Set(${[...v].map(inspect).join(", ")}))`; - if (v instanceof RegExp) return `//js(${v})`; - if (v instanceof Date) return `//js(Date("${v.toISOString()}"))`; - if (v instanceof Function) { - const args = []; - for (const i of Array(v.length).keys()) args.push(String.fromCharCode(i + 97)); - return `//fn(${args.join(", ")}) { ... }`; - } - return inspectObject(v); +function $inspect(v) { + const t = typeof v; + if (v === true) return "True"; + if (v === false) return "False"; + if (v === null) return "//js(null)"; + if (v === undefined) return "Nil"; + if (t === "string") return $inspectString(v); + if (t === "bigint" || t === "number") return v.toString(); + if (Array.isArray(v)) return `#(${v.map($inspect).join(", ")})`; + if (v instanceof List) return `[${v.toArray().map($inspect).join(", ")}]`; + if (v instanceof UtfCodepoint) return `//utfcodepoint(${String.fromCodePoint(v.value)})`; + if (v instanceof BitArray) return `<<${Array.from(v.buffer).join(", ")}>>`; + if (v instanceof CustomType) return $inspectCustomType(v); + if ($isDict(v)) return $inspectDict(v); + if (v instanceof Set) return `//js(Set(${[...v].map($inspect).join(", ")}))`; + if (v instanceof RegExp) return `//js(${v})`; + if (v instanceof Date) return `//js(Date("${v.toISOString()}"))`; + if (v instanceof Function) { + const args = []; + for (const i of Array(v.length).keys()) args.push(String.fromCharCode(i + 97)); + return `//fn(${args.join(", ")}) { ... }`; } + return $inspectObject(v); +} - function print_debug(string) { - if (typeof process === "object" && process.stderr?.write) { - // If we're in Node.js, use `stderr` - process.stderr.write(string + "\n"); - } else if (typeof Deno === "object") { - // If we're in Deno, use `stderr` - Deno.stderr.writeSync(new TextEncoder().encode(string + "\n")); - } else { - // Otherwise, use `console.log` (so that it doesn't look like an error) - console.log(string); - } +function $isDict(value) { + try { + return value instanceof Dict; + } catch { + return false; } - - print_debug(inspect(v)); - return v; } diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__echo_with_a_simple_expression.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__echo_with_a_simple_expression.snap index 8ce72a51ef9..a0e62dbd27c 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__echo_with_a_simple_expression.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__echo_with_a_simple_expression.snap @@ -8,99 +8,103 @@ export function main() { return echo(1); } -// Debug printing code +function echo(value) { + const string = $inspect(value); + if (typeof process === "object" && process.stderr?.write) { + // If we're in Node.js, use `stderr` + process.stderr.write(string + "\n"); + } else if (typeof Deno === "object") { + // If we're in Deno, use `stderr` + Deno.stderr.writeSync(new TextEncoder().encode(string + "\n")); + } else { + // Otherwise, use `console.log` + console.log(string); + } + + return value; +} -function echo(v) { - function inspectString(str) { - let new_str = '"'; - for (let i = 0; i < str.length; i++) { - let char = str[i]; - if (char == "\n") new_str += "\\n"; - else if (char == "\r") new_str += "\\r"; - else if (char == "\t") new_str += "\\t"; - else if (char == "\f") new_str += "\\f"; - else if (char == "\\") new_str += "\\\\"; - else if (char == '"') new_str += '\\"'; - else if (char < " " || (char > "~" && char < "\u{00A0}")) { - new_str += "\\u{" + char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0") + "}"; - } else { - new_str += char; - } +function $inspectString(str) { + let new_str = '"'; + for (let i = 0; i < str.length; i++) { + let char = str[i]; + if (char == "\n") new_str += "\\n"; + else if (char == "\r") new_str += "\\r"; + else if (char == "\t") new_str += "\\t"; + else if (char == "\f") new_str += "\\f"; + else if (char == "\\") new_str += "\\\\"; + else if (char == '"') new_str += '\\"'; + else if (char < " " || (char > "~" && char < "\u{00A0}")) { + new_str += "\\u{" + char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0") + "}"; + } else { + new_str += char; } - new_str += '"'; - return new_str; } + new_str += '"'; + return new_str; +} - function inspectDict(map) { - let body = "dict.from_list(["; - let first = true; - map.forEach((value, key) => { - if (!first) body = body + ", "; - body = body + "#(" + inspect(key) + ", " + inspect(value) + ")"; - first = false; - }); - return body + "])"; - } +function $inspectDict(map) { + let body = "dict.from_list(["; + let first = true; + map.forEach((value, key) => { + if (!first) body = body + ", "; + body = body + "#(" + $inspect(key) + ", " + $inspect(value) + ")"; + first = false; + }); + return body + "])"; +} - function inspectCustomType(record) { - const props = Object.keys(record) - .map((label) => { - const value = inspect(record[label]); - return isNaN(parseInt(label)) ? `${label}: ${value}` : value; - }) - .join(", "); - return props ? `${record.constructor.name}(${props})` : record.constructor.name; - } +function $inspectCustomType(record) { + const props = Object.keys(record) + .map((label) => { + const value = $inspect(record[label]); + return isNaN(parseInt(label)) ? `${label}: ${value}` : value; + }) + .join(", "); + return props ? `${record.constructor.name}(${props})` : record.constructor.name; +} - function inspectObject(v) { - const name = Object.getPrototypeOf(v)?.constructor?.name || "Object"; - const props = []; - for (const k of Object.keys(v)) { - props.push(`${inspect(k)}: ${inspect(v[k])}`); - } - const body = props.length ? " " + props.join(", ") + " " : ""; - const head = name === "Object" ? "" : name + " "; - return `//js(${head}{${body}})`; +function $inspectObject(v) { + const name = Object.getPrototypeOf(v)?.constructor?.name || "Object"; + const props = []; + for (const k of Object.keys(v)) { + props.push(`${$inspect(k)}: ${$inspect(v[k])}`); } + const body = props.length ? " " + props.join(", ") + " " : ""; + const head = name === "Object" ? "" : name + " "; + return `//js(${head}{${body}})`; +} - function inspect(v) { - const t = typeof v; - if (v === true) return "True"; - if (v === false) return "False"; - if (v === null) return "//js(null)"; - if (v === undefined) return "Nil"; - if (t === "string") return inspectString(v); - if (t === "bigint" || t === "number") return v.toString(); - if (Array.isArray(v)) return `#(${v.map(inspect).join(", ")})`; - if (v instanceof List) return `[${v.toArray().map(inspect).join(", ")}]`; - if (v instanceof UtfCodepoint) return `//utfcodepoint(${String.fromCodePoint(v.value)})`; - if (v instanceof BitArray) return `<<${Array.from(v.buffer).join(", ")}>>`; - if (v instanceof CustomType) return inspectCustomType(v); - if (v instanceof Dict) return inspectDict(v); - if (v instanceof Set) return `//js(Set(${[...v].map(inspect).join(", ")}))`; - if (v instanceof RegExp) return `//js(${v})`; - if (v instanceof Date) return `//js(Date("${v.toISOString()}"))`; - if (v instanceof Function) { - const args = []; - for (const i of Array(v.length).keys()) args.push(String.fromCharCode(i + 97)); - return `//fn(${args.join(", ")}) { ... }`; - } - return inspectObject(v); +function $inspect(v) { + const t = typeof v; + if (v === true) return "True"; + if (v === false) return "False"; + if (v === null) return "//js(null)"; + if (v === undefined) return "Nil"; + if (t === "string") return $inspectString(v); + if (t === "bigint" || t === "number") return v.toString(); + if (Array.isArray(v)) return `#(${v.map($inspect).join(", ")})`; + if (v instanceof List) return `[${v.toArray().map($inspect).join(", ")}]`; + if (v instanceof UtfCodepoint) return `//utfcodepoint(${String.fromCodePoint(v.value)})`; + if (v instanceof BitArray) return `<<${Array.from(v.buffer).join(", ")}>>`; + if (v instanceof CustomType) return $inspectCustomType(v); + if ($isDict(v)) return $inspectDict(v); + if (v instanceof Set) return `//js(Set(${[...v].map($inspect).join(", ")}))`; + if (v instanceof RegExp) return `//js(${v})`; + if (v instanceof Date) return `//js(Date("${v.toISOString()}"))`; + if (v instanceof Function) { + const args = []; + for (const i of Array(v.length).keys()) args.push(String.fromCharCode(i + 97)); + return `//fn(${args.join(", ")}) { ... }`; } + return $inspectObject(v); +} - function print_debug(string) { - if (typeof process === "object" && process.stderr?.write) { - // If we're in Node.js, use `stderr` - process.stderr.write(string + "\n"); - } else if (typeof Deno === "object") { - // If we're in Deno, use `stderr` - Deno.stderr.writeSync(new TextEncoder().encode(string + "\n")); - } else { - // Otherwise, use `console.log` (so that it doesn't look like an error) - console.log(string); - } +function $isDict(value) { + try { + return value instanceof Dict; + } catch { + return false; } - - print_debug(inspect(v)); - return v; } diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__multiple_echos_in_a_pipeline.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__multiple_echos_in_a_pipeline.snap index 41ae47cb878..82633963b9b 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__multiple_echos_in_a_pipeline.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__multiple_echos_in_a_pipeline.snap @@ -17,99 +17,103 @@ export function main() { return echo(_pipe$2); } -// Debug printing code +function echo(value) { + const string = $inspect(value); + if (typeof process === "object" && process.stderr?.write) { + // If we're in Node.js, use `stderr` + process.stderr.write(string + "\n"); + } else if (typeof Deno === "object") { + // If we're in Deno, use `stderr` + Deno.stderr.writeSync(new TextEncoder().encode(string + "\n")); + } else { + // Otherwise, use `console.log` + console.log(string); + } + + return value; +} -function echo(v) { - function inspectString(str) { - let new_str = '"'; - for (let i = 0; i < str.length; i++) { - let char = str[i]; - if (char == "\n") new_str += "\\n"; - else if (char == "\r") new_str += "\\r"; - else if (char == "\t") new_str += "\\t"; - else if (char == "\f") new_str += "\\f"; - else if (char == "\\") new_str += "\\\\"; - else if (char == '"') new_str += '\\"'; - else if (char < " " || (char > "~" && char < "\u{00A0}")) { - new_str += "\\u{" + char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0") + "}"; - } else { - new_str += char; - } +function $inspectString(str) { + let new_str = '"'; + for (let i = 0; i < str.length; i++) { + let char = str[i]; + if (char == "\n") new_str += "\\n"; + else if (char == "\r") new_str += "\\r"; + else if (char == "\t") new_str += "\\t"; + else if (char == "\f") new_str += "\\f"; + else if (char == "\\") new_str += "\\\\"; + else if (char == '"') new_str += '\\"'; + else if (char < " " || (char > "~" && char < "\u{00A0}")) { + new_str += "\\u{" + char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0") + "}"; + } else { + new_str += char; } - new_str += '"'; - return new_str; } + new_str += '"'; + return new_str; +} - function inspectDict(map) { - let body = "dict.from_list(["; - let first = true; - map.forEach((value, key) => { - if (!first) body = body + ", "; - body = body + "#(" + inspect(key) + ", " + inspect(value) + ")"; - first = false; - }); - return body + "])"; - } +function $inspectDict(map) { + let body = "dict.from_list(["; + let first = true; + map.forEach((value, key) => { + if (!first) body = body + ", "; + body = body + "#(" + $inspect(key) + ", " + $inspect(value) + ")"; + first = false; + }); + return body + "])"; +} - function inspectCustomType(record) { - const props = Object.keys(record) - .map((label) => { - const value = inspect(record[label]); - return isNaN(parseInt(label)) ? `${label}: ${value}` : value; - }) - .join(", "); - return props ? `${record.constructor.name}(${props})` : record.constructor.name; - } +function $inspectCustomType(record) { + const props = Object.keys(record) + .map((label) => { + const value = $inspect(record[label]); + return isNaN(parseInt(label)) ? `${label}: ${value}` : value; + }) + .join(", "); + return props ? `${record.constructor.name}(${props})` : record.constructor.name; +} - function inspectObject(v) { - const name = Object.getPrototypeOf(v)?.constructor?.name || "Object"; - const props = []; - for (const k of Object.keys(v)) { - props.push(`${inspect(k)}: ${inspect(v[k])}`); - } - const body = props.length ? " " + props.join(", ") + " " : ""; - const head = name === "Object" ? "" : name + " "; - return `//js(${head}{${body}})`; +function $inspectObject(v) { + const name = Object.getPrototypeOf(v)?.constructor?.name || "Object"; + const props = []; + for (const k of Object.keys(v)) { + props.push(`${$inspect(k)}: ${$inspect(v[k])}`); } + const body = props.length ? " " + props.join(", ") + " " : ""; + const head = name === "Object" ? "" : name + " "; + return `//js(${head}{${body}})`; +} - function inspect(v) { - const t = typeof v; - if (v === true) return "True"; - if (v === false) return "False"; - if (v === null) return "//js(null)"; - if (v === undefined) return "Nil"; - if (t === "string") return inspectString(v); - if (t === "bigint" || t === "number") return v.toString(); - if (Array.isArray(v)) return `#(${v.map(inspect).join(", ")})`; - if (v instanceof List) return `[${v.toArray().map(inspect).join(", ")}]`; - if (v instanceof UtfCodepoint) return `//utfcodepoint(${String.fromCodePoint(v.value)})`; - if (v instanceof BitArray) return `<<${Array.from(v.buffer).join(", ")}>>`; - if (v instanceof CustomType) return inspectCustomType(v); - if (v instanceof Dict) return inspectDict(v); - if (v instanceof Set) return `//js(Set(${[...v].map(inspect).join(", ")}))`; - if (v instanceof RegExp) return `//js(${v})`; - if (v instanceof Date) return `//js(Date("${v.toISOString()}"))`; - if (v instanceof Function) { - const args = []; - for (const i of Array(v.length).keys()) args.push(String.fromCharCode(i + 97)); - return `//fn(${args.join(", ")}) { ... }`; - } - return inspectObject(v); +function $inspect(v) { + const t = typeof v; + if (v === true) return "True"; + if (v === false) return "False"; + if (v === null) return "//js(null)"; + if (v === undefined) return "Nil"; + if (t === "string") return $inspectString(v); + if (t === "bigint" || t === "number") return v.toString(); + if (Array.isArray(v)) return `#(${v.map($inspect).join(", ")})`; + if (v instanceof List) return `[${v.toArray().map($inspect).join(", ")}]`; + if (v instanceof UtfCodepoint) return `//utfcodepoint(${String.fromCodePoint(v.value)})`; + if (v instanceof BitArray) return `<<${Array.from(v.buffer).join(", ")}>>`; + if (v instanceof CustomType) return $inspectCustomType(v); + if ($isDict(v)) return $inspectDict(v); + if (v instanceof Set) return `//js(Set(${[...v].map($inspect).join(", ")}))`; + if (v instanceof RegExp) return `//js(${v})`; + if (v instanceof Date) return `//js(Date("${v.toISOString()}"))`; + if (v instanceof Function) { + const args = []; + for (const i of Array(v.length).keys()) args.push(String.fromCharCode(i + 97)); + return `//fn(${args.join(", ")}) { ... }`; } + return $inspectObject(v); +} - function print_debug(string) { - if (typeof process === "object" && process.stderr?.write) { - // If we're in Node.js, use `stderr` - process.stderr.write(string + "\n"); - } else if (typeof Deno === "object") { - // If we're in Deno, use `stderr` - Deno.stderr.writeSync(new TextEncoder().encode(string + "\n")); - } else { - // Otherwise, use `console.log` (so that it doesn't look like an error) - console.log(string); - } +function $isDict(value) { + try { + return value instanceof Dict; + } catch { + return false; } - - print_debug(inspect(v)); - return v; } diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__multiple_echos_inside_expression.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__multiple_echos_inside_expression.snap index a46e6111c58..5ec8376b4de 100644 --- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__multiple_echos_inside_expression.snap +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__echo__multiple_echos_inside_expression.snap @@ -9,99 +9,103 @@ export function main() { return echo(2); } -// Debug printing code +function echo(value) { + const string = $inspect(value); + if (typeof process === "object" && process.stderr?.write) { + // If we're in Node.js, use `stderr` + process.stderr.write(string + "\n"); + } else if (typeof Deno === "object") { + // If we're in Deno, use `stderr` + Deno.stderr.writeSync(new TextEncoder().encode(string + "\n")); + } else { + // Otherwise, use `console.log` + console.log(string); + } + + return value; +} -function echo(v) { - function inspectString(str) { - let new_str = '"'; - for (let i = 0; i < str.length; i++) { - let char = str[i]; - if (char == "\n") new_str += "\\n"; - else if (char == "\r") new_str += "\\r"; - else if (char == "\t") new_str += "\\t"; - else if (char == "\f") new_str += "\\f"; - else if (char == "\\") new_str += "\\\\"; - else if (char == '"') new_str += '\\"'; - else if (char < " " || (char > "~" && char < "\u{00A0}")) { - new_str += "\\u{" + char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0") + "}"; - } else { - new_str += char; - } +function $inspectString(str) { + let new_str = '"'; + for (let i = 0; i < str.length; i++) { + let char = str[i]; + if (char == "\n") new_str += "\\n"; + else if (char == "\r") new_str += "\\r"; + else if (char == "\t") new_str += "\\t"; + else if (char == "\f") new_str += "\\f"; + else if (char == "\\") new_str += "\\\\"; + else if (char == '"') new_str += '\\"'; + else if (char < " " || (char > "~" && char < "\u{00A0}")) { + new_str += "\\u{" + char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0") + "}"; + } else { + new_str += char; } - new_str += '"'; - return new_str; } + new_str += '"'; + return new_str; +} - function inspectDict(map) { - let body = "dict.from_list(["; - let first = true; - map.forEach((value, key) => { - if (!first) body = body + ", "; - body = body + "#(" + inspect(key) + ", " + inspect(value) + ")"; - first = false; - }); - return body + "])"; - } +function $inspectDict(map) { + let body = "dict.from_list(["; + let first = true; + map.forEach((value, key) => { + if (!first) body = body + ", "; + body = body + "#(" + $inspect(key) + ", " + $inspect(value) + ")"; + first = false; + }); + return body + "])"; +} - function inspectCustomType(record) { - const props = Object.keys(record) - .map((label) => { - const value = inspect(record[label]); - return isNaN(parseInt(label)) ? `${label}: ${value}` : value; - }) - .join(", "); - return props ? `${record.constructor.name}(${props})` : record.constructor.name; - } +function $inspectCustomType(record) { + const props = Object.keys(record) + .map((label) => { + const value = $inspect(record[label]); + return isNaN(parseInt(label)) ? `${label}: ${value}` : value; + }) + .join(", "); + return props ? `${record.constructor.name}(${props})` : record.constructor.name; +} - function inspectObject(v) { - const name = Object.getPrototypeOf(v)?.constructor?.name || "Object"; - const props = []; - for (const k of Object.keys(v)) { - props.push(`${inspect(k)}: ${inspect(v[k])}`); - } - const body = props.length ? " " + props.join(", ") + " " : ""; - const head = name === "Object" ? "" : name + " "; - return `//js(${head}{${body}})`; +function $inspectObject(v) { + const name = Object.getPrototypeOf(v)?.constructor?.name || "Object"; + const props = []; + for (const k of Object.keys(v)) { + props.push(`${$inspect(k)}: ${$inspect(v[k])}`); } + const body = props.length ? " " + props.join(", ") + " " : ""; + const head = name === "Object" ? "" : name + " "; + return `//js(${head}{${body}})`; +} - function inspect(v) { - const t = typeof v; - if (v === true) return "True"; - if (v === false) return "False"; - if (v === null) return "//js(null)"; - if (v === undefined) return "Nil"; - if (t === "string") return inspectString(v); - if (t === "bigint" || t === "number") return v.toString(); - if (Array.isArray(v)) return `#(${v.map(inspect).join(", ")})`; - if (v instanceof List) return `[${v.toArray().map(inspect).join(", ")}]`; - if (v instanceof UtfCodepoint) return `//utfcodepoint(${String.fromCodePoint(v.value)})`; - if (v instanceof BitArray) return `<<${Array.from(v.buffer).join(", ")}>>`; - if (v instanceof CustomType) return inspectCustomType(v); - if (v instanceof Dict) return inspectDict(v); - if (v instanceof Set) return `//js(Set(${[...v].map(inspect).join(", ")}))`; - if (v instanceof RegExp) return `//js(${v})`; - if (v instanceof Date) return `//js(Date("${v.toISOString()}"))`; - if (v instanceof Function) { - const args = []; - for (const i of Array(v.length).keys()) args.push(String.fromCharCode(i + 97)); - return `//fn(${args.join(", ")}) { ... }`; - } - return inspectObject(v); +function $inspect(v) { + const t = typeof v; + if (v === true) return "True"; + if (v === false) return "False"; + if (v === null) return "//js(null)"; + if (v === undefined) return "Nil"; + if (t === "string") return $inspectString(v); + if (t === "bigint" || t === "number") return v.toString(); + if (Array.isArray(v)) return `#(${v.map($inspect).join(", ")})`; + if (v instanceof List) return `[${v.toArray().map($inspect).join(", ")}]`; + if (v instanceof UtfCodepoint) return `//utfcodepoint(${String.fromCodePoint(v.value)})`; + if (v instanceof BitArray) return `<<${Array.from(v.buffer).join(", ")}>>`; + if (v instanceof CustomType) return $inspectCustomType(v); + if ($isDict(v)) return $inspectDict(v); + if (v instanceof Set) return `//js(Set(${[...v].map($inspect).join(", ")}))`; + if (v instanceof RegExp) return `//js(${v})`; + if (v instanceof Date) return `//js(Date("${v.toISOString()}"))`; + if (v instanceof Function) { + const args = []; + for (const i of Array(v.length).keys()) args.push(String.fromCharCode(i + 97)); + return `//fn(${args.join(", ")}) { ... }`; } + return $inspectObject(v); +} - function print_debug(string) { - if (typeof process === "object" && process.stderr?.write) { - // If we're in Node.js, use `stderr` - process.stderr.write(string + "\n"); - } else if (typeof Deno === "object") { - // If we're in Deno, use `stderr` - Deno.stderr.writeSync(new TextEncoder().encode(string + "\n")); - } else { - // Otherwise, use `console.log` (so that it doesn't look like an error) - console.log(string); - } +function $isDict(value) { + try { + return value instanceof Dict; + } catch { + return false; } - - print_debug(inspect(v)); - return v; } diff --git a/compiler-core/templates/echo.erl b/compiler-core/templates/echo.erl new file mode 100644 index 00000000000..aa32b255f46 --- /dev/null +++ b/compiler-core/templates/echo.erl @@ -0,0 +1,171 @@ +-define(is_lowercase_char(X), (X > 96 andalso X < 123)). +-define(is_underscore_char(X), (X == 95)). +-define(is_digit_char(X), (X > 47 andalso X < 58)). +-define(could_be_record(Tuple), + is_tuple(Tuple) andalso + is_atom(element(1, Tuple)) andalso + element(1, Tuple) =/= false andalso + element(1, Tuple) =/= true andalso + element(1, Tuple) =/= nil +). +-define(is_atom_char(C), + (?is_lowercase_char(C) orelse + ?is_underscore_char(C) orelse + ?is_digit_char(C)) +). + +echo(Value) -> + String = echo@inspect(Value), + io:put_chars(standard_error, [String, $\n]), + Value. + +echo@inspect(Value) -> + case Value of + nil -> "Nil"; + true -> "True"; + false -> "False"; + Int when is_integer(Int) -> erlang:integer_to_list(Int); + Float when is_float(Float) -> io_lib_format:fwrite_g(Float); + Bits when is_bitstring(Bits) -> inspect@bit_array(Bits); + Binary when is_binary(Binary) -> inspect@binary(Binary); + Atom when is_atom(Atom) -> inspect@atom(Atom); + List when is_list(List) -> inspect@list(List); + Map when is_map(Map) -> inspect@map(Map); + Record when ?could_be_record(Record) -> inspect@record(Record); + Tuple when is_tuple(Tuple) -> inspect@tuple(Tuple); + Function when is_function(Function) -> inspect@function(Function); + Any -> ["//erl(", io_lib:format("~p", [Any]), ")"] + end. + +inspect@bit_array(Bits) -> + Pieces = inspect@bit_array_pieces(Bits, []), + Inner = string:join(lists:reverse(Pieces), <<", ">>), + <<"<<", Inner/binary, ">>">>. + +inspect@bit_array_pieces(Bits, Acc) -> + case Bits of + <<>> -> + Acc; + <> -> + inspect@bit_array_pieces(Rest, [integer_to_binary(Byte) | Acc]); + _ -> + Size = bit_size(Bits), + <> = Bits, + SizeString = <<":size(", (integer_to_binary(Size)), ")">>, + Piece = <<(integer_to_binary(RemainingBits)), SizeString>>, + inspect@bit_array_pieces(<<>>, [Piece | Acc]) + end. + +inspect@binary(Binary) -> + case inspect@maybe_utf8_string(Binary, <<>>) of + {ok, InspectedUtf8String} -> + InspectedUtf8String; + {error, not_a_utf8_string} -> + Segments = [integer_to_list(X) || <> <= Binary], + ["<<", lists:join(", ", Segments), ">>"] + end. + +inspect@atom(Atom) -> + Binary = erlang:atom_to_binary(Atom), + case inspect@maybe_gleam_atom(Binary, none, <<>>) of + {ok, Inspected} -> Inspected; + {error, _} -> ["atom.create_from_string(\"", Binary, "\")"] + end. + +inspect@list(List) -> + case inspect@proper_or_improper_list(List) of + {proper, Elements} -> ["[", Elements, "]"]; + {improper, Elements} -> ["//erl([", Elements, "])"] + end. + +inspect@map(Map) -> + Fields = [ + [<<"#(">>, echo@inspect(Key), <<", ">>, echo@inspect(Value), <<")">>] + || {Key, Value} <- maps:to_list(Map) + ], + ["dict.from_list([", lists:join(", ", Fields), "])"]. + +inspect@record(Record) -> + [Atom | ArgsList] = erlang:tuple_to_list(Record), + Args = lists:join(", ", lists:map(fun echo@inspect/1, ArgsList)), + [echo@inspect(Atom), "(", Args, ")"]. + +inspect@tuple(Tuple) -> + Elements = lists:map(fun echo@inspect/1, erlang:tuple_to_list(Tuple)), + ["#(", lists:join(", ", Elements), ")"]. + +inspect@function(Function) -> + {arity, Arity} = erlang:fun_info(Function, arity), + ArgsAsciiCodes = lists:seq($a, $a + Arity - 1), + Args = lists:join(", ", lists:map(fun(Arg) -> <> end, ArgsAsciiCodes)), + ["//fn(", Args, ") { ... }"]. + +inspect@maybe_utf8_string(Binary, Acc) -> + case Binary of + <<>> -> + {ok, <<$", Acc/binary, $">>}; + <> -> + Escaped = inspect@escape_grapheme(First), + inspect@maybe_utf8_string(Rest, <>); + _ -> + {error, not_a_utf8_string} + end. + +inspect@escape_grapheme(Char) -> + case Char of + $" -> <<$\\, $">>; + $\\ -> <<$\\, $\\>>; + $\r -> <<$\\, $r>>; + $\n -> <<$\\, $n>>; + $\t -> <<$\\, $t>>; + $\f -> <<$\\, $f>>; + X when X > 126, X < 160 -> inspect@convert_to_u(X); + X when X < 32 -> inspect@convert_to_u(X); + Other -> <> + end. + +inspect@convert_to_u(Code) -> + list_to_binary(io_lib:format("\\u{~4.16.0B}", [Code])). + +inspect@proper_or_improper_list(List) -> + case List of + [] -> + {proper, []}; + [First] -> + {proper, [echo@inspect(First)]}; + [First | Rest] when is_list(Rest) -> + {Kind, Inspected} = inspect@list(Rest), + {Kind, [echo@inspect(First), ", " | Inspected]}; + [First | ImproperRest] -> + {improper, [echo@inspect(First), " | ", echo@inspect(ImproperRest)]} + end. + +inspect@maybe_gleam_atom(Atom, PrevChar, Acc) -> + case {Atom, PrevChar} of + {<<>>, none} -> + {error, nil}; + {<>, none} when ?is_digit_char(First) -> + {error, nil}; + {<<"_", _/binary>>, none} -> + {error, nil}; + {<<"_">>, _} -> + {error, nil}; + {<<"_", _/binary>>, $_} -> + {error, nil}; + {<>, _} when not ?is_atom_char(First) -> + {error, nil}; + {<>, none} -> + inspect@maybe_gleam_atom(Rest, First, <>); + {<<"_", Rest/binary>>, _} -> + inspect@maybe_gleam_atom(Rest, $_, Acc); + {<>, $_} -> + inspect@maybe_gleam_atom(Rest, First, <>); + {<>, _} -> + inspect@maybe_gleam_atom(Rest, First, <>); + {<<>>, _} -> + {ok, Acc}; + _ -> + throw({gleam_error, echo, Atom, PrevChar, Acc}) + end. + +inspect@uppercase(X) -> X - 32. diff --git a/compiler-core/templates/echo.mjs b/compiler-core/templates/echo.mjs index 142169fad36..47add4718d9 100644 --- a/compiler-core/templates/echo.mjs +++ b/compiler-core/templates/echo.mjs @@ -1,96 +1,100 @@ -// Debug printing code +function echo(value) { + const string = $inspect(value); + if (typeof process === "object" && process.stderr?.write) { + // If we're in Node.js, use `stderr` + process.stderr.write(string + "\n"); + } else if (typeof Deno === "object") { + // If we're in Deno, use `stderr` + Deno.stderr.writeSync(new TextEncoder().encode(string + "\n")); + } else { + // Otherwise, use `console.log` + console.log(string); + } + + return value; +} -function echo(v) { - function inspectString(str) { - let new_str = '"'; - for (let i = 0; i < str.length; i++) { - let char = str[i]; - if (char == "\n") new_str += "\\n"; - else if (char == "\r") new_str += "\\r"; - else if (char == "\t") new_str += "\\t"; - else if (char == "\f") new_str += "\\f"; - else if (char == "\\") new_str += "\\\\"; - else if (char == '"') new_str += '\\"'; - else if (char < " " || (char > "~" && char < "\u{00A0}")) { - new_str += "\\u{" + char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0") + "}"; - } else { - new_str += char; - } +function $inspectString(str) { + let new_str = '"'; + for (let i = 0; i < str.length; i++) { + let char = str[i]; + if (char == "\n") new_str += "\\n"; + else if (char == "\r") new_str += "\\r"; + else if (char == "\t") new_str += "\\t"; + else if (char == "\f") new_str += "\\f"; + else if (char == "\\") new_str += "\\\\"; + else if (char == '"') new_str += '\\"'; + else if (char < " " || (char > "~" && char < "\u{00A0}")) { + new_str += "\\u{" + char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0") + "}"; + } else { + new_str += char; } - new_str += '"'; - return new_str; } + new_str += '"'; + return new_str; +} - function inspectDict(map) { - let body = "dict.from_list(["; - let first = true; - map.forEach((value, key) => { - if (!first) body = body + ", "; - body = body + "#(" + inspect(key) + ", " + inspect(value) + ")"; - first = false; - }); - return body + "])"; - } +function $inspectDict(map) { + let body = "dict.from_list(["; + let first = true; + map.forEach((value, key) => { + if (!first) body = body + ", "; + body = body + "#(" + $inspect(key) + ", " + $inspect(value) + ")"; + first = false; + }); + return body + "])"; +} - function inspectCustomType(record) { - const props = Object.keys(record) - .map((label) => { - const value = inspect(record[label]); - return isNaN(parseInt(label)) ? `${label}: ${value}` : value; - }) - .join(", "); - return props ? `${record.constructor.name}(${props})` : record.constructor.name; - } +function $inspectCustomType(record) { + const props = Object.keys(record) + .map((label) => { + const value = $inspect(record[label]); + return isNaN(parseInt(label)) ? `${label}: ${value}` : value; + }) + .join(", "); + return props ? `${record.constructor.name}(${props})` : record.constructor.name; +} - function inspectObject(v) { - const name = Object.getPrototypeOf(v)?.constructor?.name || "Object"; - const props = []; - for (const k of Object.keys(v)) { - props.push(`${inspect(k)}: ${inspect(v[k])}`); - } - const body = props.length ? " " + props.join(", ") + " " : ""; - const head = name === "Object" ? "" : name + " "; - return `//js(${head}{${body}})`; +function $inspectObject(v) { + const name = Object.getPrototypeOf(v)?.constructor?.name || "Object"; + const props = []; + for (const k of Object.keys(v)) { + props.push(`${$inspect(k)}: ${$inspect(v[k])}`); } + const body = props.length ? " " + props.join(", ") + " " : ""; + const head = name === "Object" ? "" : name + " "; + return `//js(${head}{${body}})`; +} - function inspect(v) { - const t = typeof v; - if (v === true) return "True"; - if (v === false) return "False"; - if (v === null) return "//js(null)"; - if (v === undefined) return "Nil"; - if (t === "string") return inspectString(v); - if (t === "bigint" || t === "number") return v.toString(); - if (Array.isArray(v)) return `#(${v.map(inspect).join(", ")})`; - if (v instanceof List) return `[${v.toArray().map(inspect).join(", ")}]`; - if (v instanceof UtfCodepoint) return `//utfcodepoint(${String.fromCodePoint(v.value)})`; - if (v instanceof BitArray) return `<<${Array.from(v.buffer).join(", ")}>>`; - if (v instanceof CustomType) return inspectCustomType(v); - if (v instanceof Dict) return inspectDict(v); - if (v instanceof Set) return `//js(Set(${[...v].map(inspect).join(", ")}))`; - if (v instanceof RegExp) return `//js(${v})`; - if (v instanceof Date) return `//js(Date("${v.toISOString()}"))`; - if (v instanceof Function) { - const args = []; - for (const i of Array(v.length).keys()) args.push(String.fromCharCode(i + 97)); - return `//fn(${args.join(", ")}) { ... }`; - } - return inspectObject(v); +function $inspect(v) { + const t = typeof v; + if (v === true) return "True"; + if (v === false) return "False"; + if (v === null) return "//js(null)"; + if (v === undefined) return "Nil"; + if (t === "string") return $inspectString(v); + if (t === "bigint" || t === "number") return v.toString(); + if (Array.isArray(v)) return `#(${v.map($inspect).join(", ")})`; + if (v instanceof List) return `[${v.toArray().map($inspect).join(", ")}]`; + if (v instanceof UtfCodepoint) return `//utfcodepoint(${String.fromCodePoint(v.value)})`; + if (v instanceof BitArray) return `<<${Array.from(v.buffer).join(", ")}>>`; + if (v instanceof CustomType) return $inspectCustomType(v); + if ($isDict(v)) return $inspectDict(v); + if (v instanceof Set) return `//js(Set(${[...v].map($inspect).join(", ")}))`; + if (v instanceof RegExp) return `//js(${v})`; + if (v instanceof Date) return `//js(Date("${v.toISOString()}"))`; + if (v instanceof Function) { + const args = []; + for (const i of Array(v.length).keys()) args.push(String.fromCharCode(i + 97)); + return `//fn(${args.join(", ")}) { ... }`; } + return $inspectObject(v); +} - function print_debug(string) { - if (typeof process === "object" && process.stderr?.write) { - // If we're in Node.js, use `stderr` - process.stderr.write(string + "\n"); - } else if (typeof Deno === "object") { - // If we're in Deno, use `stderr` - Deno.stderr.writeSync(new TextEncoder().encode(string + "\n")); - } else { - // Otherwise, use `console.log` (so that it doesn't look like an error) - console.log(string); - } +function $isDict(value) { + try { + return value instanceof Dict; + } catch { + return false; } - - print_debug(inspect(v)); - return v; }