Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Scalars

Praxis has six scalar types: Int, Float, Bool, Char, Text and Unit. They are the leaves of every value the language builds — the elements of a Vec[Int], the keys of a Map[Text, Int], the fields of a record. All six have a literal you can write.

TypePayloadWritten as
Intsigned 64-bit42, 1_000_000
FloatIEEE-754 binary643.5, 1e10, 2e-3
Booltrue or falsetrue, false
Charone Unicode scalar value'p'
Textimmutable UTF-8"praxis"
Unitnothing()
// One value of every scalar type the language has.
var n: Int = 42
var f: Float = 3.5
var b: Bool = true
var t: Text = "praxis"
var c: Char = 'p'
var u: Unit = ()

out(n)
out(f)
out(b)
out(t)
out(c)
out(u)
42
3.5
true
praxis
p
Unit

The annotations are optional — each of those types is inferred from the initializer. See Bindings and shadowing.

One further name is legal in type position. Never is the type of an expression that produces no value (panic(...), return, break); see Control flow.

Literals

// Int literals, with `_` allowed between digits.
out(42)
out(1_000_000)

// Float literals: a fraction, an exponent, or both. `.5` is not one.
out(0.5)
out(3.141_592)
out(1.5e3)
out(2e-3)

// Bool, Unit, Text.
out(true)
out(())
out("praxis")
42
1000000
0.5
3.141592
1500.0
0.002
true
Unit
praxis

An underscore may appear between digits of any run — the integer part, the fraction and the exponent each accept them. A trailing _ is not part of the literal. A float needs a digit on both sides of its point: 0.5 is a float and neither .5 nor 2. is one. That is also what keeps 1..5 a range instead of a malformed number — a . joins a numeric literal only when a digit follows it.

A literal is typed by its syntax and by nothing else: 42 is an Int, 42.0 is a Float, and the two do not mix. That rule and everything that follows from it is Numbers.

An integer literal outside the signed 64-bit range is error[Y013]: `9223372036854775808` is outside the range of `Int` . It is raised while lowering, which praxis check does not run, so this is one of the few diagnostics a clean check will not show you and praxis run will.

-9223372036854775808 is therefore not a way to write the smallest Int: the - is a unary operator applied to a literal that is itself out of range, and the literal is what gets reported. 0 - 9223372036854775807 - 1 computes that value instead.

A text literal is a double-quoted run of UTF-8. Eight escapes are decoded: \n, \t, \r, \", \\, \0, \{ and \}. The last two exist because a { opens an interpolation hole, so a literal brace needs a spelling; a } closes nothing outside a hole and so needs no escape, but \} is accepted anyway to let a pair be written symmetrically. Anything else after a backslash is T005 invalid escape in text literal and stops compilation, with one exception: \` is accepted by the lexer — backticks delimit parser templates — and is not decoded, so it stays in the text as two characters. There is no \u{...} escape.

// The eight escapes a text literal decodes.
out("a\tb")
out("line\nbreak")
out("quote: \" backslash: \\")
out("a\rb".len())
out("a\0b".len())

// `\{` and `\}` are literal braces: a bare `{` opens an interpolation hole.
out("a hole is \{expr\}")

// A `\`` is accepted and left alone: two characters, not one.
out("a\`b".len())
a	b
line
break
quote: " backslash: \
3
3
a hole is {expr}
4

A character literal

A Char is written in single quotes: '#', 'a', ' '. It holds exactly one Unicode scalar value, and the escapes are a text literal’s plus \' for the quote itself — \', \\, \n, \r, \t, \0, \", \{ and \}. There are no \x or \u{…} escapes, in a character literal or in a text one.

// A character literal is one Unicode scalar in single quotes.
var wall = '#'
out(wall)
out(wall == "#"[0])

// The escapes are a text literal's, plus `\'` for the quote itself.
out('\n'.to_int())
out('\t'.to_int())
out('\''.to_int())
out('\\'.to_int())

// One scalar, not one byte: `é` is a single character.
out('é'.to_int())

// And a `Char` can be matched on, which is what the literal is for.
fn cell(c: Char) -> Text {
    match c {
        '#' => "wall"
        '.' => "open"
        _ => "something else"
    }
}

for c in "#.x" {
    out(cell(c))
}
#
true
10
9
39
92
233
wall
open
something else

Exactly one character is the whole rule, and the lexer holds it. '' names no character and 'ab' names two, so both are refused where they are written rather than becoming something the program did not mean:

