13 Named types — type, newtype, range, cast
What to know first
struct and fieldLooking 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
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
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
- Does a
newtypecost 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 castWhen 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)
- Converting floating point to integer truncates towards zero: 7.9 becomes 7, −2.9 becomes −2. Discarding the fraction does not stop.
- If the integer part or the value does not fit the target type, execution stops. Three billion does not fit in an
i32. - To turn a boolean into a number, use
cast:cast u8 b. .is 1 for true and 0 for false (integer types only). There is nocastthe other way — write the question, as inne n. 0 ..
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
(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 ==
layout packed .puts no padding between fields. Fields sit next to each other in declaration order.bigandlittleafter a field set its byte order. If not written, the machine’s order is used.view wire_header b. .reads a byte slice as a value of that layout without copying. If length or alignment do not fit, execution stops.
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 u8A 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 9view 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] unchangedwrite_through— writing a field of a write window turnsbinto[1,9]. A window is not a copy.copy_by_name—cast pt h. .reads the window’s fields and builds a struct value. Writing that copy leavesbas it was. A copy comes only from being written this way — a window simply put where the struct nameptgoes is refused withE-VIEW-COPY, because whether it is a window or a copy must be left in the text.pass_mut— handed to an op that takesmut pt, a write window lets the callee’s write reachb. Amutinput passes the place, not a copy.
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 type | view | why |
|---|---|---|
u8…u64 · i8…i64 · f32 · f64 | yes | fixed width, and every bit pattern is a value |
| a fixed array of those · a struct of only such fields | yes | the layout sits inside the struct as it is |
bool | try_view only — view is E-TYPE-BITCAST | a 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-BITCAST | a number no variant has may arrive — try_view checks it |
an enum without a width · slice · option · owned handle | no — E-IR-UNDEF | no 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
bitset_new 64 .makes an empty set. The width is a number fixed at translation time.bitset_insert a 1 .inserts andbitset_remove a 1 .removes. Both are statements that change the set in place.bitset_count a. .is the number of members.bitset_intersect a. b. .gives the intersection,bitset_difference a. b. .what is only ina, andbitset_complement a. .the complement, each as a new set.bitset_is_subset x. y. .asks whether all ofxis iny;bitset_is_empty x. .asks whether it is empty.
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
view_segments back. d. .binds the backing bytesbackand the descriptordinto a view of typesegments u8. The descriptor is a row of (start, length) pairs; here 4 bytes from 0 and 4 bytes from 8.- From the standard module
segview,segview.piece_count ss. .gives the piece count 2, andsegview.piece_at ss. 1 .gives the second piece as an ordinaryslice u8. Byte 1 of the second piece is the 30 at original position 9. - There is no new machine instruction. Building lowers to building a grouping, and taking a piece to reading fields and slicing, so the cost is visible.
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.
| Kind | Words | What the tool does |
|---|---|---|
| usable | numbers · bool · void · slice · array · segments · set · stack · range · vec · bitset · mask · result · option · ref · mut_ref · mut · owned · region · cap · self | checks and lowers them by meaning |
| known by name | byte · char · str · string · bytes_view · dyn · atomic | W-NOT-YET — says there is no meaning |
| refused for now | shared_read · lock · rwlock | E-LOCK-NOTYET — state shared between flows (chapter 26) |
| qualifier | unsafe_ptr | a C pointer mark placed before a type (chapter 29) |
| no meaning in the canon | list · raw · addr · rng | this 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#
| Shape | Meaning | Why |
|---|---|---|
type meters u64 . | another name for the same type | shortens a type and records meaning — does not stop mixing |
newtype user_id u64 . | a new type with the same representation | translation stops ids from mixing — no run-time cost |
cast user_id n. . · cast u64 u. . | cross between the new type and the original | crossings gather in one place in the code |
input a range 0 100 . | accept only values in the range, both ends included | the contract becomes the shape of the signature |
type pct range 0 100 . | put a range on an alias | many ops inherit the same range |
cast i32 x. . | a conversion that may change the value — stops if it does not fit | a mark that says “I know the value may change here” |
type ten_bits bits 10 . | an integer of 1 … 64 bits | the width is the contract |
layout packed . · magic u32 big . | layout without padding · byte order | make bytes mean the same outside |
view wire_header b. . | read bytes in that layout without copying | stops 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 layout | at 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 bytes | mut 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 operations | a 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 i | removes the gathering copy |
type words such as byte · lock | W-NOT-YET · E-LOCK-NOTYET | if 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.