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
39 changes: 39 additions & 0 deletions exercises/practice/rna-transcription/.approaches/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"introduction": {
"authors": [
"keiravillekode"
]
},
"approaches": [
{
"uuid": "ff1e1b6a-5e9d-488f-b002-7cb4b7c25a82",
"slug": "switch",
"title": "Switch",
"blurb": "Map each nucleotide to its complement with a switch expression.",
"authors": [
"massivelivefun"
],
"contributors": [
"keiravillekode"
]
},
{
"uuid": "b458705a-ded6-479a-bcb8-2a9bae34d499",
"slug": "lookup-table",
"title": "Lookup table",
"blurb": "Precompute a 256-byte table and index it by each nucleotide.",
"authors": [
"keiravillekode"
]
},
{
"uuid": "34c0f10e-3d03-4b18-8fca-cd769d852618",
"slug": "simd",
"title": "SIMD",
"blurb": "Transcribe a whole block of nucleotides at once with vector operations.",
"authors": [
"keiravillekode"
]
}
]
}
68 changes: 68 additions & 0 deletions exercises/practice/rna-transcription/.approaches/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Introduction

Transcription replaces each DNA nucleotide with its RNA complement: `G`↔`C` and `A`→`U`, `T`→`A`. The result is caller-owned memory.

Since the output is the same length as the input and every nucleotide maps independently, every approach allocates a result buffer of `dna.len` bytes and fills it; they differ only in how a single nucleotide is mapped.

## Approach: switch

```zig
pub fn toRna(allocator: mem.Allocator, dna: []const u8) mem.Allocator.Error![]const u8 {
const rna = try allocator.alloc(u8, dna.len);
for (dna, rna) |nucleotide, *out| {
out.* = switch (nucleotide) {
'A' => 'U',
'C' => 'G',
'G' => 'C',
'T' => 'A',
else => unreachable,
};
}
return rna;
}
```

A `switch` maps each nucleotide to its complement, one character at a time.
For details, see the [switch approach][approach-switch].

## Approach: lookup table

```zig
const complement = blk: {
var table: [256]u8 = undefined;
table['A'] = 'U';
table['C'] = 'G';
table['G'] = 'C';
table['T'] = 'A';
break :blk table;
};

pub fn toRna(allocator: mem.Allocator, dna: []const u8) mem.Allocator.Error![]const u8 {
const rna = try allocator.alloc(u8, dna.len);
for (dna, rna) |nucleotide, *out| out.* = complement[nucleotide];
return rna;
}
```

A 256-byte table computed at compile time turns each mapping into a single array index, with no branches in the loop.
For details, see the [lookup table approach][approach-lookup-table].

## Approach: SIMD

```zig
fn transcribeBlock(block: Block) Block {
var rna: Block = block;
rna = @select(u8, block == @as(Block, @splat('A')), @as(Block, @splat('U')), rna);
rna = @select(u8, block == @as(Block, @splat('C')), @as(Block, @splat('G')), rna);
rna = @select(u8, block == @as(Block, @splat('G')), @as(Block, @splat('C')), rna);
rna = @select(u8, block == @as(Block, @splat('T')), @as(Block, @splat('A')), rna);
return rna;
}
```

A whole block of nucleotides is loaded into a `@Vector` and transcribed at once with vector compares and selects, processing many characters per iteration.
For details, see the [SIMD approach][approach-simd].

[approach-switch]: https://exercism.org/tracks/zig/exercises/rna-transcription/approaches/switch
[approach-lookup-table]: https://exercism.org/tracks/zig/exercises/rna-transcription/approaches/lookup-table
[approach-simd]: https://exercism.org/tracks/zig/exercises/rna-transcription/approaches/simd
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Lookup table

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

const complement = blk: {
var table: [256]u8 = undefined;
table['A'] = 'U';
table['C'] = 'G';
table['G'] = 'C';
table['T'] = 'A';
break :blk table;
};

pub fn toRna(allocator: mem.Allocator, dna: []const u8) mem.Allocator.Error![]const u8 {
const rna = try allocator.alloc(u8, dna.len);
for (dna, rna) |nucleotide, *out| {
out.* = complement[nucleotide];
}
return rna;
}
```

Instead of deciding the complement with control flow, this approach precomputes it.
A 256-entry table — one slot per possible byte value — is built once at compile time, with the four DNA bases filled in.
The [labeled block][labeled-block] `blk: { ... break :blk table; }` runs at `comptime` because it initializes a `const`, so the table is baked into the binary as data rather than constructed at run time.

The loop body is then a single array index, `complement[nucleotide]`, with no branches at all.
Iterating over `dna` and `rna` together binds `out` as a pointer into the result, so the transcribed byte is written straight through it.

The table is deliberately `[256]u8` rather than something smaller: indexing by the raw byte value needs no range check or offset subtraction, and the untouched entries are simply never read for valid input.

[labeled-block]: https://ziglang.org/documentation/master/#Blocks
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const complement = blk: {
var table: [256]u8 = undefined;
table['A'] = 'U';
table['C'] = 'G';
table['G'] = 'C';
table['T'] = 'A';
break :blk table;
};
68 changes: 68 additions & 0 deletions exercises/practice/rna-transcription/.approaches/simd/content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# SIMD

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

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

/// Transcribes a whole block of DNA nucleotides at once.
fn transcribeBlock(block: Block) Block {
// Every base is replaced; the seed only survives for bytes that are not
// a base, which valid DNA never contains.
var rna: Block = block;
rna = @select(u8, block == @as(Block, @splat('A')), @as(Block, @splat('U')), rna);
rna = @select(u8, block == @as(Block, @splat('C')), @as(Block, @splat('G')), rna);
rna = @select(u8, block == @as(Block, @splat('G')), @as(Block, @splat('C')), rna);
rna = @select(u8, block == @as(Block, @splat('T')), @as(Block, @splat('A')), rna);
return rna;
}

fn transcribe(nucleotide: u8) u8 {
return switch (nucleotide) {
'A' => 'U',
'C' => 'G',
'G' => 'C',
'T' => 'A',
else => unreachable,
};
}

pub fn toRna(allocator: mem.Allocator, dna: []const u8) mem.Allocator.Error![]const u8 {
const rna = try allocator.alloc(u8, dna.len);
var i: usize = 0;
while (i + block_len <= dna.len) : (i += block_len) {
rna[i..][0..block_len].* = transcribeBlock(dna[i..][0..block_len].*);
}
// Transcribe the remaining nucleotides one at a time.
for (dna[i..], rna[i..]) |nucleotide, *out| {
out.* = transcribe(nucleotide);
}
return rna;
}
```

