Neuro Documentation
Status: Alpha. Phase 1 (Core Language) is complete; Phase 2 (Tensors and MLIR) is open. Per-phase status lives in one place, the Quick Roadmap; what each release changed is in CHANGELOG.md. The feature list below describes what the compiler accepts today.
Quick Links
Documentation Structure
Getting Started
- Installation Guide: Install Neuro on Linux or macOS
- Quick Start Guide: Basic usage and workflow
- Your First Program: Step-by-step tutorial
Language Reference
- Types: Primitive types (integers, floats, bool, string)
- Variables:
val,mut, reassignment, scoping - Functions: Declarations, parameters, implicit returns
- Expressions: Expression syntax and evaluation
- Control Flow: if/else, while, loop, range-for, break/continue
- Operators: Arithmetic, comparison, logical, bitwise, cast operators
- Structs: User-defined types, methods, associated functions
- Modules: Multi-file programs,
mod.nrdirectories, qualified paths,import, inlinemoduleblocks,export importre-exports,exportvisibility
User Guides
- CLI Usage:
neurc check,neurc compile, flags - Troubleshooting: Common problems and solutions
Compiler Architecture
- Compilation Pipeline: End-to-end compilation process
- Lexical Analysis: Tokenizer
- Syntax Parsing: AST generation
- Module Resolution: Multi-file expansion, imports, visibility
- Argument Binding: Named arguments resolved to declaration order
- Semantic Analysis: Type checking
- HIR Lowering: AST → typed High-Level IR (
neuro-hir) - LLVM Backend: Native code generation (from HIR)
- MLIR Backend: Experimental HIR → MLIR path (1D scaffold, off by default)
What is Neuro?
Neuro is a compiled language designed for high-performance AI workloads. It generates native code via an LLVM 20 backend, with a roadmap toward MLIR-based tensor operations, IR-level automatic differentiation (Enzyme), and GPU acceleration via MLIR GPU dialects.
Key design goals:
- Static typing with inference for safety and performance
- Tensor primitives as first-class language types (Phase 2+)
- IR-level AD via Enzyme MLIR (no runtime gradient tape) (Phase 3+)
- GPU acceleration via MLIR
nvgpu/rocdl/Triton dialects (Phase 4+) - Zero-copy Python interop via DLPack (Phase 7+)
Current Features
Comments
- Line comments
// ...to end of line - Block comments
/* ... */, which nest:/* outer /* inner */ still outer */. Each/*needs its own*/, so a block already containing a comment can be commented out wholesale; a file ending with a comment still open is a lex error
Types
- Primitive integers:
i8,i16,i32,i64,u8,u16,u32,u64 - Floating point:
f32,f64;.is_nan()detects NaN, which no comparison operator can (IEEE-754 makes every NaN comparison false) - Half-precision:
f16andbf16, scalar primitives with a narrow storage/cast/compare contract (no arithmetic; compute inf32) - Boolean:
bool - Character:
char, a single 32-bit Unicode scalar value - String: fat-pointer ABI (
{ ptr, i64 }), literals with escape sequences (\n,\t,\",\\,\xNN,\u{NNNN}) - Integer and float literal type suffixes:
42i64,255u8,1.5f32,2.0f64,1.5f16,0.02bf16 - Contextual numeric literal inference with range validation
- Struct types: definition, instantiation, field access, field mutation
- Statically shaped tensors:
Tensor<T, [d0, ...]>, rank-0 through rank-N, over any fixed-width scalar element; a shape or element mismatch is a compile error. Values are built from a nested array literal under aTensor<...>annotation, or withTensor::<T, [...]>::zeros()/ones()/identity()/random_normal(mean:, std:)/scalar()/from(). A tensor owns its buffer and moves rather than copies:.clone()is the explicit copy,&Tensor<T, S>shares one without consuming it, and.to(device)consumes it and hands it back on the requestedDevice(the host is the only one this compiler can lower to). The buffer is an out-of-line allocation with a stable address, released when its last owner leaves scope, so a tensor of any size compiles at any optimization level. Reading one back — indexing, arithmetic, reductions — is later work
Variables
- Immutable (
val) and mutable (mut) bindings with type-safe reassignment - Compile-time constants:
const NAME: Type = exprat module and function scope - Lexical scoping
Functions
- Typed parameters and return types
- Explicit
returnand implicit trailing-expression returns - Recursion and forward references
- Named arguments (1H): an argument may be passed by name (
connect("localhost", port: 8080)), in any order, after any number of positional ones. A parameter declaredexternal internal: Trequires the external name at the call site and uses the internal one in the body; one declared_ internal: Tis positional-only. Free functions, associated functions, and methods all accept them; they are bound before type checking and produce the same IR as the positional call, see functions.md - Generics (1F): generic functions
func identity<T>(x: T) -> T, generic structsstruct Pair<T, U>, and generic inherent implsimpl<T> Wrapper<T>, monomorphized (one specialized copy per concrete type-argument set, zero runtime cost); type arguments inferred from value/field arguments or written explicitly (Pair<i32, f64>); trait bounds<T: Trait>are enforced; type arguments restricted toCopythis phase - Traits (1F):
traitdeclarations with required and default (provided) methods;impl Trait for Typechecked for conformance; trait-bounded genericsfunc f<T: Shape>(x: &T)dispatch trait methods on the type parameter, checked at the call site. Fully monomorphized and erased, so there is no vtable and no runtime cost. A trait may declare associated types (type Item), bound by each impl (type Item = i32) and named asSelf::Itemon either side, including nested (Option<Self::Item>). A bound constrains one withT: Trait<Item = i32>, in the parameter list, awhereclause, or animpl Traitposition, which is what lets a generic body call a method whose signature names it; the concrete type argument's own binding must match. A bareT: Traitbound still cannot type such a call, and a trait declaring an associated type is not object-safe (adynerases the implementor). A trait declaration itself carries no generic parameters yet - Static & dynamic dispatch (1F):
impl Traitin argument position (func train(m: &impl Model)) and return position (func make() -> impl Shape) is anonymous-generic sugar: monomorphized at zero cost, and eachimpl Traitparameter is its own anonymous type parameter.dyn Traitis a runtime trait object behind&dyn Trait/&mut dyn Trait, dispatched through a per-(trait, type) vtable, so one function body serves every implementor. Object safety is enforced: every method of adyn-usable trait must take&selfor&mut self - Closures & lambdas (1F): anonymous callables
|x: i32| x * x,|x: i32| -> i32 { ... },|| expr, andmove |x| ..., capturing Copy free variables by value; the function type(T1, ...) -> R; and higher-order functions (func apply(v: i32, f: (i32) -> i32)). Each closure compiles to a{ fn_ptr, env_ptr }value with no heap allocation. Parameter-type inference and passing a closure to a generic higher-order function come later - Standard output (2A):
print(text: string)andprintln(text: string)write to stdout and returnvoid. Exactly one argument, no variadic form and no call-site format string — interpolation renders every hole into one ordinarystringfirst, soprintln("{x:.2}")is a plain one-argument call. An ownedstringor an immutable&stringslice both work, and the text is read rather than moved. Compiler builtins like the panic family, so a local declaration of the name shadows one and@no_preludedoes not remove them. Buffered, and drained on every path out of the program — including a panic, ahead of its diagnostic — with line buffering when standard output is a terminal, see functions.md
Control Flow
if/else if/elseas statements and as expressions (value-producing)- Bare block expressions as values, with statements newline-separated and the final expression is the block's value:
val r = { val a = 3 val b = 4 a + b} whileloopsloop { }infinite loops (canonical infinite loop; exit viabreak)- Range-for loops: exclusive (
for i in 0..n) and inclusive (for i in 0..=n) breakandcontinue- The
IntoIterator/Iteratorprotocol (2A):for x in ecallse.into_iter()once and then.next()until it answersNone, so any type implementing either prelude trait — an adapter wrapping another iterator included — stands in aforhead. The built-in heads (range, array,Vec<T>,&[T]) keep their counted-loop lowering, see control-flow.md .map(f)/.filter(p)adapter methods on aforhead (2A): they apply to every head shape, compose left to right to any depth, and sit under an outermost.enumerate()whose position counts what the chain yielded, see control-flow.md- Codepoint iteration over text (2A):
.chars()yields an iterator over Unicode scalar values (O(1) per step) that composes with.enumerate()and the adapters, andfor (offset, c) in s.char_indices()binds each scalar's byte offset — the offset.slice(range)takes, see control-flow.md - Attribute system:
@allow(prefer_loop_over_while_true)suppresses thewhile truelint
Operators
- Arithmetic:
+,-,*,/,% - Compound assignment:
+=,-=,*=,/=,%= - Comparison:
==,!=,<,>,<=,>=(IEEE-754 ordered for floats) - Logical:
&&,||,! - Bitwise:
&,|,^,~,<<(integer types only) - Type cast:
n as f64,pi as i32 - Null-coalescing
??: unwraps anOption<T>/Result<T, E>to its payload, else evaluates the fallback (R-to-L associativity, lazy fallback,Errpayload discarded) - String equality:
==and!=via length-check +memcmp - Builtin method dispatch on primitive & string receivers:
string.len() -> u64(O(1) fat-pointer read),.clone(),.slice(a..b) -> &string(zero-copy sub-slice; byte indices, panics on out-of-bounds or mid-codepoint boundary), and.char_slice(a..b) -> &string(the same view indexed by code point, O(n) scan; panics on out-of-bounds only);.chars() -> Chars, the prelude iterator over Unicode scalar values (O(1) per step), with.char_indices()as thefor-head form binding each scalar's byte offset - String interpolation
"Hello, {name}!"with the format mini-language ({x:.2},{n:08d},{s:^10},{n:x},{n:b},{d:+d},{v:?}), see expressions.md - Triple-quoted block strings
"""…""": multi-line text dedented to the column of the closing delimiter, with the same escapes and interpolation holes as a"…"literal, see expressions.md - Growable
Stringbuffer for building text incrementally, the mutable counterpart to the immutablestring:String::new()/.push_str(text)/.len()/.clear()/.to_string(), see types.md
Structs and Methods (1E)
structdefinitions with any primitive or struct field typesimplblocks: instance methods (&selfand&mut self) and associated functions (TypeName::func)&mut selfmethods mutateself.fieldin place (passed by pointer); calling one needs amutreceiver and takes an exclusive borrow for the call. Consumingselfis still rejected- Nominal typing; forward-reference support (definition order independent)
Arrays (1E)
- Fixed-size
[T; N]ofCopyscalar elements: literals (with element-type inference), index read/write,.len()(compile-timeu64) - Iteration
for x in arrandfor x in &arr, lowered as a counted loop over the storage for (i, x) in arr.enumerate()binds the counted loop's own index as au64position alongside the element; the same head works overVec<T>and over a parenthesised range- Out-of-bounds index panics in debug builds (
-O0); release builds omit the check
Derived traits (2A)
@derive(Copy, Clone, Debug, PartialEq)— the derivable-and-implemented set. Any other name in the list is a compile error, never a silent no-op:Hashableis specified but not generated yet, and anything else is unknown@derive(Debug)gives a struct its{p:?}rendering —Point { x: 1, y: 2 }— recursing into a nested struct and quoting astringorcharfield. A struct has no display form, so"{p}"is an error even with the derive@derive(PartialEq)gives a struct field-wise==/!=, recursing the same way. It is generated inline rather than through a method, so deriving it and writingimpl PartialEq forthe same struct is rejected — and aHashMap/BTreeMapstruct key, which calls the trait method, still needs the hand-writtenimpl- Both derives require every field to be renderable / comparable by the same rules; a field that is not names itself in the diagnostic, see structs.md
Borrowed slices (2A)
&[T]/&mut [T]: a non-owning(ptr, len)view over a contiguous run, the array-and-Vecanalogue of&string.[T]alone is unsized and is rejected outside a reference- One signature serves every source:
&[T; N],&Vec<T>, and&[T]all satisfy a&[T]parameter, and the&mutforms a&mut [T]one. Mutability must match exactly .slice(a..b)/.slice(a..=b)on an array, aVec<T>, or a slice yields a sub-range view with no copy; an out-of-range range panics in every build, since the view outlives the check.len()is O(1) over the borrowed run; indexing is bounds-checked as on the owner, andxs[i] = vthrough a&mut [T]reaches the owner's bufferfor x in xsand.enumerate()work as they do on an array; a live view is a shared borrow of its source, so a&mutof that source is rejected while it lives
Tuples (1E)
- Anonymous
(T1, T2, ...)ofCopyelements: literals,.0/.1constant index access - Destructuring binds
val (a, b) = twith_wildcards and nesting (val ((a, b), c) = ...) - Usable as function parameters and return types; a single
(x)stays grouping
Struct + array destructuring (1E)
- Struct patterns
val Point { x, y } = pbind each field by its own name - Array patterns
val [a, b, c] = arrbind positionally;val [first, ..rest] = arrcaptures the remainder as a fresh[T; N - k]array, and a bare..ignores it - Rest-less array patterns are arity-checked against the array length; patterns nest
and work with
mut
Enums (1E)
- Tagged unions
enum E { A, B(i32), C { x: f64 } }with unit, tuple, and struct-field variants - Construct via
E::A/E::B(1)/E::C { x: 1.0 }; usable as bindings, function parameters/returns, and struct fields; an enum isCopy - Scalar
Copypayloads only
Generic enums, Option and Result (1G)
enum Slot<T> { Filled(T), Vacant }is monomorphized per type-argument set, so each instance is its own tagged union with its own payload width and zero runtime cost- Type arguments come from the expected type, the payload (
Slot::Filled(4)→T = i32), or the enclosing function's return type (which is what a tailifbranch relies on) - A
matchpattern names the base enum and binds payloads at the scrutinee instance's types Option<T> { Some(T), None }andResult<T, E> { Ok(T), Err(E) }come from the implicit prelude: available in every program with no declaration and no import, the four variants included, soSome(n)andErr(e)read bare. A local declaration of the same name shadows themchecked_add/checked_sub/checked_mulon any integer type returnOption<T>over the receiver's type:Option::Some(result)when it fits,Option::Noneon overflow. It is branchless: the LLVM*.with.overflowoverflow bit picks the variant??reads either type without amatch:lookup(k) ?? 0yields theSome/Okpayload, else the fallback. TheErrpayload is discarded, the fallback is lazy, anda ?? b ?? cchains right-to-left. Desugared to a two-armmatchduring HIR lowering, so neither backend sees itexpr?propagates instead of defaulting: it yields theSome/Okpayload, or returns the failure (None/Err(e), rebuilt as the enclosing function's own instance) to the caller. The function must return the same fallible enum, and the error travels unconverted, so there is noFrom/Into. Also a lowering-time desugar to a two-armmatchwhose failure armreturnsval PATTERN = value else |binding| { ... }unwraps a variant or leaves the scope: the pattern's bindings stay live for the rest of the enclosing block, and theelsebranch must diverge (return/break/continue/panic/unreachable). Theelse |name|form is type-directed: aResult'sErrpayload, nothing on anOption(only|_|), and the whole scrutinee for any other enum- Limits: scalar
Copypayloads per instance (Option<string>awaits heap payloads),Copytype arguments, noimplblocks on enums, no lifetime parameters
Standard Collections (1G)
Vec<T>,HashMap<K, V>,BTreeMap<K, V>, andStringare heap-backed library types the compiler knows by name, since the language exposes no allocator to build them from- Not
Copy: they move on assignment and free their buffer at scope exit; a mutating method needs amutbinding Vec:push/pop/get/len/clear,v[i]read+write (bounds-checked in every build), andfor x in v- Maps:
insert/get/contains_key/remove/len/clear/keys;keys()returns aVec<K>, ascending forBTreeMap - Keys are integer /
bool/char/string, or a struct withimpl PartialEqplusimpl Hashable(hashed) orimpl Comparable(ordered). Raw float keys are rejected, because the prelude'sOrderedF32/OrderedF64wrappers reject NaN and provide the total order Hashableis a compiler-known lang-item trait:func hash(&self) -> u64Stringis the same machinery over a byte buffer and takes no type arguments, so its bare name is a complete type:push_str/len/clear/to_string(see the strings section above)- Limits:
pop/getbuild anOption<T>, so they need anOption-carryable element type; astringinside a collection is not freed with it
Modules & Visibility (1G)
- A program may span several files: every
.nrfile is a module, and a directory holding amod.nris a module with children. You compile the root; every module it reaches comes with it - An
import, or a qualified path written without one, is what pulls a module in:math::sqrt,utils::io::read,geometry::Point, in value position and in type annotations alike import math,import ./utils::io,import math::{sqrt, sin},import math::sin as sine,import math::matrix as mat, andimport Shape::{Circle, Square}are all available. An imported variant reads unqualified as a value and as amatchpattern- Only referenced modules load, so a directory of unrelated single-file programs still compiles one
at a time. A leaf
math.nrhas no children; only amod.nrdirectory opens a level - A locally declared type wins over a same-named file, so
Point::newkeeps meaning the associated function even with aPoint.nrbeside it - A declaration is private to its file unless it carries
export, and a struct field carries its own marker, so an exported struct may still keep a field to itself. From another module a private field can be neither read, written, listed in a literal, destructured, nor copied through..base. Methods have no marker: animpldeclares no name, so its methods follow their type - Item visibility is reported while modules resolve; field visibility needs the receiver's type and is reported by the type checker. Neither rule is visible in a single-file program, since one file is one module
- An inline
module Name { ... }block is a module with no file of its own: same visibility rule, reached by the same qualified path, and blocks nest. The file declaring a block is outside it, soexportis the only way in; a block has no file children, and it wins over a same-named file beside it export importre-exports: it binds names locally like any import and makes them reachable through the importing module, so a facade offers a flatter API than its internals. A rename is undone on the way through, and facades chain. Only an item can be re-exported. A module or an enum variant is an error rather than a silent no-op- Modules still share one flat namespace, so qualification is checked but never required, and a name declared by two loaded modules is a reported collision rather than a silent winner, even when both keep it private, and a block buys a private surface rather than a private namespace
- Every module begins with an implicit prelude:
Option,Result, and their variantsSome/None/Ok/Errare in scope with noimportof any kind. A local declaration of one of those names, or an explicit import of it, wins inside that module rather than colliding. A file opts out with@no_preludeon its first line. On a non-root file that drops its bindings; on the root it drops the prelude's declarations from the whole program, since the merged namespace is flat - See the modules reference for the resolution rules and diagnostics
Pattern Matching (1E)
matchas an exhaustive expression; the first matching (and guard-passing) arm supplies the value- Patterns:
_wildcard, bare binding, literals,a..=b/a..branges,|or-patterns, and enum variant patterns (E::Unit,E::Tuple(a),E::Struct { field }) that bind their payload ifguards on arms; exhaustiveness enforced (enum variant coverage / both bools / a_arm)- Phase-1E limits: scrutinee is enum/integer/
char/bool; payload sub-patterns are binding-or-_;|-alternatives may not bind
Newtypes (1E)
newtype Meters = i32creates a distinct nominal type wrapping an inner type- Not interchangeable with the inner type (unlike a transparent
typealias) - Construct with
Meters(30); read the wrapped value with.0; forwardsCopy/Clone - Usable as a binding, function parameter/return, and struct field
- Limits: the inner type must be
Copy; animplblock cannot target a newtype, so operator and trait impls are not available on one
Compilation
- Full LLVM 20 backend via inkwell 0.10.0
- Native executable generation
- Signedness-aware integer codegen
print/printlnlower to a module-private buffered writer on fd 1: bytes are copied into a page-sized.bssbuffer and drained through one helper carrying the short-write retry loop, so a large buffer is never truncated on a pipe. The drain is inserted at every exit —main's returns and the panic runtime'sabort— and after everyprintlnwhen fd 1 is a terminal; a string too large for the buffer bypasses it in a single write- Integer
/and%guard the two operand pairs the LLVM instruction leaves undefined: a zero divisor panics in every build, since it has no wrapping answer to fall back on, andMIN / -1follows the integer-overflow rule — a panic in debug builds, the two's-complement wrap in release, produced by dividing by1rather than by handing-1to the instruction - Debug-build
+/-/*overflow panics with a located diagnostic through the same machinery every other guard uses, rather than executing a barellvm.trapthe programmer sees only asSIGILL - Integer interpolation holes render through a digit loop rather than
snprintf, so a hole costs one pass and one allocation instead of two format-string traversals and a probe call; float holes still call the C library, but once, into a scratch buffer sized for the widest conversion the format mini-language admits - Error-path outlining: every panic-family failure path (
panic,assert,unreachable, and the array,Vec, string-slice, UTF-8-boundary, division, and overflow guards) is emitted into a module-private cold function and called from the failure site, so the diagnostic machinery never sits inline in the function that can fail; guard branches carry!profweights keeping the failure edge off the fall-through path - Full workspace test suite green on every push (see the CI badge in the root README)
Compilation Pipeline
Source File (.nr)
→ Lexical Analysis : tokenization
→ Syntax Parsing : AST generation
→ Module Resolution : multi-file expansion, imports, visibility
→ Argument Binding : named arguments → declaration order
→ Semantic Analysis : type checking
→ HIR Lowering : AST → typed High-Level IR (neuro-hir)
→ LLVM Backend : object code (consumes HIR; inkwell / LLVM 20)
→ System Linker : native executable
The typed High-Level IR (neuro-hir) is the backend-agnostic contract: every backend lowers
from it. The LLVM backend consumes it today; the experimental mlir-backend consumes the same HIR
behind the off-by-default mlir feature (1D scaffold).
Planned extension (Phase 2+):
Tensor/AI path (lowers the same typed HIR):
→ MLIR (linalg / tensor / func / arith)
→ Enzyme MLIR AD pass (@grad)
→ GPU dialects (nvgpu / rocdl / Triton) or llvm dialect
→ inkwell → native code
Example Programs
Every snippet below is taken verbatim from a runnable file in examples/.
Arithmetic (examples/basics/hello.nr; compiles to a binary that exits 26)
func add(a: i32, b: i32) -> i32 { return a + b} func calculate(x: i32) -> i32 { val doubled: i32 = x * 2 val result: i32 = doubled + 10 return result} func main() -> i32 { val x: i32 = 5 val y: i32 = 3 val sum: i32 = add(x, y) val calculated: i32 = calculate(sum) return calculated}Recursion (examples/basics/factorial.nr)
func factorial(n: i32) -> i32 { val result: i32 = 0 if n <= 1 { val result: i32 = 1 return result } else { val prev: i32 = factorial(n - 1) val result: i32 = n * prev return result } return result} func main() -> i32 { val result: i32 = factorial(5) return result}Range-For Loop (examples/control_flow/for_range.nr)
func sum_range(start: i32, end: i32) -> i32 { mut sum: i32 = 0 for i in start..end { sum = sum + i } return sum} func main() -> i32 { return sum_range(0, 5)}Structs and Methods (examples/structs/neuron.nr)
struct Neuron { weight: f64, bias: f64} impl Neuron { func new(weight: f64, bias: f64) -> Neuron { Neuron { weight: weight, bias: bias } } // ReLU activation: pass-through if positive, clamp to zero otherwise func activate(&self, input: f64) -> f64 { val z = (input * self.weight) + self.bias if z > 0.0 { z } else { 0.0 } }}The full file adds an is_active method and a main that scales the activation into its exit code.
More examples in examples/.
Building from Source
# Arch Linux / CachyOS
sudo pacman -S llvm20
export LLVM_SYS_201_PREFIX=/usr/lib/llvm20
git clone https://github.com/PanzerPeter/Neuro.git
cd Neuro
cargo build --release
cargo test --workspaceSee Installation Guide for other distributions.
Roadmap
See the Quick Roadmap in the project README for the phase-by-phase status, and CONTRIBUTING.md for the active Phase 2 priorities.
Architecture
Neuro uses Vertical Slice Architecture (VSA): the code is organized by language capability, not by technical layer.
Principles:
- Slice independence: each feature crate is self-contained
- Infrastructure sharing: common utilities live in the
infrastructure/layer and hold no business logic - Clear boundaries:
pub(crate)by default, withpubonly for slice entry points - No cross-slice imports: feature slices do not import from each other
See CONTRIBUTING.md for the full architecture guide.
Backend Stack
| Component | Library | Version |
|---|---|---|
| CPU codegen | inkwell | 0.10.0 (LLVM 20) |
| MLIR construction | melior | 0.25.1 (LLVM/MLIR 20), integrated 1D in the mlir-backend slice behind the off-by-default mlir feature |
| Autodiff (Phase 3+) | Enzyme (MLIR dialect) | built against LLVM 20 |
| GPU (Phase 4+) | MLIR nvgpu/rocdl/Triton | LLVM 20 backends |
Project Resources
- README.md: project overview
- CHANGELOG.md: version history
- CONTRIBUTING.md: contribution guidelines and architecture rules
- LICENSE: Neuro Shared Source License v2.1
Status: see the Quick Roadmap Rust: 1.85+ | LLVM: 20 | inkwell: 0.10.0