Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions exercises/practice/isogram/.approaches/bitfield/content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Bit field

```zig
const std = @import("std");

pub fn isIsogram(str: []const u8) bool {
var seen: u32 = 0;
for (str) |c| {
if (!std.ascii.isAlphabetic(c)) continue;
const bit = @as(u32, 1) << @intCast((c | 0x20) - 'a');
if (seen & bit != 0) return false;
seen |= bit;
}
return true;
}
```

Since there are only 26 letters, the set of seen letters fits in the bits of a single `u32`: bit `0` stands for `a`/`A` and bit `25` for `z`/`Z`.

Non-letters are skipped with [`std.ascii.isAlphabetic`][is-alphabetic].
For a letter, `c | 0x20` sets the bit that distinguishes lowercase from uppercase in ASCII (`'A'` is `0x41`, `'a'` is `0x61`), converting the letter to lowercase; subtracting `'a'` then yields the letter's index.
The `@intCast` narrows that index to the `u5` shift amount that shifting a `u32` requires.

If the letter's bit is already set in `seen`, a letter has repeated and the function returns `false` immediately, so the input is only scanned as far as the first repeated letter.

Compared with the [bool array approach][approach-bool-array], the state is a single register-sized integer, and set membership and insertion are single bitwise operations.

[is-alphabetic]: https://ziglang.org/documentation/master/std/#std.ascii.isAlphabetic
[approach-bool-array]: https://exercism.org/tracks/zig/exercises/isogram/approaches/bool-array
8 changes: 8 additions & 0 deletions exercises/practice/isogram/.approaches/bitfield/snippet.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
var seen: u32 = 0;
for (str) |c| {
if (!std.ascii.isAlphabetic(c)) continue;
const bit = @as(u32, 1) << @intCast((c | 0x20) - 'a');
if (seen & bit != 0) return false;
seen |= bit;
}
return true;
28 changes: 28 additions & 0 deletions exercises/practice/isogram/.approaches/bool-array/content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Bool array

```zig
pub fn isIsogram(s: []const u8) bool {
var letters: [26]bool = @splat(false);
for (s) |c| {
const i = switch (c) {
'A'...'Z' => c - 'A',
'a'...'z' => c - 'a',
else => continue,
};
if (letters[i]) return false;
letters[i] = true;
}
return true;
}
```

A `[26]bool` array records which letters have been seen so far, with element `0` standing for `a`/`A` and element `25` for `z`/`Z`.

The `switch` maps both uppercase and lowercase letters to that shared index, so case is ignored.
Its `else` prong uses `continue` as the prong body, skipping hyphens, spaces, and any other non-letter directly from within the switch — no separate `if` is needed.

For each letter, if its flag is already set, a letter has repeated and the function returns `false` immediately.
Otherwise the flag is set and scanning continues.
The early return means the input is only scanned as far as the first repeated letter.

The array lives on the stack and its size is known at compile time, so this approach performs no allocation.
8 changes: 8 additions & 0 deletions exercises/practice/isogram/.approaches/bool-array/snippet.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
var letters: [26]bool = @splat(false);
for (s) |c| {
const i = switch (c) {
'A'...'Z' => c - 'A',
'a'...'z' => c - 'a',
else => continue,
};
if (letters[i]) return false;
39 changes: 39 additions & 0 deletions exercises/practice/isogram/.approaches/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"introduction": {
"authors": [
"keiravillekode"
]
},
"approaches": [
{
"uuid": "41d585a0-fc4e-4108-8613-1d21a9477e79",
"slug": "bool-array",
"title": "Bool array",
"blurb": "Track which letters have been seen with an array of 26 booleans.",
"authors": [
"massivelivefun"
],
"contributors": [
"keiravillekode"
]
},
{
"uuid": "69518e6c-d384-434e-afcd-49afa6085488",
"slug": "bitfield",
"title": "Bit field",
"blurb": "Track which letters have been seen with the bits of a 32-bit integer.",
"authors": [
"keiravillekode"
]
},
{
"uuid": "ca88bd01-a271-4c42-97c7-d6896b88f889",
"slug": "simd",
"title": "SIMD",
"blurb": "Process blocks of input characters at once with vector operations.",
"authors": [
"keiravillekode"
]
}
]
}
77 changes: 77 additions & 0 deletions exercises/practice/isogram/.approaches/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Introduction

