Lowent Manual←↑→

13 Named types — type, newtype, range, cast

What to know first

chapter 4, Numbers · widening is automatic, narrowing is written
chapter 10, Aggregates · struct and field

Looking back

Chapter 4 said usize and u64 do not convert automatically even on machines where they have the same width. Why?

A. To make places mixing address counts with plain numbers visible in the source. When the meaning differs, the type differs even if the representation is the same, and crossing over has to be written. This chapter covers how users apply the same principle themselves — newtype — and how to write ranges, widths and layouts into types.

The need for this chapter, and its context

If user numbers and order numbers are both u64, swapping them goes unnoticed by the compiler. So does passing 300 to a parameter meant for a percentage. These defects arise because a type says only its representation and not its meaning. As the last chapter of Part III, it gathers the ways to attach names, ranges and layouts to types. It sits right before contracts (Part IV) because range has the same standing as a contract.

By the end of this chapter

You will learn that type is another name for the same type and newtype a different type with the same representation, and that cast crosses between them. You will pick up what range lo hi in a parameter position gives the caller and the body, and how cast differs from widen and narrow. You will also see bits integers from 1 to 64 bits, and layout packed and view, which pin down byte layout.

The questions this chapter answers

  1. Does a newtype cost anything at run time?

13.1 type is an alias, newtype a new type#

type <name> <type> . gives an existing type another name. The two names are the same type and interchangeable.

examples/ch13/aliases.low

module aliases .
rem run: area 3 4
rem run: half_percent 90

type meters u64 .
type pct range 0 100 .

fn area input w meters . input h meters . output u64 .
  requires le w. 1000000 . .
  requires le h. 1000000 . .
do
  return mul w. h. . .
end .

fn half_percent input p pct . output u8 .
do
  return narrow u8 div p. 2 . . .
end .

Output

$ lowentc --run area aliases.low 3 4
area(3, 4) = 12
$ lowentc --run half_percent aliases.low 90
half_percent(90) = 45

meters is just another name for u64, so the result of mul w. h. . can be returned as a u64. Aliases shorten long types (type found option u64 .) or leave meaning in the source. pct is an alias for a type with a range attached, and every place that takes this name inherits the range (covered below).

newtype <name> <type> . makes a different type with the same representation as an existing one.

examples/ch13/ids.low

module ids .
rem expect: E-TYPE-NOMINAL

newtype user_id u64 .
newtype order_id u64 .

fn load_order input id order_id . output u64 .
do
  return 0 .
end .

fn mistake input u user_id . output usize .
do
  return load_order u. . .
end .

Output

$ lowentc --check ids.low
ids.low:14:0 E-TYPE-NOMINAL: these are NOMINALLY DISTINCT types with the same representation — a `newtype` (or usize/idiff) is not interchangeable with its base or with another newtype. Convert explicitly with `cast <type> <value>` (RFC-0002 §6.3.1)
ids.low:14:0 E-TYPE-NOMINAL: these are NOMINALLY DISTINCT types with the same representation — a `newtype` (or usize/idiff) is not interchangeable with its base or with another newtype. Convert explicitly with `cast <type> <value>` (RFC-0002 §6.3.1)

user_id and order_id are both represented as 64-bit numbers, but cannot be swapped. Such mistakes are hard to find with tests — when the numbers happen to coincide, the wrong order is fetched without a sound. Split them with newtype and translation catches it.

 type meters u64 .                newtype user_id u64 .

   meters ──┐                     user_id ◀── cast ──▶ u64
            ├──▶ u64
   u64 ─────┘                     same representation, two types
   two names, one type            swapping them is E-TYPE-NOMINAL
   nothing stops a mix            every crossing shows as a cast

When you mean to cross over, write cast.

examples/ch13/ids_ok.low

module ids_ok .
rem run: roundtrip 42

newtype user_id u64 .

fn user_of input n u64 . output user_id .
do
  return cast user_id n. . .
end .

fn raw_of input u user_id . output u64 .
do
  return cast u64 u. . .
end .

fn roundtrip input n u64 . output u64 .
do
  return raw_of user_of n. . . .
end .

Output

$ lowentc --run roundtrip ids_ok.low 42
roundtrip(42) = 42

With only one or two crossing ops like user_of and raw_of, the places where a raw number enters as a user_id gather into those two. Those are the only places that need checking.

Q. Does a newtype cost anything at run time?

A. No. The representation is the same, so in native code user_id is just a 64-bit integer. The distinction exists only at translation time, and cast user_id n. . does not change the value. It is a free distinction, worth using freely.

13.2 range — a contract that became the shape of a parameter#

Writing range <low> <high> in a parameter’s type position makes that parameter accept only values between the two ends, inclusive.

examples/ch13/ranges.low

