Embedding the Sophia runtime into Go applications

Table of Contents

Tags:

Tip

Suppose you want to create a webserver and make it scriptable via a minimal, fast and go interoperability supporting language - Sophia is here for you and makes it as easy as possible.

Installing the runtime

Install sophia as a project dependency:

1$ go get github.com/xnacly/sophia

Initial embedding

The skeleton imports the embed package - this package provides abstractions over configuring, instantiating and starting the sophia runtime.

 1package main
 2
 3import (
 4	"os"
 5
 6	"github.com/xnacly/sophia/embed"
 7)
 8
 9func main() {
10	embed.Embed(embed.Configuration{})
11	file, err := os.Open("config.phia")
12	if err != nil {
13		panic(err)
14	}
15	embed.Execute(file, nil)
16}

Tip

The embed.Configuration structure allows for a more in depth configuration, such as enabling the linkage of the go standard library into the sophia language via embed.Configuration.EnableGoStd or toggling the debug mode via embed.Configuration.Debug.

On the other hand the embed.Execute function takes *os.File and io.Writer, is the writer nil the runtime prints error message to os.Stdout, otherwise the writer is used. The embed.Execute function also returns an error if the execution was faulty.

Configuration script

Lets add a structure we want to configure with a sophia script:

 1package main
 2
 3import (
 4	"fmt"
 5	"os"
 6
 7	"github.com/xnacly/sophia/embed"
 8)
 9
10type Configuration struct {
11	Port int
12}
13
14var config = &Configuration{}
15
16func main() {
17	embed.Embed(embed.Configuration{})
18	file, err := os.Open("config.phia")
19	if err != nil {
20		panic(err)
21	}
22	embed.Execute(file, nil)
23}

And write the corresponding sophia script, named config.phia:

1(let port 8080)
2(set-port port)

Interfacing with go

Sophia supports interoperability with go by allowing the declaration and injection of functions written in go into its runtime, this can be done by adding a function of type type.KnownFunctionInterface to the embed.Configuration.Functions map:

 1package main
 2
 3import (
 4	"fmt"
 5	"os"
 6
 7	"github.com/xnacly/sophia/core/token"
 8	"github.com/xnacly/sophia/core/types"
 9	"github.com/xnacly/sophia/embed"
10)
11
12type Configuration struct {
13	Port int
14}
15
16var config = &Configuration{}
17
18func main() {
19	embed.Embed(embed.Configuration{
20		Functions: map[string]types.KnownFunctionInterface{
21			"set-port": func(t *token.Token, n ...types.Node) any {
22				return nil
23			},
24		},
25	})
26	file, err := os.Open("config.phia")
27	if err != nil {
28		panic(err)
29	}
30	embed.Execute(file, nil)
31}

Input validation

We only want exactly one parameter, thus we check for the length and use the serror package (short for sophia error) for creating an error with the given token (the first element after our desired argument length).

 1package main
 2
 3import (
 4	"fmt"
 5	"os"
 6
 7	"github.com/xnacly/sophia/core/serror"
 8	"github.com/xnacly/sophia/core/token"
 9	"github.com/xnacly/sophia/core/types"
10	"github.com/xnacly/sophia/embed"
11)
12
13type Configuration struct {
14	Port int
15}
16
17var config = &Configuration{}
18
19func main() {
20	embed.Embed(embed.Configuration{
21		Functions: map[string]types.KnownFunctionInterface{
22			"set-port": func(t *token.Token, n ...types.Node) any {
23				if len(n) > 1 {
24					serror.Add(n[1].GetToken(), "Too many arguments", "Expected 1 argument for set-port, got %d", len(n))
25					serror.Panic()
26				}
27				return nil
28			},
29		},
30	})
31	file, err := os.Open("config.phia")
32	if err != nil {
33		panic(err)
34	}
35	embed.Execute(file, nil)
36	fmt.Println("port:", config.Port)
37}

If we pass two ports to our set-port function we will get the following error message:

 1$ cat config.phia
 2(let port 8080)
 3(set-port port port)
 4$ go run .
 5error: Too many arguments
 6
 7        at: /home/teo/programming/embedding_sophia/config.phia:3:16:
 8
 9            1| ;; vim: syntax=lisp
10            2| (let port 8080)
11            3| (set-port port port)
12             |                ^^^^
13
14Expected 1 argument for set-port, got 2

Type validation

Lets evaluate the result of the argument passed to our function, cast it to a float64 and assign it to config.Port:

 1package main
 2
 3import (
 4	"fmt"
 5	"os"
 6
 7	"github.com/xnacly/sophia/core/serror"
 8	"github.com/xnacly/sophia/core/token"
 9	"github.com/xnacly/sophia/core/types"
10	"github.com/xnacly/sophia/embed"
11)
12
13type Configuration struct {
14	Port int
15}
16
17var config = &Configuration{}
18
19func main() {
20	embed.Embed(embed.Configuration{
21		Functions: map[string]types.KnownFunctionInterface{
22			"set-port": func(t *token.Token, n ...types.Node) any {
23				if len(n) > 1 {
24					serror.Add(n[1].GetToken(), "Too many arguments", "Expected 1 argument for set-port, got %d", len(n))
25					serror.Panic()
26				}
27				res := n[0].Eval()
28				port, ok := res.(float64)
29				if !ok {
30					serror.Add(n[0].GetToken(), "Type error", "Expected float64 for port, got %T", res)
31					serror.Panic()
32				}
33
34				config.Port = int(port)
35
36				return nil
37			},
38		},
39	})
40	file, err := os.Open("config.phia")
41	if err != nil {
42		panic(err)
43	}
44	embed.Execute(file, nil)
45	fmt.Println("port:", config.Port)
46}

Again, lets check the error handling:

 1$ cat config.phia
 2(let port "8080")
 3(set-port port)
 4$ go run .
 5error: Type error
 6
 7        at: /home/teo/programming/embedding_sophia/config.phia:3:11:
 8
 9            1| ;; vim: syntax=lisp
10            2| (let port "8080")
11            3| (set-port port)
12             |           ^^^^
13
14Expected float64 for port, got string

Resulting embedding of Sophia

Simply running our script with valid inputs according to our previous checks will result in the following output:

1$ cat config.phia
2(let port 8080)
3(set-port port)
4$ go run .
5port: 8080

Thats it, a simple configuration structure and two method calls, can’t really get shorter than that - I am now going to migrate my package manager mehr to be configured with sophia.