Zig's [`@Vector`][vectors] type provides portable SIMD: an operation on a vector is applied to all of its elements at once, compiling to the target's vector instructions where available.
Rather than transcribe one nucleotide per iteration, this approach loads a whole block of them into a vector and transcribes the block in one pass, following the pattern in [Everyone Should Know SIMD][mitchellh-simd].

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

`transcribeBlock` handles all four bases the same way: one [`@select`][select] per base, each comparing the whole block against that base and, in the lanes that match, replacing the running result with its complement (`'A'`→`'U'`, `'C'`→`'G'`, `'G'`→`'C'`, `'T'`→`'A'`).
A lane matches at most one base, so the order of the selects does not matter.
The seed value is the input block itself; for valid DNA every lane matches exactly one base and is overwritten, so the seed only shows through for bytes that are not a base.

The main loop copies `block_len` bytes out of `dna`, transcribes them, and stores the vector straight into the matching slice of `rna` — `dna[i..][0..block_len].*` dereferences a slice as a fixed-size array, which coerces to the vector, and the store on the left does the reverse.

Any nucleotides past the last full block are handled by a scalar `switch`, reusing the same mapping as the [switch approach][approach-switch].
Unlike the isogram-style problems, transcription has no notion of an "invalid" block, so the tail needs no special padding — just a short remainder loop.

The loop body is branch-free and consumes many nucleotides at once, so this is the fastest approach on long sequences.
The trade-off is the most code and the need to reason about lane-wise selection and the scalar tail; for short input the simpler [switch][approach-switch] or [lookup table][approach-lookup-table] approaches are preferable.

[vectors]: https://ziglang.org/documentation/master/#Vectors
[suggest-vector-length]: https://ziglang.org/documentation/master/std/#std.simd.suggestVectorLength
[select]: https://ziglang.org/documentation/master/#select
[mitchellh-simd]: https://mitchellh.com/writing/everyone-should-know-simd
[approach-switch]: https://exercism.org/tracks/zig/exercises/rna-transcription/approaches/switch
[approach-lookup-table]: https://exercism.org/tracks/zig/exercises/rna-transcription/approaches/lookup-table
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
fn transcribeBlock(block: Block) Block {
var rna: Block = block;
rna = @select(u8, block == @as(Block, @splat('A')), @as(Block, @splat('U')), rna);
rna = @select(u8, block == @as(Block, @splat('C')), @as(Block, @splat('G')), rna);
rna = @select(u8, block == @as(Block, @splat('G')), @as(Block, @splat('C')), rna);
rna = @select(u8, block == @as(Block, @splat('T')), @as(Block, @splat('A')), rna);
return rna;
}
29 changes: 29 additions & 0 deletions exercises/practice/rna-transcription/.approaches/switch/content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Switch

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

pub fn toRna(allocator: mem.Allocator, dna: []const u8) mem.Allocator.Error![]const u8 {
var rna_slice = try allocator.alloc(u8, dna.len);
for (dna, 0..) |dna_nucleotide, i| {
switch (dna_nucleotide) {
'A' => rna_slice[i] = 'U',
'C' => rna_slice[i] = 'G',
'G' => rna_slice[i] = 'C',
'T' => rna_slice[i] = 'A',
else => unreachable,
}
}
return rna_slice;
}
```

A [`switch`][switch] maps each of the four DNA bases to its RNA complement.

The `else => unreachable` prong states that no other byte can occur.
[`unreachable`][unreachable] is an assertion: in safe build modes reaching it panics, and in `ReleaseFast`/`ReleaseSmall` it is undefined behavior the optimizer may assume never happens.
The exercise's inputs only ever contain `A`, `C`, `G`, and `T`, so the prong is never taken.

[switch]: https://ziglang.org/documentation/master/#switch
[unreachable]: https://ziglang.org/documentation/master/#unreachable
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
switch (dna_nucleotide) {
'A' => rna_slice[i] = 'U',
'C' => rna_slice[i] = 'G',
'G' => rna_slice[i] = 'C',
'T' => rna_slice[i] = 'A',
else => unreachable,
}
Loading