module ranges .
rem run: scale 100
rem trap: scale 101

fn scale input a range 0 100 . output u8 .
do
  return narrow u8 mul a. 2 . . .
end .

Output

$ lowentc --run scale ranges.low 100
scale(100) = 200
$ lowentc --run scale ranges.low 101
== ir diagnostics (1) ==
0:0 E-VM-CONTRACT: a parameter's declared range was violated at the program boundary (the value entered from outside — no caller proved it)

The body of scale uses the fact that a is at most 100. So mul a. 2 . does not exceed 200, narrow u8 cannot fail, and both overflow checks are removed. Keeping to the range is the caller’s responsibility. If the type of the value passed is already wider than the range (such as u64), translation is refused; a value of a type that can fit, such as u8, is checked at the call. Values that come from outside the program (here, --run’s arguments) are also checked at the boundary, and 101 stops there. The specification says translation is refused when the caller cannot prove the range, but the compiler in this edition hands unproven u8 values to a run-time check.

Who measures the range, and where, in one picture (the literal 101 was measured on this edition).

 value the caller passes                         before entering scale
 42 (literal)     ── measured at translation ─▶  passes
 101 (literal)    ── measured at translation ─▶  E-TYPE-RANGE (refused)
 a u8 value       ── checked at the call ─────▶  stops if 101
 a --run argument ── checked at the boundary ─▶  stops if 101
 a u64 value      ── already wider ───────────▶  E-TYPE-WIDTH (refused)

The same could be written as requires le a. 100 . .. The difference is where it is written. range becomes the shape of the parameter, so the caller sees it from the signature alone, and it can be carried into several ops through an alias (type pct range 0 100 .).

13.3 cast — where a value may change#

widen is only for places where the value does not change. Conversions that change sign or cross kinds (floating point ↔ integer) are written cast <type> <value>.

examples/ch13/casts.low

module casts .
rem run: truncate 7.9
rem run: truncate -2.9
rem run: to_signed 200
rem trap: to_signed 3000000000

fn truncate input x f64 . output i32 .
do
  return cast i32 x. . .
end .

fn to_signed input x u32 . output i32 .
do
  return cast i32 x. . .
end .

Output

$ lowentc --run truncate casts.low 7.9
truncate(7.9) = 7
$ lowentc --run truncate casts.low -2.9
truncate(-2.9) = -2
$ lowentc --run to_signed casts.low 200
to_signed(200) = 200
$ lowentc --run to_signed casts.low 3000000000
== ir diagnostics (1) ==
0:0 E-VM-CAST: value does not fit the target width (use narrow_wrap / narrow_sat / narrow_try)

cast is not a mark meaning “anything goes”. It is a mark meaning I know the value may change here. It does not break the principle that no value-losing conversion happens implicitly (chapter 4), because the author wrote the word.

A common misconception. cast moves the bits unchanged, like a C cast

C’s (int32_t)x truncates or wraps values that do not fit, depending on the implementation. Lowent’s cast moves the value and stops when it does not fit. Keeping the bits and changing only how they are read is a separate job, done by bit_cast (chapter 20). Two jobs do not share one word.

13.4 bits — from 1 to 64 bits#

type <name> bits <count> . makes an integer type with that many bits. The width need not be a power of two.

examples/ch13/tiny.low

module tiny .
rem run: grow 1000
rem trap: grow 1023

type ten_bits bits 10 .

fn grow input x ten_bits . output ten_bits .
do
  return add x. 1 . .
end .

Output

$ lowentc --run grow tiny.low 1000
grow(1000) = 1001
$ lowentc --run grow tiny.low 1023
== ir diagnostics (1) ==
0:0 E-VM-OVERFLOW: integer overflow at the declared width (use wrap_*/sat_*, or prove the range)

ten_bits is 0 … 1023. Adding 1 to 1023 overflows ten bits and stops. When dealing with protocol bit fields or packed tables, the width becomes the contract. But when the width does not match the machine’s word, there is no promise of speed. What the language promises is the correct answer computed in ten bits.

13.5 Pinning down byte layout#

A struct’s layout is normally chosen by the processor to suit the machine. When bytes must mean the same outside — a header going onto the wire, a register a device reads — write the layout.

examples/ch13/packed.low

module packed .

struct wire_header do
  layout packed .
  magic u32 big .
  length u16 big .
  kind u8 .
end .

fn header_kind input b slice u8 . output u64 .
do
  let v view wire_header view wire_header b. . .
  return widen u64 field v. kind . . .
end .

Output

$ lowentc --check packed.low
== check: ok ==

