Stringle

From Esolang
Jump to navigation Jump to search

Stringle is a programming language based around string manipulation created by User:Function call without parameters in 2024. Its name is a portmanteau of string and wrangle, being a language to process and modify strings.

A quick reference to the operators and predicate symbols in Stringle.
Stringle Cheat Sheet

The most notable constructs of the language are string operators, basically functions evaluated on and modifying strings, and predicates, different statements (sort of like limited-use functions) which decide when a conditional statement should run.

An interpreter for it has been implemented in Visual Basic 6, which will be released later.

It is very similar in concept to TypeString despite not having been inspired by it.

It is also similar in name, and somewhat in its concepts as well, to Sortle despite not being inspired by that language either. It also shares a few important similarities with SNOBOL, a venerated language for string processing used widely from the 1960s to the 1980s.

Stringle could be used as a crude and contrived replacement to awk or sed, for writing simple pass-through text filters.

It is by all chances Turing-complete because a brainfuck and a BCT interpreter have both been written in it.

Syntax

A sentence is basically a line of code, while a word is a part of the code delimited by spaces or tabs (thereby simply whitespace), sometimes called a token in other contexts.

The number of words in a sentence determines what command the sentence will execute. This feature was inspired by ferNANDo.

The first word of a sentence can be preceded with an arbitrary amount of whitespace.

