Rubysyn: Clarifying Ruby's Syntax and Semantics

Rubysyn is an experimental project that introduces a Lisp-based syntax for Ruby to clarify its complex semantics and remove syntactic sugar.
[WIP, 2026-04-01] This is an experiment in clarifying some aspects of Ruby syntax and semantics. For that we're going to introduce an alternative Lisp-based syntax for Ruby, preserving Ruby semantics.
The goal is to define a comprehensive, trivially-parsable and sugar-free syntax.
As I started working on this, I had to find a better explanation for some aspects of Ruby than what is available in standard documentation. So we also discuss some aspects of standard Ruby syntax and semantics.
See the spec/ directory for some corner cases of Ruby syntax and semantics that we are interested here.
Table of Contents
- Array literals: full version
- Single-variable assignment
- Multi-variable assignment
- Logical operators
- Semantic primitives
- Control flow
- Blocks and lambdas
- Classes, modules and methods
- Rubysyn: literals
For some reason, the standard documentation does not explain full syntax of array literals.
Most common case of array literals is extremely well known:
- empty array:
[]; - array of three elements:
[1, 2, 3]; - string-array literals:
%w(...)and%W(...); - symbol-array literals:
%i(...)and%I(...);
Additionally, array literals support so called "constructing array splat" syntax: [1, 2, *foo, 3]
The asterisk before the value replaces it with zero or more values, depending on what is in foo:
- if
foois an array,*foois replaced by its elements:
foo = [10, 11]
[1, 2, *foo, 3]
# [1, 2, 10, 11, 3]
- if
fooresponds toto_amethod, that method is called, and*foois replaced by the result array; - finally, for all other values
*foois replaced by the value offoo:
foo = "hello"
[1, 2, *foo, 3]
# [1, 2, "hello", 3]
Particularly, nil.to_a returns an empty array:
foo = nil
[1, 2, *foo, 3]
# [1, 2, 3]
If foo is a hash, *foo is replaced by a list of two-element arrays, one for each hash key:
foo = { foo: :bar, quux: 23 }
[1, 2, *foo, 3]
# [1, 2, [ :foo, :bar ], [ :quux, 23 ], 3]
Constructing array splat is pure syntactic sugar. You can easily implement it as a simple Ruby function:
def array_splat(arr, chunk)
case
when chunk.is_a?(Array)
return arr.concat(chunk)
when chunk.respond_to?(:to_a)
tmp = chunk.to_a
if tmp.is_a?(Array)
return arr.concat(tmp)
else
raise TypeError.new("can't convert #{chunk.class} to Array")
end
else
return arr.append(chunk)
end
end
Single-variable assignment has a very simple base syntax: a = 3. Variable assignment automatically declares variable in the current binding, if it was not already declared. Newly-declared variables have a value of nil.
In Rubysyn, we decouple variable declaration from variable assignment:
(var <var>): Declares variables and initializes them tonil.(assign var value): Assigns a single value to a single variable.
Multi-variable assignment: a, b, c = 1, 2, 3. On the left side of assignment operator (=) there is a list of two or more variable names. One, and only one variable on the left hand side could be marked with a special "*" (asterisk) syntax. This variable will get assigned an array value that contains all values left after other variables are assigned.
Source: Hacker News












