Skip to content

davegurnell/atlas

develop
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Code

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
 
 
 
 
 
 
 
 
 
 
 
 

Atlas

A tiny embedded scripting language implemented in Scala.

Copyright 2018 Dave Gurnell. Licensed Apache 2.

Build Status Coverage status Maven Central

Why?

Atlas is a super-simple scripting language created for use in Cartographer to allow us to define fragments of logic over user-defined data types. It has the following features:

  • simple, concise, expression-oriented syntax;
  • Scheme-like functional semantics;
  • parser and interpreter written in Scala;
  • serializable in compiled and uncompiled forms;
  • support for "native functions" written in Scala.

Atlas is currently a work-in-progress.

Show me some examples!

Factorials are the "hello world" of scripting languages, right?

let factorial = n ->
  if n <= 1
  then 1
  else n * factorial(n - 1)

factorial(10)

Function bodies are lazily bound allowing letrec-style recursive references:

let even = n ->
  if n == 0 then true else odd(n - 1)

let odd = n ->
  if n == 0 then false else even(n - 1)

even(10)

Quick language reference

Basic literals are Javascript-like:

'foo'      # single-quoted string
"foo"      # double-quoted string
1          # integer
1.2        # double
true       # boolean
null       # null

[a, b]     # array
{a:1, b:2} # object

There are a fixed set of built-in prefix and infix operators. In order of decreasing precedence these are:

!a         # boolean not
-a         # negation
+a         # erm... non-negation

a*b        # multiplication
a/b        # floating point division

a+b        # addition
a-b        # addition

a < b      # comparisons
a > b      # (integer, double, string, or boolean)
a <= b     #
a >= b     #

a == b     # value equality and
a != b     # function reference equality

a && b     # boolean and

a || b     # boolean or

Function literals are written with the -> symbol. Parentheses are optional if there is only one argument:

(a, b) -> a + b
n -> n + 1

In addition to literals, variable references, and infix and prefix operators, there are several types of expression.

Function applications look like Javascript:

max(1, 2)

as do field references:

foo.bar.baz

Conditionals are introduced with the if, then, and else keywords. The else clause is mandatory. The result is the value of the expression in the relevant arm:

if expr then expr else expr

Blocks introduce scopes and allow the definition of intermediate variables. The result is the value of the final expression:

do
  stmt
  stmt
  expr
end

Statements are expressions (evaluated for their side-effects) or declarations, introduced with the let keyword:

let add = (a, b) -> a + b
let ten = add(3, 7)

Function bodies can refer to earlier or later bindings in the block where they are defined, allowing mutually recursive definitions:

let even = n ->
  if n == 0 then true else odd(n - 1)

let odd  = n ->
  if n == 0 then false else even(n - 1)

even(10)

Comments are written with the # symbol and run to the end of the line:

# Calculate a factorial:
let fact = n ->
  if n == 1
  then 1
  else n * fact(n - 1)

Complete programs have the same semantics as blocks but are written without the do and end keywords. If the program ends with a statement, an implicit null expression is added to the end:

let fib = n ->
  if n <= 2
  then 1
  else fib(n - 1) + fib(n - 2)

fib(10)

Interaction with Scala

There are two string interpolators for defining code fragments: expr for expressions and prog for complete programs:

import atlas._
import atlas.syntax._

val anExpression: Expr =
  expr"""
  1 + 2 + 3
  """

val aProgram: Expr =
  prog"""
  let fib = n ->
    if n <= 2
    then 1
    else fib(n - 1) + fib(n - 2)

  fib(10)
  """

Syntax errors raised by the macros result in a Scala compilation error.

You can alternatively use the Parser.expr or Parser.prog methods to parse a regular Scala string: Syntax errors using the parser result in an Either:

val anotherExpression: Either[Parser.Error, Expr] =
  Parser.expr("1 + 2 + 3")

Expressions (and, by extension, programs) can be evaluated using the Eval.apply method. Runtime errors are captured in an Either:

Eval(anExpression) // => Right(IntValue(6))

Eval(aProgram)     // => Right(IntValue(55))

You can optionally pass an Env object to Eval.apply specifying an initial environment:

val program = prog"a + b"
val env = Env.create
  .set("a", 10)
  .set("b", 32)
Eval(program, env) // => Right(IntValue(42))

Although Eval can internally mutate environments to enable mutually recursive function bodies, any environment you pass to Eval.apply should be returned unharmed.

You can implement "native functions" in Scala:

val program = prog"average(10, 5)"
val env = Env.create
  .set("average", native((a: Double, b: Double) => (a + b) / 2))
Eval(program, env) // => Right(DoubleValue(7.5))

Conversion between Atlas and Scala values is implemented using a pair of type classes called ValueEncoder and ValueDecoder. These can be used with the as[A] and asValue extension methods from atlas.syntax:

123.asValue      // => IntValue(123)

IntValue.as[Int] // => Right(123)

Acknowledgements

Thanks to Nic Pereira for naming the project and saving us all from "davescript" :)

About

A tiny embedded scripting language implemented in Scala.

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages