Natural Language Programming

Using the Ring programming language, we can create Natural programming languages based on classes and objects.

The idea is to create embedded domain‑specific languages that look like external domain‑specific languages. These DSLs are defined and executed at runtime, and they can be freely mixed with regular Ring code.

We can build libraries on top of the concepts introduced in this chapter. For example, see the Natural Library chapter to learn how an abstraction layer can improve the productivity of defining new DSLs.

History

In 2010, I developed a new programming language called Supernova, built using PWCT. The language allowed developers to write code that resembled natural‑language statements to create simple GUI applications.

In the Ring programming language, we can achieve similar results—but with far greater power and flexibility. Ring enables developers to design and use natural‑language‑style code in any domain they choose, not just GUI development.

Ring carries the spirit of Supernova, but expands it through broader generalization and a blend of ideas inspired by multiple programming languages.

Example

The next example presents how to create a class that define two instructions

The first instruction is : I want window

The second instruction is : Window title = <expr>

Also keywords that can be ignored like the ‘the’ keyword

New App
{
        I want window
        The window title = "hello world"
}

Class App

        # Attributes for the instruction I want window
                i want window
                nIwantwindow = 0
        # Attributes for the instruction Window title
        # Here we don't define the window attribute again
                title
                nWindowTitle = 0
        # Keywords to ignore
                the

        func geti
                if nIwantwindow = 0
                        nIwantwindow++
                ok

        func getwant
                if nIwantwindow = 1
                        nIwantwindow++
                ok

        func getwindow
                if nIwantwindow = 2
                        nIwantwindow= 0
                        see "Instruction : I want window" + nl
                ok
                if nWindowTitle = 0
                        nWindowTitle++
                ok

        func settitle cValue
                if nWindowTitle = 1
                        nWindowTitle=0
                        see "Instruction : Window Title = " + cValue + nl
                ok

Output:

Instruction : I want window
Instruction : Window Title = hello world

Change the Ring Keyword ‘And’

What if we want to connect between the two instructions using ‘and’

We have a problem because in Ring ‘and’ is a keyword

We can change that using the ChangeRingKeyword command.

Syntax:

ChangeRingKeyword  <oldkeyword>  <newkeyword>

Note

remember to restore the keyword again

Tip

The ChangeRingKeyword command is executed in the scanner stage by the compiler (before parsing).

Example:

ChangeRingKeyword       and  _and

New App
{
                I want window and the window title = "hello world"
}

Class App

                # Attributes for the instruction I want window
                                i want window
                                nIwantwindow = 0
                # Attributes for the instruction Window title
                # Here we don't define the window attribute again
                                title
                                nWindowTitle = 0
                # Keywords to ignore
                                the  and

ChangeRingKeyword       _and  and

                func geti
                        if nIwantwindow = 0
                                nIwantwindow++
                        ok

                func getwant
                        if nIwantwindow = 1
                                nIwantwindow++
                        ok

                func getwindow
                        if nIwantwindow = 2
                                nIwantwindow= 0
                                see "Instruction : I want window" + nl
                        ok
                        if nWindowTitle = 0
                                nWindowTitle++
                        ok

                func settitle cValue
                        if nWindowTitle = 1
                                nWindowTitle=0
                                see "Instruction : Window Title = " + cValue + nl
                        ok

                func getand
                        see "Using : and" + nl

Output:

Instruction : I want window
Using : and
Instruction : Window Title = hello world

Change the Ring Operator ‘+’

What if we want to define a new behavior for any operator like the “+” operator.

We can do this change using the ChangeRingOperator command to hide operator (change it’s name)

Then we can use the operator as identifier that we can handle it’s behaviour

Syntax:

ChangeRingOperator  <oldoperator>  <newoperator>

Note

remember to restore the operator again

Tip

The ChangeRingOperator command is executed in the scanner stage by the compiler (before parsing).

Example:

ChangeRingOperator + _+

New App {
        +
}

Class App
        +
        func get+
                see "Plus operator"

ChangeRingOperator _+ +

Output:

Plus operator

Change the ‘=’ operator to ‘is’

Example:

ChangeRingKeyword       and  _and
ChangeRingOperator      =    is

New App
{
                I want window and the window title is "hello world"
}

