Important
Sole language target as of now is compiling into Nix.
(+ a b ...)sums two or more numbers(- a b)subtractsbfroma-ainverts sign of number (parentheses not needed)(* a b ...)multiplies two or more numbers(/ a b)divides likea / b(> a b)likea > b(< a b)likea < b(>= a b)likea >= b(<= a b)likea <= b
(all a b ...)likea == b- takes two or more arguments(any a b ...)likea || b- takes two or more arguments(not= a b)likea != b(not a)like!a(thus a b ...)likea -> b -> ...- takes two or more arguments-
This is Nix's logical implication operator, where
a -> bmeans: ifAis true, thenBmust also be true, ifAis false just return true - it is equivalent to the pseudocode below:if A: if B: return true else: return false return true
-
(. a b)or, if not dynamic, simplya.bis fine(or (. a b) (. c d))equivalent to Nix'sa.b or c.d(? a b)likea ? b(whereais a map/attrset andbis an attrpath)
(+ a b ...) concatenates two or more strings and/or paths
(++ a b ...)concatenates two or more lists
(// a b ...)updates two or more maps/attrsets
(a x y z)applies a functionawith argumentsx,y,zequivalent toa x y zin Nix
Warning
the function operators below, known as pipe operators are not included by default in the Nix language for most evaluators, and thus if you mean to use them you should certify they're enabled as an experimental option to avoid issues, follow this link for more info.
(|> a b ...)- takes two or more functions - likea |> b(<| a b ...)- takes two or more functions - likea <| b
- Numbers (
num)- No distinction between floats, integers, and so on
123-1.24
- Booleans (
bool)true-false
- Nil (
nil)- Compiles to Nix's
null nil
- Compiles to Nix's
- Strings (
str)- May be interpolated, compiling down to Nix's equivalent string
interpolation syntax:
"1+1 is ${1+1}"- Interpolated with the syntax:
$"1+1 is {(+ 1 1)}" - Interpolation can be canceled by wrapping the curly braces, as
in
1+1 is in the set {{2}} - Interpolated values will be passed to a
builtins.toStringcall, thus they get implicitly converted - this will trigger eval-time errors for non-coercible types such as attribute-sets.
- Interpolated with the syntax:
- May be interpolated, compiling down to Nix's equivalent string
interpolation syntax:
- Paths (
path)- Differently from Nix, Paths are similar to strings, with a
pprefix, likep"./config"compiling into./config- thus paths must still have quotes. - Interpolation might be done by also prefixing
$before the string, but the required order for this must bep$- so
p$"./config/{dynamic}"is valid - and
$p"./config/{dynamic}"is not
- so
- Differently from Nix, Paths are similar to strings, with a
- List (
list)- Compiles down to a Nix list
- Wrapped in square brackets like
[1 2 3 4]
- Map (
map)-
Compiles down to a Nix attribute set
-
Wrapped in curly braces
-
Setting nested keys can use the regular
.syntax from Nix -
As with Nix, keys are still implicitly strings - but different from Nix, that doesn't need to be the case! They eventually will need to be compiled down to strings, but they aren't represented as such and can be declared types, integers, and so on.
- This is useful for mapping non-string keys to other values, like
-
For yielding an string attrset path from non-string forms, use the helper
!syntax as seen below:{this-is-one 1 this-is-two 2 here.are.nested.keys "wow" the.sum.of.$"{(+ 1 1)}.and.one" 3 "quoted"."strings"."also work" {} ! SomeEnumIHave.SomeEnumValue "bla bla bla"} -
Thus, with the
!syntax, keys may be of any value that can become a string, and will only actually become strings once they get compiled-down into Nix code- The
!key is not necessarily equivalent to a Nix dynamic string attr like${value}- if this is needed, use string interpolations like$"{value}" - This is because the
!will attempt handling an expression at compile-time, and thus any evaluation-time logic required cannot be garanteed by using it
- The
-
A compilation error occurs if a type cannot be converted into a string, or if a converted string conflicts with an already-existing key at the map.
-
The types Winter may compile into strings, which for the sake of brevity we'll refer here as 'coercible types' are:
- numbers
- booleans
- nil
- paths
- lists/maps known at compile-time that have elements only of coercible types
- any declared types that have elements of coercible types
- any enums that have elements of coercible types
-
let works similarly as in the let ... in ... from Nix, by taking
a list of variable bindings and injecting them into some 'body'
expression, which in the example below is an empty map.
(let
[x "value"
y 1.2]
{})locals is similar to a let call, but references to its bindings
can be done within the same scope, without needing to wrap all
consuming expressions inside the let call itself:
(locals
x "value"
y "value")
{}Under the hood, locals will be compiled down to a let call in Nix
based on the scope:
- bindings from a
(locals)call must precede all of its consumers - consumers must be in the same scope, or a nested scope as the
(locals)call - a
letwill be generated, based on these rules, to accomodate the outer-most consumer.
(locals
special-num 1)
{favorite-num special-num}Becomes:
let
special-num = 1;
in
{favorite-num = special-num;}(fn {config pkgs ...}
(locals
custom-data config.my-namespace.some-data)
{custom-option.enable config.networking.enable
custom-option.data custom-data
environment.systemPackages [pkgs.btop]})Becomes:
{config, pkgs, ...}: let
custom-data = config.my-namespace.some-data;
in {
custom-option.enable = config.networking.enable;
custom-option.data = custom-data;
environment.systemPackages = [pkgs.btop];
}(fn mk-module [module-name module-opts module-cfg]
(locals
module-extra-opts (// module-opts {enable (make-enable-option true)}))
(fn {config ...}
(locals
module-extra-cfg (// module-cfg {blabla 2}))
{options module-extra-opts
config module-extra-cfg}))Becomes:
module-name: module-opts: module-cfg: # mk-module
{config, ...}: let
# let only generated here, due to only consumers being here
module-extra-opts = module-opts // {enable = make-enable-option true;};
module-extra-cfg = module-cfg // {blabla = 2;};
in {
options = module-extra-opts;
config = module-extra-cfg;
}Inherit has some differences from Nix's inherit, mainly:
- As with
localsmay be used outside of aletbinding - May be used within a list - so it replaces the need for the Nix's
infamous
withsemantics.
Thus, inherit will differ in behavior depending on its placement:
- when inside a
map- will resolve to aninheritinside that map/attrset - when inside a
list- will resolve towithblocks - if used in a
let- will resolve to aninheritinside thatletblock - if used elsewhere - will resolve to a
localsblock with that inherit, thus resolving to aletwith that inheritance
The syntax for inherit is inverted from Nix's counterpart in regards to usage of parentheses to denote what is being inherited or being inherited from:
- the namespace we're inherting a value from is not wrapped in parentheses, but its values being inherited are
- to just inherit like you would with
inherit abc;- you also won't use parentheses - Winter determines whether to inherit a namespace's objects or the
namespace itself: to differ between what would be a
inherit (abc) xyzand aninherit abc xyz- is the usage of parentheses as seen below
- (using parentheses will imply the items within the form wrapped in parentheses will be inherited from the namespace identified by the preceding form)
; on its own:
(inherit
lib (getExe)
pkgs (steam)
mylib ; will inherit mylib itself
)
; within a locals/let:
(locals
my-var 1
my-str "bla"
(inherit stuff (my-bool)))
(let
[my-var 1
my-str "bla"
(inherit stuff (my-bool))]
{cool-str my-str
cool-var my-var
cool-bool my-bool})
; within a list:
{home.packages [(inherit pkgs (btop librewolf htop emacs))]}
; within a map:
{some-key some-val
another-key another-val
(inherit external-stuff (yet-another-key and-yet-another-more-key))}Nix lambdas/functions follow the form below:
(fn [x y]
(+ x y))
; can also include a name
; which will get auto-resolved to a local variable based on scope
; like a `locals` call
(fn sum-values [x y]
(+ x y))
; unpacking map (attrset) arguments also works here
; with the same `?` coallescing syntax from nix
; also ellipsis work the same with nix to allow for extra unused keys
(fn make-potion
[cauldron
{use-element-x ? false
use-uranium ? true
...}]
(do-stuff))
; if map is only argument to `fn`, square brackets can be omitted:
(fn make-potion
{use-uranium ? false
use-element-y ? true}
(do-stuff))
; naming an unpacked map is done via angle-brackets like below:
(fn make-pizza
[eggs
dough
<extra-ingredients {use-cheddar ? true
use-cheetos ? false
...}>]
(do-pizza))May also include type annotations (more on those at [SECTION]):
(fn sum-values@num [x@num y@num]
(+ x y))Characters in a line that are preceded by ; are considered comments,
unless the semi-colon is inside a string.
; this is a comment
"; this is not a comment"Warning
This section of the specification is work-in-progress.
Documentation comments, or simply doc-comments, are a special comment type will be compiled-down into an appropriately-formatted documentation comment depending on the format of choice. In Winter, it will use a singular structure and validation, and its compiler will compile such comments into a chosen documentation standard, with the default being nixdoc.
Type descriptions are notably excluded from the syntax for these comments because the Winter compiler will auto-resolve them from given type annotations to the code.
The syntax for doc-comments is as follows:
{;=
desc: This function adds two numbers
sample:
(add 4 5) 9
args:
a The first number
b The second number
;=
(fn add@num [a@num b@num]
(+ a b))}- all fields aside from
descare optional - each line item in
samplewill take a form depending on the documented symbol:- for a function, it will expect an application of said function, and the evaluated result
- each line item in
argsis expected to be an argument name followed by its description
Compiles to:
{
/**
This function adds two numbers
# Example
```nix
add 4 5
=>
9
```
# Type
```
add :: Number -> Number -> Number
```
# Arguments
a
: The first number
b
: The second number
*/
add = a: b: a + b;
}Refer to the section of Scalar Data Types for checking the type identifiers of all scalar types.
(fn add@num [x@num y@num]
(+ x y))The additional type any exists for denoting any type.
(list@T where T is some type)
(fn average@num [numbers@list@num])(map@(K V) where K and V are types for the keys and values of the
map respectively)
(fn tallest_person@str [people_map@map@(str num)])Functions type annotations take the form of fn@(X Y) where X is
a list of types for every positional parameter of the function, and Y
is the return type.
(fn call_a_file_deleter
[; function that takes a string (file to delete)
; and a boolean for whether to force deletion if its a directory
file_deleter@fn@((str bool) nil)
file_to_delete@str]
(file_deleter file_to_delete true))Unions may be done with a @(| X Y ...) syntax where it takes an
arbitrary amount of types (denoted by the ellipsis) to make an union
out of.
; deletes either a file or a list of files
(fn delete_file@nil [files@(| str list@str)])Intersections combine a new type out of two or more types, by combining their fields. Notably this will only work with compound types that are declared (see below in Declared Types for more info on the topic)
The syntax is the same as unions, but using the & syntax.
(type Identity
id@num
name@str)
(type Contact
email@str
phone@str)
(type Employee (& Identity Contact))Nix won't see these types, so they're only compile-time assurances, akin to Typescript:
- The Winter compiler will turn instantiated types into plain maps.
(type Person
name@str
age@num)Maps identifiers to typed values
(enum FileType@str
PNG "png"
JPGEG "jpeg")
(enum FileSizes&num
KILO 1024
MEGA (* 1024 1024)
GIGA (* 1024 1024 1024))Warning
This section of the specification is work-in-progress.
; a person containing a secret value of type T
(type Person@T
name@str
age@num
secret@T)
; a function with generics
(fn@T multiply@T [x@T y@T]
(* x y))
; a function with multiple generic types
(fn@(X, Y) bla_bla@(| X Y) [x@(| X Y) y@(| X Y)])Caution
This section of the specification is NOT DONE AT ALL.
- Macros should feel like regular code that is done at compile-time.
(macro module! [name raw_opts body]
(fn handle_raw_opt [x]
(locals
key (first x)
value (rest v)
opt_fn_name (first value)
opt_fn_call (rest value)
opt_fn (. `o opt_fn_name))
{key `(opt_fn ,(unpack opt_fn_call))})
(locals
opts (transform raw_opts handle_raw_opt))
`(fn [_]
{den.aspects.server.nixos
(fn [mylib config inputs pkgs lib]
(locals
my (mylib.use config)
o my.options
s my.secrets
t lib.types)
(o.module
,name
,opts
(fn [opts]
,body)))}))
; used like:
(module!
"unit.octodns"
{enable (toggle "Enable OctoDNS" false)}
(o.when opts.enable {systemd.services.octodns-sync {}}))transform- iterates a transformer functionf(x)over a sequence, wherexwill be:- for maps, a sequence like
(k v) - for lists, plainly a value of the list
v
- for maps, a sequence like
first- roughly equivalent tocar- takes first form of a sequence- for
(k v)will takek - for
["x" "z" "w"]will take"x" - for
{x 0 y 1 z 2}will take(x 0)
- for
rest- roughly equivalent tocdr- yields whatever is left of sequence without the result offirst, still in the form of that type of sequence, otherwise returningnilunpack- unpacks elements of a sequence into an outer form["x" "y" (unpack z)]withzbeing["z" "w" "u"]yields:["x" "y" "z" "w" "u"](a b c (unpack z))yields(a b c "z" "w" "u")- for maps it will expect unpacked expression to be a sequence of
sequences (which a map already is)
{x 0 y 1 (unpack z)}andz={z 2 w 3 u 4}yields{x 0 y 1 z 2 w 3 u 4}- also works if
zis of form((z 2) (w 3) (u 4))or is a list like[["z" 2] ["w" 3] ["u" 4]]
Caution
This section of the specification is VERY MUCH NOT DONE AT ALL.
- Requirements can form a directed acyclic graph
- Namespaces are declared at the top of files with a
namespacecall- A file cannot have multiple namespace calls
- Namespaces don't care which directory they're at, and many files across directories can 'contribute' to a namespace, with conflicts being reported at compile-time.
- Requirements are gathered on a node-basis, where they're consumed, for checking if there are circular imports.
- Exported symbols through the namespaces are solely top-level ones.
- If the last symbol of a file evaluates to a function, that function can be named for the symbol to be exported and used elsewhere through the namespace.
(namespace common)
(macro module! [...]
...)(requires
common (module!)) ; like the `inherit` syntax
(namespace units.octodns-sync)
(module!
"unit.octodns"
{enable (toggle "Enable OctoDNS" false)}
(o.when opts.enable {systemd.services.octodns-sync {}}))