Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

Select the types of activity you want to include in your feed.

Fix incorrect JS for overlapping string prefixes

Pattern matching on overlapping string prefixes with guards could
generate incorrect JavaScript. Distinguish hoistable guards from
pattern-bound guards so fall-through to shorter prefixes works.

+573 -49
+4
CHANGELOG.md
··· 479 479 - Fixed a bug where warnings and errors in the arguments of a call to a 480 480 function literal would be reported multiple times. 481 481 ([John Downey](https://github.com/jtdowney)) 482 + 483 + - Fixed a bug where pattern matching on overlapping string prefixes with guards 484 + could generate incorrect JavaScript. 485 + ([John Downey](https://github.com/jtdowney))
+221 -49
compiler-core/src/exhaustiveness.rs
··· 136 136 /// 137 137 alternative_index: usize, 138 138 checks: Vec<PatternCheck>, 139 - guard: Option<usize>, 139 + guard: Option<BranchGuard>, 140 140 body: Body, 141 141 } 142 142 143 + /// A branch's guard, holding the index of the clause it belongs to and whether 144 + /// it references variables bound by the branch's patterns. 145 + #[derive(Clone, Copy, Eq, PartialEq, Debug)] 146 + struct BranchGuard { 147 + clause_index: usize, 148 + uses_pattern_bindings: bool, 149 + } 150 + 143 151 impl Branch { 144 152 fn new( 145 153 clause_index: usize, 146 154 alternative_index: usize, 147 155 checks: Vec<PatternCheck>, 148 - has_guard: bool, 156 + guard: Option<BranchGuard>, 149 157 ) -> Self { 150 158 Self { 151 159 clause_index, 152 160 alternative_index, 153 161 checks, 154 - guard: if has_guard { Some(clause_index) } else { None }, 162 + guard, 155 163 body: Body::new(clause_index), 156 164 } 157 165 } ··· 2362 2370 // if the condition evaluates to `True` we can run its body. 2363 2371 // Otherwise, we'll have to keep looking at the remaining branches 2364 2372 // to know what to do if this branch doesn't match. 2365 - Some(guard) => { 2373 + Some(BranchGuard { clause_index, .. }) => { 2366 2374 let if_true = first_branch.body.clone(); 2367 2375 // All the remaining branches will be compiled and end up 2368 2376 // in the path of the tree to choose if the guard is false. 2369 2377 let _ = branches.pop_front(); 2370 2378 let if_false = self.compile(branches); 2371 - Decision::guard(guard, if_true, if_false) 2379 + Decision::guard(clause_index, if_true, if_false) 2372 2380 } 2373 2381 }, 2374 2382 } ··· 2600 2608 /// 2601 2609 fn new_checks( 2602 2610 &mut self, 2611 + subject: &Variable, 2603 2612 for_pattern: &Pattern, 2604 2613 after_succeding_check: &RuntimeCheck, 2605 2614 ) -> Vec<PatternCheck> { ··· 2697 2706 // we know it already starts with `"wibble"` we can just check that the remaining 2698 2707 // part after that starts with the missing part of the prefix: 2699 2708 // `prefix0 is "st" <> rest1`. 2709 + // 2710 + // The opposite can also happen. The prefix we've already checked might be longer 2711 + // than (and start with) the pattern's prefix. This comes up when an earlier branch 2712 + // matched the longer prefix under a guard that then failed, so control falls 2713 + // through to this shorter-prefix branch. For example we might be checking the 2714 + // pattern `"wib" <> rest1` knowing that `"wibble" <> rest0` already succeeded. 2715 + // 2716 + // This rests on an invariant established by `check_overlaps`. A shorter prefix is 2717 + // only ever paired with an already-succeeded longer prefix when that longer prefix 2718 + // starts with it. Two disjoint prefixes (say `"b"` reached after `"aaa" <> rest0` 2719 + // succeeded) are never routed here together. So when `prefix1.strip_prefix(prefix0)` 2720 + // returns `None` below it is precisely because `prefix0` starts with `prefix1`, and 2721 + // the pattern's prefix is therefore already guaranteed to match. If it binds nothing 2722 + // we're done. Otherwise we rebind its `rest1` to the value with the (shorter) prefix 2723 + // stripped off. 2700 2724 ( 2701 2725 Pattern::StringPrefix { 2702 2726 prefix: prefix1, 2703 - prefix_name: _, 2727 + prefix_name, 2704 2728 rest: rest1, 2705 2729 }, 2706 2730 RuntimeCheck::StringPrefix { ··· 2708 2732 rest: rest0, 2709 2733 }, 2710 2734 ) => { 2711 - let remaining = prefix1.strip_prefix(prefix0.as_str()).unwrap_or(prefix1); 2712 - // If the prefixes are exactly the same then the only remaining check 2713 - // is for the two remaining bits to be the same. 2714 - if remaining.is_empty() { 2715 - vec![rest0.is(*rest1)] 2735 + if let Some(remaining) = prefix1.strip_prefix(prefix0.as_str()) { 2736 + // The pattern's prefix extends (or equals) the one we've already checked. 2737 + // If the prefixes are exactly the same then the only remaining check is 2738 + // for the two remaining bits to be the same. 2739 + if remaining.is_empty() { 2740 + vec![rest0.is(*rest1)] 2741 + } else { 2742 + vec![rest0.is(self.string_prefix_pattern(remaining, *rest1))] 2743 + } 2744 + } else if prefix_name.is_none() && matches!(self.pattern(*rest1), Pattern::Discard) 2745 + { 2746 + // The prefix we've already checked is longer than the pattern's prefix, so 2747 + // the pattern is guaranteed to match. As it binds nothing there's nothing 2748 + // left to do. 2749 + vec![] 2716 2750 } else { 2717 - vec![rest0.is(self.string_prefix_pattern(remaining, *rest1))] 2751 + // The pattern is guaranteed to match but it does bind something, so we 2752 + // re-issue it on the original value to bind `rest1` to the correctly sliced 2753 + // string (preserving any prefix name like `"a" as first <> rest1`). 2754 + let pattern = self.patterns.alloc(Pattern::StringPrefix { 2755 + prefix: prefix1.clone(), 2756 + prefix_name: prefix_name.clone(), 2757 + rest: *rest1, 2758 + }); 2759 + vec![subject.is(pattern)] 2718 2760 } 2719 2761 } 2720 2762 ··· 2925 2967 .to_runtime_check_kind() 2926 2968 .expect("no unconditional patterns left"); 2927 2969 2928 - let indices_of_overlapping_checks = self.indices_of_overlapping_checks(&kind); 2929 - if indices_of_overlapping_checks.is_empty() { 2930 - // This is a new choice we haven't yet discovered as it is not overlapping 2931 - // with any of the existing ones. So we add it as a possible new path 2932 - // we might have to go down to in the decision tree. 2970 + let CheckOverlaps { 2971 + overlapping, 2972 + needs_new_choice, 2973 + } = self.check_overlaps(&kind); 2974 + 2975 + // The branch is relevant to every existing choice its check overlaps 2976 + // with, so we add it (together with any newly discovered checks) to all 2977 + // of those paths. For a string prefix this also includes any longer 2978 + // prefixes that start with it. When one of those matched but a guard 2979 + // then failed, control falls through to this shorter prefix. 2980 + for index in overlapping.iter() { 2981 + let (overlapping_check, branches) = self 2982 + .choices 2983 + .get_mut(*index) 2984 + .expect("check to already be a choice"); 2985 + 2986 + let mut branch = branch.clone(); 2987 + for new_check in compiler.new_checks(&pattern_check.var, &pattern, overlapping_check) { 2988 + branch.add_check(new_check); 2989 + } 2990 + branches.push_back(branch); 2991 + } 2992 + 2993 + // Unless the check is fully subsumed by an existing choice, we also add 2994 + // it as a new path we might have to go down to in the decision tree. 2995 + // A shorter prefix still needs its own choice even when it overlaps 2996 + // earlier longer prefixes, so it can match values outside those. 2997 + if needs_new_choice { 2933 2998 self.save_index_of_new_choice(kind.clone()); 2934 2999 2935 3000 let check = compiler.fresh_runtime_check(kind, branch_mode); 2936 - for new_check in compiler.new_checks(&pattern, &check) { 3001 + for new_check in compiler.new_checks(&pattern_check.var, &pattern, &check) { 2937 3002 branch.add_check(new_check); 2938 3003 } 2939 3004 let mut branches = self.fallback.clone(); 2940 3005 branches.push_back(branch); 2941 3006 2942 3007 self.choices.push((check, branches)); 2943 - } else { 2944 - // Otherwise, we know that the check for this branch overlaps with 2945 - // (possibly more than one) existing checks and so is relevant only 2946 - // as part of those existing paths. 2947 - // We'll add the branch with its newly discovered checks only to those 2948 - // paths. 2949 - for index in indices_of_overlapping_checks.iter() { 2950 - let (overlapping_check, branches) = self 2951 - .choices 2952 - .get_mut(*index) 2953 - .expect("check to already be a choice"); 2954 - 2955 - let mut branch = branch.clone(); 2956 - for new_check in compiler.new_checks(&pattern, overlapping_check) { 2957 - branch.add_check(new_check); 2958 - } 2959 - branches.push_back(branch); 2960 - } 2961 3008 } 2962 3009 2963 3010 // Then we have to update all variant checks with any new name/module ··· 2970 3017 .. 2971 3018 } = pattern 2972 3019 { 2973 - for index in indices_of_overlapping_checks { 3020 + for index in overlapping { 2974 3021 let (check, _) = self 2975 3022 .choices 2976 3023 .get_mut(index) ··· 3083 3130 }; 3084 3131 } 3085 3132 3086 - fn indices_of_overlapping_checks(&self, kind: &RuntimeCheckKind) -> Vec<usize> { 3133 + fn choice_has_guard_using_pattern_bindings(&self, index: usize) -> bool { 3134 + self.choices.get(index).is_some_and(|(_, branches)| { 3135 + branches.iter().any(|branch| { 3136 + branch 3137 + .guard 3138 + .is_some_and(|guard| guard.uses_pattern_bindings) 3139 + }) 3140 + }) 3141 + } 3142 + 3143 + fn check_overlaps(&self, kind: &RuntimeCheckKind) -> CheckOverlaps { 3087 3144 match kind { 3088 3145 // All these checks will only overlap with a check that is exactly the 3089 3146 // same, so we just look up their index in the `indices` map using the 3090 - // kind as the lookup. 3147 + // kind as the lookup. An identical existing choice fully subsumes this 3148 + // one, so we only need a new choice when there isn't one already. 3091 3149 RuntimeCheckKind::Int { .. } 3092 3150 | RuntimeCheckKind::Float { .. } 3093 3151 | RuntimeCheckKind::Tuple { .. } 3094 3152 | RuntimeCheckKind::Variant { .. } 3095 3153 | RuntimeCheckKind::EmptyList 3096 3154 | RuntimeCheckKind::NonEmptyList => { 3097 - self.indices.get(kind).cloned().into_iter().collect_vec() 3155 + let overlapping = self.indices.get(kind).cloned().into_iter().collect_vec(); 3156 + let needs_new_choice = overlapping.is_empty(); 3157 + 3158 + CheckOverlaps { 3159 + overlapping, 3160 + needs_new_choice, 3161 + } 3098 3162 } 3099 3163 3100 3164 // String patterns are a bit more tricky as they might end up overlapping ··· 3117 3181 // That is, we look for a prefix of the pattern we're checking in the prefix 3118 3182 // trie. 3119 3183 RuntimeCheckKind::StringPrefix { prefix: value } => { 3120 - ancestors_values(&self.prefix_indices, value).collect_vec() 3184 + // A prefix pattern overlaps with any of its own prefixes, and also with a 3185 + // longer prefix it is itself a prefix of. Once that longer prefix matched, 3186 + // this shorter one is guaranteed to match too. 3187 + match self.prefix_indices.get_ancestor(value.as_str()) { 3188 + // One of `value`'s own (shorter) prefixes is already a choice. Any 3189 + // value that could match `value` is routed into that choice first, so 3190 + // `value` doesn't need its own. It just joins that choice (and any 3191 + // longer prefixes nested under it whose guard could fall through to it). 3192 + Some(_) => { 3193 + let overlapping = overlap_candidates(&self.prefix_indices, value) 3194 + .filter(|&(prefix, index)| { 3195 + if value.starts_with(prefix) { 3196 + // An ancestor (or the prefix itself). `value` extends it, 3197 + // so it always belongs in that choice. 3198 + true 3199 + } else { 3200 + // A longer prefix which is only relevant as a fall-through 3201 + // target, which only happens when it can match its prefix 3202 + // but fail a guard. 3203 + prefix.starts_with(value.as_str()) 3204 + && self.choice_has_guard_using_pattern_bindings(index) 3205 + } 3206 + }) 3207 + .map(|(_, index)| index) 3208 + .collect_vec(); 3209 + 3210 + CheckOverlaps { 3211 + overlapping, 3212 + needs_new_choice: false, 3213 + } 3214 + } 3215 + // No shorter prefix of `value` is a choice yet, so `value` needs its 3216 + // own choice to match values outside any longer prefixes. It is added to 3217 + // an existing longer prefix only when that prefix can match but fail a 3218 + // guard, so control has to fall through to this shorter prefix rather 3219 + // than escaping to the next choice. 3220 + None => { 3221 + let overlapping = descendant_candidates(&self.prefix_indices, value) 3222 + .filter(|&(prefix, index)| { 3223 + prefix.starts_with(value.as_str()) 3224 + && self.choice_has_guard_using_pattern_bindings(index) 3225 + }) 3226 + .map(|(_, index)| index) 3227 + .collect_vec(); 3228 + 3229 + CheckOverlaps { 3230 + overlapping, 3231 + needs_new_choice: true, 3232 + } 3233 + } 3234 + } 3121 3235 } 3122 3236 3123 3237 // Strings are almost exactly the same, except they could also have an exact ··· 3125 3239 // another string pattern (if they're matching on the same value), or with 3126 3240 // one or more string prefix patterns with a matching prefix. 3127 3241 RuntimeCheckKind::String { value } => { 3242 + // Unlike a prefix pattern, an exact literal can only overlap with one of 3243 + // its own prefixes. A longer prefix can never match an exact literal (the 3244 + // literal is too short), so we leave those out. 3128 3245 let first_index = self.indices.get(kind).cloned(); 3129 - first_index 3246 + let overlapping = first_index 3130 3247 .into_iter() 3131 - .chain(ancestors_values(&self.prefix_indices, value)) 3132 - .collect_vec() 3248 + .chain( 3249 + overlap_candidates(&self.prefix_indices, value) 3250 + .filter(|&(prefix, _)| value.starts_with(prefix)) 3251 + .map(|(_, index)| index), 3252 + ) 3253 + .collect_vec(); 3254 + let needs_new_choice = overlapping.is_empty(); 3255 + 3256 + CheckOverlaps { 3257 + overlapping, 3258 + needs_new_choice, 3259 + } 3133 3260 } 3134 3261 } 3135 3262 } 3136 3263 } 3137 3264 3138 - fn ancestors_values(trie: &Trie<String, usize>, key: &str) -> impl Iterator<Item = usize> { 3265 + /// Describes how a branch's runtime check relates to the choices discovered so 3266 + /// far when splitting branches. 3267 + struct CheckOverlaps { 3268 + /// The indices of the existing choices this branch is relevant to and so 3269 + /// must be added to 3270 + overlapping: Vec<usize>, 3271 + /// Whether the check also needs to become a new choice of its own. A new choice is a 3272 + /// fresh branch of the decision tree, carrying its own runtime check. 3273 + /// 3274 + /// For example, suppose `"wib" <> _` is reached after `"wibble" <> _` is already a 3275 + /// choice. A string can start with `"wib"` without starting with `"wibble"` 3276 + /// (for example `"wibz"`), so `"wib"` is not subsumed and needs its own choice to 3277 + /// match those values. With the order reversed, `"wibble"` would route through 3278 + /// the existing `"wib"` choice and need no choice of its own. 3279 + needs_new_choice: bool, 3280 + } 3281 + 3282 + fn overlap_candidates<'a>( 3283 + trie: &'a Trie<String, usize>, 3284 + key: &str, 3285 + ) -> impl Iterator<Item = (&'a str, usize)> { 3139 3286 trie.get_ancestor(key) 3140 3287 .into_iter() 3141 - .flat_map(|ancestor| ancestor.values().copied()) 3288 + .flat_map(|ancestor| ancestor.iter()) 3289 + .map(|(prefix, index)| (prefix.as_str(), *index)) 3290 + } 3291 + 3292 + fn descendant_candidates<'a>( 3293 + trie: &'a Trie<String, usize>, 3294 + key: &str, 3295 + ) -> impl Iterator<Item = (&'a str, usize)> { 3296 + trie.get_raw_descendant(key) 3297 + .into_iter() 3298 + .flat_map(|descendant| descendant.iter()) 3299 + .map(|(prefix, index)| (prefix.as_str(), *index)) 3142 3300 } 3143 3301 3144 3302 #[derive(Debug)] ··· 3318 3476 checks.push(var.is(pattern)) 3319 3477 } 3320 3478 3321 - let has_guard = branch.guard.is_some(); 3322 - let branch = Branch::new(self.number_of_clauses, alternative_index, checks, has_guard); 3479 + let guard = branch.guard.as_ref().map(|guard| { 3480 + let referenced = guard.referenced_variables(); 3481 + let uses_pattern_bindings = patterns.iter().any(|pattern| { 3482 + pattern 3483 + .bound_variables() 3484 + .iter() 3485 + .any(|bound| referenced.contains(&bound.name())) 3486 + }); 3487 + 3488 + BranchGuard { 3489 + clause_index: self.number_of_clauses, 3490 + uses_pattern_bindings, 3491 + } 3492 + }); 3493 + 3494 + let branch = Branch::new(self.number_of_clauses, alternative_index, checks, guard); 3323 3495 self.branches.push(branch); 3324 3496 } 3325 3497 ··· 3338 3510 .subject_variables 3339 3511 .first() 3340 3512 .expect("wrong number of subject variables for pattern"); 3341 - let branch = Branch::new(self.number_of_clauses, 0, vec![var.is(pattern)], false); 3513 + let branch = Branch::new(self.number_of_clauses, 0, vec![var.is(pattern)], None); 3342 3514 self.number_of_clauses += 1; 3343 3515 self.branches.push(branch); 3344 3516 }
+35
compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__strings__exact_string_after_longer_prefix.snap
··· 1 + --- 2 + source: compiler-core/src/javascript/tests/strings.rs 3 + expression: "\npub fn classify(input: String, a: Bool, b: Bool) -> String {\n case input {\n \"aa\" <> _ if a -> \"aa-prefix\"\n \"a\" <> _ if b -> \"a-prefix\"\n \"a\" -> \"exact-a\"\n _ -> \"other\"\n }\n}\n" 4 + --- 5 + ----- SOURCE CODE 6 + 7 + pub fn classify(input: String, a: Bool, b: Bool) -> String { 8 + case input { 9 + "aa" <> _ if a -> "aa-prefix" 10 + "a" <> _ if b -> "a-prefix" 11 + "a" -> "exact-a" 12 + _ -> "other" 13 + } 14 + } 15 + 16 + 17 + ----- COMPILED JAVASCRIPT 18 + export function classify(input, a, b) { 19 + if (input.startsWith("aa") && a) { 20 + return "aa-prefix"; 21 + } else if (input.charCodeAt(0) === 97) { 22 + if (b) { 23 + return "a-prefix"; 24 + } else { 25 + let $ = input.slice(1); 26 + if ($ === "") { 27 + return "exact-a"; 28 + } else { 29 + return "other"; 30 + } 31 + } 32 + } else { 33 + return "other"; 34 + } 35 + }
+35
compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__strings__non_overlapping_sibling_string_prefix.snap
··· 1 + --- 2 + source: compiler-core/src/javascript/tests/strings.rs 3 + expression: "\npub fn classify(input: String, a: Bool, b: Bool) -> String {\n case input {\n \"aa\" <> _ if a -> \"aa\"\n \"a\" <> _ if b -> \"a-guard\"\n \"ac\" <> _ -> \"ac\"\n _ -> \"other\"\n }\n}\n" 4 + --- 5 + ----- SOURCE CODE 6 + 7 + pub fn classify(input: String, a: Bool, b: Bool) -> String { 8 + case input { 9 + "aa" <> _ if a -> "aa" 10 + "a" <> _ if b -> "a-guard" 11 + "ac" <> _ -> "ac" 12 + _ -> "other" 13 + } 14 + } 15 + 16 + 17 + ----- COMPILED JAVASCRIPT 18 + export function classify(input, a, b) { 19 + if (input.startsWith("aa") && a) { 20 + return "aa"; 21 + } else if (input.charCodeAt(0) === 97) { 22 + if (b) { 23 + return "a-guard"; 24 + } else { 25 + let $ = input.slice(1); 26 + if ($.charCodeAt(0) === 99) { 27 + return "ac"; 28 + } else { 29 + return "other"; 30 + } 31 + } 32 + } else { 33 + return "other"; 34 + } 35 + }
+30
compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__strings__overlapping_string_prefixes_with_guard.snap
··· 1 + --- 2 + source: compiler-core/src/javascript/tests/strings.rs 3 + expression: "\npub fn classify(input: String, flag: Bool) -> String {\n case input {\n \"aa\" <> _rest if flag -> \"wibble\"\n \"a\" <> _rest if flag -> \"wobble\"\n \"a\" <> _rest -> \"woo\"\n _ -> \"other\"\n }\n}\n" 4 + --- 5 + ----- SOURCE CODE 6 + 7 + pub fn classify(input: String, flag: Bool) -> String { 8 + case input { 9 + "aa" <> _rest if flag -> "wibble" 10 + "a" <> _rest if flag -> "wobble" 11 + "a" <> _rest -> "woo" 12 + _ -> "other" 13 + } 14 + } 15 + 16 + 17 + ----- COMPILED JAVASCRIPT 18 + export function classify(input, flag) { 19 + if (input.startsWith("aa") && flag) { 20 + return "wibble"; 21 + } else if (input.charCodeAt(0) === 97) { 22 + if (flag) { 23 + return "wobble"; 24 + } else { 25 + return "woo"; 26 + } 27 + } else { 28 + return "other"; 29 + } 30 + }
+27
compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__strings__string_prefix_binding_rest_with_guard.snap
··· 1 + --- 2 + source: compiler-core/src/javascript/tests/strings.rs 3 + expression: "\npub fn classify(input: String, flag: Bool) -> String {\n case input {\n \"aa\" <> rest if flag -> rest\n \"a\" <> rest -> rest\n _ -> \"other\"\n }\n}\n" 4 + --- 5 + ----- SOURCE CODE 6 + 7 + pub fn classify(input: String, flag: Bool) -> String { 8 + case input { 9 + "aa" <> rest if flag -> rest 10 + "a" <> rest -> rest 11 + _ -> "other" 12 + } 13 + } 14 + 15 + 16 + ----- COMPILED JAVASCRIPT 17 + export function classify(input, flag) { 18 + if (input.startsWith("aa") && flag) { 19 + let rest = input.slice(2); 20 + return rest; 21 + } else if (input.charCodeAt(0) === 97) { 22 + let rest = input.slice(1); 23 + return rest; 24 + } else { 25 + return "other"; 26 + } 27 + }
+34
compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__strings__string_prefix_guard_on_bound_variable_falls_through.snap
··· 1 + --- 2 + source: compiler-core/src/javascript/tests/strings.rs 3 + expression: "\npub fn classify(input: String) -> String {\n case input {\n \"aa\" <> rest if rest == \"x\" -> \"aa-x\"\n \"a\" <> rest -> rest\n _ -> \"other\"\n }\n}\n" 4 + --- 5 + ----- SOURCE CODE 6 + 7 + pub fn classify(input: String) -> String { 8 + case input { 9 + "aa" <> rest if rest == "x" -> "aa-x" 10 + "a" <> rest -> rest 11 + _ -> "other" 12 + } 13 + } 14 + 15 + 16 + ----- COMPILED JAVASCRIPT 17 + export function classify(input) { 18 + if (input.startsWith("aa")) { 19 + let rest = input.slice(2); 20 + if (rest === "x") { 21 + return "aa-x"; 22 + } else if (input.charCodeAt(0) === 97) { 23 + let rest = input.slice(1); 24 + return rest; 25 + } else { 26 + return "other"; 27 + } 28 + } else if (input.charCodeAt(0) === 97) { 29 + let rest = input.slice(1); 30 + return rest; 31 + } else { 32 + return "other"; 33 + } 34 + }
+27
compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__strings__string_prefix_name_binding_after_longer_prefix.snap
··· 1 + --- 2 + source: compiler-core/src/javascript/tests/strings.rs 3 + expression: "\npub fn classify(input: String, flag: Bool) -> String {\n case input {\n \"aa\" <> _ if flag -> \"aa\"\n \"a\" as first <> rest -> rest <> first\n _ -> \"other\"\n }\n}\n" 4 + --- 5 + ----- SOURCE CODE 6 + 7 + pub fn classify(input: String, flag: Bool) -> String { 8 + case input { 9 + "aa" <> _ if flag -> "aa" 10 + "a" as first <> rest -> rest <> first 11 + _ -> "other" 12 + } 13 + } 14 + 15 + 16 + ----- COMPILED JAVASCRIPT 17 + export function classify(input, flag) { 18 + if (input.startsWith("aa") && flag) { 19 + return "aa"; 20 + } else if (input.charCodeAt(0) === 97) { 21 + let first = "a"; 22 + let rest = input.slice(1); 23 + return rest + first; 24 + } else { 25 + return "other"; 26 + } 27 + }
+44
compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__strings__string_prefix_nested_guard_falls_through_to_shorter.snap
··· 1 + --- 2 + source: compiler-core/src/javascript/tests/strings.rs 3 + expression: "\npub fn classify(input: String, flag: Bool) -> String {\n case input {\n \"w\" <> _ if flag -> \"w\"\n \"wibble\" <> rest if rest == \"x\" -> rest\n \"wib\" <> rest -> rest\n _ -> \"other\"\n }\n}\n" 4 + --- 5 + ----- SOURCE CODE 6 + 7 + pub fn classify(input: String, flag: Bool) -> String { 8 + case input { 9 + "w" <> _ if flag -> "w" 10 + "wibble" <> rest if rest == "x" -> rest 11 + "wib" <> rest -> rest 12 + _ -> "other" 13 + } 14 + } 15 + 16 + 17 + ----- COMPILED JAVASCRIPT 18 + export function classify(input, flag) { 19 + if (input.charCodeAt(0) === 119) { 20 + if (flag) { 21 + return "w"; 22 + } else { 23 + let $ = input.slice(1); 24 + if ($.startsWith("ibble")) { 25 + let rest = $.slice(5); 26 + if (rest === "x") { 27 + return rest; 28 + } else if ($.startsWith("ib")) { 29 + let rest = $.slice(2); 30 + return rest; 31 + } else { 32 + return "other"; 33 + } 34 + } else if ($.startsWith("ib")) { 35 + let rest = $.slice(2); 36 + return rest; 37 + } else { 38 + return "other"; 39 + } 40 + } 41 + } else { 42 + return "other"; 43 + } 44 + }
+116
compiler-core/src/javascript/tests/strings.rs
··· 256 256 "# 257 257 ); 258 258 } 259 + 260 + // https://github.com/gleam-lang/gleam/issues/5856 261 + #[test] 262 + fn overlapping_string_prefixes_with_guard() { 263 + assert_js!( 264 + r#" 265 + pub fn classify(input: String, flag: Bool) -> String { 266 + case input { 267 + "aa" <> _rest if flag -> "wibble" 268 + "a" <> _rest if flag -> "wobble" 269 + "a" <> _rest -> "woo" 270 + _ -> "other" 271 + } 272 + } 273 + "#, 274 + ); 275 + } 276 + 277 + // https://github.com/gleam-lang/gleam/issues/5856 278 + #[test] 279 + fn string_prefix_binding_rest_with_guard() { 280 + assert_js!( 281 + r#" 282 + pub fn classify(input: String, flag: Bool) -> String { 283 + case input { 284 + "aa" <> rest if flag -> rest 285 + "a" <> rest -> rest 286 + _ -> "other" 287 + } 288 + } 289 + "#, 290 + ); 291 + } 292 + 293 + // https://github.com/gleam-lang/gleam/issues/5856 294 + #[test] 295 + fn non_overlapping_sibling_string_prefix() { 296 + assert_js!( 297 + r#" 298 + pub fn classify(input: String, a: Bool, b: Bool) -> String { 299 + case input { 300 + "aa" <> _ if a -> "aa" 301 + "a" <> _ if b -> "a-guard" 302 + "ac" <> _ -> "ac" 303 + _ -> "other" 304 + } 305 + } 306 + "#, 307 + ); 308 + } 309 + 310 + // https://github.com/gleam-lang/gleam/issues/5856 311 + #[test] 312 + fn exact_string_after_longer_prefix() { 313 + assert_js!( 314 + r#" 315 + pub fn classify(input: String, a: Bool, b: Bool) -> String { 316 + case input { 317 + "aa" <> _ if a -> "aa-prefix" 318 + "a" <> _ if b -> "a-prefix" 319 + "a" -> "exact-a" 320 + _ -> "other" 321 + } 322 + } 323 + "#, 324 + ); 325 + } 326 + 327 + // https://github.com/gleam-lang/gleam/issues/5856 328 + #[test] 329 + fn string_prefix_name_binding_after_longer_prefix() { 330 + assert_js!( 331 + r#" 332 + pub fn classify(input: String, flag: Bool) -> String { 333 + case input { 334 + "aa" <> _ if flag -> "aa" 335 + "a" as first <> rest -> rest <> first 336 + _ -> "other" 337 + } 338 + } 339 + "#, 340 + ); 341 + } 342 + 343 + // https://github.com/gleam-lang/gleam/issues/5856 344 + #[test] 345 + fn string_prefix_guard_on_bound_variable_falls_through() { 346 + assert_js!( 347 + r#" 348 + pub fn classify(input: String) -> String { 349 + case input { 350 + "aa" <> rest if rest == "x" -> "aa-x" 351 + "a" <> rest -> rest 352 + _ -> "other" 353 + } 354 + } 355 + "#, 356 + ); 357 + } 358 + 359 + // https://github.com/gleam-lang/gleam/issues/5856 360 + #[test] 361 + fn string_prefix_nested_guard_falls_through_to_shorter() { 362 + assert_js!( 363 + r#" 364 + pub fn classify(input: String, flag: Bool) -> String { 365 + case input { 366 + "w" <> _ if flag -> "w" 367 + "wibble" <> rest if rest == "x" -> rest 368 + "wib" <> rest -> rest 369 + _ -> "other" 370 + } 371 + } 372 + "#, 373 + ); 374 + }