A header holding magic 1 · length 2 · kind 9 lies in bytes like this. big puts the high byte first.

 byte:    0    1    2    3    4    5    6
        ┌────┬────┬────┬────┬────┬────┬────┐
        │ 00 │ 00 │ 00 │ 01 │ 00 │ 02 │ 09 │
        └────┴────┴────┴────┴────┴────┴────┘
         └───── magic ─────┘ └─ length ┘ kind
          u32 big            u16 big     u8

A field can also carry an access mark such as rw, ro or wo. In a struct that maps device registers, reading a write-only field is refused at translation (chapter 30). Pinning the layout takes choices away from the processor, so pin it only where needed.

13.6 Both directions of a layout, and a view that can fail#

view reads bytes as a value of that layout. The opposite direction — making bytes of that layout from a value — is encode.

examples/ch13/encoded.low

module encoded .
rem run: header_bytes 9

struct wire_header do
  layout packed .
  magic u32 big .
  length u16 big .
  kind u8 .
end .

rem the opposite of `view` --- turn a value into bytes of that layout
fn header_bytes input k u8 . output u64 .
do
  let h lit wire_header do magic 1 . length 2 . kind k. . end . .
  let bytes slice u8 encode wire_header h. . .
  return add mul cast u64 len bytes. . . 1000 . widen u64 idx bytes. 6 . . . .
end .

Output

$ lowentc --run header_bytes encoded.low 9
header_bytes(9) = 7009

encode wire_header h. . produces seven bytes in the byte order written on each field (big). The result 7009 puts the length 7 and the 9 of the last byte, kind, side by side. The side that builds a header going onto the wire uses encode; the side that reads a received header uses view.

 bytes 00 00 00 01 00 02 09 ── view · try_view ──▶ a wire_header value
 (wire · file · device)     ◀──── encode ───────── magic 1 · length 2 · kind 9

view stops when the length or alignment is off, because it treats that as a broken contract. Bytes from a network, however, are often short. In such places use try_view, which does the same work but reports failure as a value (chapter 17).

examples/ch13/tryview.low

module tryview .
rem run: kind_or_zero [1,2,3,4,0,5,9]
rem run: kind_or_zero [1,2,3]

struct wire_header do
  layout packed .
  magic u32 big .
  length u16 big .
  kind u8 .
end .

rem bytes from outside may be short --- `try_view` gives `none` instead of stopping like `view`
fn kind_or_zero input b slice u8 . output u64 .
do
  let o option view wire_header try_view wire_header b. . .
  guard is_some o. . else return 0 . .
  let v view wire_header some_value o. . .
  return widen u64 field v. kind . . .
end .

Output

$ lowentc --run kind_or_zero tryview.low [1,2,3,4,0,5,9]
kind_or_zero([1,2,3,4,0,5,9]) = 9
  arg0 (written) = [1,2,3,4,0,5,9]
$ lowentc --run kind_or_zero tryview.low [1,2,3]
kind_or_zero([1,2,3]) = 0
  arg0 (written) = [1,2,3]

With seven bytes it reads kind 9; with three it gets none and returns 0. Use the stopping view inside, where things are already checked, and the value-reporting try_view at the boundary.

13.7 What a struct view looks at#

A view does not copy the bytes. It is a window: the struct’s shape laid over the bytes. Reading a field through it reads the bytes in that place; writing a field writes them. It is the same thing as turning the address of a byte array into a struct pointer in C, except that the length and the alignment are checked when the view is laid on, and the translation works out what may be written.

A window says so in its type. view pt is a read window, mut view pt a write window. It is a different type from the struct value type pt, so one name is enough to tell whether it looks at bytes or lives apart from them. The program below shows three shapes.

examples/ch13/structview.low

module structview .
rem run: write_through [1,2]
rem run: copy_by_name [1,2]
rem run: pass_mut [1,2]

struct pt do
  x u8 .
  y u8 .
end .

rem writing a field of a write window (mut view) writes the bytes there --- b becomes [1,9]
proc write_through input b mut slice u8 . output u8 . effects none . do
  let h mut view pt view pt b. . .
  set field h. y . 9 .
  return idx b. 1 . .
end .

rem `cast` builds a copy --- writing k leaves b as it was
proc copy_by_name input b mut slice u8 . output u8 . effects none . do
  let h view pt view pt b. . .
  var k pt cast pt h. . .
  set field k. y . 7 .
  return idx b. 1 . .
end .

rem handed to a mut input, the callee's write reaches b
proc put input p mut pt . output u8 . effects none . do
  set field p. y . 6 .
  return 0 .
end .
proc pass_mut input b mut slice u8 . output u8 . effects none . do
  let h mut view pt view pt b. . .
  let r u8 put h. . .
  return add idx b. 1 . r. . .
end .

Output

$ lowentc --run write_through structview.low [1,2]
write_through([1,9]) = 9
  arg0 (written) = [1,9]
$ lowentc --run copy_by_name structview.low [1,2]
copy_by_name([1,2]) = 2
  arg0 (written) = [1,2]
