CraftScript
  • CraftScript
  • Syntax
    • Kinds
    • Keywords
    • Language Features
  • Tutorials
    • Getting Started
      • Assurance Keywords
      • Value Keywords
      • Condition Keywords
      • Other Keywords
    • Using the Language
      • The Do Block
    • Importing External Scripts
    • Adding CraftScript to a Server
    • Environments and Contexts
  • Libraries
    • Reflection
    • Debug
    • Math
    • Parser
    • Global
Powered by GitBook
On this page
  • Environment Sanitation
  • Variable Locality
  • Passing Data
  • #1: Initial Variables
  • #2: Structured Environments
  • #3: Global Variables
  • #4: Reflection
  1. Tutorials

Environments and Contexts

Learn about the ecosystem that a program runs in.

PreviousAdding CraftScript to a ServerNextReflection

Last updated 1 year ago

Introduction

An environment is the space that code runs in.

This space contains all of the functions, scripts, variables, libraries and other resources that the code can interact with.

Graphic

This is a depiction of the environment.

Any process is allowed to access resources from a box it is inside, but not those from another box.

E.g. a function in my script cannot access variables from something in other script.

The context is a map of where we are in the environment.

It contains information such as:

  • The variables available here

  • The current line of the script (for printing an error)

  • The current script

  • Where this script started from

  • A link to the script manager

Environment Sanitation

CraftScript attempts to keep the environment as clean as possible, in order to avoid pollution.

Pollution occurs when data belonging to a process is altered by a different process in an unexpected or unintended way.

Example of Pollution

Imagine a script my.script.

my.script
number = 10
// wait 5 seconds
if number > 0 /print yes

Having set number to 10 in line 1, we can be certain that number is 10 (and so greater than zero) in line 3.

An example of environment pollution would be if another script, other.script, changed or deleted the value of the number variable between lines 1 and 3, so our if-statement would fail.

Pollution is unhealthy for development because it can affect how a program will run in a way that a developer cannot expect or account for.

Variable Locality

The most common offender of environment pollution is through variables.

Variable pollution occurs when two separate domains want to access the same variable at the same time.

Process #1
Process #2

In the above example, Process #1 will read the number variable, expecting it to be 10 but Process #2 has polluted it by changing its value to 5 between the initial write and the final read.

If, for example, process 2 comes from a third-party program that the creator of the process 1 program does not know about, the creator of process 1 has no way to anticipate or to prevent this error.

Variable Pollution in Browser JavaScript

Non-modular JavaScript files running in the browser share a global variable context.

This makes it easy to pass data between two scripts, since a variable foo can be written to in one script and then read from in another.

However, it also means that one script can unintentionally change or break the variable structure of another, e.g. by overwriting a value.

CraftScript avoids variable pollution by keeping variables local to their context.

When a script is run it starts with its own variable container. When the script finishes running the container is discarded for the garbage collector to tidy up.

Example

When each of the scripts below is run it will have its own number variable.

One script cannot change the other's number variable value.

other.script
/print {number} // null
number = 10
/print {number} // 10
my.script
/print {number} // null
number = 5
/print {number} // 5
run other.script
/print {number} // 5

The same is also true of functions.

A function inside a script has its own variable container, so it cannot read or write variables outside the function in the rest of the script.

Functions need to have their own variable container since they can be run anywhere, including in a different script from where they were created.

Example

The function in the script below has its own variable container. Changing the number variable inside the function does not affect the number variable in the outer script.

my.script
number = 10
func = function {
    /print {number} // null
    number = 5
    /print {number} // 5
}
/print {number} // 10
run func
/print {number} // 10

The function object can be stored and run later or from a different script and will not be affected by changes to the environment.

Passing Data

While it is important to keep variable containers separate for environment hygeine, scripts also need a way to share important data between them.

CraftScript has a fairly atypical approach to this, since the standard parameter/argument setup does not exist in the language.

#1: Initial Variables

When a script or function is run from within another script, variables may be assigned in the run statement.

Initial Map

This can be done by providing a key<->value map object as data.

greet = function {
    /print hello, {name}
}

run greet [name="Jeremy"]

In the above example, a new variable container is allocated for the greet function and a name variable is set to Jeremy before the function is run.

