Overview

Denim is a comfortable, familiar programming language designed for interoperability.

Pitch

So, you know how pretty much every modern, garbage-collected language feels eerily like the others lately? I think we can blame this phenomenon on the fact that many of these languages are converging on the same features and concepts; how many languages have added first-class functions, co-routines, data classes, and language-level immutablity recently?

The only tangible differences between one language and another are the ecosystems and platforms that they can unlock for you. Go gets you into the cloud and terminal ecosystems, while JS/TS gets you into the browser and to the edge. Swift and Java get you onto phones, and with C# you can ship on an Xbox.

And it got me thinking: if the languages we use to write our apps are this similar, why on earth are we writing the same logic over and over again? Why can't we write most of our logic, constants, and types once, and use them anywhere? What if there was a language purely designed to be interoperable with other languages?

Denim is that language.

The intent behind Denim is to incorporate the smallest set of common features from these garbage-collected languages sufficient to:

  • Create common types
  • Implement business logic
  • Declare common constants

Of course, it wouldn't hurt to end up with a language that is pleasant to use and maintain while we're at it.

Why "Denim"?

Well, because "denim goes with everything" rimshot.

While our slogan is a little playful and intended to earn a few chuckles, I think it accurately embodies the vision of the language. Denim aims to be as comfortable and complementary as any well-worn pair of jeans. This is a language at its best as a part of your outfit (or codebase for that matter).

Design

Denim is not designed to be particularly fast, sexy, interesting, or well-suited for any specific domain. It should fit right into the source code powering any piece of software - from servers to smart fridges. Denim's guiding design principles, are to be maximally:

  • Easy to reason about and read
  • Pleasing to look at and work with
  • Quick to learn and manipulate

Denim should never feel as esoteric and ornate as Rust, but it should feel a smidge more expressive than Go. It should be easy to read, follow, and document like Java, while getting out of your way and letting you practically solve your problem like Node.js.

Inspiration

As Denim is designed to feel familiar, it borrows heavily from some popular programming languages/runtimes:

  • Dependency management from Deno
  • Module system and batteries-included standard library championed by Go
  • Syntax largely stolen from Rust with a few tricks from Dart and Python included
  • Extensibility workflow taken from both Dart and Rust

tl;dr "Dart with Rust syntax and Go's packaging model".

Compatibility

For Denim to be useful, in needs to be able to interop with most of the major languages with an established industrial presence. Initially, Denim is being developed with the following transpilation targets in mind:

  1. TypeScript for web (and also everything else)
  2. Python for data science
  3. Swift for Apple's ecosystem
  4. Kotlin for Google's ecosystem

Thereafter, compatibility could be coming to a friendly neighborhood language near you!

Journal

Here lies all of @skeswa's notes on Denim's implementation and design over time.

Denim

I think Denim needs to be a quite a bit simpler than the original design.

Principles

In order of importance:

  1. Low-mental overhead
  2. Aesthetic
  3. Expeditious

Sub-principles

  • Low-mental overhead
    • Familiar
    • Scannable
    • Aggressive complexity containment
  • Aesthetic
    • Fluent (things ergonomically chain together)
    • DSL friendly
    • Terse
  • Expeditious
    • More meaning in less syntax
    • Highly extensible

Main Ideas

Code Samples

Common-sense builtins

#![allow(unused)]
fn main() {
let u1: u8  = 1u8;
let u2: u32 = 12u32;
let i:  i32 = 123i32;
let i:  i64 = 123i64;
let f1: f32 = 1.2f32;
let f2: f64 = 1.2345e-6f64;

let b: bool = false;
let s: str  = "hello";

let list: [i32]     = [1, 2, 3];
let set:  [:i32]    = [:1, :2, :3];
let map:  [str:i32] = ["a": 1, "b": 2, "c": 3];
}

Dart-style doc comments

#![allow(unused)]
fn main() {
/// This is a doc comment.
///
/// These comments are intended to document your code in greater detail. To
/// facilitate this greater detail, these kinds of comments have:
/// 1. **full** _Markdown_ `support`
/// 2. Dart-style `[]` code links
///    For example, [abc] references the variable created above explicitly.
let forty_two = 42;
}

Go-style packaging and visibility

foo/xy.πŸ‘–

#![allow(unused)]
fn main() {
type X = i32 | str;
pub type Y = { a: 1, b: false };

fn x() {
  print("x");
}

fn y() {
  print("y");
}
}

foo/z.πŸ‘–

#![allow(unused)]
fn main() {
pub type Z = X & Y;  // all of foo/xy.πŸ‘– is visible to the whole foo/ directory

pub fn z() {
  x();
  y();
}
}

bar/b.πŸ‘–

#![allow(unused)]
fn main() {
pub type B = Y | Z;

pub fn b() {
  z();
}
---
from ~/foo use Y, Z, z;  // Can only see `pub` decls from `~/foo`
}

Rust-style enums

#![allow(unused)]
fn main() {
// Create an `enum` to classify a notification event. Note how both
// names and type information together specify the variant:
// `MessageReceived != MessageSent` and `UserJoined(String) != UserLeft(String)`.
// Each is different and independent.
enum NotificationEvent {
  // An `enum` variant may either be `unit-like`,
  MessageReceived,
  MessageSent,
  // like tuple structs,
  UserJoined(String),
  UserLeft(String),
  // or c-like structures.
  Reaction { emoji: char, message_id: u64 },
}

// A function which takes a `NotificationEvent` enum as an argument and
// returns nothing.
fn handle_notification(event: NotificationEvent) {
  event.match {
    NotificationEvent::MessageReceived => print("message received"),
    NotificationEvent::MessageSent => print("message sent"),
    // Destructure `username` from inside the `enum` variant.
    NotificationEvent::UserJoined(username) => print("user '{}' joined", username),
    NotificationEvent::UserLeft(username) => print("user '{}' left", username),
    // Destructure `Reaction` into `emoji` and `message_id`.
    NotificationEvent::Reaction { emoji, message_id } => {
      print("reaction '{}' on message {}", emoji, message_id);
    },
  }
}
}

Rust-style pattern matching

#![allow(unused)]
fn main() {
// A function which takes a `NotificationEvent` enum as an argument and
// returns nothing.
fn handle_notification(event: NotificationEvent) {
  event.match {
    NotificationEvent::MessageReceived => print("message received"),
    NotificationEvent::MessageSent => print("message sent"),
    // Destructure `username` from inside the `enum` variant.
    NotificationEvent::UserJoined(username) => print("user '{}' joined", username),
    NotificationEvent::UserLeft(username) => print("user '{}' left", username),
    // Destructure `Reaction` into `emoji` and `message_id`.
    NotificationEvent::Reaction { emoji, message_id } => {
      print("reaction '{}' on message {}", emoji, message_id);
    },
  }
}
}

Strict param labeling

#![allow(unused)]
fn main() {
fn short_story(name: str) -> str {
  "My name is $name"
}

print(short_story("Galois"));

fn story(age: i32, name: str) -> str {
  "${short_story(name)} and I am $age years old"
}

print(story(name: "Galois", age: 20));
print(story(age: 20, name: "Galois"));
print(story(20, "Galois"));  // Compile time error

fn tuple_story((age, name): (i32, str)) -> str {
  story(age, name)
}

print(tuple_story((20, "Galois")));
}

Imports at the bottom

#![allow(unused)]
fn main() {
pub fn my_func(ctx: str) {
  let something: SomeType = some_func(ctx);

  let some_value = something.match {
    SomeVariant => 42,
    _ => 0,
  };

  let cool_thing: CoolThing = some_value.a_func_from_a_trait_impl(ctx);

  cooler.cool_down(cool_thing);
}

---
from cool_lib use CoolThing;
from cooler_lib use * as cooler;

from ~/repo_relative/dir/a_file use SomeType::ATraitImpl;

from relative/sub_dir/some_file use
  SomeType,
  some_func,
  SomeEnum::SomeVariant;
}

Mutation semantics

