agentlang-index · task easy

Shift a lowercase ASCII string by a Caesar offset, or error on bad input

018-caesar-cipher. Read two lines from standard input.

Prompt

This is the natural-language brief given to every model, verbatim. The harness prefixes a language-specific calling-convention block and suffixes a "return only the source code" instruction. Nothing else.

# 018-caesar-cipher

Read two lines from standard input.

Line 1 is a non-negative decimal integer `shift` in the range 0 through
25 inclusive.

Line 2 is the plaintext: a non-empty string of lowercase ASCII letters
(bytes `a` through `z`).

Rotate each plaintext byte forward by `shift` positions within the
lowercase alphabet, wrapping `z + 1` back to `a`. Write the
resulting ciphertext followed by a single newline.

If any of the following hold, write the literal string `error\n`
instead and exit:

1. Line 1 fails to parse: empty after trimming whitespace, contains
   non-digit bytes, or evaluates to a value greater than 25.
2. Line 2 is empty.
3. Line 2 contains any byte outside `a` through `z` (uppercase,
   digits, punctuation, whitespace, non-ASCII all reject).

Trailing whitespace on line 1 is tolerated and trimmed before parsing.
Line 2 is taken verbatim (no trimming, no normalization).

Output exactly one line followed by `\n`. Do not write to standard
error. Exit with status 0 in every case.

## Examples

Input (stdin):

```
3
abc
```

Output: `def\n`

Input (stdin):

```
0
hello
```

Output: `hello\n` (shift of zero is the identity)

Input (stdin):

```
25
abc
```

Output: `zab\n` (each byte shifted back one position, with wrap)

Input (stdin):

```
13
xyz
```

Output: `klm\n` (rot13 on the lowercase alphabet)

Input (stdin):

```
26
abc
```

Output: `error\n` (shift > 25 rejects)

Input (stdin):

```
5
Hello
```

Output: `error\n` (uppercase H rejects)

## Zero input convention

Zero 0.1.2 has no exposed stdin capability. The Zero reference is a
multi-file project under `zero/`: `zero.json` is the package manifest,
`src/main.0` is the driver, and `src/lib.0` exports
`is_lowercase_letter` and `shift_letter`. The driver reads `shift`
from `argv[1]` and the plaintext from `argv[2]`; values are
interpreted exactly as the two stdin lines for other languages
(line 1 trimmed, line 2 verbatim). Invoked as
`zero run zero -- <shift> <plaintext>`.

Acceptance

A task counts as passed only when every public and hidden test case agrees on these fields. No fuzzy matching, no "off by one trailing newline is fine."

stdout (byte-exact, per case) true
stderr (exact bytes) ""
exit code 0
wall time max (ms) 5000
tags string-transform, module-boundary, multi-file

Results

Each cell is one attempt. Pass means stdout matched byte-exact on every test case, stderr empty, exit zero. Hover a failure to see the captured first line of the diagnostic.

Model ZeroTypeScriptRustGoPython
gpt-4o compile
gpt-4o-mini compile other
gpt-5 compile

Failure excerpts

4 of 15 attempts failed. Each card is one attempt, with the captured first line of the diagnostic.

  1. gpt-4o Zero compile
    zero/src/lib.0:8:1 PAR100: unexpected character '`'
  2. gpt-4o-mini Zero compile
    zero/src/main.0:1:1 IMP001: unknown package-local import '"src/lib.0"'
  3. gpt-4o-mini Go other
    # command-line-arguments
  4. gpt-5 Zero compile
    zero/src/lib.0:10:1 PAR100: unexpected character '`'

Reference implementations

The hand-written reference each language ships with. Every reference passes the same public and hidden test suite under the pinned toolchain before any model touches the task.

Click a language to expand

Zero 130 lines
// src/main.0
// Caesar cipher main driver for AgentLang Index slot 018.
//
// Zero 0.1.2 has no exposed stdin, so shift and plaintext come
// from argv[1..2]. Lib (src/lib.0) provides is_lowercase_letter
// and shift_letter; main parses + validates + composes.
//
// Failure modes collapse to "error\n":
//   - argv[1] or argv[2] missing
//   - shift fails to parse, has non-digit bytes, or is > 25
//   - plaintext is empty
//   - any plaintext byte is outside [a-z]

use lib