// A character literal names exactly one character. These are lexical errors,
// which is the whole point: `"##"[0]` was a well-typed program that quietly
// meant `#`, and `""[0]` was a fault at run time.
var two = '##'
var none = ''
out(two)
out(none)
error[T007]: a character literal holds exactly one character

  char-literal-not-one-character.px:4:11
  4 | var two = '##'
    |           ^^^^ a character literal holds exactly one character

help: write it as a text literal
      "##"

error[T007]: empty character literal: `''` names no character

  char-literal-not-one-character.px:5:12
  5 | var none = ''
    |            ^^ empty character literal: `''` names no character

praxis: 2 error(s)

That is the point of the literal rather than a convenience. "##"[0] was a well-typed program that quietly meant #, and ""[0] was a fault at run time; neither is expressible as a literal.

"#"[0] still works and still means what it did. It is the spelling for a character read out of a text the program did not write down — a line it just parsed, a name it was given — and the literal is the spelling for one the program chose. t[i] == '#' is the common shape, with one of each.

The literal is also a load rather than a call. An Int literal is two loads out of an interned table, and an ASCII Char literal is the same; "#"[0] is a runtime call that re-evaluates every time it is reached.

The literal is what makes a Char matchable, which is the part that is not cosmetic — see pattern matching.

Every value is an object

All runtime values are garbage-collected objects reached through a handle, including an Int. No storage location a program can name holds an unboxed scalar: a variable, a field, a tuple element, an enum payload, a captured binding and a collection slot all hold a reference.

You cannot observe this. Scalars and Text are immutable, so aliasing one is indistinguishable from copying it, and the language has no identity comparison — == always asks about values. What the uniform model buys is that the crash debugger can print every live binding with its type, and that no generic function needs a boxing rule of its own.

What it does not cost is an allocation per number. The runtime interns Int values from -256 to 1024 and Char values from 0 to 127 into immortal tables, so the loop counters and ASCII characters a puzzle program actually handles are a table read rather than a heap block.

Equality

== and != are defined for every scalar, and they compare values rather than addresses. Two Texts built in different ways are equal when their characters are:

// `==` works on every scalar. `<` works on Int, Float, Char and Text.
out(42 == 42)
out(3.5 != 3.6)
out(true == true)
out(() == ())
out("abc" == "ab" + "c")
out('x' == 'x')

out(1 < 2)
out(1.5 <= 1.5)
out("Z" < "a")
out('a' < 'b')
true
true
true
true
true
true
true
true
true
true

Float equality is IEEE-754, so NaN == NaN is false and 0.0 == -0.0 is true. Both are in Numbers.

Equality extends structurally to tuples, records, enums and collections built out of equatable types. Function values are the one thing that is never equatable. See Capabilities.

Ordering

<, >, <= and >= are defined for exactly four types: Int, Float, Char and Text.

  • Int compares as a signed 64-bit number.
  • Float compares by IEEE-754, so any comparison involving NaN is false.
  • Char compares by Unicode scalar value.
  • Text compares lexicographically by UTF-8 bytes, which for UTF-8 is exactly code-point order.

Bool, Unit, tuples, records, enums, collections and functions have no order. Using one where an order is required is Y006, at check time:

// Bool and Unit have no order. Only Int, Float, Char and Text do.
var ready = true
var done = false
out(ready < done)
$ praxis check bool-order.px --color never
error[Y006]: values of type `Bool` cannot be ordered

  bool-order.px:4:13
  4 | out(ready < done)
    |             ^^^^ values of type `Bool` cannot be ordered

praxis: 1 error(s)

The same rule governs sorted() and heap elements: a Vec[(Int, Int)] cannot be sorted and a MinHeap[(Int, Int)] cannot be pushed to, because a tuple has no order. A lexicographic order over composites is conventional elsewhere and is not defined here: ordering a composite means choosing a semantics for it, and rejecting the program is the honest answer until one is chosen.

Ordering inside a container is a separate question, with one deliberate difference. A container needs a total order or it corrupts its own invariants, so the ordering a heap or a sort uses places a Float NaN after every number and ties it with itself. The source-level < is untouched and stays IEEE-754.

Where to go next

  • Numbers — checked Int arithmetic, Float semantics, and the full operator and precedence table.
  • Text and Char — concatenation, indexing, iteration, and the two Char conversions.
  • The method catalog — every method on every type.