#![allow(unused)]
fn main() {
let immutable_var = (1, 2, 3);

// immutable_var = (2, 3, 4); // compile time error
// immutable_var.0 = immutable_var.0 + 1; // compile time error

let mut mutable_var = (1, 2, 3);

mutable_var = (2, 3, 4);
// mutable_var.0 = mutable_var.0 + 1; // compile time error

let mutable_val_in_immutable_var = (1, 2, 3).mut;

// mutable_val_in_immutable_var = (2, 3, 4); // compile time error
imutable_val_in_mmutable_var.0 = imutable_val_in_mmutable_var.0 + 1;

mutable_var.0 = 2;
mutable_var.1 = 3;
mutable_var.2 = 4;

mutable_var.inc();

print(mutable_var); // Prints "(3, 4, 5)"

immutable_var.inc(); // Compile time error

impl (i32, i32, i32) {
  fn inc(mut self) {
    self.0 = self.0 + 1
    self.1 = self.1 + 1
    self.2 = self.2 + 1
  }
}
}

* not any

Denim uses the equivalent of TypeScript's unknown, but calls it * instead.

#![allow(unused)]
fn main() {
let some_json = "{hello: \"world\"}";

let parsed_json: {str: *} = parse_json(some_json);

let world = some_json["hello"];

world.lower.print(&mesage); // Compile time error because `world` is `*`.

world.as(str).lower.print(&mesage); // πŸ‘ŒπŸΎ
---
from convert use parse_json;
}

Keyword suffixing

#![allow(unused)]
fn main() {
choice.match { Yes => print("yes!"), No => print("no.") };

result.try;

predicate.if { print("true") } else { print("false") };

eventual.await;

operation.async;

let three_point_o = 12.as(f32) / 4;

let ref_to_some_fn = some_fn.fn;

[1, 2, 3].mut.add(4);
}

Optional chaining

#![allow(unused)]
fn main() {
type Node<T> = {
  next: Node?,
  val: T,
};

fn fifth_node<T>(node: Node<T>) -> T where T: str|i32 {
  node.next.next.next.next.or_else("fallback") // No need for `?.`-style chaining - it is built in
}
}

Selfification

#![allow(unused)]
fn main() {
fn add(a: i32, b: i32) -> i32 {
  a + b
}

print(add(40, 2));
print(40.add(&a, 2));
}

Getterification

Caveats:

  • .fn for function references
  • Must return something
#![allow(unused)]
fn main() {
fn rand(min = 0.0, max = 1.0) -> f32 {
  let (actual_max, actual_min) = if max > min { (max, min) } else { (min, max) };

  (actual_max - actual_min) * secure_rand.trunc + actual_min
}

print(rand(min: -2.0, max: 2.0));
print(rand(max: 23.0))
print(rand());
print(rand);

fn print_hello() {
  print("hello")
}

print_hello; // compile time error
---
from crypto use secure_rand;
}

body param

Inspired by Kotlin trailing lambdas.

#![allow(unused)]
fn main() {
fn do(when: Time = Time::now(), body: fn() -> void) {
  pause(until: when);

  body();
}

do(when: Time::now(), body: || print("hello world"))

do(Time::now()) {
  print("hello world")
}

do {
  print("hello world")
}

fn do_with_args(body: fn(args: *) -> void) {
  body("args")
}

do_with_args {
  print(it);
}

fn do_with_return(body: fn() -> u8) {
  print(body);
}

do_with_return {
  8
}
---
from clock use pause, Time;
}

Operator aliasing for Option and Result

#![allow(unused)]
fn main() {
fn why_write(a: Option<T>) {}
fn when_you_could_write(a: T?) {}

fn why_do(a: Result<T>) {}
fn when_you_could_do(a: T!) {}

fn why_use_many_word(a: Result<Option<T>>) {}
fn when_few_word_do_trick(a: T?!) {}
}

Destructuring

#![allow(unused)]
fn main() {
let tuple = (1, 2, 3);

let (a, b, c) = tuple;
let (..a_and_b, still_c) = tuple;
let (a, b, ..c_in_a_tuple) = tuple;

let struct = { w: (1.1, 2.2), x: 3, y: "4", z: false };
let { w: (w1, w2), x, y, z} = struct;
let {..everything_but_z, z} = struct;

let list = [1, 2, 3];
let [first_from_list] = list;
let [_, second_from_list] = list;

let map = ["i": -1, "j": -2, "k": -3];
let ["i": i, ..everything_but_i] = map;

enum E {
  Bools { m: bool, n: bool },
  Nums(i32),
  Nothing,
}

let e1 = E::Bools { m: false, n: true };
let e2 = E::Nums(0.01);
let E::Bools { m } = e1;
let E::Nums(num) = e2;

}

Autoboxing

#![allow(unused)]
fn main() {
let optional_num: i32? = 123; // `123` gets turned into `Some(123)` automatically
fn box_me(n: i32) -> i32? {
  n
}

let str_result: str! = "hello"; // `"hello"` gets turned into `Ok("hello")` automatically
fn box_me_again(s: str) -> str! {
  s
}

let optional_bool_result: bool?! = true; // `true` gets turned into `Ok(Some(true))` automatically
fn box_me_baby_one_more_time(b: bool) -> bool!? {
  b
}
}

Lexical concurrency

#![allow(unused)]
fn main() {
fn waste_time() -> i32 {
  // t = 0s
  sleep(1.seconds);
  let a = 1;
  sleep(50.seconds);
  let b = 2;
  sleep(2.seconds);

  // t = 50s
  let eventual = sleep(1000.seconds).async;

  // t = 50s
  eventual.await;

  // t = 1050s
  a + b
}
}

Anonymous structs

#![allow(unused)]
fn main() {
type Foo = { foo: bool };
type Bar = { bar: i32 };

type FooBar = Foo | Bar;

fn print_foobar(foobar: FooBar) {
  let { foo, bar } = foobar;

  foo.match {
    Some(foo) => print("foo: $foo"),
    None => print("bar: ${bar.unwrap}}"),
  }
}
}

A little of everything

  • Common-sense builtins
  • Dart-style doc comments
  • Go-style packaging and visibility
  • Rust-style enums
  • Rust-style pattern matching
  • Strict param labeling
  • Imports at the bottom
  • * not any
  • Mutation semantics
  • Keyword suffixing
  • Optional chaining
  • Selfification
  • Getterification
  • body param
  • Operator aliasing for Option and Result
  • Destructuring
  • Lexical concurrency
  • Anonymous structs
  • Minimum viable operators
  • Function overloading
  • Trait-based inheritance
  • impl anything
#![allow(unused)]
fn main() {
enum Weather {
  Cloudy,
  Sunny { uv_index: f32 },
  PartlyCloudy,
  Rainy(RainForecast),
}

/// Fetches information about the wetaher for the specified [date] in the
/// location indicated by [zip].
pub fn fetch_weather_data(date: Date, zip: ZipCode) -> [WeatherData]! {
  let raw_weather_data =
      fetch("https://theweather.com/$zip?date=${date.as_dd_mm_yyyy}")
      .try
      .as_str;

  raw_weather_data
    .lines
    .map(|line| WeatherData::parse(line))
    .collect(to_result_list.fn)
    .ctx("Parsing each line of raw weather data")
}

fn seven_day(zip: ZipCode) -> [WeatherData!] {
  let today = Date::today;

  let list_of_eventuals: [Eventual<WeatherData!>] = 0..7.iter()
      .map(|day_offset| today.prev_day(day_offset))
      .map(|date| date.fetch_weather_data(&date, zip).async)
      .collect(to_list.fn)

  let eventual_of_list: Eventual<[WeatherData!]> = list_of_eventuals.flattened;

  eventual_of_list.await
}

impl DoI {
  fn need_an_umbrella_today(home_zip: ZipCode, work_zip: ZipCode) -> bool {
    // The two invocations of `fetch_weather_data` happen concurrently:
    let home_weather_data = fetch_weather_data(date: Date::today, zip: home_zip);
    let work_weather_data = fetch_weather_data(date: Date::today, zip: work_zip);

    home_weather_data.matches(Weather::Rainy)
      or work_weather_data.matches(Weather::Rainy)
  }
}

impl WeatherData {
  fn as_weather(self) -> Weather {
    self.match {
      WeatherData { rain_data } if rain_data.pct_chance_of_rain > .5 =>
          Weather::Rainy(RainForecast::from(rain_data))
      WeatherData { sun_quotient, uv_index } if sun_quotient > .7 =>
          Weather::Sunny { uv_index },
      WeatherData { sun_quotient } if sun_quotient <= .7 and sun_quotient > .25 =>
          Weather::PartlyCloudy,
      WeatherData { sun_quotient } => Weather::Cloudy,
    }
  }
}

---
from location use ZipCode;
from time use Date;

from ~/my/weather/lib use WeatherData;

from ./util/web use fetch;
}

