Installation

# Add this to your shard.yml
dependencies:
  dragonstone:
    github: Vallereya/dragonstone
    version: ~> 0.1.7-Alpha

Then run:

shards install

shard.yml

Crystal
>= 1.0.0
License
Apache-2.0
Author
Vallereya
Target
  • dragonstone from bin/dragonstone

Dependencies

This version declares no dependencies.

README

What is Dragonstone?

Dragonstone is a general purpose, high-level, object-oriented programming language. It is both an interpreted and compiled language, it's inspired by Ruby and Crystal but designed for programmer happiness, productivity, and choice.

โš™๏ธ Project Setup

Requirements

  1. The Crystal needs to be installed (1.17.1+ but has been tested down to 1.11.2 on Ubuntu).
  2. (optional) LLVM/Clang; While Crystal also installs LLVM/Clang, installing a standalone version is recommended if you want to target dragonstone build --target llvm or dragonstone build --target c.
  3. (optional) The Ruby is needed if you want to use dragonstone build --target ruby (3.4.6+).

To Build from Source (All Platforms)

(MacOS/Linux/Windows)
  1. Clone this repository.
  2. cd to the project directory.
  3. Run shards build --release

Tip: Always use the --release flag for production builds as it significantly improves performance, without it a standard build is made and the dragonstone interpreter will run files roughly about 3-5x slower.

To Build from Source (Linux)

(Linux Recommended)
  1. Clone this repository.
  2. cd to the project directory.
  3. Run ./bin/dragonstone.sh --rebuild
  4. (optional) Run ./bin/dragonstone.sh --install which adds .\bin to your user PATH.
    • After running that command please restart your terminal, then you can use dragonstone from anywhere.
(Linux Alternative)
  1. Run shards build --release for a standard build and without PATH.

Tip for Linux Users: If you want to rebuild the project, after already doing so, you can use ./bin/dragonstone.sh --clean which will remove the build files, then you can use ./bin/dragonstone.sh --rebuild to build it again. For an automated version you can use ./bin/dragonstone.sh --clean-rebuild which will clean first then rebuild for you.

To Build from Source (Windows)

(Windows Recommended)
  1. Clone this repository.
  2. cd to the project directory.
  3. Run .\bin\dragonstone.bat --rebuild
    • This builds with the icons/resources and adds .\bin to your user PATH.
    • After running that command please restart your terminal, then you can use dragonstone from anywhere.
(Windows Alternative)
  1. Run shards build --release for a standard build, without any icons/resources or PATH.

Tip for Windows Users: If you want to rebuild the project, after already doing so, you can use .\bin\dragonstone.bat --clean which will remove the build files, then you can use .\bin\dragonstone.bat --rebuild to build it again. For an automated version you can use .\bin\dragonstone.bat --clean-rebuild which will clean first then rebuild for you.

Tip for Windows Users: An installer is also available in the Releases section.

๐Ÿงช Usage

Run Files via Interpreter.

    # On PATH:
    dragonstone run examples/hello_world.ds

    # Not on PATH:
    ./bin/dragonstone.exe run examples/hello_world.ds

Select a Backend to Use.

For now this is a temporary flag so I can verify some of my --backend build targets, this may change in the future when the self-host/bootstrap process begins.
    # Select between native (interpreter) or core (compiler) backends.
    dragonstone run --backend native examples/hello_world.ds
    dragonstone run --backend core examples/hello_world.ds

    # Without specifying it will automatically use the native (interpreter) or 
    # with auto it can choose the correct backend based on what is being ran 
    # within the file.
    dragonstone run examples/hello_world.ds
    dragonstone run --backend auto examples/hello_world.ds

    # However, if you don't want to specify the flag every time you can just 
    # set it change it anytime, this works on the fly.
    DRAGONSTONE_BACKEND=core dragonstone run examples/hello_world.ds

    # Or export the flag once, to set it permanently for the CI:
    export DRAGONSTONE_BACKEND=core

Build and Run Files via the Compiler with a target.

    # Supported Target Flags: bytecode, llvm, c, crystal, and ruby
    dragonstone build --target bytecode examples/hello_world.ds

    # Build and immediately execute the produced artifacts.
    dragonstone build-run --target bytecode examples/hello_world.ds

Run Test/Spec (still building out unit tests).

    # To run the full spec/unit testing suite.
    crystal spec

    # Helper script that runs the spec for just a specific backend.
    ./scripts/backend_ci.sh spec --backend native
    ./scripts/backend_ci.sh spec --backend core

โœจ Examples

Example of a sting output using echo:

    echo "Hello World!"

Example of comments/block comments using # and #[ ]#:

    # This is a Single Line Comment.

    #[
        This is a Multi-Line Comment.
    ]#

    message = "Hello!" # Trailing Comment.

    #[ Multi-Line Comment on same line. ]#     numbers = 10    #[ Inside or outside. ]#

Some Examples:

Examples of a String.
    # Using echo
    name = "Ringo"

    echo name

    # OUTPUT: Ringo
    # Using e!, for printing debug information.
    name = "Jules"
    e! name         
    
    # OUTPUT: name # -> "Jules"
    # Using eecho, without a newline.
    greet = "Hello, "
    name = "Ringo!"

    eecho greet
    eecho name

    # OUTPUT: Hello, Ringo!
    # Using ee!, for printing debug information without a newline.
    greet = "Hello, "
    name = "Jules!"

    ee! greet  
    ee! name        
    
    # OUTPUT: greet + name # -> "Hello, " + "Jules!"
In dragonstone you can optionally be very explicit. Using con for constants, var for variables, fix for block-scoped constants, and let for block-scoped variables.
    con first = 20
    var mul = 2

    def example
        fix second = 10
        let div = first // second

        echo first
        echo second
        echo div
        echo first * mul
    end

    example
Example of a Module with con, an immutable constant, and :: for scope resolution for modules and . for classes. Dragonstone also supports both module and mod for user preference.
    module Grades
        con Score = 100

        class Greeting
            def greet
                "Hello! I got a #{Grades::Score}%!"
            end
        end
    end

    echo Grades::Score
    echo Grades::Greeting.greet
Example of a Class, dragonstone also support both class and cls for user preference.
    class Person
        happy = true

        def greet
            if happy
                echo "Hello!"
            end
        end
    end

    person = Person.new
    person.greet
Also Ascii and Unicode are supported.
    class ๐Ÿ”ฅ
        ใ‚ = true
  
        def ้“
            if ใ‚
                echo "Hello!"
            end
        end
    end

    ๐Ÿ”ฅ.้“
Example of a def Method and String Interpolation, and supports both def and define.
    def greet(name)
        echo "Hello, #{name}!"
    end

    greet("Jules")
Example of fun, which is a first-class citizen and a function object that can be passed around. Dragonstone also supports both fun and function for preference.
    # Store a function object in a variable.
    add = fun(a, b)
        return a + b
    end
    echo add.call(2, 3)

    # Define a reusable function object.
    fun shout(text)
        return text + "!"
    end

    def print_one(word, fmt)
        echo fmt.call(word)
    end
    print_one("Ringo", shout)

    # And an advanced version, to pass an anonymous function inline.
    def print_formatted(words, fmt)
        words.each do |w|
            echo fmt.call(w)
        end
    end

    names = ["Ringo", "Peet", "Jules"]

    print_formatted(
        names, fun(text) do
            return ">> " + text + " <<"
        end
    )
Example of a Abstract Class and Super, dragonstone supports both abstract and abs for preference.
    abstract class Animal
        def make_sound(sound)
            echo "#{sound}"
        end
    end

    class Dog < Animal
        def make_sound(sound)
            super(sound)
            super("The sound a dog makes!")
        end
    end

    dog = Dog.new
    echo dog.make_sound("Woof!")
Two examples of a ternary and being nested.
    age = 20
    status = age >= 18 ? "Adult" : "Minor"
    echo "Status: #{status}"

    # Nested ternary
    score = 85
    grade = score > 90 ? "A" : (score > 80 ? "B" : "C")
    echo "Grade: #{grade}"
Example of a Map literal with key -> value pairs.
    ages = { "Jules" -> 32, "Ringo" -> 29, "Peet" -> 35 }
    echo ages["Jules"]

    ages["Ringo"] = 30
    echo ages["Ringo"]

    ages.each do |name, age|
        echo "#{name} is #{age}"
    end

    e! ages.keys
    e! ages.values
Example of enum.
    enum Direction
        North
        East
        South
        West
    end

    Direction.each do |direction|
        echo "Direction: #{direction}"
    end

Some More Examples but with Optional Types:

Example of a String with types.
    name: str = "Peet"

    echo name
Example of some math/integers with types.
    a: int = 10
    b: int = 10
    numbers = a + b

    echo numbers
Two examples of a Class with types, one of which showing Unicode support again.
    def ๐Ÿ˜ฑ(name: str) -> str
        echo "Hello, #{name}!"
    end

    ๐Ÿ˜ฑ("V")
