Skip to content

Commit bc3b3e8

Browse files
committed
Add Unicode-scalar string padding
1 parent 248b961 commit bc3b3e8

17 files changed

Lines changed: 417 additions & 1 deletion

‎crates/splitscript-syntax/src/migration.rs‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ pub const CSHARP_STRING_REPLACE_DIAGNOSTIC: MigrationDiagnosticId =
189189
MigrationDiagnosticId::new("csharp.string.replace-call");
190190
pub const CSHARP_STRING_TRIM_DIAGNOSTIC: MigrationDiagnosticId =
191191
MigrationDiagnosticId::new("csharp.string.trim-call");
192+
pub const CSHARP_STRING_PADDING_DIAGNOSTIC: MigrationDiagnosticId =
193+
MigrationDiagnosticId::new("csharp.string.padding-call");
192194
pub const CSHARP_STRING_IS_NULL_OR_EMPTY_DIAGNOSTIC: MigrationDiagnosticId =
193195
MigrationDiagnosticId::new("csharp.string.is-null-or-empty-call");
194196
pub const CSHARP_STRING_JOIN_DIAGNOSTIC: MigrationDiagnosticId =
@@ -424,6 +426,19 @@ pub const DIAGNOSTICS: &[MigrationDiagnostic] = &[
424426
"there is no automatic rewrite because the compiler cannot prove that the input's surrounding whitespace is ASCII",
425427
],
426428
},
429+
MigrationDiagnostic {
430+
id: CSHARP_STRING_PADDING_DIAGNOSTIC,
431+
concept: MigrationConceptId::new("string.padding"),
432+
message: "C# string padding needs an explicit width-model review",
433+
primary_label: "use `padStart(width, fill)` or `padEnd(width, fill)`",
434+
notes: &[
435+
"rewrite `text.PadLeft(width, fill)` as `text.padStart(width, fill)` and `text.PadRight(width, fill)` as `text.padEnd(width, fill)`",
436+
"SplitScript always requires one `char` fill; pass `' '` explicitly for C# overloads that omit the padding character",
437+
"SplitScript width counts Unicode scalar values, while C# width counts UTF-16 code units; copied widths are equivalent for proven ASCII text",
438+
"padding returns the original immutable string when it is already wide enough and otherwise performs one exact-sized allocation",
439+
"there is no automatic rewrite because direction, omitted fill, and the surrounding width assumptions need to remain visible",
440+
],
441+
},
427442
MigrationDiagnostic {
428443
id: CSHARP_STRING_IS_NULL_OR_EMPTY_DIAGNOSTIC,
429444
concept: MigrationConceptId::new("string.null-or-empty"),
@@ -494,6 +509,7 @@ pub fn legacy_string_method_diagnostic(name: &str) -> Option<MigrationDiagnostic
494509
"LastIndexOf" => Some(CSHARP_STRING_LAST_INDEX_OF_DIAGNOSTIC),
495510
"Replace" => Some(CSHARP_STRING_REPLACE_DIAGNOSTIC),
496511
"Trim" => Some(CSHARP_STRING_TRIM_DIAGNOSTIC),
512+
"PadLeft" | "PadRight" => Some(CSHARP_STRING_PADDING_DIAGNOSTIC),
497513
_ => None,
498514
}
499515
}
@@ -933,6 +949,19 @@ pub const CONCEPTS: &[MigrationConcept] = &[
933949
cookbook_anchor: Some("c-string-operations"),
934950
spellings: &[],
935951
},
952+
MigrationConcept {
953+
id: MigrationConceptId::new("string.padding"),
954+
name: "String padding",
955+
sources: CSHARP,
956+
support: MigrationSupport::TypedPattern,
957+
summary: "Use `padStart(width, fill)` or `padEnd(width, fill)` with an explicit character; review C# UTF-16 widths against SplitScript's Unicode-scalar widths.",
958+
targets: &[
959+
MigrationTarget::StandardLibraryItem("String.padStart"),
960+
MigrationTarget::StandardLibraryItem("String.padEnd"),
961+
],
962+
cookbook_anchor: Some("c-string-operations"),
963+
spellings: &[],
964+
},
936965
MigrationConcept {
937966
id: MigrationConceptId::new("string.null-or-empty"),
938967
name: "Nullable string emptiness",

‎docs/ASL_PORTING.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,20 @@ immutable string when nothing changes. The compiler does not rewrite `Trim()`
128128
automatically because Unicode whitespace, character-array overloads,
129129
`TrimStart`, and `TrimEnd` have different semantics.
130130

131+
C# `PadLeft` and `PadRight` map to directionally named immutable operations:
132+
133+
```splitscript
134+
let chapter = chapterNumber as String
135+
let chapterKey = chapter.padStart(2, '0')
136+
let column = chapterKey.padEnd(8, ' ')
137+
```
138+
139+
SplitScript always requires the fill `char`; pass `' '` explicitly for C#
140+
overloads that omit it. Width counts Unicode scalar values rather than .NET
141+
UTF-16 code units or terminal display columns, so copied widths are directly
142+
equivalent only for text proven to be ASCII. An already-wide string is reused;
143+
otherwise the result is allocated once at its exact UTF-8 byte length.
144+
131145
C# combines nullability and emptiness in `String.IsNullOrEmpty(value)`.
132146
SplitScript keeps those concerns in the type. A required `String` cannot be
133147
null, so use its source-defined method directly:

‎docs/LANGUAGE.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1433,6 +1433,7 @@ is involved:
14331433
| `toAsciiLowerCase()` | Lowercase ASCII letters; preserve every other UTF-8 byte |
14341434
| `toAsciiUpperCase()` | Uppercase ASCII letters; preserve every other UTF-8 byte |
14351435
| `trimAsciiWhitespace()` | Remove ASCII boundary whitespace; preserve interior and non-ASCII bytes |
1436+
| `padStart(width, fill)` / `padEnd(width, fill)` | Pad to a minimum Unicode-scalar length with one `char` |
14361437
| `split(delimiter)` | Fallible exact split preserving leading, repeated, and trailing empty segments |
14371438
| `parse<T>()` | Strict fallible ASCII decimal parsing into an inferred numeric type |
14381439
| `byteAt(byteIndex)` | Fallible raw UTF-8 byte lookup; continuation bytes remain observable |

‎docs/MIGRATION_CAPABILITIES.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ This index maps common source-language concepts to canonical SplitScript APIs an
1818
| `string.last-index-of` — Last substring position | C# | Use a typed pattern | Use `lastIndexOf` for an optional final UTF-8 byte offset; review C# UTF-16 index arithmetic and replace the `-1` sentinel with Option handling. Canonical targets: `String.lastIndexOf`. [Recipe](ASL_PORTING.md#c-string-operations). |
1919
| `string.replacement` — Exact string replacement | C# | Use a typed pattern | Use fallible `replaceAll` for immutable exact replacement; explicitly handle failure and translate a null C# replacement to an empty string only when deletion was intended. Canonical targets: `String.replaceAll`. [Recipe](ASL_PORTING.md#c-string-operations). |
2020
| `string.ascii-trim` — ASCII whitespace trimming | C# | Use a typed pattern | Use `trimAsciiWhitespace` for text known to use ASCII boundary whitespace; review Unicode and character-set trimming explicitly. Canonical targets: `String.trimAsciiWhitespace`. [Recipe](ASL_PORTING.md#c-string-operations). |
21+
| `string.padding` — String padding | C# | Use a typed pattern | Use `padStart(width, fill)` or `padEnd(width, fill)` with an explicit character; review C# UTF-16 widths against SplitScript's Unicode-scalar widths. Canonical targets: `String.padStart`, `String.padEnd`. [Recipe](ASL_PORTING.md#c-string-operations). |
2122
| `string.null-or-empty` — Nullable string emptiness | C# | Use a typed pattern | Use `String.isEmpty` for required strings and match `String?` explicitly when absence should also count as empty. Canonical targets: `String.isEmpty`. [Recipe](ASL_PORTING.md#c-string-operations). |
2223
| `string.join` — String collection joining | C# | Use a typed pattern | Use `String.join(values, separator)` for a typed string array; convert C# object, variadic, enumerable, and range overloads explicitly. Canonical targets: `String.join`. [Recipe](ASL_PORTING.md#c-string-operations). |
2324
| `string.numeric-parse` — Numeric string parsing | C# | Supported directly | Replace static Parse/TryParse calls and output parameters with fallible `text.parse()` and ordinary Result handling. Canonical targets: `String.parse`. [Recipe](ASL_PORTING.md#c-string-operations). |

‎docs/ROADMAP_ARCHIVE.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# SplitScript roadmap
22

3+
## 2026-08-10: Unicode-scalar string padding
4+
5+
- Added immutable `String.padStart(width, fill)` and
6+
`String.padEnd(width, fill)` for corpus-backed zero-padding and diagnostic
7+
table alignment without adopting C# method aliases or optional parameters.
8+
- Defined width as a count of Unicode scalar values. The runtime counts UTF-8
9+
leading bytes, reuses an already-wide string, and otherwise encodes the fill
10+
`char` directly into one exact-sized output allocation.
11+
- Added focused `PadLeft`/`PadRight` migration guidance covering direction,
12+
explicit space filling, and the difference between .NET UTF-16 code units,
13+
Unicode scalar values, and terminal display columns.
14+
- Extended catalog, documentation, diagnostics, and deterministic Wasm runtime
15+
coverage across ASCII and multibyte receivers and fill characters.
16+
317
## 2026-08-09: allocation-free reverse string search
418

519
- Added `String.lastIndexOf(substring) -> u32?` for the recurring path,

‎src/codegen/expression.rs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2185,6 +2185,18 @@ fn compile_expr_unconverted(
21852185
.function(RuntimeHelperId::StringTrimAsciiWhitespace),
21862186
));
21872187
}
2188+
IntrinsicId::StringPadStart | IntrinsicId::StringPadEnd => {
2189+
compile_receiver(function, target, context);
2190+
compile_expr(function, args[0], context);
2191+
compile_expr(function, args[1], context);
2192+
function
2193+
.instruction(&Instruction::I32Const(
2194+
(builtin == IntrinsicId::StringPadEnd) as i32,
2195+
))
2196+
.instruction(&Instruction::Call(
2197+
context.runtime_helpers.function(RuntimeHelperId::StringPad),
2198+
));
2199+
}
21882200
IntrinsicId::StringReplaceAll => {
21892201
compile_receiver(function, target, context);
21902202
compile_expr(function, args[0], context);

‎src/codegen/runtime_helper_registry.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ pub(super) const DESCRIPTORS: &[RuntimeHelperDescriptor] = &[
105105
helper!(StringInspect, (StringValue, I32, I32) -> (I32, I32), deps [], imports [], build_string_inspect),
106106
helper!(StringSlice, (StringValue, I32, I32) -> (StringValue), deps [], imports [], build_string_slice),
107107
helper!(StringTrimAsciiWhitespace, (StringValue) -> (StringValue), deps [StringSlice], imports [], build_string_trim_ascii_whitespace),
108+
helper!(StringPad, (StringValue, I32, I32, I32) -> (StringValue), deps [], imports [], build_string_pad),
108109
helper!(ScanProcessRange, (I64, I64, I64, I32, I32, I32) -> (I64), deps [], imports [ProcessRead], build_scan_process_range),
109110
helper!(ReadRelative32, (I64, I64) -> (I64), deps [], imports [ProcessRead], build_read_relative32),
110111
helper!(StringFromMemory, (I32, I32) -> (StringValue), deps [], imports [], build_string_from_memory),

‎src/codegen/runtime_helpers.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,10 @@ pub(super) fn build_string_trim_ascii_whitespace(inputs: &RuntimeHelperInputs<'_
157157
)
158158
}
159159

160+
pub(super) fn build_string_pad(inputs: &RuntimeHelperInputs<'_>) -> Function {
161+
strings::compile_string_pad(inputs.gc)
162+
}
163+
160164
pub(super) fn build_scan_process_range(inputs: &RuntimeHelperInputs<'_>) -> Function {
161165
process::compile_scan_process_range(inputs.abi, inputs.memory.scratch().scan)
162166
}

0 commit comments

Comments
 (0)