Denim's Language Design Takes

Programming language design opinions sorted more or less from hottest (most unique/surprising) to coldest (Least unique/surprising).

Hot Takes

Concurrency

In a typical concurrency regime, like async/await, programs can "opt in" to asynchrony in specific spots. Additionally, special functionality like Promise.all(...) is necessary to express the idea that an operation should wait for multiple prior operations to complete before it can begin.

Denim takes a very different approach: in Denim, concurrency is a first-class syntactic concern.

Firstly, all operations in Denim are "awaited" automatically. When you don't need to wait for something to complete, you can let in complete without blocking using the async keyword.

#![allow(unused)]
fn main() {
quick_operation_1();

// We wait for `quick_operation_1` to complete before starting
// `quick_operation_2` without having to use a keyword - just works.
quick_operation_2();

let eventual_value = super_slow_operation().async;
thing_that_needs_to_happen_right_away();

print("it's ready now: ${eventual_value.await}");
}

Secondly, and perhaps most importantly, Denim allows the programmer to ergonomically stipulate which things should happen at the same time, and which should happen in sequence.

Statements separated a blank line are executed in sequence.

#![allow(unused)]
fn main() {
let greeting = "hello";

// Performs a network call to figure out what planet we are on.
let planet = get_current_planet();

// Waits for a 3 second time to elapse.
wait_for_3_seconds();

// Nothing is printed to the screen until 3 seconds **after** `planet` is
// fetched.
print("$greeting, $planet!");
}

Statements not separated by a blank line are executed concurrently.

#![allow(unused)]
fn main() {
let greeting = "hello";
let planet = get_current_planet();
wait_for_3_seconds();

// We print the moment all 3 of the statements in the previous group complete.
print("$greeting, $planet!");
}

The result is a terse yet readable way to sequence asynchronous logic.

Fluency

In many lanuages, lots of the good stuff is only usable when invoking a function as a member of a thing. Take for instance ?. in JavaScript.

const foo = Math.random() > 0.5 ? { bar() {} } : null;

foo?.doSomethingMaybe(); // this is dope

function baz(foo) {
  // ...
}

// I need this grossness to **safely** call `foo`:
if (foo) {
  baz(foo);
}

This seems silly. ?. is one example of the fact that we humans love to create sequential, causal chains of things to do. Breaking these chains with control flow like if does not feel good.

Denim is designed to make "chaining" operations as ergonomic. In a sense, Denim takes Rust's .await syntactic concept to its logical conclusion: let's make everything that can be used as a "prefix" usable as a "suffix".

#![allow(unused)]
fn main() {
fn baz(foo: Foo, scalar = 1.0) -> double {
  // ...
  scalar * 123
}

foo.baz(foo: it, scalar: 2.0).if it < 200 {
  print("it is $it!")
}
}

Denim accomplishes a language feature called "fluency" by:

  • Allowing control flow keywords to be suffixed like .try and .if
  • Supporting the it keyword which is the value of the preceding expression in the "chain"

"DSL"-ability

Denim is designed to make library APIs easy on the eyes. It accomplishes this via two language quirks:

  1. Functions with zero arguments may be invoked without a trailing ()
    This allows functions to behave like properties, encapsulating complexity the same way getters do in other languages.
    #![allow(unused)]
    fn main() {
    fn forty_two() -> int { 42 }
    
    fn ten(to_the_power_of = 1.0) -> int { (10 ** to_the_power_of).round() }
    
    print(forty_two * ten) // Prints "420"
    }
  2. Functions can have a special parameter block of type fn() -> T that adds aesthetically pleasing syntax sugar\
    #![allow(unused)]
    fn main() {
    fn foo(a: string, block: fn() -> string) -> string {
      "a $block"
    }
    
    foo("hello") {
      foo("darkness") {
         "my old friend"
      }
    }
    }

2+ function parameters must be explicitly labeled

Denim requires that parameters are labeled when 2 or more parameters are included in a function invocation.

#![allow(unused)]
fn main() {
fn add(a: int, b = 0) -> int {
  a + b
}

add(1) // compiles

add(1, 2)       // does not compile
add(a: 1, b: 2) // compiles
}

When all you need is positional arguments, consider a tuple or array.

#![allow(unused)]
fn main() {
fn add(nums: (int, int?)) -> int {
  nums.0 + (nums.1 ?? 0)
}

add((1, 2)) // compiles
}

Medium Takes

Imports at the bottom of the file

This is normal:

import stuff up here

yada yada yada

maybe some exports

mhm yeah

**finally** the stuff you came here to read

Denim puts imports and exports a the bottom of the file after the logic and stuff.

#![allow(unused)]
fn main() {
pub type ImportantStuff = struct {
  // ...
};

---
from "github.com/some/lib" use something;

from "~/internal/lib" show something_else;

}

No > or >= comparison operators

Why use > or >= when < and <= do trick?

No bitwise operators whatsoever

Denim does not have bitwise and, or, zor, and not. Why? Most logic doesn't use these operators. Logic that needs to do bitwise math should use good ol' fashioned functions. Good riddance.

and and or instead of && and ||

Pretty much only Python does this, but I think it reads nicely and reduces parsing ambiguity (|| could be the beginning of a lambda).

Everything is an expression

Like in Rust, most things in Denim are expressions. This means they yield a value. ; is used to turn an expression into statement. The value of a statement is ignored.

#![allow(unused)]
fn main() {
print(if value > 8 { "pretty big" } else { "not that big" });
}

Spaces instead of tabs

Yeah. 2 space indent. Deal with it 😎.

Cold Takes

Rust-style enum

Denim steals Rust's enums because they are super expressive while remaining practical and readable.

#![allow(unused)]
fn main() {
enum Take {
  Hot { is_outta_pocket: bool },
  Medium,
  Cold(temp: f32),
}
}

Rust-style pattern matching

Denim steals Rust's pattern matching because it gets a lot right.

#![allow(unused)]
fn main() {
match number {
  // Match a single value.
  1 => print("One!"),
  // Match several values.
  2 | 3 | 5 | 7 | 11 => print("This is a prime"),
  // Match an inclusive range.
  13..=19 => print("A teen"),
  // Handle the rest of cases.
  _ => print("Ain't special"),
}

// Match is an expression too
let binary = match boolean {
  // The arms of a match must cover all the possible values
  false => 0,
  true => 1,
};
}

The only thing it was missing is being able to eaily match a single arm in a if statement:

#![allow(unused)]
fn main() {
if thing is Some::EnumVariant {
  print("Bingo!");
}
}

Dart-style string syntax

Dart makes declaring, concatenating, and interpolating values within strings super easy. Denim steals (most) of this syntax. A notable exception Denim string literals use " instead of '.

let abc = "123""xyz" // concat just by putting literals next to each other.
let multiline = """
  take
    all
      the
        space
          you
            need
""";

let a = 1;
let b = 2;

let c = "$a + $b = ${a = b}" // Use `$` for string interpolation!

Tour

There is always where you want to start with a new language - what will Denim look like? The answer is a lot like Rust. I just happen to think that Rust gets a lot of stuff right. That said, expect some deviations made in the interest of developer ergonomics and Denim's particular domain challenges.

Primitives

