Welcome to Ragnar

A Rebol-inspired programming language hosted on .NET.

Ragnar is a programming language designed to combine the unique, expressive dialecting capabilities of Rebol with the power, performance, and library ecosystem of the .NET Platform.

Key Philosophies

  • Vibecoded & Lightweight: Designed for fun, expressiveness, and ease of scripting directly from your command line.
  • Fully Lexically Scoped: Unlike traditional Rebol, Ragnar's variables are lexically scoped and functions behave as true closures.
  • Tail-Call Optimized (TCO): Supports elegant recursive algorithms (including mutual recursion trampolining) without overflowing the stack.
  • Erlang-inspired Actor Model: Built-in support for concurrent, isolated actor processes communicating via message-passing.

Getting Started

Ragnar runs on .NET 10.0 and can be built and executed easily on Windows, macOS, or Linux.

Prerequisites

Ensure you have the .NET 10.0 SDK installed on your machine. You can verify this by running:

dotnet --version

Clone and Build

Clone the repository and run the project using the just command-runner or dotnet CLI:

# Clone the repository
git clone https://github.com/tormaroe/ragnar.git
cd ragnar

# Build the project
just build   # Or: dotnet build

Running Ragnar

Ragnar features both an interactive REPL mode and the ability to execute script files directly.

Interactive REPL

Launch the REPL by running:

just run     # Or: dotnet run --project src/Ragnar.csproj

Executing Scripts

You can execute a `.r` source script file by passing its path to the compiler:

just eval examples/pingpong.r   # Or: dotnet run --project src/Ragnar.csproj examples/pingpong.r

Configuration

When starting up, Ragnar looks for a configuration file named .ragnar.r in your user home directory. You can use it to configure your REPL prompt, set up common shortcuts, or print messages on startup.

Configuring the REPL

Here is an example showing how you can write a configuration script directly from the REPL:

>> config-path: join home rc-file-name
== %C:\Users\bob/.ragnar.r

>> write config-path mold/only [
..   me: "Bob"
..   print ["Hello" me "it is now" now/time]
..   system/console/prompt: "?? "
.. ]

The next time you restart your REPL, the message will print and your custom prompt will be displayed:

Hello Bob it is now 10:53:01 AM
REPL Mode (type 'quit' to exit)
?? me
== "Bob"
?? 

Reloading Configuration

You can force-reload your configuration file at any time by executing:

do join home rc-file-name

REPL & Reflection

Ragnar provides a robust interactive REPL for immediate evaluation, along with powerful reflection capabilities to inspect types, functions, and active word bindings.

Reflective Commands

Use reflective functions to discover the environment:

  • type?: Returns the datatype of a given value.
  • help (or shortcut ?): Inspects the definition, arguments, and details of a word.
  • what: Lists all available global functions and their brief descriptions.
  • probe: Prints a value to output and returns it (useful in chains).

REPL Interactive Session

>> type? 42
== integer!

>> type? "hello"
== text!

>> type? [1 2 3]
== block!

>> help add
WORD: add
TYPE:  Native Function
TITLE: Returns the sum of two values.
ARITY: 2
ARGS:  [ a b ]

>> what
add             Returns the sum of two values.
print           Prints a value to the output.
...

>> probe 10 + 20
30
== 30

Core Language Features

Ragnar syntax is built around words, blocks, assignment, and conditional evaluation. Values inside blocks are not evaluated until explicitly requested, allowing you to use blocks as control structures or custom data formats.

Variable Assignment

name: "Ragnar"
age: 25

Conditionals & Logic

Conditionals in Ragnar use Rebol-style words:

  • if [cond] [block]: Evaluates block if condition is true.
  • either [cond] [true-block] [false-block]: If-else branching.
  • all [blocks...]: Returns true if all expressions are truthy.
  • any [blocks...]: Returns true if any expression is truthy.
either age > 18 [
    print "Adult"
] [
    print "Minor"
]
; Output: Adult

all [age > 18 name == "Ragnar"] ; returns true
any [age < 18 name == "Ragnar"] ; returns true

Loops and Collections

Iterate through blocks and manipulate values easily:

; Foreach loop
data: [10 21 30 43 50]
evens: []
foreach n data [
    if (n // 2) == 0 [ append evens n ]
]
; evens is now [10 30 50]

; Path navigation & Series manipulation
pick evens 1   ; returns 10 (1-indexed)
evens/2        ; returns 30 (path access)
select [a 1 b 2] 'b ; returns 2

.NET Interop

Ragnar is hosted on the .NET platform and features native, seamless integration with C# and the .NET Base Class Library (BCL). You can instantiate .NET classes, invoke instance or static methods, access or set properties, and iterate over any standard collection that implements IEnumerable.

1. Instantiation and Method Calls

Use the new native function to instantiate types. You can call instance methods using the call-method function:

; Instantiate a StringBuilder with initial text "Hello"
builder: new "System.Text.StringBuilder" ["Hello"]

; Call instance method to append text
call-method builder "Append" [" World"]

; Print the string value
print [call-method builder "ToString" []]
; Output: Hello World

2. Path Navigation Syntax

Ragnar supports path notation to access and mutate .NET fields, properties, and static members in a clean and readable format:

; Retrieve an instance property (Length)
len: builder/Length
print ["Length:" len] ; Output: 11

; Mutate an instance property
builder/Length: 5
print [call-method builder "ToString" []] ; Output: Hello

; Access a static property or field
pi: System.Math/PI
print ["PI value:" pi] ; Output: 3.141592653589793

3. Enumerating IEnumerables

Ragnar provides the enumerate mezzanine function to iterate through .NET collections that implement IEnumerable. You bind a block variable to each item and evaluate a body block:

; Retrieve a Dictionary containing environment variables
vars: call-static "System.Environment" "GetEnvironmentVariables" []

; Print the total count (property access)
print ["Total variables:" vars/Count]

; Iterate and print Key/Value properties
enumerate vars item [
    print ["Key:" item/Key "Value:" item/Value]
]

Objects Support

Objects in Ragnar are dynamic contexts containing key-value pairs (bindings). They allow you to bundle state and behavior. You can access and mutate fields using path notation, and perform dynamic scoping or lookup using in and bind.

Creating and Using Objects

Define objects using make object!:

square: make object! [
    side: 0
    area: does [ self/side * self/side ]
    perimeter: does [ 4 * self/side ]
    multiply: func [x] [
        self/side: x * self/side
    ]
]

; Set property
square/side: 3

; Retrieve property/method result
square/area      ; returns 9
square/perimeter ; returns 12

Dynamic Scoping

You can retrieve a word bound to an object context using the in keyword, which allows you to inspect or mutate that specific binding:

word: in square 'side
get word ; returns 3

Parse Dialect

One of Ragnar's most powerful features is its Rebol-style parsing engine. It supports both simple string splitting and complex dialect-based pattern matching with full backtracking.

1. Simple Splitting

Split a string by a simple delimiter:

parse "alice,30,engineer" ","
; == [ "alice" "30" "engineer" ]

2. Dialect Pattern Matching

Build complex grammars using Ragnar's parse dialect. You can match character sets, check sequence orders, and enforce rules:

; Define a set of digits
digits: charset "0123456789"

; Define a phone number rule (3 digits, a dash, and 4 digits)
phone-num: [3 digits "-" 4 digits]

; Match the rule against a string
parse "467-8000" phone-num
; == true

Tail-Call Optimization (TCO)

Functional programming constructs often rely heavily on recursion. To prevent stack overflows, Ragnar implements tail-call optimization, allowing functions in the tail position to reuse the current stack frame.

Tail Recursion

Here is an implementation of a tail-recursive factorial function using a nested loop closure:

factorial: func [n] [
    loop: func [i accum] [
        either i > n [
            accum
        ] [
            loop (i + 1) (accum * i)  ; Recursion in tail position
        ]
    ]
    loop 1 1 
]

factorial 10  ; returns 3628800

Trampolining for Mutual Recursion

Ragnar also supports mutual recursion trampolining. Two or more functions calling each other in tail positions can run indefinitely without expanding the stack:

is-even?: func [n] [
    either n == 1 [ false ] [ is-odd? (n - 1) ]
]
is-odd?: func [n] [
    either n == 1 [ true ] [ is-even? (n - 1) ]
]

is-even? 10001  ; returns false
is-even? 10002  ; returns true

Actor Model Concurrency

Ragnar features a lightweight actor model implementation inspired by Erlang. Actors run on separate tasks and communicate strictly via asynchronous message-passing, removing the need for explicit threads and locks.

Actor Operations

  • spawn [block]: Starts a new actor process executing the block in the background.
  • receive: Blocks the calling actor until a message is sent to its mailbox. Returns a [sender message] block.
  • tell actor msg: Sends a message asynchronously to another actor's mailbox (implicitly packaging the sender as [sender message]).
  • kill actor: Terminates the target actor's thread.

Area Server Example

start-area-server: does [
    spawn [  ; Starts a new actor process
        forever [
            msg: receive  ; Wait for a message in mailbox, returns [sender message]
            client: first msg  ; Sender actor reference
            shape: second msg  ; Content of shape details
            
            switch/default first shape [
                rectangle [
                    tell client reform [
                        "area of rectangle is" (shape/2 * shape/3) ]
                ]
                circle [
                    tell client reform [ 
                        "area of circle is" (3.14159 * (shape/2 * shape/2)) ]
                ]
            ] [
                tell client reform [ 
                    "i don't know what the area of a" shape/1 "is." ] 
            ]
        ]
    ]
]

server: start-area-server
tell server [rectangle 5 10]
print ["Response:" second receive]
; Response: area of rectangle is 50

tell server [circle 5]
print ["Response:" second receive]
; Response: area of circle is 78.53975

tell server [triangle 5 10]
print ["Response:" second receive]
; Response: i don't know what the area of a triangle is.

kill server

Functional Programming

Ragnar treats functions as first-class citizens. It supports lexical closures, partial application, and function composition operations.

1. Lexical Closures

Functions capture the lexical environment of where they are defined, enabling state-carrying closures:

make-counter: func [start] [
    current: start
    func [] [       
        current: current + 1
    ]
]
counter: make-counter 10
counter ; returns 11
counter ; returns 12

2. Partial Application

Partially apply arguments to functions to create specialized variations using the partial keyword:

add-five: partial :add 5
add-five 10 ; returns 15

3. Function Composition

Combine multiple functions together using composition operators (forward >> and backward <<):

inc: func [n] [n + 1]
double: func [n] [n * 2]

f-forward: :inc >> :double  ; (x + 1) * 2
f-backward: :inc << :double ; (x * 2) + 1

f-forward 5  ; returns 12
f-backward 5 ; returns 11

4. Higher-Order Mezzanine Functions

Ragnar provides built-in higher-order functions to map, filter, and reduce collections:

; 1. funcmap - Applies a function to each item
double: func [x] [x * 2]
funcmap :double [1 2 3]  ; returns [2 4 6]

; 2. funcflatmap - Applies a function and flattens the results
expand: func [x] [reduce [x x * 10]]
funcflatmap :expand [1 2 3]  ; returns [1 10 2 20 3 30]

; 3. funcfilter - Keeps items where the function returns true
even?: func [x] [x // 2 = 0]
funcfilter :even? [1 2 3 4 5 6]  ; returns [2 4 6]

; 4. funcfold - Reduces a block using a binary function (optional /initial)
sum: func [a b] [a + b]
funcfold :sum [1 2 3 4]  ; returns 10
funcfold/initial :sum [1 2 3 4] 10  ; returns 20

SQL Server Library

Ragnar integrates seamlessly with .NET. By utilising standard .NET assemblies like Microsoft.Data.SqlClient, we can create libraries to communicate with SQL databases in just a few lines of code.

Library Source

The library (available in lib/sqlserver.r) wraps the ADO.NET SQL client connection and command processes:

make object! [
    connect: func [connection-string] [
        conn: new "Microsoft.Data.SqlClient.SqlConnection" [connection-string]
        conn/open
        conn
    ]

    query: func [connection query-text parameters] [
        cmd: connection/CreateCommand
        cmd/CommandText: query-text

        if not none? parameters [
            idx: 1
            while [idx <= length? parameters] [
                param-name: pick parameters idx
                param-val: pick parameters (idx + 1)
                clean-name: join "@" replace (to-string param-name) ":" ""
                cmd/Parameters/AddWithValue clean-name param-val
                idx: idx + 2
            ]
        ]

        reader: cmd/ExecuteReader
        result: copy []
        while [reader/Read] [
            row: copy []
            col-idx: 0
            while [col-idx < reader/FieldCount] [
                append row reader/GetName col-idx
                append row reader/GetValue col-idx
                col-idx: col-idx + 1
            ]
            append result row
        ]
        reader/close
        result
    ]
]

How to Use

Load the library into your script using do, open a connection, and execute parameterized queries:

; 1. Load the library
sql: do %lib/sqlserver.r

; 2. Open connection
conn: sql/connect "Data Source=localhost;Initial Catalog=master;Integrated Security=True"

; 3. Run query with parameters
result: sql/query conn {
    SELECT name, database_id
    FROM sys.databases
    WHERE state_desc = @state
} [
    state: "ONLINE"
]

; 4. Parse output
print ["Row count:" length? result] 
db1: first result ; Get first row
print ["Database Name:" select db1 "name" "ID:" select db1 "database_id"]

; 5. Close connection
conn/close