$ lowentc --run pass_mut structview.low [1,2]
pass_mut([1,6]) = 6
  arg0 (written) = [1,6]
 b:  ┌────┬────┐            let h mut view pt view pt b. .
     │ 01 │ 02 │  ◀──────── h.x  h.y      (a window on the same bytes)
     └────┴────┘
       set field h. y . 9 .   →   b = [01, 09]

     let h view pt view pt b. .  ·  var k pt cast pt h. . .
       k = { x 1 · y 2 }      (a copy — it lives apart from b)
       set field k. y . 7 .   →   b = [01, 02] unchanged

Binding a window to another window name (let k mut view pt h. .) looks at the same bytes. Two windows laid on the same bytes see each other’s writes. try_view gives option view pt (or option mut view pt), and its some_value is a window. Putting a whole struct value into a window name does not match its type and is refused too — to change the bytes, set field each field, or copy in the bytes encode built. An op that returns a window on its input’s bytes says output view pt.

13.7.1 Whether bytes may be changed is written in the window’s type#

A write window comes only from bytes that may be written. Binding mut view from a slice received read-only, a slice bound without mut, a string literal, a read window or an op that gives one is refused with E-TYPE-MUT. Writing a field through a read window is E-TYPE-MUT, handing a read window to a mut input is E-TYPE-ARGMUT, returning a write window over a read-only input is E-TYPE-RETMUT. To only read, bind view pt.

A fn cannot write through a window on bytes it took as mut either — whether it laid the window itself or another op gave it back, the write is visible to the caller, so it breaks the fn’s purity (E-EFFECT-PURITY). An op that changes bytes through a window is written as a proc.

13.7.2 Which structs a view can be laid on#

A view reads the bytes as they are, so the struct must have a fixed byte layout, and every field must be a meaningful value whatever bytes land in it.

field typeviewwhy
u8…u64 · i8…i64 · f32 · f64yesfixed width, and every bit pattern is a value
a fixed array of those · a struct of only such fieldsyesthe layout sits inside the struct as it is
booltry_view only — view is E-TYPE-BITCASTa byte other than 0 or 1 would be neither true nor false — try_view checks it
an enum with a width (layout u8 .)try_view only — view is E-TYPE-BITCASTa number no variant has may arrive — try_view checks it
an enum without a width · slice · option · owned handleno — E-IR-UNDEFno fixed bytes (such a struct cannot be encoded either)

Table 13.1 — fields a struct view can be laid on

You can measure why a bool field is a problem. Before it was refused, a field that read byte 2 was true to if, false to eq … true, and false to not as well. It is the same reason bit_cast bool is refused (chapter 20). Laying such a struct, or bool, with view_array is refused for the same reason. encode, which writes a value down into bytes, has no such limit — it writes its own value.

13.7.3 try_view checks the fields#

To read bytes from outside as a bool or an enum, use try_view. Besides the length and the alignment, it checks those fields: a bool byte other than 0 or 1, or an enum number that no variant has, gives none. Fields of a nested struct are checked too.

To put an enum in bytes, write its width in the enum declaration — one line, layout u8 . (chapter 10). The bytes hold the variant’s number: reading gives the variant with that number, writing puts the variant’s number. The compiler does not pick the width itself, because adding one variant must not silently change a layout that crosses a wire.

examples/ch13/trycheck.low

module trycheck .
rem run: kind_of [7,1,2]
rem run: kind_of [2,1,2]
rem run: active_of [1,5]
rem run: active_of [2,5]

rem an enum with a width --- one byte holds the variant's number
enum kind do
  layout u8 .
  ping .
  data 7 .
  close .
end .

struct hdr do
  layout packed .
  k kind .
  len u16 big .
end .

struct flag do
  active bool .
  n u8 .
end .

rem try_view gives none unless the number is 1, 7 or 8
fn kind_of input b slice u8 . output u8 . do
  let o option view hdr try_view hdr b. . .
  guard is_some o. . else return 0 . .
  let h view hdr some_value o. . .
  match field h. k . do
    case ping do return 1 . end .
    case data do return 2 . end .
    case close do return 3 . end .
  end .
end .

rem a bool byte other than 0 or 1 gives none too
fn active_of input b slice u8 . output u8 . do
  let o option view flag try_view flag b. . .
  guard is_some o. . else return 9 . .
  let h view flag some_value o. . .
  if field h. active . do return 1 . end .
  return 0 .
end .

Output

$ lowentc --run kind_of trycheck.low [7,1,2]
kind_of([7,1,2]) = 2
  arg0 (written) = [7,1,2]
$ lowentc --run kind_of trycheck.low [2,1,2]
kind_of([2,1,2]) = 0
  arg0 (written) = [2,1,2]