Denim's primitives are mostly stolen from Go. It has:

  • bool
    Boolean value that can be true or false.

    A bool literal is either true or false.

  • byte
    An 8-bit unsigned integer, often in strings. NOTE: in the context of a string, byte does not encapsulate the semantic concept of a "character" since in some encodings, like UTF-8, a character may be expressed with more than one byte.

    A byte literal is just a non-negative number like 128.

  • float
    A 64-bit signed floating point number.

    A float literal is either dot-delimited decimal numbers like -12.80001, or scientific notation like 1.2e-12.

  • int
    A 64-bit signed integer number.

    An int literal is just a number like 11 or -7. Notably, Denim does not support binary, hex, or octal int literals.

  • rune
    A 32-bit unsigned integer number intended to represent the semantic concept of a "character" in strings.

    A rune literals is just a single-quoted character like 'k' or 'πŸ’©'.

  • string
    A sequence of bytes semantically associated with text.

    A string literal is usually a quoted span of text like "hello world", but it comes in other flavors too.

    "\"With escaped characters\t";
    
    """A
    multiline
    string""";
    
    r#"
     a raw string where \n escaped characters are not a thing
     "#;
    

    Denim also supports Dart-style string concatentation. You can concatentate string literals by declaring them adjacent to one another. Though many other languages support it however, Denim will not support string concatentation via the + operator.

    "this a long string that I'm' worried will end up exceeding the line limit, "
    "but luckily I can just continue with another adjacent string on the "
    "next line";
    

    string interpolation will look very familar to fans of ES2015+ or Dart.

    "${1 + 2 + 3 + 4 + 5} is fifteen";
    
    "You can also reference $some_variable without the `{}`";
    

Special primitives

It is important to note that Denim does not have a null-type like Go's nil. The closest idea that Denim has in this regard is the void. The void type has exactly one value, void, and is used when there is no other meaningful value that could be returned. void, much like Rust's (), is most commonly seen implicitly: functions without a -> implicitly have a void return type.

#![allow(unused)]
fn main() {
// The functions below are equivalent:

fn long() -> void {}
fn short() {}
}

Denim also steals the unknown type from TypeScript. unknown represents all possible values. Every type is assignable to type unknown. Therefore the type unknown is a universal supertype of the type system. However, the Denim compiler won't allow any operation on values typed unknown - the values must first be cast to a narrower type. For more on this concept, check out some information on TypeScript's unknown.

Variables

Denim steals variable declaration from Rust.

#![allow(unused)]
fn main() {
let abc = 123;
}

Like in Rust, Denim's let creates immutable variables by default. This means that abc cannot by be assigned a new value.

#![allow(unused)]
fn main() {
let abc = 123;
abc = 321; // Compile-time error
}

To create a mutable variable, you need to add a mut prefix to the let keyword.

#![allow(unused)]
fn main() {
let mut xyz = 123;

xyz = 456; // πŸ‘
}

Importantly, Denim does not have a notion of const. Instead let is also used to declare constants at the top-level lexical scope,

Printing

Denim ships with one main way to print out to the console - print.

print is a function (explained in greater depth later) that takes a string which it prints to its own line in the console. In the browser, this means it calls console.log(...) under the hood. Outside of the browser, it appends a line to stdout.

#![allow(unused)]
fn main() {
print("hello world"); // Prints "hello world" on its own line.
}

It is worth noting that more sophisticated logging facilities are available via library.

Tuples

Tuples are a fixed-size collection of different types. They can be helpful in situations where you want to group a few different pieces of data without creating a dedicated, named data structure for them. Rust Tuples are great. Why mess with a good thing? Denim Tuples are functionally identical.

#![allow(unused)]
fn main() {
// Tuple of an `int`, a `string`, and a `bool`. By the way, `let` is how we
// create variables in Denim. We'll elaborate in depth a little later.
let tuple = (123, "456", true);

// You can read different parts of a Tuple with dot notation.
print(tuple.0); // Prints "123"
print(tuple.1); // Prints "456"
print(tuple.2); // Prints "true"

print(tuple.7); // Compile-time error
}

While Denim Tuples are always immutable, they can be quite ergonomic to use and manipulate. Tuples can be composed together via the ... operator, and split apart in a similar way through de-structuring.

#![allow(unused)]
fn main() {
let x = (1.2e-3, 'e', false);
let y = ("yo", ...x, x.1);

print(y); // Prints "(yo, 0.0012, e, false, e)"

let (first, ...middle_stuff, last) = y;

print(first); // Prints "yo"
print(third); // Prints "e"

// NOTE: `middle_stuff` is itself a Tuple.
print(middle_stuff); // Prints "(0.0012, e, false)"
}

Array

Perhaps the most common collection in most languages is an array - an ordered sequence of values that supports random access. In JavaScript, it is called Array while in Rust it is called Vec. Denim Arrays should look feel and behave like Dart's List or JavaScript's Array.

#![allow(unused)]
fn main() {
let array = [1, 2, 3];
}

Like in other languages, Denim Arrays support random access by index with the [] operator.

#![allow(unused)]
fn main() {
// The type of `array` is inferred to be `[int]` here.
let array = [1, 2, 3];

print(array[0]); // Prints "1"
print(array[2]); // Prints "3"

print(array[17]); // Compile-time error
}

Need your Array to be mutable? Prefix the literal with a mut.

#![allow(unused)]
fn main() {
let mutable_array = mut [1, 2, 3];
}

Sometimes when you have a mutable Array, it starts empty. In this situation, the inner type of the Array is ambiguous, so it falls back to unknown by default. You can help provide more type information on the variable or explicitly cast the Array literal to correct this.

#![allow(unused)]
fn main() {
let another_array: mut [string] = mut [];
// In Denim, like in Rust, you can cast a value with the `as` keyword.
let yet_another_array = [] as [bool];
}

Denim can also infer the inner type of the Array later on from context.

#![allow(unused)]
fn main() {
// `one_more_for_the_road` is starts as a `mut [unknown]`.
let one_more_for_the_road: = mut [];
// Now Denim knows that `one_more_for_the_road` must be an `mut [int]`.
one_more_for_the_road.add(2)
}

Denim Arrays have lots of helpful methods focused on mutation.

#![allow(unused)]
fn main() {
let some_array: mut [int] = mut [];
some_array.add(2);
some_array.add(1);

print(some_array); // Prints "[2, 1]"

some_array.remove_at(0);

print(some_array); // Prints "[1]"

print(array[2]); // Prints "3"
print(mutable_array[0]); // Prints "2"
}

Denim Arrays ship with special syntax to instantiate arrays with a fixed number of identical values.

#![allow(unused)]
fn main() {
let eight_zeroes = [0; 8]; // `eight_zeroes` is an `[int]`
let four_strings = [""; 4]; // `eight_zeroes` is an `[string]`
}

Denim Arrays are spreadable with ... just like JavaScript arrays.

#![allow(unused)]
fn main() {
let x = [1, 2, 3];
let y = [...x, 4, 5, ...x, 6];

print(y); // Prints "[1, 2, 3, 4, 5, 1, 2, 3, 6]"
}

Denim allows for ergonomic slicing and dicing of arrays via de-structuring. Denim Array de-structuring is very similar to JavaScript Array de-structuring.

#![allow(unused)]
fn main() {
let y = [1, 2, 3, 4, 5, 6];

let [first, second, ...middle_stuff, last] = y;

print(first); // Prints "1"
print(second); // Prints "2"
print(last); // Prints "6"

// NOTE: `middle_stuff` is itself an Array.
print(middle_stuff); // Prints "[3, 4, 5]"
}

Enums

Enums are a great way to model data that is best described in categories. For example, the concept of "days of the week" could be accurately described a string, but since there are only seven kinds of them, enum is a better fit. enum allows you to explicitly enumerate each variant.

#![allow(unused)]
fn main() {
enum DayOfTheWeek {
  Monday,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday,
  Sunday,
}
}

Broadly speaking, explicit enumeration via enum makes validation and pattern matching over your data more ergnomic and less error prone. Sometimes, it makes sense to also attach data to each variant, allowing enum to function more like structs or classes in other languages.