An isogram is a word with no repeated letters, so every approach comes down to detecting whether any letter occurs twice, ignoring case and ignoring characters that are not letters.
The approaches differ in how they remember which letters have already been seen.

## General guidance

Since there are only 26 letters, the state that needs to be tracked is small and of fixed size.
No allocation is needed: an array, an integer, or a vector on the stack is enough.
Case can be ignored by mapping both `'A'...'Z'` and `'a'...'z'` to an index in the range `0...25`.

## Approach: bool array

```zig
pub fn isIsogram(s: []const u8) bool {
var letters: [26]bool = @splat(false);
for (s) |c| {
const i = switch (c) {
'A'...'Z' => c - 'A',
'a'...'z' => c - 'a',
else => continue,
};
if (letters[i]) return false;
letters[i] = true;
}
return true;
}
```

Each letter maps to an element of a `[26]bool` array, which records whether the letter has been seen before.
For details, see the [bool array approach][approach-bool-array].

## Approach: bit field

```zig
const std = @import("std");

pub fn isIsogram(str: []const u8) bool {
var seen: u32 = 0;
for (str) |c| {
if (!std.ascii.isAlphabetic(c)) continue;
const bit = @as(u32, 1) << @intCast((c | 0x20) - 'a');
if (seen & bit != 0) return false;
seen |= bit;
}
return true;
}
```

Each letter maps to one bit of a `u32`, so the whole set of seen letters fits in a single integer.
For details, see the [bit field approach][approach-bitfield].

## Approach: SIMD

```zig
pub fn isIsogram(str: []const u8) bool {
var seen: u32 = 0;
var i: usize = 0;
while (i + block_len <= str.len) : (i += block_len) {
const mask = letterMask(str[i..][0..block_len].*) orelse return false;
if (seen & mask != 0) return false;
seen |= mask;
}
// Pad the remaining bytes with zeroes to fill one last block.
var padded = [_]u8{0} ** block_len;
@memcpy(padded[0 .. str.len - i], str[i..]);
const mask = letterMask(padded) orelse return false;
return seen & mask == 0;
}
```

The input is processed a block of characters at a time: each block is loaded into a `@Vector`, and vector operations lowercase all its characters, find the letters, and turn them into a bit mask in one pass.
For details, see the [SIMD approach][approach-simd].

[approach-bool-array]: https://exercism.org/tracks/zig/exercises/isogram/approaches/bool-array
[approach-bitfield]: https://exercism.org/tracks/zig/exercises/isogram/approaches/bitfield
[approach-simd]: https://exercism.org/tracks/zig/exercises/isogram/approaches/simd
67 changes: 67 additions & 0 deletions exercises/practice/isogram/.approaches/simd/content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# SIMD

```zig
const std = @import("std");

const block_len = std.simd.suggestVectorLength(u8) orelse 8;
const Block = @Vector(block_len, u8);

/// Returns a bit mask of the letters in `block`, or null if `block`
/// contains a repeated letter.
fn letterMask(block: Block) ?u32 {
const lowered = block | @as(Block, @splat(0x20));
// Every non-letter lane is clamped into an extra bucket, index 26.
const indices = @min(lowered -% @as(Block, @splat('a')), @as(Block, @splat(26)));
const bits = @as(@Vector(block_len, u32), @splat(1)) << @intCast(indices);
const mask = @reduce(.Or, bits) & ((1 << 26) - 1);
const letter_count = std.simd.countTrues(indices != @as(Block, @splat(26)));
if (@popCount(mask) != letter_count) return null;
return mask;
}

pub fn isIsogram(str: []const u8) bool {
var seen: u32 = 0;
var i: usize = 0;
while (i + block_len <= str.len) : (i += block_len) {
const mask = letterMask(str[i..][0..block_len].*) orelse return false;
if (seen & mask != 0) return false;
seen |= mask;
}
// Pad the remaining bytes with zeroes to fill one last block.
var padded = [_]u8{0} ** block_len;
@memcpy(padded[0 .. str.len - i], str[i..]);
const mask = letterMask(padded) orelse return false;
return seen & mask == 0;
}
```

