diff --git a/CHANGELOG.md b/CHANGELOG.md index c3544989741..4be83b7217a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -208,6 +208,34 @@ ([Giacomo Cavalieri](https://github.com/giacomocavalieri)) +- The language server now suggests a code action to generate a dynamic decoder + for a custom type. For example, this code: + + ```gleam + pub type Person { + Person(name: String, age: Int) + } + ``` + + Will become: + + ```gleam + import gleam/dynamic/decode + + pub type Person { + Person(name: String, age: Int) + } + + fn person_decoder() -> decode.Decoder(Person) { + use name <- decode.field("name", decode.string) + use age <- decode.field("age", decode.int) + + decode.success(Person(name:, age:)) + } + ``` + + ([Surya Rose](https://github.com/GearsDatapacks)) + ### Formatter - The formatter now adds a `todo` inside empty blocks. diff --git a/compiler-core/src/ast.rs b/compiler-core/src/ast.rs index 9a2d3ea246d..58321380fea 100644 --- a/compiler-core/src/ast.rs +++ b/compiler-core/src/ast.rs @@ -729,6 +729,7 @@ pub struct ModuleConstant { } pub type UntypedCustomType = CustomType<()>; +pub type TypedCustomType = CustomType>; #[derive(Debug, Clone, PartialEq, Eq)] /// A newly defined type with one or more constructors. diff --git a/compiler-core/src/ast/visit.rs b/compiler-core/src/ast/visit.rs index 3df1b1c0dd9..9d3b4c34580 100644 --- a/compiler-core/src/ast/visit.rs +++ b/compiler-core/src/ast/visit.rs @@ -53,9 +53,9 @@ use crate::type_::Type; use super::{ untyped::FunctionLiteralKind, AssignName, BinOp, BitArrayOption, CallArg, Definition, Pattern, - SrcSpan, Statement, TodoKind, TypeAst, TypedArg, TypedAssignment, TypedClause, TypedDefinition, - TypedExpr, TypedExprBitArraySegment, TypedFunction, TypedModule, TypedModuleConstant, - TypedPattern, TypedPatternBitArraySegment, TypedStatement, TypedUse, + SrcSpan, Statement, TodoKind, TypeAst, TypedArg, TypedAssignment, TypedClause, TypedCustomType, + TypedDefinition, TypedExpr, TypedExprBitArraySegment, TypedFunction, TypedModule, + TypedModuleConstant, TypedPattern, TypedPatternBitArraySegment, TypedStatement, TypedUse, }; pub trait Visit<'ast> { @@ -75,6 +75,10 @@ pub trait Visit<'ast> { visit_typed_module_constant(self, constant); } + fn visit_typed_custom_type(&mut self, custom_type: &'ast TypedCustomType) { + visit_typed_custom_type(self, custom_type); + } + fn visit_typed_expr(&mut self, expr: &'ast TypedExpr) { visit_typed_expr(self, expr); } @@ -493,7 +497,7 @@ where match def { Definition::Function(fun) => v.visit_typed_function(fun), Definition::TypeAlias(_typealias) => { /* TODO */ } - Definition::CustomType(_custom_type) => { /* TODO */ } + Definition::CustomType(custom_type) => v.visit_typed_custom_type(custom_type), Definition::Import(_import) => { /* TODO */ } Definition::ModuleConstant(constant) => v.visit_typed_module_constant(constant), } @@ -596,6 +600,12 @@ where { } +pub fn visit_typed_custom_type<'a, V>(_v: &mut V, _custom_type: &'a TypedCustomType) +where + V: Visit<'a> + ?Sized, +{ +} + pub fn visit_typed_expr<'a, V>(v: &mut V, node: &'a TypedExpr) where V: Visit<'a> + ?Sized, diff --git a/compiler-core/src/language_server/code_action.rs b/compiler-core/src/language_server/code_action.rs index b8bc6aa04e5..c3a5593ab53 100644 --- a/compiler-core/src/language_server/code_action.rs +++ b/compiler-core/src/language_server/code_action.rs @@ -14,12 +14,13 @@ use crate::{ type_::{ self, error::{ModuleSuggestion, VariableOrigin}, - printer::Printer, + printer::{Names, Printer}, FieldMap, ModuleValueConstructor, Type, TypedCallArg, }, - Error, + Error, STDLIB_PACKAGE_NAME, }; use ecow::{eco_format, EcoString}; +use heck::ToSnakeCase; use im::HashMap; use itertools::Itertools; use lsp_types::{CodeAction, CodeActionKind, CodeActionParams, Position, Range, TextEdit, Url}; @@ -2875,3 +2876,292 @@ impl<'ast> ast::visit::Visit<'ast> for VariablesNames { let _ = self.names.insert(name.clone()); } } + +/// Builder for code action to apply the "generate dynamic decoder action. +/// +pub struct GenerateDynamicDecoder<'a> { + module: &'a Module, + params: &'a CodeActionParams, + edits: TextEdits<'a>, + printer: Printer<'a>, + actions: &'a mut Vec, +} + +const DECODE_MODULE: &str = "gleam/dynamic/decode"; + +impl<'a> GenerateDynamicDecoder<'a> { + pub fn new( + module: &'a Module, + line_numbers: &'a LineNumbers, + params: &'a CodeActionParams, + actions: &'a mut Vec, + ) -> Self { + let printer = Printer::new(&module.ast.names); + Self { + module, + params, + edits: TextEdits::new(line_numbers), + printer, + actions, + } + } + + pub fn code_actions(&mut self) { + self.visit_typed_module(&self.module.ast); + } +} + +impl<'ast> ast::visit::Visit<'ast> for GenerateDynamicDecoder<'ast> { + fn visit_typed_custom_type(&mut self, custom_type: &'ast ast::TypedCustomType) { + let range = self.edits.src_span_to_lsp_range(custom_type.location); + if !overlaps(self.params.range, range) { + return; + } + + // For now, we only generate dynamic decoders for types with one variant. + let constructor = match custom_type.constructors.as_slice() { + [constructor] => constructor, + _ => return, + }; + + let name = eco_format!("{}_decoder", custom_type.name.to_snake_case()); + + let Some(fields): Option> = constructor + .arguments + .iter() + .map(|argument| { + Some(RecordField { + label: RecordLabel::Labeled( + argument.label.as_ref().map(|(_, name)| name.as_str())?, + ), + type_: &argument.type_, + }) + }) + .collect() + else { + return; + }; + + let mut decoder_printer = DecoderPrinter::new( + &self.module.ast.names, + custom_type.name.clone(), + self.module.name.clone(), + ); + + let decoders = fields + .iter() + .map(|field| decoder_printer.decode_field(field, 2)) + .join("\n"); + + let decoder_type = self.printer.print_type(&Type::Named { + publicity: ast::Publicity::Public, + package: STDLIB_PACKAGE_NAME.into(), + module: DECODE_MODULE.into(), + name: "Decoder".into(), + args: vec![], + inferred_variant: None, + }); + let decode_module = self.printer.print_module(DECODE_MODULE); + + let mut field_names = fields.iter().map(|field| field.label.variable_name()); + let parameters = match custom_type.parameters.len() { + 0 => EcoString::new(), + _ => eco_format!( + "({})", + custom_type + .parameters + .iter() + .map(|(_, name)| name) + .join(", ") + ), + }; + + let function = format!( + " + +fn {name}() -> {decoder_type}({type_name}{parameters}) {{ +{decoders} + + {decode_module}.success({constructor_name}({fields}:)) +}}", + type_name = custom_type.name, + constructor_name = constructor.name, + fields = field_names.join(":, ") + ); + + self.edits.insert(custom_type.end_position, function); + maybe_import(&mut self.edits, self.module, DECODE_MODULE); + + CodeActionBuilder::new("Generate dynamic decoder") + .kind(CodeActionKind::REFACTOR) + .preferred(false) + .changes( + self.params.text_document.uri.clone(), + std::mem::take(&mut self.edits.edits), + ) + .push_to(self.actions); + } +} + +fn maybe_import(edits: &mut TextEdits<'_>, module: &Module, module_name: &str) { + if module.ast.names.is_imported(module_name) { + return; + } + + let first_import_pos = position_of_first_definition_if_import(module, edits.line_numbers); + let first_is_import = first_import_pos.is_some(); + let import_location = first_import_pos.unwrap_or_default(); + let after_import_newlines = add_newlines_after_import( + import_location, + first_is_import, + edits.line_numbers, + &module.code, + ); + + edits.edits.push(get_import_edit( + import_location, + module_name, + &after_import_newlines, + )); +} + +struct DecoderPrinter<'a> { + printer: Printer<'a>, + /// The name of the root type we are printing a decoder for + type_name: EcoString, + /// The module name of the root type we are printing a decoder for + type_module: EcoString, +} + +struct RecordField<'a> { + label: RecordLabel<'a>, + type_: &'a Type, +} + +enum RecordLabel<'a> { + Labeled(&'a str), + Unlabeled(usize), +} + +impl RecordLabel<'_> { + fn field_key(&self) -> EcoString { + match self { + RecordLabel::Labeled(label) => eco_format!("\"{label}\""), + RecordLabel::Unlabeled(index) => { + eco_format!("{index}") + } + } + } + + fn variable_name(&self) -> EcoString { + match self { + RecordLabel::Labeled(label) => (*label).into(), + RecordLabel::Unlabeled(mut index) => { + let mut characters = Vec::new(); + let alphabet_length = 26; + let alphabet_offset = b'a'; + loop { + let alphabet_index = (index % alphabet_length) as u8; + characters.push((alphabet_offset + alphabet_index) as char); + index /= alphabet_length; + + if index == 0 { + break; + } + index -= 1; + } + characters.into_iter().rev().collect() + } + } + } +} + +impl<'a> DecoderPrinter<'a> { + fn new(names: &'a Names, type_name: EcoString, type_module: EcoString) -> Self { + Self { + type_name, + type_module, + printer: Printer::new(names), + } + } + + fn decoder_for(&mut self, type_: &Type, indent: usize) -> EcoString { + let module_name = self.printer.print_module(DECODE_MODULE); + if type_.is_bit_array() { + eco_format!("{module_name}.bit_array") + } else if type_.is_bool() { + eco_format!("{module_name}.bool") + } else if type_.is_float() { + eco_format!("{module_name}.float") + } else if type_.is_int() { + eco_format!("{module_name}.int") + } else if type_.is_string() { + eco_format!("{module_name}.string") + } else if let Some(types) = type_.tuple_types() { + let fields = types + .iter() + .enumerate() + .map(|(index, type_)| RecordField { + type_, + label: RecordLabel::Unlabeled(index), + }) + .collect_vec(); + let decoders = fields + .iter() + .map(|field| self.decode_field(field, indent + 2)) + .join("\n"); + let mut field_names = fields.iter().map(|field| field.label.variable_name()); + + eco_format!( + "{{ +{decoders} + +{indent} {module_name}.success(#({fields})) +{indent}}}", + fields = field_names.join(", "), + indent = " ".repeat(indent) + ) + } else { + let type_information = type_.named_type_information(); + let type_information = type_information.as_ref().map(|(module, name, arguments)| { + (module.as_str(), name.as_str(), arguments.as_slice()) + }); + + match type_information { + Some(("gleam/dynamic", "Dynamic", _)) => eco_format!("{module_name}.dynamic"), + Some(("gleam", "List", [element])) => { + eco_format!("{module_name}.list({})", self.decoder_for(element, indent)) + } + Some(("gleam/option", "Option", [some])) => { + eco_format!("{module_name}.optional({})", self.decoder_for(some, indent)) + } + Some(("gleam/dict", "Dict", [key, value])) => { + eco_format!( + "{module_name}.dict({}, {})", + self.decoder_for(key, indent), + self.decoder_for(value, indent) + ) + } + Some((module, name, _)) if module == self.type_module && name == self.type_name => { + eco_format!("{}_decoder()", name.to_snake_case()) + } + _ => eco_format!( + r#"todo as "Decoder for {}""#, + self.printer.print_type(type_) + ), + } + } + } + + fn decode_field(&mut self, field: &RecordField<'_>, indent: usize) -> EcoString { + let decoder = self.decoder_for(field.type_, indent); + + eco_format!( + r#"{indent}use {variable} <- {module}.field({field}, {decoder})"#, + indent = " ".repeat(indent), + variable = field.label.variable_name(), + field = field.label.field_key(), + module = self.printer.print_module(DECODE_MODULE) + ) + } +} diff --git a/compiler-core/src/language_server/edits.rs b/compiler-core/src/language_server/edits.rs index b0ce6c2fec7..8157169cb7c 100644 --- a/compiler-core/src/language_server/edits.rs +++ b/compiler-core/src/language_server/edits.rs @@ -61,7 +61,7 @@ pub fn add_newlines_after_import( pub fn get_import_edit( import_location: Position, - module_full_name: &EcoString, + module_full_name: &str, insert_newlines: &Newlines, ) -> TextEdit { let new_lines = match insert_newlines { diff --git a/compiler-core/src/language_server/engine.rs b/compiler-core/src/language_server/engine.rs index 00598af3c7f..16334ab67ae 100644 --- a/compiler-core/src/language_server/engine.rs +++ b/compiler-core/src/language_server/engine.rs @@ -33,8 +33,8 @@ use super::{ code_action_add_missing_patterns, code_action_convert_qualified_constructor_to_unqualified, code_action_convert_unqualified_constructor_to_qualified, code_action_import_module, code_action_inexhaustive_let_to_case, AddAnnotations, CodeActionBuilder, DesugarUse, - ExpandFunctionCapture, ExtractVariable, FillInMissingLabelledArgs, LabelShorthandSyntax, - LetAssertToCase, RedundantTupleInCaseSubject, TurnIntoUse, + ExpandFunctionCapture, ExtractVariable, FillInMissingLabelledArgs, GenerateDynamicDecoder, + LabelShorthandSyntax, LetAssertToCase, RedundantTupleInCaseSubject, TurnIntoUse, }, completer::Completer, signature_help, src_span_to_lsp_range, DownloadDependencies, MakeLocker, @@ -334,6 +334,7 @@ where actions.extend(TurnIntoUse::new(module, &lines, ¶ms).code_actions()); actions.extend(ExpandFunctionCapture::new(module, &lines, ¶ms).code_actions()); actions.extend(ExtractVariable::new(module, &lines, ¶ms).code_actions()); + GenerateDynamicDecoder::new(module, &lines, ¶ms, &mut actions).code_actions(); AddAnnotations::new(module, &lines, ¶ms).code_action(&mut actions); Ok(if actions.is_empty() { None diff --git a/compiler-core/src/language_server/tests/action.rs b/compiler-core/src/language_server/tests/action.rs index b5fd5f464ac..aa60ca2035b 100644 --- a/compiler-core/src/language_server/tests/action.rs +++ b/compiler-core/src/language_server/tests/action.rs @@ -70,6 +70,7 @@ const DESUGAR_USE_EXPRESSION: &str = "Convert from `use`"; const CONVERT_TO_USE: &str = "Convert to `use`"; const EXTRACT_VARIABLE: &str = "Extract variable"; const EXPAND_FUNCTION_CAPTURE: &str = "Expand function capture"; +const GENERATE_DYNAMIC_DECODER: &str = "Generate dynamic decoder"; macro_rules! assert_code_action { ($title:expr, $code:literal, $range:expr $(,)?) => { @@ -4034,3 +4035,117 @@ fn expand_function_capture_does_not_shadow_variables() { find_position_of("wibble").to_selection() ); } + +#[test] +fn generate_dynamic_decoder() { + assert_code_action!( + GENERATE_DYNAMIC_DECODER, + " +pub type Person { + Person(name: String, age: Int, height: Float, is_cool: Bool, brain: BitArray) +} +", + find_position_of("type").to_selection() + ); +} + +#[test] +fn generate_dynamic_decoder_complex_types() { + let src = " +import gleam/option +import gleam/dynamic +import gleam/dict + +pub type Something + +pub type Wibble(value) { + Wibble( + maybe: option.Option(Something), + map: dict.Dict(String, List(value)), + unknown: List(dynamic.Dynamic), + ) +} +"; + + assert_code_action!( + GENERATE_DYNAMIC_DECODER, + TestProject::for_source(src) + .add_module("gleam/option", "pub type Option(a)") + .add_module("gleam/dynamic", "pub type Dynamic") + .add_module("gleam/dict", "pub type Dict(k, v)"), + find_position_of("type W").to_selection() + ); +} + +#[test] +fn generate_dynamic_decoder_already_imported_module() { + let src = " +import gleam/dynamic/decode as dyn_dec + +pub type Wibble { + Wibble(a: Int, b: Float, c: String) +} +"; + + assert_code_action!( + GENERATE_DYNAMIC_DECODER, + TestProject::for_source(src).add_module("gleam/dynamic/decode", "pub type Decoder(a)"), + find_position_of("type W").to_selection() + ); +} + +#[test] +fn generate_dynamic_decoder_tuple() { + assert_code_action!( + GENERATE_DYNAMIC_DECODER, + " +pub type Wibble { + Wibble(tuple: #(Int, Float, #(String, Bool))) +} +", + find_position_of("type W").to_selection() + ); +} + +#[test] +fn generate_dynamic_decoder_recursive_type() { + let src = " +import gleam/option + +pub type LinkedList { + LinkedList(value: Int, next: option.Option(LinkedList)) +} +"; + assert_code_action!( + GENERATE_DYNAMIC_DECODER, + TestProject::for_source(src).add_module("gleam/option", "pub type Option(a)"), + find_position_of("type").to_selection() + ); +} + +#[test] +fn no_code_action_to_generate_dynamic_decoder_for_multi_variant_type() { + assert_no_code_actions!( + GENERATE_DYNAMIC_DECODER, + " +pub type Wibble { + Wibble(wibble: Int) + Wobble(wobble: Float) +} +", + find_position_of("type").to_selection() + ); +} + +#[test] +fn no_code_action_to_generate_dynamic_decoder_for_type_without_labels() { + assert_no_code_actions!( + GENERATE_DYNAMIC_DECODER, + " +pub type Wibble { + Wibble(Int, Int, String) +} +", + find_position_of("type").to_selection() + ); +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder.snap new file mode 100644 index 00000000000..8c5c24faba6 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder.snap @@ -0,0 +1,28 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "\npub type Person {\n Person(name: String, age: Int, height: Float, is_cool: Bool, brain: BitArray)\n}\n" +--- +----- BEFORE ACTION + +pub type Person { + ↑ + Person(name: String, age: Int, height: Float, is_cool: Bool, brain: BitArray) +} + + +----- AFTER ACTION +import gleam/dynamic/decode + +pub type Person { + Person(name: String, age: Int, height: Float, is_cool: Bool, brain: BitArray) +} + +fn person_decoder() -> decode.Decoder(Person) { + use name <- decode.field("name", decode.string) + use age <- decode.field("age", decode.int) + use height <- decode.field("height", decode.float) + use is_cool <- decode.field("is_cool", decode.bool) + use brain <- decode.field("brain", decode.bit_array) + + decode.success(Person(name:, age:, height:, is_cool:, brain:)) +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_already_imported_module.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_already_imported_module.snap new file mode 100644 index 00000000000..3fcf99dcc92 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_already_imported_module.snap @@ -0,0 +1,29 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "\nimport gleam/dynamic/decode as dyn_dec\n\npub type Wibble {\n Wibble(a: Int, b: Float, c: String)\n}\n" +--- +----- BEFORE ACTION + +import gleam/dynamic/decode as dyn_dec + +pub type Wibble { + ↑ + Wibble(a: Int, b: Float, c: String) +} + + +----- AFTER ACTION + +import gleam/dynamic/decode as dyn_dec + +pub type Wibble { + Wibble(a: Int, b: Float, c: String) +} + +fn wibble_decoder() -> dyn_dec.Decoder(Wibble) { + use a <- dyn_dec.field("a", dyn_dec.int) + use b <- dyn_dec.field("b", dyn_dec.float) + use c <- dyn_dec.field("c", dyn_dec.string) + + dyn_dec.success(Wibble(a:, b:, c:)) +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_complex_types.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_complex_types.snap new file mode 100644 index 00000000000..5f00ed260e0 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_complex_types.snap @@ -0,0 +1,46 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "\nimport gleam/option\nimport gleam/dynamic\nimport gleam/dict\n\npub type Something\n\npub type Wibble(value) {\n Wibble(\n maybe: option.Option(Something),\n map: dict.Dict(String, List(value)),\n unknown: List(dynamic.Dynamic),\n )\n}\n" +--- +----- BEFORE ACTION + +import gleam/option +import gleam/dynamic +import gleam/dict + +pub type Something + +pub type Wibble(value) { + ↑ + Wibble( + maybe: option.Option(Something), + map: dict.Dict(String, List(value)), + unknown: List(dynamic.Dynamic), + ) +} + + +----- AFTER ACTION + +import gleam/dynamic/decode +import gleam/option +import gleam/dynamic +import gleam/dict + +pub type Something + +pub type Wibble(value) { + Wibble( + maybe: option.Option(Something), + map: dict.Dict(String, List(value)), + unknown: List(dynamic.Dynamic), + ) +} + +fn wibble_decoder() -> decode.Decoder(Wibble(value)) { + use maybe <- decode.field("maybe", decode.optional(todo as "Decoder for Something")) + use map <- decode.field("map", decode.dict(decode.string, decode.list(todo as "Decoder for value"))) + use unknown <- decode.field("unknown", decode.list(decode.dynamic)) + + decode.success(Wibble(maybe:, map:, unknown:)) +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_recursive_type.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_recursive_type.snap new file mode 100644 index 00000000000..ce60d5b6f06 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_recursive_type.snap @@ -0,0 +1,29 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "\nimport gleam/option\n\npub type LinkedList {\n LinkedList(value: Int, next: option.Option(LinkedList))\n}\n" +--- +----- BEFORE ACTION + +import gleam/option + +pub type LinkedList { + ↑ + LinkedList(value: Int, next: option.Option(LinkedList)) +} + + +----- AFTER ACTION + +import gleam/dynamic/decode +import gleam/option + +pub type LinkedList { + LinkedList(value: Int, next: option.Option(LinkedList)) +} + +fn linked_list_decoder() -> decode.Decoder(LinkedList) { + use value <- decode.field("value", decode.int) + use next <- decode.field("next", decode.optional(linked_list_decoder())) + + decode.success(LinkedList(value:, next:)) +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_tuple.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_tuple.snap new file mode 100644 index 00000000000..2f7193ad984 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_tuple.snap @@ -0,0 +1,35 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "\npub type Wibble {\n Wibble(tuple: #(Int, Float, #(String, Bool)))\n}\n" +--- +----- BEFORE ACTION + +pub type Wibble { + ↑ + Wibble(tuple: #(Int, Float, #(String, Bool))) +} + + +----- AFTER ACTION +import gleam/dynamic/decode + +pub type Wibble { + Wibble(tuple: #(Int, Float, #(String, Bool))) +} + +fn wibble_decoder() -> decode.Decoder(Wibble) { + use tuple <- decode.field("tuple", { + use a <- decode.field(0, decode.int) + use b <- decode.field(1, decode.float) + use c <- decode.field(2, { + use a <- decode.field(0, decode.string) + use b <- decode.field(1, decode.bool) + + decode.success(#(a, b)) + }) + + decode.success(#(a, b, c)) + }) + + decode.success(Wibble(tuple:)) +} diff --git a/compiler-core/src/type_.rs b/compiler-core/src/type_.rs index 0aa5beab6e1..04a4d177f42 100644 --- a/compiler-core/src/type_.rs +++ b/compiler-core/src/type_.rs @@ -179,6 +179,7 @@ impl Type { pub fn tuple_types(&self) -> Option>> { match self { Self::Tuple { elems } => Some(elems.clone()), + Self::Var { type_, .. } => type_.borrow().tuple_types(), _ => None, } } @@ -264,6 +265,16 @@ impl Type { } } + pub fn named_type_information(&self) -> Option<(EcoString, EcoString, Vec>)> { + match self { + Self::Named { + module, name, args, .. + } => Some((module.clone(), name.clone(), args.clone())), + Self::Var { type_ } => type_.borrow().named_type_information(), + _ => None, + } + } + pub fn set_custom_type_variant(&mut self, index: u16) { match self { Type::Named { @@ -924,6 +935,13 @@ impl TypeVar { } } + pub fn tuple_types(&self) -> Option>> { + match self { + Self::Link { type_ } => type_.tuple_types(), + Self::Unbound { .. } | Self::Generic { .. } => None, + } + } + pub fn constructor_types(&self) -> Option>> { match self { Self::Link { type_ } => type_.constructor_types(), @@ -1009,6 +1027,13 @@ impl TypeVar { } } + pub fn named_type_information(&self) -> Option<(EcoString, EcoString, Vec>)> { + match self { + Self::Link { type_ } => type_.named_type_information(), + Self::Unbound { .. } | Self::Generic { .. } => None, + } + } + pub fn set_custom_type_variant(&mut self, index: u16) { match self { Self::Link { type_ } => Arc::make_mut(type_).set_custom_type_variant(index), diff --git a/compiler-core/src/type_/printer.rs b/compiler-core/src/type_/printer.rs index e306e691606..4fd0fa96d45 100644 --- a/compiler-core/src/type_/printer.rs +++ b/compiler-core/src/type_/printer.rs @@ -246,6 +246,10 @@ impl Names { NameContextInformation::Unimported(name.as_str()) } + + pub fn is_imported(&self, module: &str) -> bool { + self.imported_modules.contains_key(module) + } } #[derive(Debug)] @@ -318,6 +322,14 @@ impl<'a> Printer<'a> { buffer } + pub fn print_module(&self, module: &str) -> EcoString { + if let Some(module) = self.names.imported_modules.get(module) { + module.clone() + } else { + module.split("/").last().unwrap_or(module).into() + } + } + pub fn print_type_without_aliases(&mut self, type_: &Type) -> EcoString { let mut buffer = EcoString::new(); self.print(type_, &mut buffer, PrintMode::ExpandAliases);