#![allow(unused)]
fn main() {
enum IpAddress {
  // V4 IP addresses look like "192.168.1.1".
  //
  // Each `IpAddress::V4` has an inner field of type `[int]`.
  V4([int]),
  // V6 IP addresses look like "2001:db8::2:1".
  //
  // Each `IpAddress::V6` has a `segments` field of type `string` and a
  // `segment_count` of type `int`.
  V6 { segments: string, segment_count: int },
}

let some_v4_address = IpAddress::V4([192, 168, 1, 1]);

print(some_v4_address.0); // Prints "[192, 168, 1, 1]"

// `segments` does not need to be explicitly specified since it is the only
// field of `IpAddress::V4`.
let some_v4_address = IpAddress::V6 {
  segments: "2001:db8::2:1",
  segment_count: 5,
};

print(some_v6_address.segments); // Prints "2001:db8::2:1"
print(some_v6_address.segment_count); // Prints "5"
}

Special enums

TODO(skeswa): flesh this out by copying (Option, Result) from rust.

#![allow(unused)]
fn main() {
pub enum Option<T> {
  None,
  Some(value: T),
}

pub enum Result<T, E> where E = Error {
  Err(error: E),
  Ok(value: T),
}
}

We have special syntactic sugar for Option:

#![allow(unused)]
fn main() {
// This:
let x: Option<string>;
// Is the same as this:
let x: string?;
}

Option is used in much of the same way that null, nil, or undefined is used in other languages - to communicate that a value may be absent. Unfortunately, dealing with Option and other similar ideas is sort of awkward. You end up writing a lot of if-this-then-that logic dealing with whether the value is there or not.

if (!thing.x || !thing.x.y || !thing.x.y.z) {
  return "nope";
}

return thing.x.y.z.value;

To reduce the cruft, lanuages like JavaScript added the ?. and ?? operators.

return thing.x?.y?.z?.value ?? "nope";

Better right? Well, sure. Except that now you have two operators that basically do the same thing depending on context - . and ?.. They enable access into the state and behavior of values and objects.

In Denim, all instances of . behave like ?. does in other languages - it falls back to Option::None automatically when an optional value is absent. Denim retains ?? as syntactic sugar for Option::unwrap_or(..):

#![allow(unused)]
fn main() {
thing.x.y.z.value ?? "nope";
}

Maps

TODO(skeswa): flesh this out (Dart Maps).

#![allow(unused)]
fn main() {
// Below, `x` is inferred to be of type `{string: int}`.
let x = {
  "one": 1,
  "two": 2,
  "three": 3,
};

let y: {string: unknown} = {
  1: "one",
  2: [1, 2],
  3: {"hello": "world"},
};

print(x["one"]); // Prints "Some(1)"
print(y[2]); // Prints "Some([1, 2])"

print(x["four"]); // Prints "None"
print(y[7]); // Prints "None"
}

Denim Maps, like other Denim data structures, are made mutable with a mut prefix.

#![allow(unused)]
fn main() {
let mutable_map = mut {"one": 1, "two": 2};
}

Mutable Denim Maps feature useful methods and operators stolen from the Maps of other languages.

#![allow(unused)]
fn main() {
let mutable_map: mut {string: string} = mut {};
mutable_map["hello"] = "world";
mutable_map["foo"] = "bar";

print(mutable_map); // Prints "{hello: world, foo: bar}"

mutable_map.remove_key("hello");

print(mutable_map); // Prints "{foo: bar}"
}

TODO(skeswa): spread notation

#![allow(unused)]
fn main() {
let x = {
  "one": 1,
  "two": 2,
  "three": 3,
};
let y = {...x, "four": 4};
}

TODO(skeswa): destructuring

#![allow(unused)]
fn main() {
let {"one": one, "two": two, ...everything_else} = y;
}

Sets

TODO(skeswa): flesh this out (Dart Sets).

#![allow(unused)]
fn main() {
// The type of `x` is inferred to be `{string}` here.
let x = {"one", "two", "three"};
let y = mut {1, 2.2, 3};
}

TODO(skeswa): spread notation

#![allow(unused)]
fn main() {
let x = {
  "one": 1,
  "two": 2,
  "three": 3,
};
let y = {...x, "four": 4};
}

TODO(skeswa): no destructuring

Type Aliases

Denim allows you to come up with another name for an existing type using something called a type alias. Like Rust type aliases, Denim type aliases are declared with the keyword type. For example, the following defines the type Point as a synonym for the type (int, int):

#![allow(unused)]
fn main() {
type Point = (int, int);

let p: Point = (41, 68);
}

Type aliases can be useful to abbreviate long, verbose types. Type aliases also come in handy when attaching more semantic meaning or description to a common type, like a tuple in the case above, would make your code easier to reason about.

Operators

  • and, or
    Denim steals these logical comparison operators from Python. Why? Well, truth be told, it is mostly to reduce the ambiguity of || (see: Rust closure syntax). But also, I think it sorta reads nicely since keywords are highlighted to be pretty eyecatching usually.
  • ==, !=
    The strict equality and inequality operators work just the way that you think they do: they check if primitives are equal, or if non-primitives point to the same address in memory.
  • ===, !==
    These two operators are congruence and incongruence operators in Denim. They are meant to check if two values are qualitatively equal or not. We use the Eq trait to implement these operators.
  • +, -, *, /, and %
    Arithmetic operators can only be applied to numbers of the same kind.
  • **, the exponentiation operator, is stolen from Python
  • ~/, the truncating division operator, is stolen from Dart

You might be wondering where the bitwise operators are - there are none! Looking for operator overloads? You won't find them here.

Good riddance.

Comments

Comments are almost purely stolen from Rust.

#![allow(unused)]
fn main() {
// This is a simple line comment.

let abc /* this is an inline comment */ = /* stick these anywhere */ 123;

/// This is a doc comment.
///
/// These comments are intended to document your code in greater detail. To
/// facilitate this greater detail, these kinds of comments have:
/// 1. **full** _Markdown_ `support`
/// 2. Dart-style `[]` code links
///    For example, [abc] references the variable created above explicitly.
let forty_two = 42;
}

Expressions and statements

Following Rust's lead, Denim is (mostly) an expression language. Rust's documentation does a good job describing what this means, and some of its implications:

Most forms of value-producing or effect-causing evaluation are directed by the uniform syntax category of expressions. Each kind of expression can typically nest within each other kind of expression, and rules for evaluation of expressions involve specifying both the value produced by the expression and the order in which its sub-expressions are themselves evaluated.

In contrast, statements in Rust serve mostly to contain and explicitly sequence expression evaluation.

The quoted description can be a bit difficult to fully understand, but it basically boils down to a simple mantra: in Denim, almost everything, including control flow like if...else, is an "expression" can be used like a value. Expressions can be terminated, and their values contained, by capping them with a ; character. Loosely, a terminated expression is a "statement".

Perhaps the best way to visualize this is to demonstrate an example involving if...else, Denim's simplest branching control flow expression.

#![allow(unused)]
fn main() {
// Pretend that `some_random_int` is defined elsewhere and is a randomly
// generated `int`.
let x = some_random_int;

// Below, `message`'s value results from an `if...else if...else` expression
// on `x`. When `x` is `4`, `message` is `"x is four"`. Also, if `x` is not `3`
// or `4`, `message` is the empty string.
let message = if x == 4 {
  "x is four"
} else if x == 3 {
  "x is three"
} else {
  ""
};

// As you can see, expressions can also be used in a sort of stand-alone
// fashion.
if !message.is_empty() {
  print(message);
} else {
  print("Looks like there is nothing to say!");
}
}

Keywords

  • Stolen from Rust
    • as
    • async
      • Suffixable
    • await
      • Suffixable
    • break
    • continue
    • else
    • enum
    • extern
    • false
    • fn
    • for
    • if
      • Suffixable
    • impl
    • in
    • let
    • loop
    • match
      • Suffixable
    • mod
    • pub
      • pub(pkg) for directory-level visibility
      • pub(repo) for repository-level visibility
    • return
    • self
    • Self
      • Special type
    • struct
    • trait
    • true
    • type
    • use
    • where
    • while
      • Suffixable
  • Stolen from Dart
    • is
      • Used for Dart-style type checking
    • show
    • try
      • Suffixable
    • void
  • Stolen from Python
    • from
  • Stolen from TypeScript
    • unknown
  • Originals
    • fork
      • Suffixable
    • tandem