$ lowentc --run active_of trycheck.low [1,5]
active_of([1,5]) = 1
  arg0 (written) = [1,5]
$ lowentc --run active_of trycheck.low [2,5]
active_of([2,5]) = 9
  arg0 (written) = [2,5]

kind_of reads the 7 in byte 0 as data and returns 2; at 2, which is no variant’s number, it gets none and returns 0. active_of does not take byte 2 as a bool — it gets none. If the bytes underneath change by another path after the check, so that a field holds no variant’s number, reading that field stops.

The length and alignment conditions are those of the previous section. view stops when they fail (E-VM-VIEW · E-VM-ALIGN); try_view gives none.

13.8 bitset — a set of small numbers#

bitset <bits> is a set recording whether each number from 0 up to (not including) that count is present. Its name resembles bits (the width of one integer), but the meaning differs: bitset is not a tool for the bits of a word but a set, and the bits of a word are handled by bit operations such as bit_and and shl.

examples/ch13/sets.low

module sets .
rem run: overlap 5
rem run: overlap 3
rem run: flipped

rem a collection of which numbers in 0 … 63 are present --- a set, not the bits of a word
fn overlap input n u64 . output usize .
  requires le n. 60 . .
do
  var a bitset 64 bitset_new 64 . .
  var b bitset 64 bitset_new 64 . .
  bitset_insert a. 1 .
  bitset_insert a. 3 .
  bitset_insert a. 5 .
  bitset_insert b. 3 .
  bitset_insert b. n. .
  let both bitset 64 bitset_intersect a. b. . .
  let only_a bitset 64 bitset_difference a. b. . .
  rem remove 1 from the set --- it changes in place
  bitset_remove a. 1 .
  var code usize add mul bitset_count both. . 100 . mul bitset_count only_a. . 10 . . .
  if bitset_is_subset both. a. . do set code. add code. 1 . . end .
  if bitset_is_empty only_a. . do set code. add code. 1000 . . end .
  return code. .
end .

rem a complement only means something within the width --- put one of eight in, and seven remain
fn flipped output usize .
do
  var a bitset 8 bitset_new 8 . .
  bitset_insert a. 0 .
  return bitset_count bitset_complement a. . . .
end .

Output

$ lowentc --run overlap sets.low 5
overlap(5) = 211
$ lowentc --run overlap sets.low 3
overlap(3) = 121
$ lowentc --run flipped sets.low
flipped() = 7

In overlap 5 the intersection is {3, 5} and what is only in a is {1}, giving 211. bitset_complement only means something within the width: put 0 into an eight-slot set and its complement is the other seven. Inserting or asking about a number outside the width stops the program — the range of the set is part of its type too.

13.9 Scattered pieces as one view — segments#

Sometimes bytes do not lie in one row but are scattered over several pieces, like pieces received from the network or the two ends of a ring buffer. Gathering them into one place means copying. segments views the pieces as one without copying.

examples/ch13/pieces.low

module pieces .
rem run: main

use segview .

proc main input al cap allocator . output u8 . effects alloc .
do
  let mb option mut slice u8 alloc_bytes al. capacity 16 . .
  guard is_some mb. . else return 250 . .
  let md option mut slice u8 alloc_bytes al. capacity 32 . .
  guard is_some md. . else return 251 . .
  rem the 16 backing bytes
  var back mut slice u8 some_value mb. . .
  set idx back. 4 . 20 .
  set idx back. 9 . 30 .
  rem the descriptor: two (start, length) pairs, 4 from 0 and 4 from 8
  var d mut slice u64 view_array u64 some_value md. . . .
  set idx d. 0 . 0 .
  set idx d. 1 . 4 .
  set idx d. 2 . 8 .
  set idx d. 3 . 4 .
  rem see the two pieces as one view, without copying
  let ss segments u8 view_segments back. d. . .
  let p slice u8 segview.piece_at ss. 1 . .
  rem the piece count 2 times 10, plus byte 1 of the second piece (the 30 at original position 9)
  return cast u8 add mul segview.piece_count ss. . 10 . cast usize idx p. 1 . . . . .
end .

Output

$ lowentc --run main pieces.low
main() = 50

13.10 Three kinds of type words#

The words that may stand in a type position form a closed list set by the canon. Some of them are known by name only and have no meaning yet.

examples/ch13/notyet.low

module notyet .
rem expect: W-NOT-YET

rem byte is a type word known by name only; it has no meaning yet, so a warning is given; write a byte as u8
fn first input x byte . output u64 . do
  return 0 .
end .

rem `list` in grammar annex A.9 is the same — it was on the list, but no clause ever gave it a meaning
fn count input xs list u64 . output u64 .
do
  return 0 .
end .

Output

