Pre-release JETH is under active compiler and security review.
Language

Reference and composite types

Reference values can represent more than one EVM word or refer to storage, memory, or calldata. Their behavior depends on shape and location.

Dynamic bytes and strings#

bytes is a dynamic byte sequence. string is an ABI string represented by its UTF-8 bytes. The two types share the same ABI encoding: bytes is bytes in the ABI and string is string, but both are encoded identically on the wire as a length word followed by padded data. JETH does not provide JavaScript string methods, objects, or character-level iteration.

example.jeth
class Text {
  data: bytes;
  name: string;

  setName(next: string): External<void> {
    this.name = next;
  }

  setData(next: bytes): External<void> {
    this.data = next;
  }

  get getData(): External<bytes> {
    return this.data;
  }

  get getName(): External<string> {
    return this.name;
  }
}

String literals#

String literals use single or double quotes, matching JavaScript syntax:

example.jeth
let message: string = "hello, world";
let same: string = 'hello, world';

Standard JavaScript escape sequences are supported (\n, \t, \\, \", \', \r, \0, \xNN, \uNNNN). Literals are encoded as their raw UTF-8 bytes; the EVM does not validate or enforce UTF-8 correctness at runtime.

String literals appear wherever a string value is expected, including function arguments, require messages, and revert calls:

example.jeth
require(this.balances[msg.sender] >= amount, "insufficient balance");
revert(Error("transfer rejected"));

Template literals#

JETH supports backtick template literals for building string values at runtime. A template literal desugars to string.concat of the static parts and the interpolated expressions, byte-identical to the equivalent Solidity call.

example.jeth
class Greeter {
  greet(name: string): External<string> {
    return `Hello, ${name}!`;
  }

  compose(prefix: string, suffix: string): External<string> {
    return `${prefix}: ${suffix}`;
  }
}

A no-substitution template (no ${...} spans) is equivalent to a plain string literal:

example.jeth
let greeting: string = `hello, world`;  // same as "hello, world"

Auto-coercion of value types#

JETH automatically converts value types in template interpolations. A ${expr} whose type is one of the value types below is converted to its string representation at runtime by a built-in Yul helper:

Interpolated typeString representation
uint / uN (e.g. u256, u8)Unsigned decimal digits, no leading zeros
int / iN (e.g. i256, i8)Signed decimal; negative values get a leading -
bool"true" or "false"
address"0x" followed by 40 lowercase hex characters
bytesN (e.g. bytes4, bytes32)"0x" followed by 2*N lowercase hex characters
example.jeth
class Token {
  owner: address;
  supply: u256;
  paused: bool;

  get status(): External<string> {
    // all three interpolations are auto-converted
    return `owner=${this.owner} supply=${this.supply} paused=${this.paused}`;
  }

  get transferLog(from: address, to: address, amount: u256): External<string> {
    return `transfer: ${from} -> ${to} amount=${amount}`;
  }
}

Dynamic types (bytes, arrays, structs) do not have a built-in string representation and remain a type error (JETH454). Pass a pre-formatted string parameter if you need to include them in a template literal.

Tagged templates (` tag... `) have no on-chain meaning and are rejected with error JETH029.

Static literal parts are validated as UTF-8. A \xNN escape that produces an invalid UTF-8 byte sequence is rejected at compile time (JETH447), matching the same rule that applies to plain string literals.

Operations on bytes#

bytes exposes the following operations:

OperationDescription
.lengthNumber of bytes in the sequence. Returns u256.
data[i]Read byte i as bytes1. Out-of-bounds reverts with Panic(0x32).
keccak256(data)Hash the contents. Returns bytes32.
.slice(start, end)Calldata-only zero-copy sub-view (see below).
Assignment, returnCopy or return on supported location paths.
ABI encode / decodeUsed as a function parameter or return value.
push(value)Storage only: append a bytes1 element.
push()Storage only: append a zero byte.
pop()Storage only: remove the last byte.
example.jeth
class ByteStore {
  data: bytes;

  append(b: bytes1): External<void> {
    this.data.push(b);
  }

  get size(): External<u256> {
    return this.data.length;
  }

  get at(i: u256): External<bytes1> {
    return this.data[i];
  }
}

Operations on string#

string is a semantic type for human-readable text. It shares the ABI and storage encoding of bytes but has a narrower operation surface, matching Solidity behavior:

OperationDescription
keccak256(s)Hash the UTF-8 bytes. Returns bytes32.
.slice(start, end)Calldata-only zero-copy sub-view (see below).
Assignment, returnCopy or return on supported location paths.
ABI encode / decodeUsed as a function parameter or return value.
push(value)Storage only: append to string[] element.
pop()Storage only: remove from string[] element.

string does not expose .length or byte indexing. To inspect byte content, accept a bytes parameter instead, or use keccak256 for equality comparisons:

example.jeth
equal(a: bytes, b: bytes): bool {
  return keccak256(a) == keccak256(b);
}

bytes in error messages#

require and revert accept a string value for the Solidity-compatible Error(string) revert reason. The string can be a literal or a storage/calldata variable:

example.jeth
class Guarded {
  reason: string;

  setReason(r: string): External<void> {
    this.reason = r;
  }

  check(x: u256): External<void> {
    require(x > 0n, "x must be nonzero");
    require(x < 1000n, this.reason);
  }
}

Storage representation#

bytes and string use Solidity's short/long storage layout. Values shorter than 32 bytes are packed into the header slot (low bytes, length in the low byte). Values 32 bytes or longer write data starting at the slot whose address is keccak256(headerSlot).

example.jeth
class Label {
  short: string;  // stored in one slot when value fits
  long: string;   // uses data slots when value exceeds 31 bytes
}

Calldata slices#

Use .slice(start, end) for a zero-copy calldata sub-view. Either bound can use the supported omitted form:

example.jeth
get tail(data: bytes): External<bytes> {
  return data.slice(4n);
}

get middle(data: bytes, start: u256, end: u256): External<bytes> {
  return data.slice(start, end);
}

get argumentsOnly(): External<bytes> {
  return msg.data.slice(4n);
}

get trimmedString(s: string, start: u256, end: u256): External<string> {
  return s.slice(start, end);
}

Slicing corresponds to Solidity's data[start:end], which does not fit JETH's TypeScript-shaped parser. The bounds must satisfy start <= end <= length; an invalid range reverts with empty data. Slices can be returned, hashed, encoded, decoded, sliced again, or bound to a local calldata view.

Only calldata bytes or string values are sliceable. This includes parameters, msg.data, calldata struct fields, and calldata array elements. Storage and memory values do not gain JavaScript-style .slice() behavior.

bytes and string in events and errors#

Both types are supported as event and error fields:

example.jeth
class Notifier {
  Logged: event<{ who: indexed<address>; message: string }>;
  Rejected: error<{ caller: address; reason: string }>;

  submit(msg: string): External<void> {
    require(msg.slice(0n, 1n).length > 0n, "empty message");
    emit(Logged(msg.sender, msg));
  }
}

Dynamic string and bytes arrays#

string[] and bytes[] are fully supported as storage fields and as calldata parameters/returns:

example.jeth
class Catalog {
  entries: string[];
  chunks: bytes[];

  add(entry: string): External<void> {
    this.entries.push(entry);
  }

  addEmpty(): External<void> {
    this.entries.push();
  }

  remove(): External<void> {
    this.entries.pop();
  }

  set(i: u256, entry: string): External<void> {
    this.entries[i] = entry;
  }

  get count(): External<u256> {
    return this.entries.length;
  }

  get at(i: u256): External<string> {
    return this.entries[i];
  }
}

Storage element i in a string[] or bytes[] is itself a full short/long-encoded bytes/string field; its data slots begin at keccak256(keccak256(arrayHeaderSlot) + i).

Dynamic arrays#

T[] is a dynamic array.

example.jeth
class List {
  values: u256[];

  add(value: u256): External<void> {
    this.values.push(value);
  }

  removeLast(): External<void> {
    this.values.pop();
  }
}

Supported operations include .length, index reads/writes, push, zero-value push(), pop, whole-value return/copy on supported paths, and nested access.

Create a zero-initialized memory array with the JETH constructor form:

example.jeth
let values: u256[] = new Array<u256>(length);

The Solidity spelling new T[](length) does not fit the TypeScript parser and is not JETH syntax.

An out-of-bounds index reverts with Panic(0x32). Popping an empty storage array reverts with Panic(0x31). Excessive memory allocation reverts with Panic(0x41).

Fixed arrays#

Arr<T, N> is a fixed-length array and corresponds to Solidity T[N].

example.jeth
class C {
  pair: Arr<u256, 2>;
}

.length is the compile-time constant N. Runtime indexing is bounds-checked. A constant out-of-bounds index is a compile-time error.

Fixed arrays can contain value or reference elements where the relevant location and consumer path is supported. Deep aggregate acceptance is intentionally shape-sensitive; check the known-limitations chapter for cleanly rejected paths.

Mappings#

mapping<K, V> is a storage-only key/value structure.

example.jeth
class Ledger {
  balances: mapping<address, u256>;
  allowances: mapping<address, mapping<address, u256>>;
}

Mapping entries do not store keys or a length. A value slot is derived by hashing the canonical key word with the mapping base slot, recursively for nested mappings.

Supported key families include integers, booleans, addresses, fixed bytes, and dynamic bytes/string keys on their documented paths. Values can be value types, arrays, strings/bytes, structs, and nested mappings where supported.

A missing key reads as the zero value. A whole mapping cannot be copied, returned, deleted, or enumerated.

Structs#

Object-shaped type aliases declare structs:

example.jeth
type Position = {
  owner: address;
  size: u128;
  active: bool;
};

class Book {
  position: Position;

  open(owner: address, size: u128): External<void> {
    this.position = Position(owner, size, true);
  }
}

Struct fields follow declaration order. Storage fields are packed according to Solidity rules. ABI structs are tuples.

Struct values can be constructed positionally. Supported contexts also permit object literals and object spread:

example.jeth
let updated: Position = { ...old, size: nextSize };

A struct containing a mapping is storage-only. It cannot be constructed as a memory value, passed through the ABI, or returned.

Nested composites#

Composite types can be nested:

example.jeth
type Item = { id: u256; label: string };
type Batch = { owner: address; items: Item[] };

class C {
  batches: mapping<u256, Batch>;
  matrix: u256[][];
  labels: string[];
}

JETH's ABI codec recursively handles supported nested arrays, fixed arrays, structs, bytes, and strings. Storage access uses a unified path of fields, indices, and mapping keys.

Not every location-to-location copy of every nested shape is enabled. A compiler diagnostic on such a shape is a deliberate safety gate, not permission to bypass the type system.

Tuples#

Tuple syntax is used for multiple returns and destructuring:

example.jeth
pair(): [u256, bool] {
  return [7n, true];
}

use(): void {
  let [value, ok]: [u256, bool] = this.pair();
  [value, ok] = [value + 1n, !ok];
}

Omitted destructuring positions discard their component:

example.jeth
let [value, , owner] = this.readThree();

The complete right side is evaluated before tuple assignment writes begin.