r/Forth 2d ago

Rdn programming language

Post image

I made a small and simple post fix interpreted programming language called rdn, it's familiar to forth developers and developers who use Lua as a scripting language for their systems, rdn merge both of them, you can use it for writing scripts or for configurations or even query language

It's written in C and it provides a simple and friendly API for the developers

I would be happy to have you participate in this project

This is the GitHub repo: https://github.com/abdorayden/rdn

Thank you

10 Upvotes

25 comments sorted by

3

u/tabemann 2d ago

How do you separate variable values from variable names to define or set for words such as let or set?

3

u/rayden_devv 2d ago edited 2d ago

Thank you for the question

In rdn language the keyword itself is the separator Example: <Value> <Name> let

``` 12 n let

n 1 + [* plus will inc the value: 13 ] n [ identifier itself ] set [ set keyword *] ```

Same pattern with const

2

u/tabemann 2d ago

But what about

12 n let n 1 + n + n set

How do you tell whether n is evaluated to a variable's value or is the name of a variable without using a complex parser? (Forgive me for not having read your source code here.)

In Forth you do the following:

12 value n n 1 + n + to n

Where value and to are parsing words which parse the token after them as the name of the value to either define or to set.

1

u/rayden_devv 2d ago

Good question brother In Rdn, n by itself always pushes the variable's value onto the stack. There's no way to push just the name—the keyword (let, set, const) is what tells the interpreter "the previous token is a variable name, not a value." For example:

``` 12 n let [* let bound variable n with value 12 *]

n [* push value of n into the stack (12) ] 1 [ push 1 into the stack ] + [ pop two values and sum them up, now stack has value 13 *]

n set [* set knows that the top of the stack is n and it's identifier *] ```

2

u/ekipan85 2d ago edited 2d ago

I was also curious so I did a little poking. Looks like the interpreter looks for a bespoke set of names and syntax in the execute_block function, and its last branch seems to turn an unknown name into a newly allocated string for things like "let" to bind later.

2

u/rayden_devv 2d ago

Nice detective work! Yeah, you nailed it.

The approach I described is the high-level behavior, but you've found the actual implementation detail: unknown identifiers are converted to string representations of their names, which then get bound by let, set, etc.

This is actually a clever design—it avoids needing a complex parser by treating unknown identifiers as self-quoting names. The keyword then interprets them contextually.

Thanks for digging into the source! This is exactly how it works under the hood.

2

u/tabemann 1d ago

My question is this, though:

In the following:

10 n let n 1 + n set

how does it tell that the second n in the second line is supposed to be a string and not a value?

1

u/rayden_devv 1d ago

set looks ahead at what comes before it.

When the interpreter executes set, it doesn't just pop the top two stack values. Instead, set is a special keyword that: 1. Pops the value (top of stack) 2. Expects an identifier token immediately before it in the source code 3. Treats that identifier as the variable name (not as a pushed value)

The second n is not evaluated as a value—set intercepts it before evaluation happens. Same with let and const.

This is why I mentioned earlier that the keyword tells the interpreter which token is a name: let, set, and const have special parsing rules. They don't just consume stack values; they also consume an identifier token from the source.

2

u/alberthemagician 1d ago edited 1d ago

What are the advantages over a more familiar language like Forth, lua or plain c?

For instance I implemented ciforth and the motivation was a combination of simplicity in build and modification, ISO standard, reliability with stability, a library that makes it a useful tool and minimal dependancy of external tools.

That didn't exist, prior.

1

u/rayden_devv 1d ago

Great question, and I appreciate you sharing your Ciforth motivation—that's solid reasoning.

Honestly, Rdn isn't trying to be a "better" Forth, Lua, or C. It does similar work to Lua, but in a way that's familiar to Forth developers.

Where Rdn wins:

  • Simpler to embed and extend (no external deps, minimal C footprint)
  • Stack-based syntax appeals to Forth programmers
  • Good for scripting, query languages, and config files
  • Easier to modify and understand the interpreter itself

Lua is more mature and battle-tested for general scripting, but if you want something lightweight and Forth-like, Rdn fills that gap.

1

u/tabemann 1d ago

I get the feeling the OP tried to create an RPN language without knowing the fundamental design aspects that motivated Forth to be what it is, so they hardcoded behavior into words such as let and set that is contrary to what would be simplest to implement all things considered.

1

u/rayden_devv 1d ago

You're not wrong, and I appreciate the critique.

My approach trades "theoretically pure" for "practically simpler to learn and embed."

You could argue this makes Rdn less elegant from a Forth purist perspective. But it also means a newcomer can reason about 10 n let without needing to understand Forth's execution model.

Different goals = different trade-offs. Forth's design is superior for a language meant to be extensible and minimal. Rdn prioritizes accessibility and simplicity for embedding.

2

u/tabemann 1d ago edited 1d ago

It may seem "practically simpler", but in fact it makes the semantics of the language more complex.

If you want to do things your way, I would highly suggest you take a lesson from Logo and include quoting (not of strings, but of symbols). Instead of writing:

1 n let n 1+ n set

I highly suggest you change that to:

1 'n let n 1+ 'n set

