/goblin-lang

A programming language for sickos

Primary LanguageTypeScriptISC LicenseISC

Introduction

Goblin is a postmodern take on a 90s scripting language. It features:

  • dynamic types
  • immutable by default
  • lightweight objects, without classes or inheritance
  • pattern matching
  • novel syntax

Goblin is "research language" in the sense that it is really more intended to be an object of study and discussion, rather than a tool one uses to actually make software. But it is a real language with an implementation that does more or less everything that is described in this document.

Language overview

Literals for numbers & strings:

1 							# an integer
1.0 						# a float
"Hello, world!" # a string

Send messages to values:

"Hello, world!"{uppercase} # => "HELLO, WORLD!"
"Hello, world!"{from: 0 to: 5} # => "Hello"

Operators are syntactic sugar for sending messages. (TODO: short explanation of operator precedence)

1 + 2 	# 1{+: 2}
-1 			# 1{-}

let bindings & identfiers:

let a := 1
let _a long identifier name_ := 2

(TODO: link to more on bindings: destructuring, placeholders, etc)

Parentheses are used to disambiguate operator precedence, but they also create new binding scopes, and can contain statements.

let a := (
	let b := 1
	let c	:= 2
	b + c
) # => 3
let a := () # => `unit`, an object with no methods

if expressions:

let result := if count = 0 then
	"no items"
else if count = 1 then
	"1 item"
else
	count{to String} ++ " items"
end

Objects

Objects are collections of message handlers:

let greetings := [
	on {hello}
		"Hello!"
	on {hello: name}
		"Hello, " ++ name ++ "!"
	on {hello twice}
		self{hello} ++ " " ++ self{hello}
]

greetings{hello} # => "Hello!"
greetings{hello: "world"} # => "Hello, world!"
greetings{hello twice} # => "Hello! Hello!"

Handlers return the value of their final expression, but you can return early:

let obj := [
	on {results: results}
		if results{length} = 0 then
			return "no results"
		end
		return "some results"
]

There are no "classes" in Goblin; instead, we use objects that construct other objects:

let Point := [
	on {x: x y: y} [
		on {x} x
		on {y} y
		on {manhattan distance: other}
			(x - other{x}){abs} + (y - other{y}){abs}
	]
]

let origin := Point{x: 0 y: 0}
let point := Point{x: 1 y: 2}
point{manhattan distance: origin} # => 3

Most values in Goblin are immutable; "setters" instead return a new value:

let Point := [
	on {x: x y: y} [
		on {x} x
		on {y} y
		on {x: x'}
			Point{x: x' y: y}
		on {y: y'}
			Point{x: x y: y'}
	]
]
let a := Point{x: 1 y: 2}
let b := a{y: 3} # => Point{x: 1 y: 3}

There are no "functions" in Goblin; instead, we use objects with a single handler.

import [_Vec_] := "core"
let items := Vec{}, 1, 2, 3
let mapper := [
	on {: value}
		value * 2
]
let mapped := items{map: mapper} # => Vec{}, 2, 4, 6

When an object has only one handler, we can elide the on:

let mapped := items{map: [{: value} value * 2]}

There is no "pattern matching" in Goblin, either; instead, we send a pattern object to a receiver object, and the receiver then sends a message to the pattern object:

let Option := [
	on {some: value} [
		on {: pattern}
			pattern{some: value}
	]
	on {none} [
		on {: pattern}
			pattern{none}
	]
]
let pattern := [
	on {some: value}
		value
	on {none}
		0
]
let a := Option{some: 5}
a{: pattern} # => 5
let b := Option{none}
b{: pattern} # => 0

objects can be "destructured" in let bindings:

let [x: x y: y] := Point{x: 1 y: 2}
# equivalent to
# let temp := Point{x: 1 y: 2}
# let x := temp{x}
# let y := temp{y}

(TODO: link to more on objects & pattern matching)

Frames

Frames are a shorthand for creating simple objects with common behaviors:

let a := [x: 1 y: 2]
a = [x: 1 y: 2] # => true
a{x} # => 1
a{x: 2} # => [x: 2 y: 2]
a{->y: [{: value} value + 1]} #  => [x: 1 y: 3]
a{:[
	on {x: x y: y}
		x + y
	on {x: x y: y z: z}
		x + y + z
]} # => 3

NOTE: The expression [] produces a frame with a blank key; for an object with no methods, use ().

(TODO: link to more on frames)

Vars

var creates a binding that can be reassigned using set. Only the binding is mutable; the value associated with the binding is immutable, and accessing a var binding gets its current value:

var x := 1
let a := x # => 1
set x := 2
let b := x # a = 1, b = 2

There is a shorthand for setting a variable "in place":

var p := [x: 1 y: 2]
set p{y: 3} # set p = p{y: 3}

Var bindings cannot be closed over by objects:

var x := 1
let a := x
let obj := [
	on {foo}
		set x := 2 		 # compile error
		let value := x # compile error
		let value := a # ok
]

Handlers can take var parameters:

var x := 1
let obj := [
	on {inc: var counter}
		set counter := counter + 1
]
obj{inc: var x} # x = 2

do blocks

A do block is an object that uses its parent context instead of creating its own. For example:

  • self refers to the outer object
  • return returns from the outer handler
  • var bindings in the outer handler can be accessed and set

Do blocks are written as objects without brackets, and are typically used for pattern matching, or where other languages would use anonymous functions. However, the increased flexibility for the sender has corresponding restrictions on the receiver:

  • do parameters must be annotated
  • a do parameter cannot be stored or returned; only sent a message or sent in another message
let List := [
	on {nil} [
		on {: do match}
			match{nil}
		on {map: do f}
			self
	]
	on {head: h tail: t} [
		on {: do match}
			match{head: h tail: t}
		on {map: do f}
			let h' := f{: h}
			let t' := t{map: f}
			List{head: h' tail: t'}
	]
]

let obj := [
	on {find: item in: list}
		list{:
			on {nil}
				return [not found]
			on {head: h tail: t}
				if h = item then
					return [ok]
				end
				return obj{find: item in: t}
		}
	on {sum: list}
		var sum := 0
		list{map: {: item}
			set sum{+: item}
		}
		sum
]

provide & use

There are no global variables in Goblin -- even true and false must be imported from the core library. However, pervasive access to global resources (eg. the current time, logging) or application context (eg. the current user) is often desirable. Goblin enables both of these with provide and using, which propagate and access values via dynamic scope:

let log := [
	on {: message}
		using {logger: l}
		l{: message}
]
let mock_logger := [
	on {: message}
		# drop messages
		()
]

log{: "hello"} # logs to system logger
(
	provide{logger: mock_logger}
	log{: "hello"} # logs to mock_logger
)
log{: "goodbye"} # back to system logger

(TO IMPLEMENT: provide/using vars & do blocks, clearing context, module-level context allowlists)

error handling & control flow

defer executes after a handler exits, whether thats by returning itself, returning within a do block, or encountering a runtime error. This is most useful for managing constrained system resources, e.g. file handles:

import [_os_] := "core"
let file := [
	on {with: path do: do block}
		let handle := os{open: path}
		defer
			handle{close}
		end
		block{: handle}
]

the try-send operator ? allows for a default value if an object does not handle a message:

let defaults := [x: 1 y: 2]
let params := [x: 3]
let x := params{x} ? defaults{x} # => 3
let y := params{y} ? defaults{y} # => 2

(TODO: link to try-send idioms)