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

Configure Feed

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

Avoid redundant startsWith on prefix fall-through

When a longer guarded string prefix falls through to a shorter one,
the backend re-checked the longer prefix with startsWith before
slicing.

+207 -12
+56 -6
compiler-core/src/exhaustiveness.rs
··· 336 336 None => return false, 337 337 }, 338 338 339 + // A guaranteed-match slice binds the prefix name (if any) and 340 + // the rest variable (if any) to the correctly sliced string. 341 + Pattern::StringPrefixSlice { 342 + prefix, 343 + prefix_name, 344 + rest_name, 345 + } => { 346 + let prefix = prefix.clone(); 347 + let prefix_name_val = std::mem::take(prefix_name); 348 + let rest_name_val = std::mem::take(rest_name); 349 + if let Some(name) = prefix_name_val { 350 + self.body.assign_literal_string(name, prefix.clone()); 351 + } 352 + if let Some(name) = rest_name_val { 353 + self.body 354 + .assign_string_slice(name, check.var.clone(), prefix); 355 + } 356 + return false; 357 + } 358 + 339 359 // All other patterns are not unconditional, so we just keep them. 340 360 Pattern::Int { .. } 341 361 | Pattern::Float { .. } ··· 396 416 bit_array: Variable, 397 417 read_action: ReadAction, 398 418 }, 419 + 420 + /// `let a = subject.slice(N)` 421 + /// 422 + StringSlice { 423 + subject: Variable, 424 + prefix: EcoString, 425 + }, 399 426 } 400 427 401 428 impl Body { ··· 415 442 fn assign_literal_string(&mut self, variable: EcoString, value: EcoString) { 416 443 self.bindings 417 444 .push((variable, BoundValue::LiteralString(value))); 445 + } 446 + 447 + fn assign_string_slice(&mut self, variable: EcoString, subject: Variable, prefix: EcoString) { 448 + self.bindings 449 + .push((variable, BoundValue::StringSlice { subject, prefix })); 418 450 } 419 451 420 452 fn assign_bit_array_slice( ··· 477 509 prefix_name: Option<EcoString>, 478 510 rest: Id<Pattern>, 479 511 }, 512 + StringPrefixSlice { 513 + prefix: EcoString, 514 + prefix_name: Option<EcoString>, 515 + rest_name: Option<EcoString>, 516 + }, 480 517 Assign { 481 518 name: EcoString, 482 519 pattern: Id<Pattern>, ··· 514 551 // These patterns are unconditional: they will always match and be moved 515 552 // out of a branch's checks. So there's no corresponding runtime check 516 553 // we can perform for them. 517 - Pattern::Discard | Pattern::Variable { .. } | Pattern::Assign { .. } => return None, 554 + Pattern::Discard 555 + | Pattern::Variable { .. } 556 + | Pattern::Assign { .. } 557 + | Pattern::StringPrefixSlice { .. } => return None, 518 558 Pattern::Int { int_value, .. } => RuntimeCheckKind::Int { 519 559 int_value: int_value.clone(), 520 560 }, ··· 582 622 | Self::Float { .. } 583 623 | Self::String { .. } 584 624 | Self::StringPrefix { .. } 625 + | Self::StringPrefixSlice { .. } 585 626 | Self::Assign { .. } 586 627 | Self::Variable { .. } 587 628 | Self::Tuple { .. } ··· 2748 2789 // left to do. 2749 2790 vec![] 2750 2791 } else { 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 { 2792 + // The pattern is guaranteed to match but it does bind something. Use 2793 + // StringPrefixSlice so the backend only emits the slice, not a redundant 2794 + // startsWith check. 2795 + let rest_pattern = self.pattern(*rest1); 2796 + let rest_name = if let Pattern::Variable { name } = rest_pattern { 2797 + Some(name.clone()) 2798 + } else if matches!(rest_pattern, Pattern::Discard) { 2799 + None 2800 + } else { 2801 + unreachable!("rest should be Variable or Discard, got {rest_pattern:?}") 2802 + }; 2803 + let pattern = self.patterns.alloc(Pattern::StringPrefixSlice { 2755 2804 prefix: prefix1.clone(), 2756 2805 prefix_name: prefix_name.clone(), 2757 - rest: *rest1, 2806 + rest_name, 2758 2807 }); 2808 + 2759 2809 vec![subject.is(pattern)] 2760 2810 } 2761 2811 }
+5
compiler-core/src/javascript/decision.rs
··· 1289 1289 } => self 1290 1290 .get_segment_value(arena, variable_name) 1291 1291 .unwrap_or_else(|| self.read_action_to_doc(arena, bit_array, read_action)), 1292 + BoundValue::StringSlice { subject, prefix } => { 1293 + let subject_value = self.get_value(subject); 1294 + let prefix_size = utf16_no_escape_len(prefix); 1295 + docvec![arena, subject_value, ".slice(", prefix_size, ")"] 1296 + } 1292 1297 }; 1293 1298 1294 1299 match self.variable_assignment {
+30
compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__strings__string_prefix_discard_falls_through_to_shorter.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\" -> rest\n \"a\" <> _ -> \"short\"\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" -> rest 10 + "a" <> _ -> "short" 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 rest; 22 + } else { 23 + return "short"; 24 + } 25 + } else if (input.charCodeAt(0) === 97) { 26 + return "short"; 27 + } else { 28 + return "other"; 29 + } 30 + }
+1 -3
compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__strings__string_prefix_guard_on_bound_variable_falls_through.snap
··· 19 19 let rest = input.slice(2); 20 20 if (rest === "x") { 21 21 return "aa-x"; 22 - } else if (input.charCodeAt(0) === 97) { 22 + } else { 23 23 let rest = input.slice(1); 24 24 return rest; 25 - } else { 26 - return "other"; 27 25 } 28 26 } else if (input.charCodeAt(0) === 97) { 29 27 let rest = input.slice(1);
+32
compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__strings__string_prefix_multibyte_falls_through_to_shorter.snap
··· 1 + --- 2 + source: compiler-core/src/javascript/tests/strings.rs 3 + expression: "\npub fn classify(input: String) -> String {\n case input {\n \"🫥a\" <> rest if rest == \"x\" -> rest\n \"🫥\" <> rest -> rest\n _ -> \"other\"\n }\n}\n" 4 + --- 5 + ----- SOURCE CODE 6 + 7 + pub fn classify(input: String) -> String { 8 + case input { 9 + "🫥a" <> rest if rest == "x" -> rest 10 + "🫥" <> rest -> rest 11 + _ -> "other" 12 + } 13 + } 14 + 15 + 16 + ----- COMPILED JAVASCRIPT 17 + export function classify(input) { 18 + if (input.startsWith("🫥a")) { 19 + let rest = input.slice(3); 20 + if (rest === "x") { 21 + return rest; 22 + } else { 23 + let rest = input.slice(2); 24 + return rest; 25 + } 26 + } else if (input.startsWith("🫥")) { 27 + let rest = input.slice(2); 28 + return rest; 29 + } else { 30 + return "other"; 31 + } 32 + }
+34
compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__strings__string_prefix_name_binding_falls_through_to_shorter.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\" -> rest\n \"a\" as first <> rest -> rest <> first\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" -> rest 10 + "a" as first <> rest -> rest <> first 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 rest; 22 + } else { 23 + let first = "a"; 24 + let rest = input.slice(1); 25 + return rest + first; 26 + } 27 + } else if (input.charCodeAt(0) === 97) { 28 + let first = "a"; 29 + let rest = input.slice(1); 30 + return rest + first; 31 + } else { 32 + return "other"; 33 + } 34 + }
+1 -3
compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__strings__string_prefix_nested_guard_falls_through_to_shorter.snap
··· 25 25 let rest = $.slice(5); 26 26 if (rest === "x") { 27 27 return rest; 28 - } else if ($.startsWith("ib")) { 28 + } else { 29 29 let rest = $.slice(2); 30 30 return rest; 31 - } else { 32 - return "other"; 33 31 } 34 32 } else if ($.startsWith("ib")) { 35 33 let rest = $.slice(2);
+48
compiler-core/src/javascript/tests/strings.rs
··· 372 372 "#, 373 373 ); 374 374 } 375 + 376 + // https://github.com/gleam-lang/gleam/issues/5856 377 + #[test] 378 + fn string_prefix_name_binding_falls_through_to_shorter() { 379 + assert_js!( 380 + r#" 381 + pub fn classify(input: String) -> String { 382 + case input { 383 + "aa" <> rest if rest == "x" -> rest 384 + "a" as first <> rest -> rest <> first 385 + _ -> "other" 386 + } 387 + } 388 + "#, 389 + ); 390 + } 391 + 392 + // https://github.com/gleam-lang/gleam/issues/5856 393 + #[test] 394 + fn string_prefix_discard_falls_through_to_shorter() { 395 + assert_js!( 396 + r#" 397 + pub fn classify(input: String) -> String { 398 + case input { 399 + "aa" <> rest if rest == "x" -> rest 400 + "a" <> _ -> "short" 401 + _ -> "other" 402 + } 403 + } 404 + "#, 405 + ); 406 + } 407 + 408 + // https://github.com/gleam-lang/gleam/issues/5856 409 + #[test] 410 + fn string_prefix_multibyte_falls_through_to_shorter() { 411 + assert_js!( 412 + r#" 413 + pub fn classify(input: String) -> String { 414 + case input { 415 + "🫥a" <> rest if rest == "x" -> rest 416 + "🫥" <> rest -> rest 417 + _ -> "other" 418 + } 419 + } 420 + "#, 421 + ); 422 + }