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"
}}

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:

  1. eval 100
    • the type infered as Nat because it is a (non-negative) natural number.
  2. natural cast
    • x is annotated as Int.
    • Nat can be casted to Int naturally.
    • get 100 as Int.
  3. 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,

  1. The elements are all Nat.
  2. xs is a something array Array<_>.
  3. These facts conclude that xs is Array<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

NamecuminExampleMath
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

NamecuminExampleMath
Andandx and y\( a \land b \)
Ororx or y\( a \lor b \)
Xorxorx xor y\( a \oplus b \)
Notnotnot x\( \lnot a \)

Equality

NamecuminExampleMath
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

NamecuminExampleMath
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

NamecuminExampleMath
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

  1. Absolute path
  2. Relative path from the current directory
  3. 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`