If the first word of a sentence starts with a ` (“backtick”) character, the line is treated as a comment and skipped.

Lines containing no words are simply skipped.

Commands

loop while… (1 word)

(e.g.: x)

If x evaluates to an empty string or a numeric value of zero, continue execution. Otherwise, return back to the line after the previous line consisting of the same single word x (if there is one). This looping behavior was also inspired by ferNANDo.

The loop condition can be prepended with a ! symbol to reverse the loop’s behavior, that is, continue execution if the variable is non-empty and non-zero instead. Note that a negated loop condition must match its counterpart to the character as well.

assignment (2 words)

(e.g.: x y)

Set the value of the variable/function x to the value of y.

concatenation (3 words)

(e.g.: x y z)

Set the value of the variable/function x to the values of y and z concatenated.

conditional assignment (≥ 4 words, even-numbered)

(e.g.: p q [u v…] x y)

Set the value of the variable/function x to the value of y only if all conditions (paired words, like p and q, except the last two words of the sentence) satisfy their given predicates (as indicated by !, +, %, ^, ~ or none).

conditional concatenation (≥ 5 words, odd-numbered)

(e.g.: p q [u v…] x y z)

Set the value of the variable/function x to the values of y and z concatenated only if all conditions (paired words, like p and q, except the last three words of the sentence) satisfy their given predicates.

Data types

The only variable type is a string. All variables are initialized when first accessed. In the case of reading, they start as empty strings.

A sequence of digits is syntactic sugar for a string constant containing that numeric value (i.e. 352 is equal to "352"). Zero-padded sequences are stored verbatim (i.e. 007 is equal to "007"). You can later read how this affects predicates. Negative or fractional numbers are not supported. They obviously cannot be written to.

A string between "quotes" is a string literal. String literals may contain whitespace.

Everything else not beginning with one of . : \ # @ * is a variable name.

Variable names containing quotes or whitespace are illegal, even when stored in pointers. Any write to them will fail and any read from them will always return an empty string. Pointers containing whitespace are parsed until before the first whitespace (e.g. “x 5” in a pointer refers to x).

Number representation

Although Stringle doesn’t have an inherently numeric data type, nor any mathematical operations, it can do a basic level of mathematics nevertheless. The # (length / curtail) operator, the + (more) predicate and the use of “unary numbers” (strings of certain length) with concatenation as addition, the overloaded @ (repeat) operator as multiplication and the : (tail) operator as the predecessor function together can do surprisingly complex arithmetic calculations. See also the operators which can work with (decimal) numbers.

Any string of digits optionally surrounded by arbitrary amounts of whitespace on both sides is a valid representation of a number and can be passed to the overloaded # curtail and @ repeat operators.

Note that assigning a non-numeric value to # performs the prune operation instead.

Unary mathematics

A unary number is just a string of characters, its actual identity irrelevant, which has to meet one single requisite only: it has to be a certain length. For example, any string containing 5 characters is one valid unary representation of the number ‘5.’ Some string operators can be used to perform basic arithmetic on unary numbers, and also to convert between unary and decimal.

Special variables

Special variables may as well be thought of as functions without parameters, however, unlike string operators, they do not preclude other variables to have names with the prefixes ?, $ and $!.

The ? (rnd) variable

Reading from ? returns a different random number every time. Writing to ? has no effect.

The $ (stdio) variable

Reading from $ returns a line read from stdin or prompts the user for a line of input. Writing to $ outputs the text written to stdout.

The $! (notEOF) variable

Reading from $! returns “1” if the last input operation was successful and “0” otherwise. Writing to $! has no effect.

Predicates

Predicates modify how conditions are evaluated in conditional statements. Predicate symbols always have to be the first character of the second condition of a conditional pair. They must precede any string operators (if present). The characters ! + % ^ ~ have no special meaning outside predicates, and can be used as the first character of variable names.

The is predicate (no symbol)

This is the most basic predicate, which simply evaluates to true if its arguments are equal. It uses no predicate symbol.

For example, the predicate "paul" "paul" is true, while "paul" "john" is not.

Note that this predicate checks for string, rather than mathematical, equality, making 7 and 007 not equal. For the workaround, see the more + predicate.

The ! (not) predicate

The not predicate (p !q) evaluates to true if its arguments are not equal.

For example, the predicate "paul" !"john" is true, while "paul" !"paul" is not.

The + (more) predicate

The more predicate (p +q) evaluates to true if p is greater than or equal to q. Only makes sense when both p and q are valid numeric values, otherwise behaves like a regular equality check.

For example, the predicates 25 +12 and 25 +25 are true, while 12 +25 is not.

Two + predicates, set up in opposite directions (p +q q +p), can be used to check for mathematical, rather than string, equality (unlike the is predicate), making 7 and 007 equal.

The % (contain) predicate

The contain predicate (p %q) evaluates to true if p contains q as a substring.

For example, the predicate "example" %"amp" is true, while "example" %"paul" is not.

The ^ (extend) predicate

The extend predicate (p ^q) evaluates to true if p starts with q as a prefix.

For example, the predicate "apple" ^"app" is true, while "apple" ^"ple" is not.

\p ^\q can be used to check whether q is a suffix of p.

The ~ (join) predicate

The join predicate (p ~q) evaluates to true if p and q share a common, non-null substring.

For example, the predicate "steve" ~"peter" is true, while "steve" ~"frank" is not.

The join predicate implements a basic “set intersection” operation on two sets of characters, and checks the result for being non-empty. This can be used to efficiently implement simple database functions.

It is also the only predicate to be able to implement a rudimentary “or” conjunction: x ~"abc" will be true if x contains any of “a”, “borc”.

Negated predicates

The + more, % contain, ^ extend and ~ join predicates can be prepended with an !, that is, !+ !% !^ !~ respectively, to make to them evaluate to their negatives.

For example, the predicate "example" !%"amp" is false, while "example" !%"paul" is true.

String operators

String operators may be the most complex construct of Stringle. They are prefixed to expressions and return the result of the function applied over said expression. They can be also chained (composed) in free variation, such as #*.::x returning the length of the variable named by the third character of x, or even *@.\*.::x referring to the variable named by the ASCII code of the last character of the variable named by the third character of x.

The way of how functions are composed is called pipelining. The pipelining of operators as return values is straightforward, simply evaluating them from inside out (i.e. right to left), in turn, although the * (pointer) operator may need some explanation. On the other hand, the pipelining of overloaded operators is notably more complex; for details, see pass-through.

The \ (reverse), # (length / curtail / prune), @ (code / repeat), and * (pointer) operators are overloaded, meaning they can be assigned values to as well as read from, and have rather useful effects both ways. The . and : operators are read-only, and have no effect when assigned to (except when being passed through by curtail, prune or repeat, when they are applied first to the string being modified before passing the result to # or @).

The . (head) operator

.x returns the head (first character) of x.

The : (tail) operator

:x returns the tail (everything except the first character) of x.

The \ (reverse) operator

\x returns the reverse of x.

It can also be assigned to: \x y will set the value of the x to the reverse of y. This is functionally identical to x \y, however this overloading makes the reverse-curtail #\ composition possible.

Useful compound operators with \

  • .\x returns the last character of x.
  • \:\x returns everything except the last character of x.
  • This can be generalized to a function chain :n\:k\x which drops n characters from the start and k characters from the end of x.

The # (length) operator

#x returns the length of x as a numeric value.

The curtail / prune operator (# when written to)

The # operator is overloaded to also accept writes, having the names curtail and prune in this context, two slightly different but mainly similar operations depending on what kind of value is being assigned to it.

Assigning a valid numeric value to #x (curtail in this context) truncates the variable x to the specified length.

Meanwhile, assigning a non-numeric string y performs the prune operation, truncating the variable x to right before the first occurrence of y (if it can be found) instead.

Curtailing or pruning the result of a non-overloaded function (e.g. #:x 5) will correctly apply the curtail / prune on the result, in this case setting x to the value of :x curtailed to length 5. To understand why this works the way it does, see pass-through.

The curtail operator is a handy way to convert decimal to unary by truncating a long string to a length equal to a desired value, while prune, basically a “dynamic curtail”, can be useful for finding the position of a substring.

The reverse-curtail compound operator (#\)

#\x y ‘reverse-curtails’ x to the length specified in y, basically truncating x to the last, instead of the first, y characters.

This can be easily worked out from how # and \ operate on their own.

Note that #\x \y with a non-numeric y value performs a ‘reverse prune’ on x, i.e. dropping everything up to and including the last occurrence of the specified string. The search term must also start with a \ reverse operator.

The @ (code) operator

@x returns the ASCII code of the head of x as a numeric value.

The repeat operator (@ when written to)

The @ operator is overloaded to become repeat, fundamentally different from the @ code return function.

Assigning a valid numeric value to @x repeats (replicates) the contents of the variable x the specified times.

Note that this also functions as a unary multiplication.

The * (pointer) operator

* is the most confusing operator of Stringle, yet the most powerful as well. It can be both read and written.

*x reads from, and writes to, the variable named by the string value in x. For example, if x is equal to “kitty”, then *x refers to the variable named kitty.

String operators are evaluated inside a pointer (e.g. if x is equal to “.kitty”, *x refers to the head of the variable named kitty.) and * can take a parameter modified by string operators (e.g. if x is equal to “kitty”, *.x refers to the variable named k).

Multiple * operators can be nested (e.g. if x is equal to “kitty” and kitty is equal to “meow”, **x refers to the variable named meow).

The overloading of * is completely straightforward, replicating exactly the same behavior on read and write.

Numeric operators

The operators # and @ are unique in that they work with numeric values (notated by “◊” in the cheat sheet). Their return forms, length and code, evaluate to numeric strings representing the length or the ASCII code of (the head of) their parameter strings, respectively. On the other hand, their overloaded forms, curtail and repeat, accept numeric strings assigned to them, containing the parameter value to adjust their effect on the string.

The term numeric operators may be slightly misleading because # and @ are still string operators despite their use of numeric parameters or return values besides strings.

# curtail and @ repeat translate to the max function and multiplication, respectively, on a unary and a decimal number. # length converts a unary number to decimal.

Note that prune, also an overloaded form of #, is not a numeric function and can be performed instead by assigning a string to #.

Overloading

The term “overloading” (notated by “♭” in the cheat sheet) may sound intimidating, but in practice, in the context of Stringle, it simply means that some of the string operators may be also used on the left side of an assignment, i.e. being assigned to. Overloading simply means that a string operator also accepts being assigned to, rather than returning a result only (like . :).

This has various effects, in two of the cases, # (curtail and prune) and @ (repeat), having markedly different functions than their return operation (evaluated standing on the right side of an assignment). The overloaded versions of these operators perform advanced string functions on their parameter variable, moderated by the second (numeric, or string in the case of prune) parameter from the right side of the assignment. The change of names from length to curtail and prune, and code to repeat reflects the distinct function of the overloaded operators.

The overloaded forms of \ (reverse) and * (pointer) are functionally identical to their right side counterparts, operating on string values.

An overloaded operator, being used on the left side of an assignment, is sometimes called a verb.

Overloaded operator pass-through

The overloaded # (curtail / prune) and @ (repeat) operators have a useful little quirk, pass-through, to further facilitate splicing of strings. While the . (head) and : (tail) operators are not overloaded and so cannot be directly assigned to, they are evaluated inside an overloaded # or @ assignment. This literally means # and @ ignore (“pass through”) all further applications of . and : in the left side of an assignment, meanwhile applying them to the original string being modified instead.

For example, assigning a numeric value y to #::x will set x to a y-length substring starting from its third character. Assigning a numeric value y to @.x to will set x to its first character repeated y times.

Why pass-through is useful

For example, # (curtail) works by evaluating #x y as “set x to the first y characters of its value”.

Without pass-through, #:x y would mean “set :x to the first y characters of its value”. This can’t be done because : isn’t overloaded so it cannot be assigned to.

However, # passing through the : will interpret this statement as “set x to the first y characters of the value of :x” instead, allowing the #: compound verb to successfully perform the intended operation.

#@x y is, using another form of pass-through, interpreted as “set @x to the first y characters of the result of @x y evaluated” instead of “set @x to the first y characters of the value of @x”, literally changing the meaning of the nested @ operator from code to repeat.

Compound verbs

A compound verb is just a chain of overloaded (or passed-through) operators used on the left side of an assignment. Some of the most useful compound verbs are #\ reverse-curtail, #: behead-curtail, @. repeat-head and #@ limit-repeat, but as all six operators have their idiosyncrasies when used in compound verbs, feel free to experiment!

Examples and snippets

100 doors puzzle

d "."
#d
i d
#i
p "door" #i
*p *p "."
i d f "oc"
i d #@f #*p
i d .\f "o" $ #i
i i d
#i +101 i ""
#i
d d "."
#d +101 d ""
#d

Add two numeric values

a $
b $
e "!"
@e a
f "!"
@f b
g e f
$ #g

Bitwise Cyclic Tag interpreter

This was contributed by User:Olus2000 and proves Stringle is Turing-complete without the * operator.

program $
data $
#data
.program "0" $ .data
.program "0" data :data
.program "1" .data "1" data data .:program
.program "1" program :program .program
program :program .program
#data

brainfuck interpreter

s "++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++."

a " !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~?""
g ":"
@g 255
g "." g

#s 

h .s h
s :s
c "c" #p
f ""
b ""
m ""
.h ">" p p "."
.h "<" p :p
.h "+" *c *c "."
.h "-" *c :*c

.h "." m "o"
m "o" k #*c
m "o" k1 #:::::::::::::::::::::::::::::::*c
m "o" k 10 $ o
m "o" k 10 o ""
m "o" k !10 g1 g
m "o" k !10 #g1 k1
m "o" k !10 g1 g1 "a"
m "o" k !10 o o *g1

.h "," m "i"
m "i" #i 0 i $
m "i" k @i
m "i" i :i
m "i" g1 g
m "i" #g1 k
m "i" *c g1

.h "[" *c "" f "."
#f
#f !0 h .s h
#f !0 s :s
#f !0 .h "[" f f "."
#f !0 .h "]" f :f
#f

.h "]" *c !"" b "."
#b
#b !0 s .h s
#b !0 h :h
#b !0 .h "[" b :b
#b !0 .h "]" b b "."
#b

#s

$ o
o ""

Binary to unary conversion

a $

#a
.a 1 b b "0*"
.a !1 b b "0"
a :a
#a

#b
t .b .:b
t "*0" b "0**" ::b
t "*0" b c b
t "*0" c ""
c c .b
b :b
#b

#c
.c "*" d d "*"
c :c
#c

$ d
$ "decimal: " #d

cat

$!
$ $
$!

Extract nth character of string

s "The quick brown fox jumps over the lazy dog."
t s
#t n
ch .\t

Note that an older version, without the \ (reverse) operator, needed this unwieldy function train splicing:

s "The quick brown fox jumps over the lazy dog."
t "::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::"
t t t
t t t
t t t
t t t
t "." t
#t n
t t "s"
ch *t

find (find occurrences of a keyword in a file)

s "John"
$!
l $
n n "!"
c #n ": "
f 0
l %s f 1
f 1 $ c l
f 1 m m "!"
$!
$ #m " matches found."

FizzBuzz

i "!"
f "!"
b "!"
#i
p ""
#f 3 p "Fizz"
#f 3 f ""
#b 5 p p "Buzz"
#b 5 b ""
#p !0 $ p
#p 0 $ #i
i i "!"
#i +101 i ""
f f "!"
b b "!"
#i

Generate grammatically correct English cardinal suffix

n $
s "th"
.\n "1" .:\n !"1" s "st"
.\n "2" .:\n !"1" s "nd"
.\n "3" .:\n !"1" s "rd"
n n s
$ n

Hello, World!

$ "Hello, World!"

Profanity filter

This is using George Carlin’s famous “seven dirty words” as an example, replacing them with an equivalent length of asterisks. I think this example may be allowed on a site which prominently features programming languages having the term fuck in their names.

$!
a $
b ""
#a
a ^"shit" a "****" ::::a
a ^"piss" a "****" ::::a
a ^"fuck" a "****" ::::a
a ^"cunt" a "****" ::::a
a ^"cocksucker" a "**********" ::::::::::a
a ^"motherfucker" a "************" ::::::::::::a
a ^"tits" a "****" ::::a
b b .a
a :a
#a
a b
$ a
$!

ROT-13 encoder/decoder

a $
b "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz"
c b
#@c 78
#\c 52
#a
b %.a d " " b
b %.a #d .a
b %.a e c
b %.a #e #d
b %.a a .\e :a
f f .a
a :a
#a
$ f

Strip a set of characters from a string

a "She was a soul stripper. She took my heart!"
b "aei"
#a
c c .a
b %.\c #c #:c
a :a
#a
$ c

See also

  • TypeString, based on similar concepts of string manipulation
  • ferNANDo, with a similar command selector syntax