pub fun main(world: World) -> Void raises {
    let maybe_shift = std.args.get(1)
    let maybe_text = std.args.get(2)
    if maybe_shift.has == false {
        check world.out.write("error\n")
        return
    }
    if maybe_text.has == false {
        check world.out.write("error\n")
        return
    }

    let shift_in = std.mem.span(maybe_shift.value)
    let text_in = std.mem.span(maybe_text.value)

    // ---- Parse shift as u8 in 0..=25 ----
    let s_len = std.mem.len(shift_in)
    let mut s_acc: u64 = 0_u64
    let mut s_have: Bool = false
    let mut s_ok: Bool = true
    let mut s_i: usize = 0
    while s_i < s_len {
        let c = shift_in[s_i]
        if c >= 48_u8 {
            if c <= 57_u8 {
                let d: u64 = (c - 48_u8) as u64
                s_acc = s_acc * 10_u64 + d
                s_have = true
                s_i = s_i + 1
            } else {
                s_ok = false
                s_i = s_len
            }
        } else {
            s_ok = false
            s_i = s_len
        }
    }
    if s_ok == false {
        check world.out.write("error\n")
        return
    }
    if s_have == false {
        check world.out.write("error\n")
        return
    }
    if s_acc > 25_u64 {
        check world.out.write("error\n")
        return
    }
    let shift_val: u8 = s_acc as u8

    // ---- Validate plaintext non-empty + all lowercase letters ----
    let t_len = std.mem.len(text_in)
    if t_len == 0 {
        check world.out.write("error\n")
        return
    }
    let mut text_ok: Bool = true
    let mut t_i: usize = 0
    while t_i < t_len {
        let b = text_in[t_i]
        if is_lowercase_letter(b) == false {
            text_ok = false
            t_i = t_len
        } else {
            t_i = t_i + 1
        }
    }
    if text_ok == false {
        check world.out.write("error\n")
        return
    }

    // ---- Build ciphertext + trailing newline ----
    let mut out: [1100]u8 = [0_u8; 1100]
    let mut out_n: usize = 0
    let mut wi: usize = 0
    while wi < t_len {
        let b = text_in[wi]
        out[out_n] = shift_letter(b, shift_val)
        out_n = out_n + 1
        wi = wi + 1
    }
    out[out_n] = 10_u8
    out_n = out_n + 1
    check world.out.write(out[0..out_n])
    return
}


// src/lib.0
// Caesar cipher helpers. Pure scalar-typed exports so the seventh
// direct-backend codegen quirk (no user functions over Span/MutSpan/
// shape values) does not bite at the module boundary.

pub fun is_lowercase_letter(b: u8) -> Bool {
    if b < 97_u8 {
        return false
    }
    if b > 122_u8 {
        return false
    }
    return true
}

pub fun shift_letter(b: u8, shift: u8) -> u8 {
    // Caller is responsible for is_lowercase_letter(b) == true
    // and shift in 0..=25 inclusive. Returns the byte rotated by
    // shift positions within a-z.
    let zero_based: u8 = b - 97_u8
    let shifted: u8 = (zero_based + shift) % 26_u8
    return 97_u8 + shifted
}
TypeScript 60 lines
function isLowercaseLetter(b: number): boolean {
    return b >= 97 && b <= 122;
}

function shiftLetter(b: number, shift: number): number {
    const zeroBased = b - 97;
    return 97 + ((zeroBased + shift) % 26);
}

function parseShift(s: string): number | null {
    const t = s.trim();
    if (t.length === 0) return null;
    for (let i = 0; i < t.length; i++) {
        const c = t.charCodeAt(i);
        if (c < 48 || c > 57) return null;
    }
    const v = Number(t);
    if (!Number.isInteger(v) || v < 0 || v > 25) return null;
    return v;
}

async function readAll(): Promise<string> {
    const chunks: Buffer[] = [];
    for await (const chunk of process.stdin) {
        chunks.push(chunk as Buffer);
    }
    return Buffer.concat(chunks).toString("utf8");
}

async function main(): Promise<void> {
    const data = await readAll();
    const lines = data.split("\n");
    if (lines.length < 2) {
        process.stdout.write("error\n");
        return;
    }
    const shift = parseShift(lines[0]);
    if (shift === null) {
        process.stdout.write("error\n");
        return;
    }
    const text = lines[1];
    if (text.length === 0) {
        process.stdout.write("error\n");
        return;
    }
    const out: number[] = [];
    for (let i = 0; i < text.length; i++) {
        const c = text.charCodeAt(i);
        if (!isLowercaseLetter(c)) {
            process.stdout.write("error\n");
            return;
        }
        out.push(shiftLetter(c, shift));
    }
    process.stdout.write(Buffer.from(out).toString("ascii") + "\n");
}