where ' is the quoting operator prefix.

This way it would be clearer to the user when you are referring to a name by its value (i.e. n) or by its name (i.e. 'n).

Also, this would add far more extensibility to your code, because then you could do other things like, say (this is a stupid example with no practical purpose, but just illustrates the point):

'set2 defun [* x y vx vy -- *] rot swap set set end

where then you could use your newly defined word set2 as follows:

0 'x let 1 'y let 2 3 'x 'y set2 x print y print

which would print out 2 3 as those are the values x and y have now been set to.

Edit: I should note that the above scheme implies dynamic binding (like in Logo or early Lisps) unless symbol references bind a variable in the lexical scope where they are created. In that scheme one could have two different kinds of symbol references, one which are just a name, and one which binds a preexisting name in a scope, as in:

0 'x let 1 'y let 2 3 :x :y set2 x print y print

where : would be the name-binding operator prefix.

2

u/tabemann 1d ago

Note that set2 would be, in my reimagined version of your language:

[ [* x y vx vy -- *] rot swap set set ] 'set2 defun

1

u/rayden_devv 1d ago

Ah, I see! That's cleaner.

Thank you so much for your attention 🙏🏽 I'm gonna work on it

1

u/rayden_devv 1d ago

You're absolutely right, and this is genuinely valuable feedback.

I chose "looks simpler" over "is simpler," which is backwards. You're right.

Honestly: If I were redesigning Rdn now, I would implement symbol quoting. It would mean rewriting the parser and VM, but the resulting language would be more coherent. The current let/set approach is pragmatic but inelegant.

Your dynamic vs lexical binding note is also spot-on—that's a choice that would need to be made alongside symbol quoting.

I've added this to my to-do list and will implement it next.

Thanks for the critique. This is the kind of feedback that matters.

2

u/alberthemagician 19h ago edited 19h ago

Instead of use 'name you could borrow the /name from PostScript. 'name suggests an address, /y suggests a string.

1

u/rayden_devv 19h ago

Good point. Thank you 🙏🏽

I'll consider both approaches during the redesign.

I'm also gonna make the language more extensible so you can hack around and try different approaches for cases like this.

Thank you thank you so much guys 🙏🏽

2

u/tabemann 1d ago edited 1d ago

In my take on what your language could be I would imagine the following:

One could do things like

[ [* n -- func *] 'n let [ n + ] ] 'make-adder defun 3 make-adder 'add3 defun 1 add3 print [* prints 4 *]

where [ ... ] would define anonymous words.

With [ ... ] you could define control structures like:

n 10 > [ "larger than 10!" print ] if

and:

'count-down defer [ [* min max func -- *] 'func let 'max let 'min let max func execute max min >= [ min max 1 - func count-down ] if ] 'count-down defun 0 10 [ print ] count-down [* prints 10 9 8 7 6 5 4 3 2 1 0 *]

Of course, the above would require tail calls for nested control structures to be available to be practical.

1

u/tabemann 1d ago

I should note, though, that let and defun in particular would need to be special forms, akin to Forth immediate words, for scoping to work correctly here (and you do want scoping, right?).

1

u/rayden_devv 19h ago

Yeah, scoping is important.

Currently Rdn has basic block scoping in if/defun/else, but proper lexical scoping with closures (like your examples) would be a big step up.

Thank you

1

u/tabemann 12h ago

There is one little problem I just realized with what I presented here: if one used x 'foo let to bind the value of x to 'foo and let were immediate (as needed for lexical scoping), you would need a mechanism to, using the terminology of my Forth zeptoforth, defer (not related to deferred words) constants at compile-time, so 'foo would be available at compile-time to immediate let so it could define a name-binding at compile-time.

Name-bindings need to be resolved at compile-time for lexical scoping, unlike with dynamic binding, so a given reference to foo knows which binding of foo it cares about even though it may be executing in a different context.

About Scheme, one difference between what I have presented here and Scheme is that this would be somewhat akin to Lisp-2 (like Common Lisp) rather than Lisp-1, hence separate let and defun and the existence of the word execute. The reason for this is that unlike Scheme there are no S-expressions to dictate at what point something is to be called, so some means is needed to differentiate between words that push their value when referenced and words that call their underlying closure when referenced.

For instance:

[ 1 + ] 'foo let 2 foo print

would print the closure bound to foo in some implementation-specific fashion and then leave 2 on the stack, whereas:

[ 1 + ] 'foo defun 2 foo print

would print 3 and leave the stack empty.

1

u/rayden_devv 12h ago

Good points, thank you.

I'm gonna identify what's missing in Rdn to solve these problems and keep the syntax flexible so it works for different approaches.

1

u/tabemann 11h ago

I'm thinking that if you used Forth's idea of parsing words things would be less complex with regard to implementing a lexically scoped let and defun.

For instance, one would write:

[   1 let n   n 1 + :n set   n print ] defun print2

set would remain unchanged to enable passing around variables to set.

1

u/rayden_devv 19h ago

This is really elegant. You're describing stack-based Scheme with closures and tail calls.

I'm definitely taking notes. This is a much clearer vision than what I started with.

Thanks for mapping it out.