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