While this allows fairly granular control of what data is passed to the function, it does not indicate to the user that a name variable should be passed -- and if it is not, the function would print hello, null.

We have two approaches to fixing this.

The simplest is to require the user to provide that name variable using a require statement.

This makes sure that a name variable was provided or, if not, terminates the script.

greet = function {
    require [name]
    /print hello, {name}
}

run greet [name="Jeremy"] // good, we gave it a name
run greet // errors, we forgot our name

By terminating the script and printing an error we alert the developer that they haven't met the conditions for this function to run and that they must correct their script in order to use it properly.

However, a developer could still run the function with an explicit null name.

greet = function {
    require [name]
    /print hello, {name}
}

run greet [name=null] // uh oh

The require statement makes sure the name variable exists but permits an empty null value.

As an alternative, we could simply check that the name variable is what we expect it to be.

greet = function {
    if name == null {
        /print hello
    } else {
        /print hello, {name}
    }
}

run greet [name="Jeremy"] // good
run greet [name=null] // good
run greet // good

Initial List

As the anonymous map syntax is cumbersome a list of objects may also be provided.

greet = function {
    require [name]
    /print hello, {name}
}

run greet ["Jeremy"]

This makes use of the require statement's second feature: automatic assignment.

The objects passed in the list are anonymous variables - they have no name and so cannot be directly referenced in code.

If a require [x] statement can find no variable named x, it will assign the next anonymous variable to the label x.

In the example above, the anonymous variable "Jeremy" is being assigned to name since no name variable exists.

If a single object is provided instead of a list it will be treated as a single-element list.

The initial list is less precise than a map, since the variables must be provided in a specific order to function correctly.

#2: Structured Environments

A structure can be used in the place of a map when running a function or script.

greet = function {
    /print hello, {name}
}

person = struct {
    name = "Jeremy"
}

run greet person

Unlike the map, the structure itself is used as the variable context.

This means that any changes to variables in the function or script being run will be reflected in the structure.

rename = function {
    name = "Bearimy"
}

person = struct {
    name = "Jeremy"
}

assert person[name] == "Jeremy"
run rename person
assert person[name] == "Bearimy"

In the above example the name variable in the function directly references the name property of person. Any changes to one are reflected in the other.

Structures have a fixed property set.

You cannot assign a new property that was not created to begin with.

Since the structure has a fixed set of properties this means the script will have a fixed set of variables.

This can lead to some unexpected behaviour.

rename = function {
    name = "Bearimy"
    number = 10 // we can't set `number`
    assert number == null // the variable does not exist
}

person = struct {
    name = "Jeremy"
}

run rename person
assert person[number] == null // we can't add a new property to a struct

Using a structure as an environment for a third-party script or function is not advised, since it cannot assign new variables and will fail silently or unexpectedly.

#3: Global Variables

The global variable library allows us to set variables that are visible and mutable from anywhere in the runtime environment -- including completely unrelated script processes we did not start.

import [global]

greet = function {
    import [global]
    /print hello, {global[name]}
}

global[name="Jeremy"]

run greet

The global object we import in our function is the same as the global object we imported in our script. Any changes to one are immediately present in the other.

Any script can access the global variable map at any time.

Another script could alter your variable between your write and read, changing its value unexpectedly.

As the global variable container is a variable container it can be used as the variable container for a function or script.

import [global]

greet = function {
    /print hello, {name}
    test = 5
}

global[name="Jeremy"]

run greet global
assert global[test] == 5

While this is not recommended (given the large risk of variable pollution) it may be useful (and/or necessary) in rare cases.

The global variable container has a special safety feature that makes it constant between all programs and background tasks.

Two scripts can be run simultaneously using the global variable container and see each others' changes in real time.

#4: Reflection

The built-in reflection library allows the user to obtain a copy of the current variable container.

This can be used as the container for a function or another script, meaning all variables and changes from one will be seen by the other.

import [reflection]

greet = function {
    /print hello, {name}
    test = 5
}

name="Jeremy"

variables = run reflection[variables]
run greet variables

assert test == 5

This is inherently dangerous since it guarantees variable pollution, so it should only be used for functions and scripts the user knows the contents of.

The context must not be transferred to a background process, since it has no safety features.

set number to 10



read number


set number to 5