ChangeRingOperator      is    =

Class App

                # Attributes for the instruction I want window
                                i want window
                                nIwantwindow = 0
                # Attributes for the instruction Window title
                # Here we don't define the window attribute again
                                title
                                nWindowTitle = 0
                # Keywords to ignore
                                the  and

ChangeRingKeyword       _and  and

                func geti
                        if nIwantwindow = 0
                                nIwantwindow++
                        ok

                func getwant
                        if nIwantwindow = 1
                                nIwantwindow++
                        ok

                func getwindow
                        if nIwantwindow = 2
                                nIwantwindow= 0
                                see "Instruction : I want window" + nl
                        ok
                        if nWindowTitle = 0
                                nWindowTitle++
                        ok

                func settitle cValue
                        if nWindowTitle = 1
                                nWindowTitle=0
                                see "Instruction : Window Title = " + cValue + nl
                        ok

Using Eval() with our Natural Code

Example:

func Main

  cProgram = ' I want window and the window title is "hello world" '

  MyLanguage(cProgram)

Func MyLanguage cCode

  # We add to the code the instructions that change keywords and operators
  # Because Eval() uses a new Compiler Object (the original keywords and operators).

  cCode = '
        ChangeRingKeyword  and  _and
        ChangeRingOperator  =    is
  ' + cCode

  New App
  {
          eval(cCode)
  }


  Class App

          # Attributes for the instruction I want window
                  i want window
                  nIwantwindow = 0
          # Attributes for the instruction Window title
          # Here we don't define the window attribute again
                  title
                  nWindowTitle = 0
          # Keywords to ignore
                  the

          ChangeRingKeyword  and  _and
                  and=0
          ChangeRingKeyword  _and  and

          func geti
                if nIwantwindow = 0
                  nIwantwindow++
                ok

          func getwant
                if nIwantwindow = 1
                  nIwantwindow++
                ok

          func getwindow
                if nIwantwindow = 2
                  nIwantwindow= 0
                  see "Instruction : I want window" + nl
                ok
                if nWindowTitle = 0
                  nWindowTitle++
                ok

          func settitle cValue
                if nWindowTitle = 1
                  nWindowTitle=0
                  see "Instruction : Window Title = " + cValue + nl
                ok

BraceStart and BraceEnd Methods

We can write code that will be executed before/after using { }

Example:

o1 = new test {
        see "Hello" + nl
}

o1 {}

class test

        func bracestart
                see "start" + nl

        func braceend
                see "end" + nl

Output:

start
Hello
end
start
end

BraceExprEval Method

The next example demonstrates how to use the “BraceExprEval” method to get expressions in Natural code.

Example:

new natural {
        create 5
}

class natural
        create=0
        lkeyword = false
        func braceexpreval r
                if lkeyword lkeyword=false return ok
                see "expr eval" + nl
                see "type: " + type(r) see nl
                see "value : " see r see nl
        func getcreate
                lkeyword = true
                see "create" + nl

Output:

create
expr eval
type: NUMBER
value : 5

Real Natural Code

The next example is a more advanced example

# Natural Code
new program {
        Accept 2 numbers then print the sum
}

# Natural Code Implementation
class program
        # Keywords
                Accept=0 numbers=0 then=0 print=0 the=0 sum=0

        # Execution
        func braceexpreval x
                value = x
        func getnumbers
                for x=1 to value
                        see "Enter Number ("+x+") :" give nNumber
                        aNumbers + nNumber
                next
        func getsum
                nSUm = 0
                for x in aNumbers nSum+= x next
                see "The Sum : " + nSum
        private
                value=0 aNumbers=[]

Output:

Enter Number (1) :3
Enter Number (2) :4
The Sum : 7

BraceError() Method

The next examples demonstrates how to use the “BraceError” method to handle errors when accessing the object using braces {}.

Example:

func main
        o1 = new point {
                x=10 y=20 z=30
                TEST
                SEE test
        }

class point x y z
        func braceerror
                see "Handle Error!" + nl
                SEE "Message :" + cCatchError + nl
                if ( left(cCatchError,11) = "Error (R24)" ) and not isattribute(self,"test")
                        see "add attribute" + nl
                        addattribute(self,"test")
                        test = 10
                ok
                see "done" + nl
                return