$ lowentc --check notyet.low
notyet.low:5:0 W-NOT-YET: this type NAME is accepted but carries NO MEANING yet: lowering cannot read it, so every op in its signature falls to the interpreter (~80x). The answer stays right, so no oracle will ever see this. Declare it (`type id u64 .`) or write the underlying type
notyet.low:10:0 W-NOT-YET: this type NAME is accepted but carries NO MEANING yet: lowering cannot read it, so every op in its signature falls to the interpreter (~80x). The answer stays right, so no oracle will ever see this. Declare it (`type id u64 .`) or write the underlying type

byte is accepted, but W-NOT-YET says “no meaning”. Accepting it silently would make the writer think it works. Write a byte as u8.

KindWordsWhat the tool does
usablenumbers · bool · void · slice · array · segments · set · stack · range · vec · bitset · mask · result · option · ref · mut_ref · mut · owned · region · cap · selfchecks and lowers them by meaning
known by namebyte · char · str · string · bytes_view · dyn · atomicW-NOT-YET — says there is no meaning
refused for nowshared_read · lock · rwlockE-LOCK-NOTYET — state shared between flows (chapter 26)
qualifierunsafe_ptra C pointer mark placed before a type (chapter 29)
no meaning in the canonlist · raw · addr · rngthis edition’s tool accepts them without a warning — do not use them

Table 13.2 — Type words — usable · known by name · refused

The last row is a hole in this edition. The four words are in the canon’s list, but no clause gives them a meaning, and the tool accepts them without a word. It is recorded as a defect in the development repository.

13.11 Common mistakes#

Counter-example. Passing a value of a wider type straight to a range parameter

examples/ch13/mistake_rangewide.low

module mistake_rangewide .
rem expect: E-TYPE-WIDTH

fn scale input a range 0 100 . output u8 .
do
  return narrow u8 mul a. 2 . . .
end .

fn call_scale input x u64 . output u8 .
do
  rem ✘ `u64` is wider than 0 … 100 --- the caller has not shown the value is in range
  return scale x. . .
end .

Output

$ lowentc --check mistake_rangewide.low
mistake_rangewide.low:12:0 E-TYPE-WIDTH: value does not fit the declared type (a literal out of range, or a narrowing — use narrow / narrow_wrap / narrow_sat / round_to)

scale says in its signature that it takes only 0 … 100. A u64 value is wider than that, so unless the caller shows it is in range, translation rejects the call with E-TYPE-WIDTH. The point of rejecting is to settle who is responsible: only the caller knows what to do with an out-of-range value. Check the range with guard, answer separately when it is outside, and pass the value on with narrow when it is inside.

examples/ch13/rangewide_fixed.low

module rangewide_fixed .
rem run: call_scale 40
rem run: call_scale 400

fn scale input a range 0 100 . output u8 .
do
  return narrow u8 mul a. 2 . . .
end .

fn call_scale input x u64 . output u8 .
do
  rem answer separately when out of range; otherwise narrow and pass it on
  guard le x. 100 . else return 255 . .
  return scale narrow u8 x. . . .
end .

Output

$ lowentc --run call_scale rangewide_fixed.low 40
call_scale(40) = 80
$ lowentc --run call_scale rangewide_fixed.low 400
call_scale(400) = 255

Counter-example. Using widen to put a signed number into an unsigned type

examples/ch13/mistake_widensign.low

module mistake_widensign .
rem expect: E-WIDEN-SIGN

fn to_count input x i32 . output u64 .
do
  rem ✘ a negative number has no place in `u64` --- `widen` fails even though the width is larger
  return widen u64 x. . .
end .

Output

$ lowentc --check mistake_widensign.low
mistake_widensign.low:7:0 E-WIDEN-SIGN: a signed integer cannot widen into an unsigned one — a negative value has no place there. Use `cast` if that is what you mean

Going from i32 to u64 makes the width larger, but a negative number such as −1 has no place in u64. widen is only for places where no value changes, so this is E-WIDEN-SIGN. If you know no negative value can arrive, write cast u64 x. . to leave that judgement in the source; cast stops if a negative value does arrive. If negatives need their own handling, put guard ge x 0 first.

Counter-example. Naming a slice, an array or a matrix with type or newtype

examples/ch13/mistake_rowname.low

module mistake_rowname .
rem expect: E-TYPE-ROWNAME

rem ✘ a slice is not given another name --- write `slice u8` as it is
type bytes slice u8 .

fn first input b bytes . output u8 .
  requires gt len b. . 0 . .
do
  return idx b. 0 . .
end .

Output

$ lowentc --check mistake_rowname.low
5:12 E-TYPE-ROWNAME: a row type — `slice`, `array`, `matrix` — is not given another name: `type` and `newtype` do not take one as the whole type. Write `slice <type>` where it is used; to give a row a name of its own, make it a struct field (`struct name do bytes slice u8 . end .`). A row INSIDE another type may be named (`type maybe option slice u8 .`)