Suffixing

In Denim,some keywords can be applied as suffixes with . notation. Namely, async, await, fork, if, match, try, and while.

#![allow(unused)]
fn main() {
let some_eventual_function_value = async some_function("abc");

let some_other_eventual_function_value = some_other_function("abc").async;

print(await some_eventual_function_value);

print(some_other_eventual_function_value.await);

fork some_struct {
  some_field: "123",
};

some_struct.fork {
  some_field: "123",
};

if some_bool_expression {
  print("its bigger")
} else {
  print("its _not_ bigger")
}

some_bool_expression.if {
  print("its bigger")
} else {
  print("its _not_ bigger")
}

some_number.match {
  1..=122 => print("nope"),
  123 => print("yep"),
  _ => print("nope"),
}

match some_number {
  1..=122 => print("nope"),
  123 => print("yep"),
  _ => print("nope"),
}

fn x() -> Result {
  something_that_can_fail(123).try;
  try {
    let mut a = 123;
    a = a * 2;

    something_that_can_fail(123)
  }
}

let mut i = 0;
while i < 3 {
  print("i is $i");
}

let mut is_done = true;
is_done.while {
  is_done = false;
}
}

Functions

Syntactically, Denim functions are very similar Rust functions.

#![allow(unused)]
fn main() {
// Functions can specify a return type using the `->` symbol.
pub fn multiply_by_two(num: float) -> float {
  // Functions implicitly return the last value in their body. Since, the next
  // line is not terminated by a `;`, it evaluates to `num * 2`.
  num * 2
}

// No need to specify if a function is `void`, just say nothing at all:
fn print_hello_world() {
  // By the way, printing works the same way it does in Dart. `print` is a
  // globally visible function that takes a `string` and outputs it to
  // console/stdout.
  print("hello world");
}
}

There is just one wrinkle with Denim functions - there are no positional arguments, only named arguments that can appear in any order. When a function is invoked, its arguments must be explicitly labeled at the call-site unless a variable is passed along sharing the name of an argument. There is one exception to this rule: if a function has just a single argument, no label is necessary.

#![allow(unused)]
fn main() {
fn multiply_by_two(num: float) -> float {
  num * 2
}

print(multiply_by_two(3)); // prints "6"

fn multiply(a: float, b: float) -> float {
  a * b
}

print(multiply(a: 2, b: 5)); // prints "10"

let b = 5;

print(multiply(a: 2, b)); // prints "10"
}

There are situations where you need multiple arguments, but naming them would be a little silly. The best way to do this in Denim is to have a tuple as your only argument.

#![allow(unused)]
fn main() {
fn add(nums: (int, int)) -> int {
  nums.0 + nums.1
}
}

Denim functions can also have optional arguments. One way to accomplish this is to specify a default value for a parameter.

#![allow(unused)]
fn main() {
// Below Denim infers that `action` is a `string` from its default value.
//
// We explicitly specify a type for `times` because `= 1` would it an `int` by
// default.
//
// NOTE: default values must be immutable.
fn i_cant_wait_to(action = "take a nap", times: float = 1) {
  print("Time to $action $times time(s)!");
}

print(
  i_cant_wait_to(
    action: "eat donuts",
  ), // prints "Time to eat donuts 1 time(s)!"
);
print(
  i_cant_wait_to(
    action: "eat donuts",
    times: 1.5,
  ), // prints "Time to eat donuts 1.5 times(s)!"
);
print(i_cant_wait_to()); // prints "Time to take a nap 1 times!"
}

Another way to declare an optional argument is to make its type T?. T? represents an optional value of type T. We'll describe T? in greater depth later, but it works identically to how Rust's Option<T> works.

#![allow(unused)]
fn main() {
fn measurement(scalar: float, unit: string?) {
  let lowercase_unit = unit.to_lower() ?? "";

  "$scalar $lowercase_unit"
}

print(measurement(scalar: 12.5, unit: Some("px"))); // prints "12.5px"
print(measurement(scalar: 12.5, unit: None)); // prints "12.5"
}

Denim also includes some syntactic sugar to make using this a little less verbose by allowing Some or None to be implied by the respective inclusion or exclusion of an argument.

#![allow(unused)]
fn main() {
print(measurement(scalar: 12.5)); // prints "12.5"
print(measurement(scalar: 12.5, unit: "px")); // prints "12.5px"
}

Denim also has a convenient alternate syntax for anonymous functions, called lamba functions, that it borrows from Rust closures. Unlike Rust closures however, Denim lambda functions are just plain ol' functions with no extra or special behaviors.

#![allow(unused)]
fn main() {
fn sum_three(a: int, b: int, c: int) -> int {
  a + b + c;
}

let sum_three_as_a_lambda = |a: int, b: int, c: int| a + b + c;
}

The argument types of a lambda can be inferred if enough information is provided at the call site.

#![allow(unused)]
fn main() {
let inferred: fn(a: int) -> int = |a| a + 1;
}

By the way, function types look like this:

#![allow(unused)]
fn main() {
fn(a: A, b: B, c: C) -> D;
}

Denim has a special syntax for functions that receive an inlined anonymous function as an argument. This common when passing callbacks and in embedded DSLs. Any function argument named body can have an inline block after its invocation that works like a lambda function.

#![allow(unused)]
fn main() {
fn element(name: string, body: Option<fn() -> Element>) -> Element {
  Element { name, child: body() }
}

// This:
element(name: "div", body: || {
  element(name: "button", body: || {
    span("click me")
  })
})

// Is the same as this:
element("div") {
  element("button") {
    span("click me")
  }
}
}

Thanks to "getterification", we can make this syntax even sweeter:

#![allow(unused)]
fn main() {
fn button(body: Option<fn() -> Element>) -> Element {
  element("button", body)
}

fn div(body: Option<fn() -> Element>) -> Element {
  element("div", body)
}

div {
  button {
    span("click me")
  }
}
}

TODO(skeswa): it for the singular argument passed to body.

Modules

Denim's core packaging primitive is called a "module". Modules designed to define behavior, state, and types for a particular context or domain. Furthermore, Denim modules are intended to depend on each other through the import and export of behavior, state, and types.

Denim modules are expressed a file system as a directory with Denim source files. Each source file in an Denim module has full visibility of everything declared in the other source files of the module. Additionally, each source file can do its own importing and exporting. Denim source files can only import stuff exported by another Denim module. It might help to think of this style of packaging is a blend of Go packages and ES modules.

Like Deno and Go, remote modules are fetched from source control with an accompanying version. Version is typically inferred from a tag or reference on the imported module repository.

#![allow(unused)]
fn main() {
from "github.com/abc/xyz@v0.9.0" use { hello, world };
}

Denim allows imports from other modules in the same repository, too. These are called respository-relative imports. ~ always refers to the root of the repository containing the current module. Respository-relative imports do not specify a version because the version will always be the same as the current module.

#![allow(unused)]
fn main() {
from "~/bing/bang" use { boom: bΓΌm }; // Aliases `boom` to `bΓΌm`.
from "~/some/sub/module" use { something };

// You can use ... syntax to import "everything else".
from "github.com/foo/bar@v4.2" use { Foo, bar, ...foo_bar };
from "github.com/bing/bong@latest" use { ...bing_bong };
}

You can also re-export stuff using the show keyword.

#![allow(unused)]
fn main() {
from "github.com/abc/xyz@v0.9.0" show { hello };

from "~/bing/bang" show { boom: bΓΌm };
}

Like in Rust, you can export stuff from Denim modules with the pub keyword. Anything not declared with a pub will only be visible to things in its own module.

#![allow(unused)]
fn main() {
pub let stuff_outside_of_this_module_can_see_me = true;
}

Structs

You may now be wondering how more complex data structures are created and managed in Denim. I'm sure you are so shocked to find out that we (mostly) stole Rust syntax here too.

