Introduction
Cumin is a Configuration Language like JSON, YAML or TOML, but this is Mini-Programmable, Structured and Typed.
cumin has Rust-like syntax.
/// example.cumin
struct User {
id: Int,
name: String,
}
let names = [
User(1, "cympfh"),
User(2, "Taro"),
User(3, "John"),
];
names
The compiler cuminc generates JSON from cumin.
$ cuminc ./example.cumin
[{"id":1,"name":"cympfh"},{"id":2,"name":"Taro"},{"id":3,"name":"John"}]
Try Online
You can Try Cumin Online without any installation.
Language Spec v0.9.13
Overview
Cumin data are composed of Statements and Expressions. A cumin data must start with zero or more Statements and end with exact one Expression data.
Frankly, it is expressed like
(Statement)
:
(Statement)
(Expression)
or denoted as <cumin>
in the following (pseudo-)BNF.
<cumin> :: = <statements> <expression> | <expression>
<statements> ::= <statement> | <statement> <statements>
where
<statement> ::= (Statement)
<expression> ::= (Expression)
As Statement, there are struct
, enum
and let
.
struct
and enum
define new types.
let
gives names for data, which are variables we called.
An Expression represents a Value,
which can contain already defined types and variables.
For examples, number literals are Values and Expressions.
Arithmetic Expressions are Expressions (e.g. (1 + x) / 2
).
Primitive Values
Values are Expressions. In particular, primitive Values can be used in any context.
Numbers
Natural numbers and integers are represented as decimal literal.
For examples,
123
-100000
Zero or positive numbers are Natural numbers by default, and negative ones are Integers.
Floating numbers are denoted with period (.
), like
1.234
-0.1
NOTE:
You can omit leading zeros.
For example, .1
is 0.1
, 1.
is 1.0
.
Number Types
There are 3 types for numbers: Nat
, Int
and Float
.
Nat
is Natural Numbers. It is zero or positive integers.
Int
is Integer Numbers.
Float
is Floating Numbers (pseudo-Real Numbers).
Strings
Strings are denoted by quoting double-quotation ("
).
"Hello, World"
Type
String
is the type for Strings.
Escape
Escape with \
.
"\n\r\t\""
Booleans
There are true
and false
as Boolean Values.
No other values doesn't exist.
Type
Bool
is the type for Booleans.
Struct
Struct Declaration
You can introduce new types with struct
.
struct X {
x: Int,
n: Nat,
s: String,
}
Each fields can have default Values.
struct X {
x: Int,
n: Nat = 0,
s: String,
}
NOTE:
Cumin always allows comma-trailing.
The last ,
is optional completely, but I recommend putting ','.
This Statement syntax can be denoted as <struct>
in the following BNF.
<struct> ::= struct <id> { <fields> }
<fields> ::= <field> | <field> `,` | <field> `,` <fields>
<fields> ::= <id> `:` <type> | <id> `:` <type> `=` <expression>
where
<id> ::= (identifier)
<type> ::= (type name)
<expression> ::= (Expression)
Struct Values
After you declared structs, you can apply them.
For example, the previous struct X
has three fields x
, n
and s
.
You can create X
Values by appling three Values.
X(0, 123, "yellow")
Applied Values can be any Expression.
let n = -3;
X(12 * 3, n, "yel" + "low")
This style is similar to the function apply in many Programming Languages. Cumin has another style.
X { s = "yellow", n = 123, x = 0 }
Because any fields are named, the appling Values can be in any order. And you can omit the fields having default Values.
X { s = "yellow", x = 0 } // n is omitted, and the default Value `0` be applied.
Enum
Cumin's Enum is very simple (likewise C-language).
enum Z {
A, B, C,
}
This declaration introduces new type Z
and three Values
Z::A
, Z::B
and Z::C
.
let
let Statement
The let
Statement gives names to data.
let x: Int = 1 + 2;
Type annotation is freely optional.
let x = 1 + 2;
This Statement computes the expression 1 + 2
(the result is 3
of course),
and we call it x
.
Frankly, this is a variable,
and you can use x
as a Value.
The last ;
is required.
This Statement can be denoted by following BNF;
<let> ::= `let` <name> `=` <expression> `;`
| `let` <name> `:` <type> `=` <expression> `;`
<name> ::= <identifier>
shadowing
When some variables are defined already,
you can declare the same names with let
.
New data shadows old data.
let x = 1;
// Here, x is Nat 1.
let x = "hoge";
// Here, x is String "hoge".
Once variables are shadowed, they cannot be used.
Function
Declaration
Functions are declared with fn
keyword or let
keyword.
<function> ::=
`fn` <name> `(` <args> `)` `=` <expr> `;`
| `let` <name> `(` <args> `)` `=` <expr> `;`
<name> ::= <identifier>
<args> ::=
<empty>
| <var> `:` <type>
| <var> `:` <type> `,` <args>
Examples
fn doubled(x: Nat) = x * 2;
doubled(10) // 20
struct S {
x: Int,
}
fn inc(x: Int) = S { x = x + 1 };
let dec(x: Int) = S(x-1);
[inc(2), dec(2)] // S{x=3}, S{x=1}
Lexical Scopes
let z = 1;
fn one() = z; // this `z` is 1.
// `one` can be referred from here.
let z: String = "2";
// `two` cannot be used yet.
let x = two();
// ↑ ERROR!
fn two() = z; // this `z` is "2" now.
{{
a = one(), // 1
b = two(), // "2"
}}
NOTE: Cannot be used recursively
The internal definition of f
cannot refer to f
.
fn f(x: Int) = f(x - 1); // Error: Cannot resolve name `f`.
This is because of cumin's NO-LOOP policy.
Dictionary
Dictionary is a Value data which can have any fields with any types.
It is denoted by quoting with {{
and }}
.
{{
x = 1,
y = -2.3,
s = "yellow",
}}
If you need, each fields can have type-annotation optionally.
{{
x: Int = 1,
y = -2.3,
s: String = "yellow",
}}
This is sometimes convenience but not type-safe. If two or more dictionaries have same fields with same types, it is chance to define a struct.
Array
An Array is a Value data containing zero or more elements in order.
It is required that all elements have same types,
therefore any Array can be called an Array of type T
where T
is the type of elements.
For example,
the following is an Array of Int
.
[
1,
2,
-3,
4 - 4,
]
The type of this data is Array<Int>
.
Option
If you want null-able Values, Option
is very suitable.
For any type T
, Option<T>
is valid type and it is null-able.
Null Value can be denoted as None
.
let name: Option<String> = None;
In other hand, not null Values for null-able are denoted with Some(_)
.
let name: Option<String> = Some("MGR");
Some
is considered as a natural transformation T -> Option<T>
representing not null Values.
Tuple
Tuples are structures which can have different type values.
let x = (1, "str"); // (Nat, String)
x
is a pair of Nat
and String
.
The (Nat, String)
is the type of x
.
If you aren't familiar to tuples, this is considered as a kind of struct, which has no field names.
struct Anonymous {
__field_0: Int,
__field_1: String,
}
let x = Anonymous(1, "str");
Typing
In cumin, all data are typed gradually. Enjoy!
Types
Primitive Types
There are following types in prior.
Any
Nat
Int
Float
Bool
String
Array<_>
Option<_>
Any
is the top type for any values.
This is convenient for gradual typing.
_
is alias for Any
.
Nat
is for Natural Numbers (0 or positive integers), and Int
is for Integers.
Array
and Option
have type parameter.
<_>
is the placeholder.
In actual code, it should be filled <_>
with some type.
For example, Array<Int>
is an array of Int Values.
Type parameters can be nested.
Array<Array<Option<Int>>>
is an array of an array of option of Int Values.
Custom Types
After you declared struct
-s and enum
-s, the names are new types.
The names will be the names of types.
struct X {}
// `X` is a type now.
let x: X = X();
Type Annotation
Typed Let
let
statement may have type annotations.
let x: Int = 100;
cuminc
evaluates this in the following steps:
- eval
100
- the type infered as
Nat
because it is a (non-negative) natural number.
- the type infered as
- natural cast
x
is annotated asInt
.Nat
can be casted toInt
naturally.- get
100
asInt
.
- name it
x
In the natural cast, cuminc
doesn't coerce forcibly.
For example, String
to Int
, Int
to Nat
.
NOTE: If you need, as
-cast coerce to other types.
The type annotation is optional. If it is omitted, the step 2 will be skipped.
let x = 100;
In this example, x
is Nat
.
Typed Struct
In structs, all fields should be type annotated.
struct S {
x: Nat,
y: Int,
z: Array<String>,
}
When constructing struct values (applying), cuminc
checks the types of applied values.
S {
x = 1,
y = -2,
z = ["cumin"],
}
Type Checking
Array
In general JSON, Arrays can contain various values.
// In JSON, this is OK.
[1, 3.14, ["cumin"]]
But this is very strange and buggy. In cumin, Arrays should contain values with same type.
// In cumin, this is NG.
[1, 3.14, ["cumin"]]
This occurs and error
Error: Cannot infer type of Array([Nat(1), Float(3.14), Array(String, [Str("cumin")])]); Hint: Array cannot contain values with different types.
Any Type
Any
is the top type.
let x: Any = 100;
This is ok. And don't worry. cuminc
knows that x
is Nat
(because 100
is Nat
).
Any
is convenient in some cases.
This example is trivial, and it is equivalent to let
statement without type annotation.
For example, Array<Any>
is a type for Something Arrays.
let xs: Array<Any> = [
1, 2, 3
];
In this example,
- The elements are all
Nat
. xs
is a something arrayArray<_>
.- These facts conclude that
xs
isArray<Nat>
.
Because _
is an alias for Any
, you can write
let xs: Array<_> = [
1, 2, 3
];
It is unsafe that struct fields are declared as Any
.
struct Data {
data: Any,
}
Since data
can be any values, followings are all valid.
let x = Data {
data = 1, // Nat
};
let y = Data {
data = 3.14, // Float
};
let z = Data {
data = ["cumin"] // Array<String>
};
And cuminc
knows only that x
, y
and z
are just Data
, and ignores the type of data
.
So they have all same type!
Hack: Array with various data.
In cumin v0.9.7, This is ok.
struct Data {
data: Any,
}
let x = Data {
data = 1, // Nat
};
let y = Data {
data = 3.14, // Float
};
let z = Data {
data = ["cumin"] // Array<String>
};
[x, y, z]
Because the last data is just Array<Data>
.
NOTE: No warrantry to support this hack.
Natural Cast
For types S
and T
,
we denote S -> T
describing that S
can be casted to T
naturally.
And S -> T
if and only if the following code is valid when x
has type S
:
let y: T = x; // when x:S.
There are two rules for natural cast.
Numbers downcast
Nat -> Int -> Float
NOTE: ->
is transitive.
The fact that S -> T
, T -> U
and S -> U
are denoted as S -> T -> U
.
Any natural numbers can be considered as integers or as floating numbers. But the inverse doesn't hold on.
All values are Any
For any type S
, S -> Any
.
Union Types
type
statement
Union Types are declared with type
keyword and variants are described one or more types separated by |
.
type T = Int | String;
Using this T
,
Int
values and String
values can be same type values.
This is convenient to construct an array of Int
or String
(i.e. Array<T>
).
You can represent more complicated types with struct
.
struct A { a: Nat }
struct B { b: String }
struct C { c: Array<String> }
type T = A | B | C;
NOTE
Union Types is like union-sets, and not Sum Types (or disjoint union). For example,
type T = Int | Int;
this equals to just Int
exactly,
because nobody distinguish left Int
from right one.
NOTE
Union Types define subtypes. In primitive, cumin defines the following subtype system:
Nat <: Int <: Float
and induces the following implicit type-cast:
Nat -> Int -> Float
And, when type T = A | B;
,
A <: T,
B <: T
holds on.
So, you can cast A -> T
and B -> T
, but we don't cast them implicitly.
In next section, we will show how to cast them.
Casting for Union Types
The names of union types can be casting functions (or injection).
type T = Int | String;
let x: Int = 2;
let t = T(x); // Int -> T
let s = T("hello"); // String -> T
struct A { a: Nat }
struct B { b: String }
struct C { c: Array<String> }
type T = A | B | C;
let t = T(A(1)); // A -> T
let u = T(C { c = [] }); // C -> T
If you mind of the mount of parenthesis, we prepared a syntax-sugar.
Composite Applying Syntax
A.B(x)
will be A(B(x))
.
A.B{x = y}
will be A(B{x=y})
.
struct A { a: Nat }
struct B { b: String }
struct C { c: Array<String> }
type T = A | B | C;
let t = T.A(1); // T(A(1))
let u = T.C{c=[]}; // T(C { c = [] })
Yay!
as-cast
With as
keyword, Values are casted to another types.
// Inter Number Types
let x = 1.0 as Int; // Float -> Int
let y = 2 as Float; // Nat -> Float
// Stringify
let x = 1.0 as String; // "1.0"
let y = -2 as String; // "-2"
// Parse String
let x = "1.0" as Float; // 1.0
let y = "-2" as Int; // -2
NOTE: Type casting is a coercion procedure. It sometimes occurs runtime errors.
blocks
{}
makes a new scope block.
In blocks, you can write whole cumin data.
<block> ::= `{` <statements> <expression> `}`
<statements> ::= <statement> | <statement> <statements>
For example, following code is a valid cumin data.
let x = 1;
x + 1
So, you can write
let z = {
let x = 1;
x + 1
};
z
Here, z
has Value 2
, and the x
is invisible from outer.
This notation can make private variables.
operators
Number Operators
Name | cumin | Example | Math |
---|---|---|---|
Addition | + | x + y | \( x+y \) |
Subtract | - | x - y | \( x-y \) |
Minus | - | -x | \( -x \) |
Multiply | * | x * y | \( xy \) |
Division | / | x / y | \( x/y \) |
Modulo | % | x % y | \( x \bmod y \) |
Power | ** | x ** y | \( x^y \) |
Priority | () | (x + y) * z | \( (x+y)\times z \) |
There are three types for Numbers:
Nat
, Int
and Float
.
The operations do implicit type casting.
For example, the result of Nat + Float
has Float
.
It is one-way Nat -> Int -> Float
.
Bool Operators
Name | cumin | Example | Math |
---|---|---|---|
And | and | x and y | \( a \land b \) |
Or | or | x or y | \( a \lor b \) |
Xor | xor | x xor y | \( a \oplus b \) |
Not | not | not x | \( \lnot a \) |
Equality
Name | cumin | Example | Math |
---|---|---|---|
Equal | == | 1 == 2 [1,2] == [1,2] | \( x = y \) |
Inequal | != | 1 != 2 [1,2] != [1,2] | \( x \ne y \) |
All objects containing your custom struct values can be compared with ==
and !=
.
Orderity of Numbers
Name | cumin | Example | Math |
---|---|---|---|
LessThan | < | x < y | \( x < y \) |
LessEqual | <= | x <= y | \( x \leq y \) |
GreaterThan | > | x > y | \( x > y \) |
GreaterEqual | >= | x >= y | \( x \geq y \) |
Only numbers can be compared.
Array Operators
Name | cumin | Example | Math |
---|---|---|---|
Concat | ++ | [1] ++ [2, 3] |
Modules
Module file
Module files must contain only statements.
<module> ::= <statements>
<statements> ::= <statement> | <statement> <statements>
Importing modules
use "./path/to/module.cumin";
The path will be read as
- Absolute path
- Relative path from the current directory
- Relative path from the file
Tools
cuminc
cuminc is a compiler for cumin.
Installation
To build from source code, cargo is required. It is recommended to use rustup to install cargo.
From crates.io
$ cargo install cumin
From Github
$ git clone git@github.com:cympfh/cumin.git
$ make install
$ export PATH=$PATH:$HOME/.cargo/bin/
$ which cuminc
Usage
cuminc compiles cumin data into other data format, JSON by default.
$ cuminc <file.cumin>
$ cat <file.cumin> | cuminc
Example
$ echo '{{three = 1 + 2}}' | cuminc
{"three":3}
$ echo '{{three = 1 + 2}}' | cuminc -T yaml
---
three: 3
cumin-py
cumin-py is a Python binding for cumin.
Installation
Firstly cargo is required.
From PyPI
pip install cumin-py
From Github
$ git clone git@github.com:cympfh/cumin-py.git
$ pip install .
Usage
From Python code,
import cumin
data = cumin.loads("{{three = 1 + 2}}")
# {'three': 3}
data = cumin.load("./data.cumin")
# import file `data.cumin`