slice, array and matrix are types whose element type, length and dimensions are spelled out in the words themselves. Give one another name and the reader has to find the declaration to learn what is behind it. So type and newtype do not take any of the three as the whole type — a slice is rejected with E-TYPE-ROWNAME, an array with E-TYPE-ARRAY, a matrix with E-MATRIX-USE. Write the type where it is used. One inside another type may be named (type found option slice u8 .). When a type with a name of its own is needed, make it a struct field — a struct with a different name is a different type.

examples/ch13/rowname_fixed.low

module rowname_fixed .

rem when a name is needed, make it a struct field --- a different name is a different type
struct packet do body slice u8 . end .

fn first input b slice u8 . output u8 .
  requires gt len b. . 0 . .
do
  return idx b. 0 . .
end .

fn size input p packet . output usize .
do
  return len field p. body . . .
end .

Output

$ lowentc --check rowname_fixed.low
== check: ok ==

Counter-example. Turning a number into a bool with cast

examples/ch13/mistake_boolcast.low

module mistake_boolcast .
rem expect: E-TYPE-KIND

fn nonzero input n u64 . output bool .
do
  rem ✘ which values count as true is decided by the question --- write `ne n. 0 .`
  return cast bool n. . .
end .

Output

$ lowentc --check mistake_boolcast.low
mistake_boolcast.low:7:0 E-TYPE-KIND: bool is not numeric: it cannot be a cast target — write the question: `ne n. 0 .` (or `eq n. 0 .`)

C treats any non-zero number as true, but which values of a number count as true is decided by the question asked at that place. So bool cannot be the target of cast, and it is rejected with E-TYPE-KIND. Write the question, as in ne n. 0 .; that one line puts the meaning in the source. The other direction works — cast u8 b. . is 1 for true and 0 for false.

Counter-example. Binding a write window over bytes received read-only

examples/ch13/mistake_viewro.low

module mistake_viewro .
rem expect: E-TYPE-MUT

struct pt do
  x u8 .
  y u8 .
end .

fn mark input b slice u8 . output u8 . do
  rem ✘ a write window (mut view) over bytes received read-only --- a field write would change the caller's b
  let h mut view pt view pt b. . .
  set field h. y . 9 .
  return 0 .
end .

Output

$ lowentc --check mistake_viewro.low
mistake_viewro.low:11:0 E-TYPE-MUT: a write window (`mut view`) comes only from bytes that may be written — these are read-only (a shared input, a `let` slice, a literal, a read window or an op that gives one). Bind `view <type>` to read, or take the bytes as `mut` (canon 6.2.13 (7))

A window is not a copy, so writing a field of h writes b. But b was received read-only — the caller trusts that this op does not change its bytes. So the mut view binding is refused with E-TYPE-MUT. To only read, bind view pt; to change it, take the bytes as mut. Then the caller sees that in the signature.

examples/ch13/viewro_fixed.low

module viewro_fixed .
rem run: peek [1,2]
rem run: mark [1,2]

struct pt do
  x u8 .
  y u8 .
end .

rem to read only, bind a read window (view)
fn peek input b slice u8 . output u8 . do
  let h view pt view pt b. . .
  return field h. y . .
end .

rem to change it, take the bytes as mut --- then the caller sees that in the signature
proc mark input b mut slice u8 . output u8 . effects none . do
  let h mut view pt view pt b. . .
  set field h. y . 9 .
  return idx b. 1 . .
end .

Output

$ lowentc --run peek viewro_fixed.low [1,2]
peek([1,2]) = 2
  arg0 (written) = [1,2]
$ lowentc --run mark viewro_fixed.low [1,2]
mark([1,9]) = 9
  arg0 (written) = [1,9]

Counter-example. A bool field in a struct read through a view

examples/ch13/mistake_viewbool.low

module mistake_viewbool .
rem expect: E-TYPE-BITCAST

struct flag do
  active bool .
  n u8 .
end .

fn is_on input b slice u8 . output bool . do
  rem ✘ a byte other than 0 or 1 in a bool field would be neither true nor false
  let h view flag view flag b. . .
  return field h. active . .
end .

Output

$ lowentc --check mistake_viewbool.low
mistake_viewbool.low:11:0 E-TYPE-BITCAST: a view reads the bytes as they are, and this struct has a `bool` or enum field — a byte other than 0 or 1 would be a bool that is neither true nor false, a number no variant has would be no variant. Every field of a viewed struct must be `plain` (canon 8.7.1, as for `bit_cast`): use `try_view`, which checks such fields and gives `none` for bad bytes, or declare the field as a number