main();
Rust 65 lines
use std::io::{self, Read, Write};

fn is_lowercase_letter(b: u8) -> bool {
    (b'a'..=b'z').contains(&b)
}

fn shift_letter(b: u8, shift: u8) -> u8 {
    let zero_based = b - b'a';
    b'a' + ((zero_based + shift) % 26)
}

fn parse_shift(s: &str) -> Option<u8> {
    let t = s.trim();
    if t.is_empty() {
        return None;
    }
    for b in t.bytes() {
        if !(b'0'..=b'9').contains(&b) {
            return None;
        }
    }
    let v: u32 = t.parse().ok()?;
    if v > 25 {
        return None;
    }
    Some(v as u8)
}

fn main() {
    let mut input = String::new();
    if io::stdin().read_to_string(&mut input).is_err() {
        let _ = io::stdout().write_all(b"error\n");
        return;
    }
    let mut lines = input.split('\n');
    let line_shift = lines.next();
    let line_text = lines.next();
    let (Some(ls), Some(lt)) = (line_shift, line_text) else {
        let _ = io::stdout().write_all(b"error\n");
        return;
    };
    let shift = match parse_shift(ls) {
        Some(v) => v,
        None => {
            let _ = io::stdout().write_all(b"error\n");
            return;
        }
    };
    let text_bytes = lt.as_bytes();
    if text_bytes.is_empty() {
        let _ = io::stdout().write_all(b"error\n");
        return;
    }
    let mut out = Vec::with_capacity(text_bytes.len() + 1);
    for &b in text_bytes {
        if !is_lowercase_letter(b) {
            let _ = io::stdout().write_all(b"error\n");
            return;
        }
        out.push(shift_letter(b, shift));
    }
    out.push(b'\n');
    let _ = io::stdout().write_all(&out);
}
Go 87 lines
package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func isLowercaseLetter(b byte) bool {
	return b >= 'a' && b <= 'z'
}

func shiftLetter(b, shift byte) byte {
	zeroBased := b - 'a'
	return 'a' + ((zeroBased + shift) % 26)
}

func parseShift(s string) (byte, bool) {
	t := strings.TrimSpace(s)
	if t == "" {
		return 0, false
	}
	for _, c := range t {
		if c < '0' || c > '9' {
			return 0, false
		}
	}
	v, err := strconv.ParseUint(t, 10, 32)
	if err != nil {
		return 0, false
	}
	if v > 25 {
		return 0, false
	}
	return byte(v), true
}

func readAll(r *bufio.Reader) string {
	var sb strings.Builder
	buf := make([]byte, 4096)
	for {
		n, err := r.Read(buf)
		if n > 0 {
			sb.Write(buf[:n])
		}
		if err != nil {
			break
		}
	}
	return sb.String()
}

func main() {
	w := bufio.NewWriter(os.Stdout)
	defer w.Flush()
	r := bufio.NewReader(os.Stdin)
	data := readAll(r)
	parts := strings.SplitN(data, "\n", 3)
	if len(parts) < 2 {
		fmt.Fprint(w, "error\n")
		return
	}
	shift, ok := parseShift(parts[0])
	if !ok {
		fmt.Fprint(w, "error\n")
		return
	}
	text := parts[1]
	if len(text) == 0 {
		fmt.Fprint(w, "error\n")
		return
	}
	out := make([]byte, 0, len(text)+1)
	for i := 0; i < len(text); i++ {
		b := text[i]
		if !isLowercaseLetter(b) {
			fmt.Fprint(w, "error\n")
			return
		}
		out = append(out, shiftLetter(b, shift))
	}
	out = append(out, '\n')
	w.Write(out)
}
Python 55 lines
#!/usr/bin/env python3
import sys


def is_lowercase_letter(b: int) -> bool:
    return 97 <= b <= 122


def shift_letter(b: int, shift: int) -> int:
    zero_based = b - 97
    return 97 + ((zero_based + shift) % 26)


def parse_shift(s: str):
    t = s.strip()
    if not t:
        return None
    for ch in t:
        if not ('0' <= ch <= '9'):
            return None
    try:
        v = int(t)
    except ValueError:
        return None
    if v > 25:
        return None
    return v


def main() -> None:
    data = sys.stdin.read()
    lines = data.split("\n")
    if len(lines) < 2:
        sys.stdout.write("error\n")
        return
    shift = parse_shift(lines[0])
    if shift is None:
        sys.stdout.write("error\n")
        return
    text = lines[1]
    if not text:
        sys.stdout.write("error\n")
        return
    text_bytes = text.encode("utf-8")
    for b in text_bytes:
        if not is_lowercase_letter(b):
            sys.stdout.write("error\n")
            return
    out = bytes(shift_letter(b, shift) for b in text_bytes)
    sys.stdout.write(out.decode("ascii") + "\n")