#![allow(unused)]
fn main() {
// We can make some or even all of the `struct` or its fields visible to
// external modules using the `pub` keyword.
pub struct User {
  active = false,
  /// You can put doc comments directly on `struct` fields.
  coolness_rating: int,
  pub name: string,
  pub nickname: string?,
}
}

We can instantiate and use struct like this:

#![allow(unused)]
fn main() {
let some_user = User {
  active: true,
  coolness_rating: 42,
  name: "Some User",
  nickname: None,
};
}

In the User struct above, two fields are optional - active and nickname. active is made optional by the specification of a default value for it, false. nickname is optional because it is of type Option<string>. Optional fields may be excluded when a struct is instantiated.

#![allow(unused)]
fn main() {
let sam = User {
  coolness_rating: 11,
  name: "Sam",
  // Since `nickname` is an `Option<string>` we can simply specify a `string`.
  nickname: "S-money",
};

let tria = User {
  active: true,
  coolness_rating: 12,
  name: "Tria",
};

print(tria.nickname); // Prints "None".

let jen = User {
  active: true,
  coolness_rating: 13,
  name: "Jen",
  // Below, `"Jenners"` is automatically wrapped in a `Some(..)`.
  nickname: "Jenners",
};
}

Denim sugarifies Rust's default impl by simply allowing you to declare methods within the struct itself. This should feel familiar to anyone coming from a language that makes heavy use of classes.

#![allow(unused)]
fn main() {
struct Car {
  make: string,
  model: string,
  owner: User,

  // `self` is a special argument. Like in Rust, it means that this method is
  // attached to any instance of `Car`.
  fn drive(self) {
    print(
      "A ${self.model} ${self.model} owned "
      "by ${self.owner.name} is driving",
    );
  }

  // Functions defined within a `struct` don't have to be attached to a
  // particular instance of that struct. They can instead function like static
  // methods in other languages. For instance, you would invoke this function
  // with `Car::create_lemon_for(some_user)`.
  fn create_lemon_for(owner: User) {
    Car { make: "AMC", model: "Pacer", owner }
  }
}

let some_user = User {
  active: true,
  coolness_rating: 42,
  name: "Some User",
};

let my_car = Car {
  make: "Mazda",
  model: "Miata",
  owner: some_user,
};

my_car.drive(); // Prints "A Mazda Miata owned by Some User is driving"
}

As structs a big part of the language, Denim has some syntactic sugar to make instantiating nested structs ergonomic.

#![allow(unused)]
fn main() {
let my_other_car = Car {
  make: "Nissan",
  model: "Altima",
  owner: {
    active: false,
    coolness_rating: -1,
    name: "Another User",
  },
};

my_car.drive(); // Prints "A Nissan Altima owned by Another User is driving"
}

One important thing to note here is that Denim structs, like most Denim data structures, are immutable by default. So, direct imperative mutation of Denim structs won't work in all the cases that you may be used to.

#![allow(unused)]
fn main() {
let my_car = Car {
  make: "Mazda",
  model: "Miata",
  owner: some_user,
};

my_car.make = "Toyota"; // Compile-time error.
}

The only way to create a mutable struct instance is to create it with a mut prefixing the struct type. In Denim, structs and traits with a mut are internally mutable.

#![allow(unused)]
fn main() {
let my_mut_car = mut Car {
  make: "Mazda",
  model: "Miata",
  owner: some_user,
};

my_mut_car.make = "Toyota"; // πŸ‘
}

All Denim structs can be shallow cloned with fork. This useful when a field inside an immutable struct value should change.

#![allow(unused)]
fn main() {
let my_car = Car {
  make: "Mazda",
  model: "Miata",
  owner: some_user,
};

let my_other_car = my_car.fork {
  // `make` is changed, but all of the other fields stay the same.
  make: "Toyota",
};

print(my_car.make) // Prints "Mazda"
print(my_other_car.make) // Prints "Toyota"

my_other_car.make = "Toyota"; // Compile-time error (`my_other_car` is not a `mut Car`)
}

But what if you need the forked struct instance to be internally mutable? This is made possible by prefixing the fork expression with a mut.

#![allow(unused)]
fn main() {
let my_car = Car {
  make: "Mazda",
  model: "Miata",
  owner: some_user,
};

let my_other_car = mut my_car.fork;

my_car.make = "Toyota"; // πŸ‘
}

Sometimes you need to fork a struct instance nested within another struct instance. Given how frequently this is necessary, it felt like a good idea for Denim to ship with a dedicated syntax.

#![allow(unused)]
fn main() {
let my_car = Car {
  make: "Mazda",
  model: "Miata",
  owner: some_user,
};

let my_other_car = mut my_other_car.fork {
  make: "Rivian",
  model: "R1T",
  user: fork {
    // `self` refers to the original value of this `User` field.
    coolness_rating: self.coolness_rating + 1,
  },
};
}

In some situations, you may want nested internal mutation: you way want to be able to directly mutate an inner struct instance nested within another struct instance. Denim supports this by declaring the inner struct field as mutable with mut. Note that this nested internal mutability is only accessible in situations where the surrounding type is itself mutable.

#![allow(unused)]
fn main() {
struct House {
  address: Address,
  owner: mut User,
}

struct Address {
  number: string,
  street: string,
}

let a_house = House {
  address: {
    number: "42",
    street: "Wallaby Way",
  },
  owner: {
    coolness_rating: -1,
    name: "P. Sherman",
  },
};

a_house.owner.active = true; // Compile-time error (`a_house` is not a `mut House`)

let another_house = mut House {
  address: {
    number: "221B",
    street: "Baker St",
  },
  owner: {
    coolness_rating: 99,
    name: "S. Holmes",
  },
};

a_house.owner.active = true; // πŸ‘
a_house.address.street = "Baker Street"; // Compile-time error (`House::address` is not an `mut Address`)
}

Control Flow