Bytes from outside can hold any value, not just 0 and 1. If a bool field gets 2, it would be neither true nor false, so it is E-TYPE-BITCAST. To sort out a wrong input, lay it with try_view (the section above — a 2 gives none). If 2 should count as true, take the field as u8 and write the question. Then whether 2 counts as true or as a wrong input is left in the source.

examples/ch13/viewbool_fixed.low

module viewbool_fixed .
rem run: is_on [2,5]
rem run: is_on [0,5]

struct flag do
  active u8 .
  n u8 .
end .

rem take the field as u8 and write the question that decides what counts as true
fn is_on input b slice u8 . output bool . do
  let h view flag view flag b. . .
  return ne field h. active . 0 . .
end .

Output

$ lowentc --run is_on viewbool_fixed.low [2,5]
is_on([2,5]) = 1
  arg0 (written) = [2,5]
$ lowentc --run is_on viewbool_fixed.low [0,5]
is_on([0,5]) = 0
  arg0 (written) = [0,5]

A common misconception. Giving a type another name with type keeps units from mixing

examples/ch13/type_units.low

module type_units .
rem run: total 100 100

type meters u64 .
type feet u64 .

rem meters and feet are added and nothing stops it --- `type` is only another name
fn total input m meters . input f feet . output meters .
  requires le m. 1000000 . .
  requires le f. 1000000 . .
do
  return add m. f. . .
end .

Output

$ lowentc --run total type_units.low 100 100
total(100, 100) = 200

meters and feet are both just other names for u64, so nothing stops you adding them: 100 meters plus 100 feet comes out as 200 meters. Meanings that must not mix are separated with newtype; then, as in ids.low, translation stops the mix with E-TYPE-NOMINAL, and every crossing shows up as a cast. Use type to shorten long names or to hand down a range.

A common misconception. cast of a negative number into an unsigned type gives a big number, as in C

examples/ch13/negcast.low

module negcast .
rem run: to_unsigned 5
rem trap: to_unsigned -1

fn to_unsigned input x i32 . output u32 .
do
  rem in C, -1 would become 4294967295 --- Lowent stops when the value does not fit
  return cast u32 x. . .
end .

Output

$ lowentc --run to_unsigned negcast.low 5
to_unsigned(5) = 5
$ lowentc --run to_unsigned negcast.low -1
== ir diagnostics (1) ==
0:0 E-VM-CAST: value does not fit the target width (use narrow_wrap / narrow_sat / narrow_try)

In C, (uint32_t)-1 is 4294967295. That wrap-around is defined by the standard, but it usually hides a bug. Lowent’s cast moves the value and stops with E-VM-CAST when it does not fit. If you really want wrapping, use a word that carries that meaning in its name, such as narrow_wrap (chapter 4).

13.12 This chapter’s syntax at a glance#

ShapeMeaningWhy
type meters u64 .another name for the same typeshortens a type and records meaning — does not stop mixing
newtype user_id u64 .a new type with the same representationtranslation stops ids from mixing — no run-time cost
cast user_id n. . · cast u64 u. .cross between the new type and the originalcrossings gather in one place in the code
input a range 0 100 .accept only values in the range, both ends includedthe contract becomes the shape of the signature
type pct range 0 100 .put a range on an aliasmany ops inherit the same range
cast i32 x. .a conversion that may change the value — stops if it does not fita mark that says “I know the value may change here”
type ten_bits bits 10 .an integer of 1 … 64 bitsthe width is the contract
layout packed . · magic u32 big .layout without padding · byte ordermake bytes mean the same outside
view wire_header b. .read bytes in that layout without copyingstops if length or alignment is off
try_view wire_header b. . · encode wire_header h. .a view that gives none on failure · a value into bytes of that layoutat a boundary, the non-stopping one
let h mut view pt view pt b. . · let h view pt view pt b. . · cast pt h. .a write window · a read window · a copy apart from the bytesmut view is refused over bytes that cannot be written
var a bitset 64 bitset_new 64 . . · bitset_insert a 1 . · bitset_intersect a. b. .a set of small numbers and its operationsa set, not the bits of a word
view_segments back. d. . · segview.piece_count ss. . · segview.piece_at ss. i. .scattered pieces as one view without copying · piece count · piece iremoves the gathering copy
type words such as byte · lockW-NOT-YET · E-LOCK-NOTYETif there is no meaning, the tool says so

Table 13.3 — Named type syntax — shape · meaning · why it looks this way

Recap

type is an alias and newtype a new type with the same representation; cast crosses between them. range lo hi is a contract that has become the shape of a parameter: the caller keeps it and the body uses it as a fact. cast is the word for conversions that may change the value and stops when the value does not fit. bits N is an arbitrary-width integer. layout packed, byte-order marks and view match byte layout with the outside world.