Zig's [`@Vector`][vectors] type provides portable SIMD: an operation on vectors is applied to all elements at once, and compiles to the target's vector instructions where available.
Instead of examining one character per loop iteration, this approach loads a whole block of input characters into a vector and processes them together, following the pattern described in [Everyone Should Know SIMD][mitchellh-simd].

[`std.simd.suggestVectorLength`][suggest-vector-length] picks the block length the target's vector registers can hold — for example 32 on x86-64 with AVX2 — and returns `null` for targets without SIMD support, so a fallback is provided.

Like the [bit field approach][approach-bitfield], the set of letters seen so far is kept as bits of a `u32`.
The work of turning a block of characters into such a bit mask is done entirely with vector operations in `letterMask`:

- `block | @splat(0x20)` sets the bit that distinguishes lowercase from uppercase in ASCII, converting every uppercase letter in the block to lowercase in one operation.
- Subtracting `'a'` gives each letter's index in the range `0...25`.
The subtraction is wrapping (`-%`), so a non-letter lane ends up with some value of at least 26, and `@min` clamps it to exactly 26 — an extra bucket where all the non-letters land.
- Each lane's index becomes a single set bit with a vector shift, and `@reduce(.Or, bits)` collapses the lanes into a single integer holding the set of letters in the block.
Masking with `(1 << 26) - 1` strips bit 26, throwing away the non-letter bucket.

A repeated letter *within* the block would produce the same bit in two lanes, and the duplicate would vanish in the `Or` reduction.
The `@popCount` check catches this: if the number of distinct bits in the mask differs from the number of letter lanes ([`std.simd.countTrues`][count-trues] of the lanes whose index is not 26), some letter occurred twice.
Repeats *across* blocks are caught by intersecting each block's mask with the accumulated `seen` set, exactly as in the bit field approach.

The final partial block is copied into a zero-padded buffer and processed the same way — padding is safe because `0 | 0x20` is `0x20` (a space), which is not a letter.
This keeps a single code path instead of a separate scalar loop for the tail.

For this exercise's short words, the setup cost of SIMD outweighs its benefit, and the [bool array][approach-bool-array] or bit field approaches are simpler.
The technique shines on long inputs, where each iteration consumes `block_len` characters instead of one.

[vectors]: https://ziglang.org/documentation/master/#Vectors
[suggest-vector-length]: https://ziglang.org/documentation/master/std/#std.simd.suggestVectorLength
[count-trues]: https://ziglang.org/documentation/master/std/#std.simd.countTrues
[mitchellh-simd]: https://mitchellh.com/writing/everyone-should-know-simd
[approach-bool-array]: https://exercism.org/tracks/zig/exercises/isogram/approaches/bool-array
[approach-bitfield]: https://exercism.org/tracks/zig/exercises/isogram/approaches/bitfield
7 changes: 7 additions & 0 deletions exercises/practice/isogram/.approaches/simd/snippet.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
var seen: u32 = 0;
var i: usize = 0;
while (i + block_len <= str.len) : (i += block_len) {
const mask = letterMask(str[i..][0..block_len].*) orelse return false;
if (seen & mask != 0) return false;
seen |= mask;
}
2 changes: 1 addition & 1 deletion exercises/practice/isogram/.meta/example.zig
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
pub fn isIsogram(s: []const u8) bool {
var letters = [_]bool{false} ** 26;
var letters: [26]bool = @splat(false);
for (s) |c| {
const i = switch (c) {
'A'...'Z' => c - 'A',
Expand Down
Loading