With this example using a bit more complex features.
    class Person
        property name: str

        def initialize(name: str)
            self.name = name
        end

        def greet
            echo "Hello, my name is #{self.name}"
        end
    end

    person = Person.new("Jules")
    person.greet
Examples of struct.
    struct Point
        property x: int
        property y: int

        def initialize(@x: int, @y: int)
        end
    end

    point = Point.new(10, 20)
    echo "x: #{point.x}, y: #{point.y}"
Example of a record, similar to struct but are are immutable data structures that automatically generate the struct, the constructor, and getters for those fields.
    record Person
        name: str
        age: int
    end

    greet = Person.new("V", 30)
    echo "Name: #{greet.name}, Age: #{greet.age}"
Examples of Type Casting/Switching.
    def process(input)
        case input

        when input: str
            echo "It is a string: #{input}"
        when input: int
            echo "It is a number: #{input}"
        when input: bool
            echo "It is a boolean"
        else
            echo "Unknown type"
        end
    end

    process("Hello")
    process(42)
    process(true)

Example of using/importing other files with use.

From (./examples/use.ds)
    use "./test_use"

    echo add(magic, 8)
What's being grabbed from (./examples/test_use.ds).
    magic = 42

    def add(a, b)
        a + b
    end

Two examples of para, this is the Dragonstone version of what another languages calls a Proc.

For any {} used within Dragonstone, these can also be split between lines or placed on the same line.
greet = ->(name: str) {
    "Hello, #{name}!" 
}

echo greet.call("Jalyn")
Another bit more complex para.
square: para(int, int) = ->(x: int) { x * x }

echo square.call(6)

Examples the interop (some done but still a work in progress).

    # Call puts from Ruby.
    # This works too: ffi.call_ruby("puts", ["Hello from Ruby!"])
    Invoke Ruby
        with puts

        as {
            "Hello from Ruby!"
        }
    end

    # Call puts from Crystal.
    # This works too: ffi.call_crystal("puts", ["Hello from Crystal!"])
    Invoke Crystal
        with puts

        as {
            "Hello from Crystal!"
        }
    end

    # Call printf from C.
    # This works too: ffi.call_c("printf", ["Hello from C!"])
    Invoke C
        with printf

        as {
            "Hello from C!"
        }
    end
Imports via use are built out, so you can import a file, selectively import as well, and the same applies by importing through a url.
    # Both by file and selectively using `from` with it.
    use "./test_use"
    use { divide } from "./raise"

    # Any both by file and selectively via a url, I used `cdn.jsdelivr.net`
    # because it was the only thing I could find that would grab the examples from GitHub.
    use "https://cdn.jsdelivr.net/gh/vallereya/dragonstone@main/examples/unicode"
    use { MyModule } from "https://cdn.jsdelivr.net/gh/vallereya/dragonstone@main/examples/resolution"

See the examples/ directory for more sample .ds files.

โšก Benchmark Information

  • Built with --release.
  • Results are for this specific benchmark + machine; expect variance across CPUs/Operating Systems.
  • "Nested" means an extra loop layer (measuring loop-overhead vs a single loop), not extra work.
  • <2% overhead at scale.
  • Near identical for loops vs single.

You can run these yourself from the ./scripts/benchmark directory.

1 Billion Nested Loop Iteration Benchmark (Interpreter)

    Iterations:             ~4.47M iterations/s
    Per-iteration cost:     ~223.71 ns
    Actual Time:            223.71 s (~3.73 min)

1 Billion Nested Loop Iteration Benchmark (LLVM Compiler)

    Iterations:             ~811M iterations/s
    Per-iteration cost:     ~1.23 ns
    LLVM Compiler Time:     1.2326 s
    Results:                ~182ร— vs Interpreter

Comparison Context (Rough context, not direct comparable)

For 1 billion iterations of this benchmark (Interpreter):
    Ruby v2.X.X         = ~15-30 minutes    (varies by version)
    Python v3.X.X       = ~5-15 minutes     (varies by version)
 -> Dragonstone         = ~3.7 minutes
    Lua                 = ~1-2 minutes
    JavaScript          = ~10-30 seconds    (using V8)
For 1 billion iterations of this benchmark (Compiler/LLVM):
    C                   = ~0.5-1.5 seconds
    Rust                = ~0.5-1.5 seconds
 -> Dragonstone LLVM    = ~1.23 seconds
    GO                  = ~1-2 seconds
    Java                = ~2-5 seconds      (using JIT)
    PyPy                = ~10-20 seconds    (using JIT)
    Node.js             = ~10-30 seconds    (using V8)