Output:

Handle Error!
Message :Error (R24) : Using uninitialized variable : test
add attribute
done
10

Example:

new point {
        x=10 y=20 z=30
        test()
        see "mmm..." + NL
}

class point x y z
        func braceerror
                see "Handle Error!" + nl
                see "Message :" + cCatchError + nl
                see self
                see "Done" + NL

Output:

Handle Error!
Message :Error (R3) : Calling Function without definition !: test
x: 10.000000
y: 20.000000
z: 30.000000
Done
mmm...

Clean Natural Code

Instead of typing the literal as “literal” we can accept the words directly.

Example:

The next example accept hello world instead of “hello world”

But this example uses braceend() to check the end of the instruction

This means that this class process only one natural statement that end with literal.

ChangeRingKeyword       and  _and

New App
{
                I want window and the window title is hello world
}

Class App

                # Attributes for the instruction I want window
                                i want window
                                nIwantwindow = 0
                # Attributes for the instruction Window title
                # Here we don't define the window attribute again
                                title is
                                nWindowTitle = 0
                # Keywords to ignore
                                the  and
                # Data
                                literal = ""

ChangeRingKeyword       _and  and

                func geti
                        if nIwantwindow = 0
                                nIwantwindow++
                        ok

                func getwant
                        if nIwantwindow = 1
                                nIwantwindow++
                        ok

                func getwindow
                        if nIwantwindow = 2
                                nIwantwindow= 0
                                see "Instruction : I want window" + nl
                        ok
                        if nWindowTitle = 0
                                nWindowTitle++
                        ok

                func gettitle
                        if nWindowTitle = 1
                                nWindowTitle=2
                        ok

                func getis
                        if nWindowTitle = 2
                                nWindowTitle=3
                        ok

                func braceend
                        if nWindowTitle = 3
                                see "Instruction : Window Title = " + literal + nl
                                nWindowTitle = 0
                        ok

                func braceerror
                        c= substr(cCatchError,":")
                        while c > 0
                                c= substr(cCatchError,":")
                                cCatchError=substr(cCatchError,c+1)
                        end
                        literal += substr(cCatchError,1)

Flexible Statement Separation

The Ring language allows the use of commas (,) as an alternative to semicolons (;) when separating statements.

Ring also provides a mechanism for extracting identifiers from error messages. By using the braceError() method together with the cCatchError variable, you can capture and analyze the identifier that triggered the error.

Example:

new xBaseUserInterface {
        @10, 10 say "Hello, World!"
        @11, 10 say "I Love Programming!"
}

class xBaseUserInterface

        func braceError
                ? getVarName(cCatchError)

        func getVarName cError
                if left(cError,11) = "Error (R24)"
                        return substr(cError,45)
                ok

        func braceExprEval vValue
                if vValue ? vValue ok

Output:

@10
10
say
Hello, World!
@11
10
say
I Love Programming!

Using Keywords as Identifiers

The next keywords could be used as variables/attributes/etc.

This is useful when creating domain-specific languages that uses these keywords in the commands.

  • Again

  • But

  • Case

  • Catch

  • Done

  • Else

  • From

  • In

  • Off

  • Ok

  • On

  • Other

  • Step

  • To

Example:

new Love {
        I will say it Again and Again
        YOU ARE MY LOVE
        Come with me To the Sky
}

class Love

        To Again

        func getTo
                ? "Where?"

        func getAgain
                ? "Really?"
                return True

        func braceError

Output:

Really?
Really?
Where?

Newline Callbacks Inside Braces

BraceNewLine() is a callback that Ring automatically triggers whenever a logical newline is encountered when we access an object using braces. If a line contains expressions, the method is called after those expressions are processed. If the block contains one or more empty lines, Ring treats all consecutive empty lines as a single break, so is invoked only once no matter how many blank lines appear.

Example:

new SumRows {
        10 20 30      # 60
        10            # 10
        400 100       # 500
        30 40         # 70
}

class SumRows

        lSum     = False
        nSum     = 0
        nLastRow = 0

        func braceExprEval  value

                lSum  = True
                nSum += value

        func braceNewLine

                if lSum ? nSum nSum=0 lSum=False ok

Output:

60
10
500
70