Basic syntax¶
Comments¶
A line comment begins with // and ends at the end of the line. A block
comment starts with /* and ends with */. Block comments nest, so
/* a /* b */ c */ is one comment; this makes it safe to comment out code that
already has block comments.
// comment
a = 3 // another comment
b = /* inline */ 4
/* outer
c = 5 /* inner */
still in the outer comment
*/
Nothing inside a comment affects parsing: an operator inside a comment does not continue a statement, and quotes or braces inside it do not open a string or an interpolation.
Constants¶
Integers¶
Pyrope has unlimited precision signed integers. Any literal starting with a digit is a likely integer constant.
cassert(0xF_a_0 == 4000) // Underscores have no meaning
cassert(0b1100 == 12) // error: use 0ub1100 or 0sb1100
cassert(0ub1100 == 12) // ub explicit unsigned binary
cassert(0sb1110 == -2) // sb signed binary
cassert(33 == 33) // 33 in decimal
cassert(0o111 == 73) // octal
cassert(0111 == 111) // decimal (some languages use octal here)
Since powers of two are very common, Pyrope decimal integers can use the K, M, G, and T modifiers.
cassert(1K == 1024)
cassert(1M == 1024*1024)
cassert(1G == 1024*1024*1024)
cassert(1T == 1024*1024*1024*1024)
Several hardware languages support unknown bits (?) or high-impedance (z). Pyrope
aims at being compatible with synthesizable Verilog, as such ? is also supported in
the binary encoding.
0ub? // 0 or 1 in decimal (unsigned, `0ub` explicit prefix)
0sb? // 0 or -1 in decimal (signed)
0ub?0 // 0 or 2 in decimal
0ub10??1?01 // mixed known and unknown bits (unsigned)
0sb0?0 // 0 or 2 in decimal (signed)
The Verilog high impedance z is not supported. Tri-state behavior can be
expressed with unique if, which EDA tools can optimize to tri-state buffers
when appropriate.
Like in many HDLs, Pyrope supports unknown bits for Verilog compatibility,
but only as digits inside a binary integer literal — never as a standalone
value. 0ub?, 0sb?, 0ub101?, and 0ub??10 are valid integer
values; bare ? is not an integer and cannot be used in arithmetic
(? + 1 is a syntax error; 0sb? + 1 is a valid operation with unknown
bits). There is no bare ?
placeholder either: every declaration supplies its value (see
Initialization).
Outside binary literals, ? is reserved for a future validity check such as
foo? ("is foo valid?"). Both bare ? and foo? are syntax errors for now.
There are two distinct "no value" concepts in Pyrope — unknown-bit integer
literals (0sb? / 0ub?) and nil:
-
0sb?/0ub?(undefined bits): An integer value in which one or more bits have not been decided by the designer, but the resulting circuit must be correct whether each bit turns out to be 0 or 1. During simulation, each?bit is randomly resolved to 0 or 1 to verify correctness under both possibilities. For every operation involving unknowns, Dlop tries to resolve the result but may leave bits unknown even when a precise result could be proved. It never returns an incorrect definite result. For example,0ub? | 1may resolve to1or remain unknown; it cannot resolve to0. In synthesized hardware, the synthesis tool may choose concrete values for unspecified bits to produce a smaller or faster circuit. Unknowns do not imply that a condition is unreachable; useassumeto express those constraints. -
nil(invalid): An invalid value that must never be used in any expression. Any arithmetic or decision withniltriggers a simulation assertion error. The only allowed operation is copying or checking for validity (x.[valid]returns false fornil). The compiler must prove that allniluses are eliminated at compile time, or a compile error is generated.nilnever exists in synthesized hardware — it is a compile-time and simulation-time safety mechanism.
const masked = 0ub? | 1 // may resolve to 1 or remain unknown
cassert((0sb? + 1) == 0sb??) // error: comparison is unknown, so cassert fails
cassert((nil | 1)==nil) // error: nil is invalid, not unknown
Notice that nil is a state in the integer basic type, it is not a new type by
itself, it does not represent an invalid pointer, but rather an invalid
integer.
The advice is not to use unknown-bit literals (0sb?, 0ub?, …)
outside of match pattern matching. It is less error prone to use the
concrete default value (zero or empty string), but sometimes it is easier to
use nil when converting Verilog code to Pyrope.
Strings¶
Pyrope accepts single line strings with a single quote (') or double quote
("). Single quote does not have escape character, double quote supports escape
sequences. A raw newline inside either kind of string is a syntax error (write
\n); inside a {...} interpolation hole a newline is ordinary whitespace.
const a = "hello \n newline"
const b = 'simpler here'
The escape sequences are exactly these (the Python/Rust common set):
\n: newline\t: tab\r: carriage return\\: backslash\": double quote\': single quote\`: backtick quote\0: NUL character\xNN: hexadecimal 8 bit character (2 digits)\u{N}: Unicode character UTF-8 encoded, 1 to 6 hexadecimal digits inside the braces (\u{41},\u{2287},\u{1F600})
Any other \c is an error. In particular \uNNNN without braces is an error
(write \u{NNNN}), and \{ and \} are not escapes: a literal brace is
written doubled ({{, }}).
Pyrope allows string interpolation only when double quote is used ("bla {expression:format_style} bla").
The format style is like C++23 std::format, and it needs an expression: "{:b}"
is a syntax error, and so is the empty hole "{}". There are no positional
placeholders filled by extra arguments, not even in puts/print: write
puts("a={a} b={b}"), not puts("a={} b={}", a, b). A hole holds exactly one
expression ("{x y}" is an error), and a format style after the : is not
empty and holds no brace and no backslash ("{x:}", "{x:{}}", and
"{x:\x41}" are errors). In a double quoted string a doubled brace is a
literal brace, as in Python or Rust format strings: {{ is the text { and
}} is the text }, so "{{num}}" is the text {num} (no interpolation). A
lone } outside a hole is an error (write }}). A comment opener inside a
string ("//", "/*") is text, while inside a {...} hole the normal code
rules apply, comments included: a {, }, or : inside such a comment never
closes the hole or starts a format style.
const num = 2
const color = "blue"
const extension = "s"
const txt1 = "I have {num:d} {color} potato{extension}"
cassert(txt1 == "I have 2 blue potatos")
const txt3 = 'I have {num}' // single quote does not do interpolation
cassert(txt3 == "I have {{num}}") // a doubled brace escapes the interpolation
cassert("{{{num}}}" == '{2}') // literal braces around an interpolation
cassert("a // b" == 'a // b') // a comment opener inside a string is text
cassert("{num /* } */ + 1}" == "3") // a comment inside a hole is a comment
cassert("\u{41}\x42" == 'AB') // escapes: \u{...} and \xNN
const bad = "{:b}" // error: format style without an expression
const bad2 = "{num:}" // error: empty format style
const bad3 = "{num color}" // error: a hole holds one expression
const bad4 = "num={}" // error: empty hole, write "num={num}"
const bad5 = "a } b" // error: lone `}`, write "a }} b"
const bad6 = "I have \{num\}" // error: not escapes, write "I have {{num}}"
const bad7 = "\u2287" // error: write "\u{2287}"
const bad8 = "{num:\x41}" // error: backslash inside a format style
comptime const text4 = "I have {num+1} x"
cassert(text4 == "I have 3 x")
String(value) converts an integer to decimal text:
const value = 127
const text = String(value)
cassert(text == "127")
Strings are opaque; they do not implicitly convert to numeric values or bit vectors.
Newlines and spaces¶
Spaces do not have meaning but new lines do. Several programming languages like Python use indentation level (spaces) to know the parsing meaning of expressions. In Pyrope, spaces do not have meaning, and newlines combined with the first token after newline is enough to decide the end of statement.
By looking at the first character after a new line and the last one the previous line, it is possible to know if the rest of the line belongs to the previous statement or it is a new statement.
If the line starts with an alphanumeric ([a-z0-9] that excludes operators
like or, and) value or an open parenthesis ((), the rest of the line
belongs to a new statement. A line that starts with a binary operator (+,
-, *, /, %, |, ==, ., ...), a : (type annotation or attribute),
or a # bit selector (#[0..=3], #|[..], ...) continues the previous
statement. A timing read @[N] is not an operator: it stays on the line of its
name (a line starting with @ is an error, also inside an if condition).
mut (a,b,c,d) = (0,0,0,0)
a = 1
+ 3 // 1st stmt
(b,c) = (1,3) // 2nd stmt
cassert(a == 4 and b == 1 and c == 3)
d = 1 + // OK, but not formatted to style
3
mut e
:U8 = 0xF5 // same statement: `mut e:U8 = 0xF5`
const lo = e
#[0..=3] // same statement: `e#[0..=3]`
cassert(lo == 5)
const m = 17
% 5 // same statement: `17 % 5`
cassert(m == 2)
and, or, implies, in, has, does, equals, or case continues the
previous statement. case always continues, even though it is also the
match arm keyword. The match is whole-word, so a line that starts with an
identifier like order or index is a new statement.
const r = false
or true // same statement
cassert(r)
const ok = (const x=1, const y=2)
has "x" // same statement
cassert(ok)
This functionality allows parallelizing the parsing and elaboration in Pyrope. More important, it makes the code more readable, by looking at the beginning of the line, it is possible to know if it is a new statement or a continuation of the last one. It also helps to standardize the code format by allowing only one style.
Identifiers¶
An unescaped identifier is a non-reserved name that starts with an underscore or an
alphabetic character, followed by letters (non-ASCII letters included), digits,
or underscores. $ is not an identifier character, and neither is any other
non-ASCII symbol (emoji, a no-break or zero-width space, —). Since Pyrope is designed to
support any synthesizable Verilog automatic translation, any nonempty sequence of
characters between backticks (`) can form a valid identifier, so a Verilog
name like foo$bar is written `foo$bar`. The identifier uses the same
escape sequence as strings.
const `foo is . strange!\nidentifier` = 4
const `for` = 3
cassert(`for`+1 == `foo is . strange!\nidentifier`)
Backticks are required whenever an ordinary name matches a reserved keyword
or type spelling exactly, or contains characters outside the identifier
rules above. Reserved-word matching is case-sensitive: only the exact spelling
is reserved, so if is a keyword but IF and If are ordinary names. This is
one rule for every name: declarations, destructuring,
parameters and outputs, generics, loop variables, tuple fields, methods,
enum members, named arguments, dotted selectors, and attributes. There are no
field-name or attribute-name exceptions. Name lookup is case-sensitive too:
`if` and IF are distinct names.
Keywords used as language syntax keep their normal spelling: if starts a
conditional and comptime is a declaration modifier. When the same text names
an attribute, field, or other entity, escape it: x.[`comptime`],
x.`if`, f(`in`=x), and f<`type`=U8>(x).
const cfg = (const `type` = 1, const `if` = 2)
cassert(cfg.`type` == 1 and cfg.`if` == 2)
const `in` = 3
const IF = 4 // OK: only the exact spelling `if` is reserved
const If = 5 // OK: also an ordinary name
// const in = 3 // error: reserved word used as a name
for `for` in 0..<2 { cassert(`for` < 2) }
`foo` and foo are the same name only when foo is a non-reserved word
made of identifier characters. A reserved word keeps its meaning only
unescaped: a backticked reserved word is an ordinary name in every position,
so `else` is not else.
The formatter preserves backticks around reserved keywords and type spellings
(and banned old spellings) by the same exact-spelling match: `else`,
`u8`, and `U8` keep their backticks, while `ELSE` is a
non-reserved word and formats as ELSE.
The built-in type words U<N>, S<N>, Unsigned, Signed, Bool, String,
Clock, and Reset (type system) are reserved words too.
U<N> and S<N> stand for U or S followed by any digit string (U0,
U1333, S99999999). They cannot be declared or used as a variable, port,
parameter, lambda, or field name, not even after a . (foo.U33 is an error);
a name with that spelling must be backticked, and `U4` is then a
variable, never the type (foo.`U33` is a field). Only these exact
spellings are reserved: Clock and Reset need backticks as names, but the
lowercase clock and reset are ordinary names (so the minted clock:Clock
and reset:Reset inputs need no backticks), and so are BOOL, STRING, or
UNSIGNED.
The old lowercase type spellings are banned words: u, s, or i followed by
digits (u8, s4, i32), bool, boolean, unsigned, signed, and
string, in exactly that lowercase spelling (BOOL or I32 are ordinary
names). Using one anywhere (as a type, a cast, a variable, lambda, or field
name) is an error that names the new spelling (u8 was renamed U8). So s1,
i0, and u4 are not legal names; pick another name (st1, in0) or
backtick it (`s1`), since a backticked banned word is an ordinary name.
const `U4` = 3 // a variable named U4
mut x:U4 = `U4` // the type U4 holding the variable U4
const U4 = 3 // error: U4 is a reserved type word, use `U4`
const s1 = 1 // error: s1 is a banned old spelling (now S1)
const st1 = 1 // OK
const clock = 1 // OK: only `Clock` is a type word, not `clock`
const `Clock` = 1 // a variable named Clock (bare Clock is the type)
const BOOL = 1 // OK: only the exact spellings are reserved
const `s1` = 1 // OK: a backticked banned word is an ordinary name
const cfg2 = (const `U8` = 1, const `bool` = true)
cassert(cfg2.`U8` == 1)
const `foo$bar` = 2 // `$` needs backticks
const foo$bar = 2 // error: `$` is not an identifier character
The backticks are Pyrope spelling only; they never reach the generated Verilog.
There the bare name is emitted, and Verilog's own \name escape is added only
when Verilog itself needs it: `if` becomes \if because if is a Verilog
keyword, while `in` — not reserved in Verilog — is emitted as plain in.
Identifiers are case-sensitive like Verilog. User-defined type and variable
names may use uppercase or lowercase letters; capitalization is not enforced
by the compiler. The style guide recommends starting type names with an
uppercase letter (Pixel, GcdModel) and variable names with a lowercase
letter (pixel, count).
The reserved type words and banned old spellings listed above still require backticks when used as ordinary names. That restriction is independent of the capitalization style recommendation.
comptime is not inferred from casing. To require compile-time evaluation,
prefix the declaration with comptime explicitly (e.g.,
comptime const size = 16). The .[`comptime`] query checks whether the current
value is known at compile time, regardless of the declaration's modifier.
The bare underscore (_) and names matching _[digit][alnum]* (e.g., _0,
_1a, _23abc) are reserved for future syntax (anonymous lambda
placeholders). Here a digit is a Unicode decimal digit, and alphanumeric
means a Unicode letter or decimal digit. These unescaped names are syntax
errors in every position (a binding, a destructuring slot, a parameter, a
value, or a field). The pattern matches the whole name: _1_a and __0
remain legal. Using the backtick form (`_`, `_0`, `_1a`)
bypasses the reservation if a Verilog import really needs that exact spelling.
const _ = 8 // error: `_` is reserved
(_, b) = f() // error: `_` is reserved, also as a destructuring slot
const x = _ + 1 // error: `_` is reserved, also as a value
const _1a = 8 // error: `_[digit][alnum]*` is reserved
const `_` = 8 // OK: a backticked `_` is an ordinary name
const `_1a` = 8 // OK: a backticked reserved name
Semicolons¶
Semicolons are not needed to separate statements. In Pyrope, a semicolon (;)
has the same meaning as a newline. Sometimes it is possible to add
semicolons to separate statements. Since newlines affect the meaning of the
program, a semicolon can do too.
const a = 1 ; const b = 2
Printing and debugging¶
Printing messages is useful for debugging. puts prints a message and the string
is formatted using the c++23 std::format. There is an implicit newline printed.
The same without a newline can be achieved with print.
const a = 1
const msg = "Hello a is {a}"
puts(msg)
cassert(msg == "Hello a is 1")
Since many modules can print at the same cycle, it is possible to put a
relative priority between puts calls (priority). If no relative priority is
provided, a default 0 priority is provided. Messages are kept to the end of the
cycle, and then printed in alphabetical order for a given priority. This is
done to be deterministic. Higher priority (higher value) are printed after
lower priority. Messages generated by assertions also get serialized like puts
statements but have the highest priority.
To avoid breaking down different puts inside the same method. All the puts
in a given cycle are shown together.
This example will print "hello world" even though there are 2 puts/prints in different files.
// src/file1.prp
puts(priority=2, msg=" world")
// src/file2.prp
print(priority=1, msg="hello")
The available puts/print arguments:
* msg: the message string.
* priority: relative order to print in a given cycle.
* file: file to send the message. E.g: stdout, stderr, my_large.log,...
Arguments follow the normal argument naming
rules. msg and file are both strings, so once priority or file is
passed the message is named too: puts(priority=2, " world") is an error.
Use string interpolation to build a string without printing it.
puts/print are a bit special. In most languages, IO operations like puts are
considered to have side-effects. In Pyrope, the puts can not modify the
behavior of the synthesized code and it is considered a non-side-effect lambda
call. This allows to have puts calls in functions.
Lambda or Routines¶
Pyrope only supports anonymous lambdas, but the lambdas can have attributes that restrict
the lambda functionality to combinational only (comb), pipeline stages
that have all the outputs with the same delay (pipe), or modules that can
do anything — including orchestrating pipelined calls with explicit timing
(mod). Lambda section has more details on the allowed
syntax.
comb f(a, b) -> (r) { r = a + b }
cassert(f(a=2, b=3) == 5)
Pyrope naming for consistency:
-
combis pure combinational logic (zero cycles). Can userefto modify tuples (equivalent to implicit output). -
pipe[N]is a fixed N-cycle pipeline withN > 0(every output lands exactly N cycles after its inputs; no combinational input-to-output path). A zero-cycle block iscomb, notpipe[0]. -
pipe[A..=B]is a flexible A-to-B cycle pipeline, with positive cycle counts; the caller picks a concrete latency viastage[N] -
Bare
pipeleaves the latency fully flexible; the caller picks a positive latency viastage[N]at the call site -
modhas no constraints on registers or output structure (can be Mealy or Moore), operates cycle by cycle, and is also the kind used to orchestrate pipelined calls —stage[N]and@[N]are the timing constructs available insidemod. Unlikepipe, eachmodoutput declares its own landing cycle@[N](Nfrom0ton) at the interface (mod f(a:U8) -> (x:U8@[2], y:U8@[0])); a cycle-0 output is a combinational feedthrough, legal inmodbut forbidden inpipe. To generate an LGraph module (for Verilog or simulation), the concretecomb/pipe/modinterface must be fully typed and have a fixed port list. This can be written directly in the declaration, or deferred until a call binds untyped parameters, generics, and varargs to concrete declared actuals. Acombwith an untyped input or generics is a template: it is always inlined and never generates its own LGraph. A fully typedcomb(even one with a defaulted input) is a normal unit with its own LGraph. -
stageis a reserved declaration modifier used insidemodblocks. -
comb,pipe, ormodthat declaresselfas its FIRST parameter is also called a method; methods may be called via UFCS (obj.method(...)) or directly (method(obj, ...))
Evaluation order¶
Statements are evaluated one after another in program order. The main source of conflicts come from expressions.
The expression evaluation order is important if the elements in the expression can have side effects. Pyrope constrains the expressions so that no matter the evaluation order, the synthesis result is the same.
Languages like C++11 do not have a defined order of evaluation for
all types of expressions. Calling call1() + call2() is not defined. Either
call1() first or call2() first.
In many languages, the evaluation order is defined for logical expressions.
This is typically called short-circuit evaluation. Pyrope and/or always
short-circuit like most modern languages: in a and b, b is not evaluated
if a is false; in a or b, b is not evaluated if a is true. Since
Pyrope expressions have no side effects, short-circuit produces the same
hardware as evaluating both sides — the compiler is free to optimize either
way.
Short-circuit is still observable at compile time. Since the skipped operand is
never evaluated, an illegal operation inside
it is not reported, which makes and/or the way to guard a compile-time
computation:
const N = 0
// `64 % N` is skipped, so the modulo by zero is never evaluated
cassert(N == 0 or (N > 0 and (64 % N == 0)))
The untaken arm of an if behaves the same way, and there it is the only way to
guard a computation whose operands are not booleans.
The programmer can also set evaluation order with control expressions
(if/else, match, for). An expression can have many comb calls because
those have no side-effects, and hence the evaluation order is not important.
A pipe can update state internally and has one or more cycle delays. As
such, pipe statements can do many calls to comb lambdas, but not to
other pipe lambdas. pipe lambdas can only be called inside mod
lambdas, where their outputs are consumed via stage[N] with an explicit
latency.
Expressions also can have code blocks ({ }) as long as there are no
side-effects. In a way, expression code blocks can be seen as a type of
comb lambda that is called immediately after definition.
mut a = {mut d=3 ; d+1} + 100 // OK
cassert(a == (3+1+100))
cassert(a == {3+1+100}) // same, expression block evaluates to 104
For most expressions, Pyrope is more restrictive than other languages because
it wants to be a fully defined deterministic independent of implementation. To
handle logging/messaging in comb calls, Pyrope treats puts as a special
instruction. Pyrope runtime delays the puts output until the end of the cycle.
See the Printing section above for more details.
To illustrate the evaluation order, it is useful to see a Verilog example. The
following Verilog sequence evaluates differently in VCS and Icarus Verilog.
Pyrope treats puts and assertion messages in a special way. The reason why
some methods may be called is dependent on the optimization (in this case,
testing(1) got optimized away by vcs).
module test();
function testing(input [0:3] a);
begin
$display("test called with %d",a);
testing=1;
end
endfunction
initial begin
if (0 && testing(1)) begin
$display("test1");
end
if (1 && testing(2)) begin
$display("test2");
end
if (0 || testing(3)) begin
$display("test3");
end
if (1 || testing(4)) begin
$display("test4");
end
end
test called with 1
test called with 2
test2
test called with 3
test3
test called with 4
test4
test called with 2
test2
test called with 3
test3
test called with 4
test4
test called with 2
test2
test called with 3
test3
test4
If an order is needed and a function call can have debug side-effects or
synthesis side-effects, the statement must be broken down into several
statements. Since and/or short-circuit, they provide a defined left-to-right
evaluation order for logical expressions.
mut r3 = mcall1() + mcall2() // error:
// error: only if mcall1/mcall2 can have side effects
mut r1 = fcall1()
if not r1 {
r1 = fcall2()
}
mut r2 = fcall1()
if r2 {
r2 = fcall2()
}
mut r3 = fcall1()
r3 += fcall2()
mut r1 = fcall1() or fcall2()
mut r2 = fcall1() and fcall2()
mut r3 = fcall1()
r3 += fcall2()
Basic gates¶
Pyrope allows a low level or structural direct basic gate instantiation. There are some basic gates to which to which the compiler translates Pyrope code to. These basic gates are also directly accesible:
__sumfor addition and substraction gate.__multfor multiplication gate.__divfor divisions gate.__andfor bitwise and gate__orfor bitwise or gate__xorfor bitwise xor gate__rorfor bitwise reduce-or gate__notfor bitwise not gate__get_maskfor extrating bits using a mask gate__set_maskfor replacing bits using a mask gate__sextfor sign-extension gate__ltfor less-than comparison gate__gtfor greater-than comparison gate__eqfor equal comparison gate__shlfor shift left logical gate__srafor shift right arithmetic gate__lutfor Look-Up-Table gate__muxfor a priority multiplexer__hotmuxfor a one-hot encoded multiplexer__memoryfor a memory gate__flopfor a flop gate__latchfor a latch gate
Each of the basic gates operate always over signed integers like Pyrope, but
their semantics vary. A more detailed explanation is available at LiveHD cell
type section. A basic gate is an ordinary call:
its arguments are named with the LGraph pin names (__sum(`as`=(a, b)),
__mux(s=cond, p1=b, p2=a)), see instantiation.
Pyrope has a modulo operator a % b, but only the cases that lower cheaply to
shift/mask are accepted as hardware; a general modulo is not. The semantics are
truncated (the remainder's sign follows the dividend, like Verilog %),
because the signed semantics for "mod" differ across languages like Python and
C, and a full divider is expensive. The lowered cases are:
ba compile-time power of two, with a non-negativea→a & (b-1).bprovably larger thana(a's value range fits in[0, |b|)) →a(no remainder is possible).b == 3(compile time), with a non-negativea→ a base-4 digit-sum reduction (since4 ≡ 1 (mod 3)).
Any other % b (a runtime divisor, a possibly-negative dividend, or a divisor
that is none of the above) is a compile error. A fully compile-time a % b
still folds to a constant.
Initialization¶
Each variable declaration (mut or const) must have an assigned value. There
is no implicit default: write a concrete value (0, false, ""), nil for
no value yet, or 0sb? for unknown bits (see
Variable initialization).
a = 3 // error: no previous const or mut
mut b = 3
b = 5 // OK
b += 1 // OK
cassert(b == 6)
const (a,b2) = (1,"string_inferred")
cassert(a == 1 and b2 == "string_inferred")
const (a3:U32,b3) = (1,"x") // error: destructuring slots never carry a type
const d = "hello" // OK
d = "bar" // error: 'd' is immutable
mut d = "bar" // error: 'd' already declared
mut e:U32 = 33
cassert(e == 33)
const Foo = 33 // immutable because of `const`; casing carries no meaning
Foo = 34 // error: `Foo` already declared as immutable
When the variable is a tuple or a range style, 0sb? can not be applied
because it is restricted for integers. nil should be used in those cases.
mut tup = nil
assert(cond.[`comptime`]) // Tuples are compile time, it would fail otherwise
if cond == true {
tup = (const a=1, const b=2)
}else{
tup = (const a=1, const b:U4=3, const c=3)
}
cassert(tup.a == 1)
cassert(cond implies tup.b==2)
cassert(!cond implies tup.b==3)
Casing does not affect comptime. The declaration modifier requires
compile-time evaluation; .[`comptime`] checks whether the current value is
known at compile time, even when the declaration has no comptime modifier.
assert(something.[`comptime`])
comptime const A_xxx = something // comptime
assert(A_xxx.[`comptime`]) // also comptime