TODO(skeswa): document branching TODO(skeswa): document loops TODO(skeswa): document loop labels (https://doc.rust-lang.org/rust-by-example/flow_control/loop/nested.html)

"Selfification" (a.k.a Functions as Methods)

Here, we take:

  • "function" to mean a subroutine without a self reference to some piece of state
  • "method" to mean a subroutine with a self reference to some piece of state
#![allow(unused)]
fn main() {
fn authenticate(environment: Environment, auth_user: User) -> bool {
  // ...
}

// `&` is the secret sauce here
fn do_stuff(environment: Environment, user: User?) {
  user.authenticate(environment, &auth_user).if {
    print("yes!");
  } else {
    print("no!");
  };
}
}

"Getterification" (a.k.a Functions as Fields)

In Denim, functions can be treated as fields if they are not passed any arguments.

#![allow(unused)]
fn main() {
struct Foo {
  bar: int,

  pub fn baz(self, extra = 0) -> int {
    self.bar + extra + 1
  }
}

let foo = Foo { bar: 27 };

print(foo.baz(2)); // prints "30"
print(foo.baz()); // prints "28"
print(foo.baz); // prints "28"

}

Autoboxing

Given the prevalance and importance of special enums Option<T> and Result<T> in Denim, you might find yourself wrapping things in Some(..) and Ok(..) alot. To sweeten this syntax a little bit, Denim will automatically infer a T as Some(T) or a Ok(T) depending on context; this is called "autoboxing".

#![allow(unused)]
fn main() {
// Why do this:
fn foo() -> string? {
  Some("bar")
}

// when you could simply do this:
fn autoboxed_foo() -> string? {
  "bar"
}

// `Result` is also autoboxable:
let mut x: Result<int> = Ok(1);
x = 2;

// Autoboxing works in structs too!
struct X(Option<int>);
let x = X(1234);
}

Traits

TODO(skeswa): flesh this out.

#![allow(unused)]
fn main() {
pub trait Summary {
  fn summarize(self) -> string;
}

pub trait Tweak<T> {
  tweakable: T?;

  fn tweak(mut self, some_other_arg: string) -> Self;
}

pub trait DefaultImpl {
  fn foo(mut self) -> string {
    "bar"
  }
}

pub trait MethodGeneric {
  fn some_method<T>(self) -> T;
}
}

Impls

Declaration:

#![allow(unused)]
fn main() {
// non-public
impl B for A {
  fn b(self) {
    print("b");
  }
}

// non-public
impl A for X {
  fn a(self) {
    print("a");
  }
}
impl Y for X {
  fn y(self) {
    print("y");
  }
}
impl Z for X {
  fn z(self) {
    print("z");
  }
}

// non-public
impl X {
  fn new_functionality_for_x(self) {
    print("that newnew");
  }
}

// non-public
pub impl X {
  fn eat(self) {
    print("omnomnom");
  }
}
}

Importing:

#![allow(unused)]
fn main() {
from "~/foo/bar" use {
  B for A,
  Y + Z for X,
  X,
  A,
  X::new_functionality_for_x,
};
}

Generics

TODO(skeswa): flesh this out.

TODO(skeswa): where.

Type Unions and Intersections

TODO(skeswa): flesh this out (Rust Type Unions (trait + trait) and TS Intersections (type | type).

#![allow(unused)]
fn main() {
type IntOrString = int | string;

fn do_a_thing(data: IntOrString) {
  if data is int {
    print("it is an int")
    return;
  }

  print("it is a string")
}

fn do_another_thing(data: int | string | byte) {
  match data {
    is int => print("it is an int"),
    is! string => print("it isn't an string"),
    _ => print("it is probably a string"),
  }
}

trait Foo {
  fn bar(self) -> int,
  fn baz(self) -> bool,
}

trait Ping {
  fn bar(self) -> int,
  fn pong(self),
}

fn do_one_more_thing(data: Foo | Ping) {
  print(data.bar())
}

type FooAndPing = Foo & Ping;

fn do_one_last_thing(data: FooAndPing) {
  data.pong();

  print(data.bar())
}
}

Error handling

You can hoist the error out of returned Result within a function that returns Result with the try keyword.

TODO(skeswa): flesh this out.

#![allow(unused)]
fn main() {
fn do_a_thing() -> Result<void> {
  let csv_file_path = "a/b/c.csv";

  let csv_data = read_file(csv_file_path).try.to_utf8();

  basic_dance_move().context("tried to bust a move").try;
}
}

TODO(skeswa): flesh this out.

#![allow(unused)]
fn main() {
pub struct Error<ErrorKind> {
  pub cause: Error?;
  pub kind: ErrorKind;
}
}

TODO(skeswa): flesh this out.

#![allow(unused)]
fn main() {
panic("Oh no! We cannot recover from this!");
}

Pattern matching

TODO(skeswa): flesh this out.

Concurrency

In Denim, function invocations work differently than in other languages - they do not "block" by default. Instead, Denim functions work like co-routines, suspending themselves while invoking other functions, and resuming when they return.

#![allow(unused)]
fn main() {
// This call to `fetch` blocks, meaning that "done" will not be printed until
// the fetch is finished.
let weather = fetch(url: "https://get.theweather.com");
print("done");

// The code below:
// 1. Prints "a"
// 2. Waits for the first thing to be fetched
// 3. Prints "b"
// 4. Waits for the second thing to be fetched
// 5. Prints "c"

print("a");
let first_thing = fetch(url: "https://first.thing.com");
print("b");
let second_thing = fetch(url: "https://second.thing.com");
print("c");
}

To call a function concurrently, use the async keyword. Using async on a function invocation wraps its return value in a special type called Eventual, like Future or Promise in other languages, is a concurrency monad that allows you to subscribe to the completion of an asynchronous operation.

#![allow(unused)]
fn main() {
// This logic prints "a" then "b" then "c", printing the time in joburg and in
// nyc (in no particular order) when the fetches complete later on.

print("a");
let time_in_joburg = fetch(url: "https://worldtime.com/in/joburg").async;
print("b");
let time_in_nyc = fetch(url: "https://worldtime.com/in/nyc").async;
print("c");

time_in_joburg.then(|time| print("time in joburg: $time"));
time_in_nyc.then(|time| print("time in nyc: $time_in_nyc"));
}

Denim provides a way to "wait" for the Eventual values returned by async functions - the await keyword. With await, working with asynchronous operations can be quite ergonomic.

#![allow(unused)]
fn main() {
// This logic:
// 1. Starts fetching the time in atlanta
// 2. Prints "a"
// 3. Waits for the time in atlanta to be available before printing it
// 4. Prints "b"

let time_in_atlanta = fetch(url: "https://worldtime.com/in/atlanta").async;

print("a");
print(time_in_atlanta.await);
print("b");
}

There are a garden variety of helpful ways to wait on multiple asynchronous operations at once in Denim. In particular, tuples are a great way to wait on Eventual values with different types.

#![allow(unused)]
fn main() {
// Any tuple of `Eventual` values has a method called `.after_all()` that waits
// for every `Eventual` to complete.
let (google_icon, _, c_text) = (
  fetch(url: "https://google.com/favicon.ico").async,
  pause(Duration::new(milliseconds: 100)).async,
  readFile(path: "./a/b/c.txt").async,
).after_all().await;

// Arrays of `Eventual` also support `.after_all()`.
let icons = [
  fetch(url: "https://nba.com/favicon.ico").async,
  fetch(url: "https://nhl.com/favicon.ico").async,
  fetch(url: "https://mlb.com/favicon.ico").async,
].after_all().await;
}

Since this is quite a common pattern in Denim, there is a special syntax for it enabled by the tandem keyword. tandem { ... } is simply syntactic sugar for the concurrent invocation of multiple potentially asynchronous functions normally enabled by the combined usage of .async and .after_all().await. As such, a tandem { ... } expression evaluates to a tuple of the return values of each comma delimited sub-expression.

#![allow(unused)]
fn main() {
let (google_icon, _, c_text) = tandem {
  fetch(url: "https://google.com/favicon.ico"),
  pause(Duration::new(milliseconds: 100)),
  readFile(path: "./a/b/c.txt"),
};

let icons = tandem {
  fetch(url: "https://nba.com/favicon.ico"),
  fetch(url: "https://nhl.com/favicon.ico"),
  fetch(url: "https://mlb.com/favicon.ico"),
};
}

Both tuples and arrays of Eventual support .race() to wait for the first Eventual to complete, wrapping values in Option::None to mark Eventual values that lost the race.

#![allow(unused)]
fn main() {
let results = [
  fetch(url: "https://later.com").async,
  fetch(url: "https://now.com").async,
  fetch(url: "https://way.later.com").async,
].race().await;

print("$results"); // Prints "[None, Some(data), None]"
}

Interop

TODO(skeswa): flesh this out.

#![allow(unused)]
fn main() {
trait Pluralizer {
  fn pluralize(self, text: string) -> string;
}

extern(go) {
  // Automatically compiles a Denim-friendly interface.
  from(go) "github.com/gertd/go-pluralize@0.2" use { ...pluralize };

  struct GoPluralizer {
    client: pluralize.Client,
  }

  impl Pluralizer for GoPluralizer {
    fn pluralize(self, text: string) -> string {
      self.client.plural(text)
    }
  }

  pub fn new_pluralizer() -> impl Pluralizer {
    GoPluralizer { client: pluralize.NewClient() }
  }
}

extern(ts) {
  #[deno_types("npm:@types/pluralize@0.0.29")]
  from(ts) "npm:pluralize@8.0.0" use { pluralize };

  struct TsPluralizer {}

  impl Pluralizer for TsPluralizer {
    fn pluralize(self, text: string) -> string {
      pluralize(text)
    }
  }

  pub fn new_pluralizer() -> impl Pluralizer {
    TsPluralizer {}
  }
}
}

Testing

TODO(skeswa): flesh this out.

In any "*.(spec|test).denim" file (spec = unit test, test = integeration test):

describe, before, test are all functions that only apply to tests.

#![allow(unused)]
fn main() {
describe("Something") {
  before {

  }

  test("Something") {

  }

  #[timeout(seconds: 500)]
  test("Something") {

  }

  if !is_dev {
    test("Something") {

    }
  }
}
}