if __name__ == "__main__":
    main()

Design notes

Algorithm, failure modes, cross-language parity, and where Zero needed a workaround. From corpus/018-caesar-cipher/notes.md.

Algorithm

Read shift (0..=25) and plaintext (lowercase a-z, non-empty). For each plaintext byte b, output 'a' + ((b - 'a' + shift) % 26). Trailing newline on output. If shift fails to parse or exceeds 25, or plaintext is empty, or any plaintext byte is outside a..z, write error\n. Process exit is 0 in every case.

Multi-file arc opener

This task opens the multi-file arc. The Zero implementation lives under zero/ as a proper Zero project:

zero/
  zero.json       # package metadata (cli exe target, linux-musl-x64)
  src/
    main.0        # parses argv, validates, composes lib calls
    lib.0         # is_lowercase_letter, shift_letter helpers

The driver imports the library with use lib, then calls is_lowercase_letter(b) on each plaintext byte for validation and shift_letter(b, shift_val) to produce each ciphertext byte. The project is invoked as zero run zero -- <shift> <plaintext> — project directory, double-dash, two argv values.

Lib signature constraint

Zero 0.1.2's direct ELF64 backend forbids user functions that take or return Span<u8>, MutSpan<u8>, or shape values (the seventh quirk surfaced on task 014). Lib signatures are pinned to pure scalars to stay below that line:

pub fun is_lowercase_letter(b: u8) -> Bool
pub fun shift_letter(b: u8, shift: u8) -> u8

The plaintext bytes are walked one at a time in main.0, calling into lib.0 for each byte. This keeps the module boundary purely scalar and produces a clean compile across both modules.

Maybe construction constraint (discovery)

I briefly tried to have lib expose a parse_shift(s: Span<u8>) -> Maybe<u8> helper. Two problems compounded:

  1. Span<u8> at the module boundary trips the seventh quirk.
  2. Even with scalar inputs, constructing a Maybe<u8> for the success case fails: user code cannot construct Some(value). The shape-literal form Maybe<u8>{ has: true, value: x } is rejected with PAR100 "expected '}' after block". User code can only return null for None; Some values come back from stdlib calls returning Maybe<T> directly.

The workaround keeps parse logic inline in main.0 (parses argv[1] as a u64 accumulator, bounds-checks against 25, casts down to u8) and exports only the per-byte scalar helpers from lib. This shape both avoids the boundary quirk AND avoids the Maybe construction limitation, while still demonstrating a real lib/main split.

This is recorded as Zero 0.1.2 quirk #9 in the running tally (user-code Maybe::Some construction not supported), but it does not block any task in the corpus — every error path can collapse through error\n without needing to thread Some values across module boundaries.

Cross-implementation parity

All five share the same dispatch:

  1. parse shift (0..=25 inclusive)
  2. validate plaintext non-empty
  3. validate each plaintext byte in [a-z]
  4. emit 'a' + ((byte - 'a' + shift) % 26) for each, then \n

Byte-exact agreement on every case across zero/ts/rust/go/python.

Zero-specific notes

  • argv[1..2] carry shift and plaintext (no exposed stdin in Zero 0.1.2).
  • The output buffer is [1100]u8 = [0_u8; 1100] — generous fixed upper bound for the test corpus; no dynamic allocation needed.
  • main ends with an explicit return to dodge the trailing-write byte-count-as-exit-code codegen quirk surfaced on task 012.
  • std.parse.parseU32 would in principle work for shift parsing but it's still unusable for runtime data per the eighth quirk (CGEN004 on direct ELF64 for non-literal input from task 015).
  • The shift accumulator uses u64 to keep overflow-free across the digit loop, then bounds-checks against 25 before casting to u8.

No new codegen quirks surfaced during 018 — the nine quirks now known were all visible from prior tasks or from the Maybe construction probe done here. The multi-file build worked end-to-end on first compile of both modules.


Cost

Model Prompt tokens Completion tokens API ms
gpt-4o 4,077 1,497 12,429
gpt-4o-mini 4,077 1,629 28,849
gpt-5 4,072 22,153 227,636

Tokens and API ms are summed across the five languages this model attempted for this task.


Compare

Model deep-dives: gpt-4o · gpt-4o-mini · gpt-5 . Back to the leaderboard and methodology.