Safe Scripting Syntax: Difference between revisions

From Adam Colton
Jump to navigation Jump to search
No edit summary
 
(6 intermediate revisions by the same user not shown)
Line 1: Line 1:
Maybe just do this: https://github.com/perlin-network/life
I've already done a first pass of this with the dist.ribut.us VM but the idea fascinates me so I may try and do a more advanced version.
I've already done a first pass of this with the dist.ribut.us VM but the idea fascinates me so I may try and do a more advanced version.


Line 15: Line 17:


Memory is use bounded. So is computation.
Memory is use bounded. So is computation.
== VM Definition ==
The spec recognizes 3 types; data, references and code. Code is byte code that can run by a VM instance.
A VM is primarily defined by it's commands. It can also be configured to limit memory use, running time, running steps and so on.
A running instance has scopes for code and memory. A memory scope has data, references and a write flag. There is a pointer to the current execution scope and index; 2 reference registers and 2 data registers. The call stack holds scope and index values, but does not store register values. There are two data registers to allow for binary operations. There are two reference registers because that is necessary to copy between scopes. But most operations use the scope in $R0.
I'm not sure about concurrency. I like it, but it adds complexity. I think the right approach would be to do a V1 with no concurrency, then possibly do a V2 with concurrency.


== Simplicity ==
== Simplicity ==
Line 20: Line 31:


== Assembly ==
== Assembly ==
The assembly starts by expressing the expected arguments and requesting memory. It then requests which language extensions and modules it needs.
Assembly is really the core. The byte code needs to be assembled per machine (because the commands are not fixed values).
 
There are variables and conditions, but no loops. This way we can define modules that wrap commands - If A is not available as command define A as a function.
 
An assembly first defines the number of memory blocks it wants. Each memory block starts with a data length, reference length and a type; populated ram, unpopulated ram or rom. For rom it can set literal values or request them from the VM (this is how modules get linked).
 
All operations internal to the scope use data values as ints. So a jump command uses an int.
 
Let's walk through this; I want one piece of code to call x -> x^2 + 1 with x=5. Well call the initial scope A and the function scope B.
 
Register version
  -A-
  set $D0 5
  call B.foo
  -B-
  foo:
  mul $D0 $D0
  add $D0 1
  return
 
Lets do (x,y,z)->x^2+y*z with x=6, y=7 and z=8


I like the idea of memory pages, but it may make more sense to tag pages as read-only, read-write or execute.
Have A setup the memory
  -A-
  open 24 0 # 24 data bytes and 0 references, sets $R0 to new scope
  write 0 6 # writes to the address in the scope at $R0
  write 8 7
  write 16 8
  call B.foo
  -B-
  foo:
  read $D0 0
  mul $D0 $D0
  write 0 $D0
  read 8 $D0
  read 16 $D1
  mul $D0 $D1
  read 0 $D1
  add $D0 $D1
  write 0 $D0
  return


At the assembly level commands are named and scoped
Have B setup the memory
   IaddVal $0, 100
   -A-
   foo.bar $0, 400, 3.1415
  # $D0 is the current memory location
  write $D0 6
  add $D0 8
  write $D0 7
  add $D0 8
  write $D0 8
  add $D0 -16
  call B.foo
  -B-
  foo:
  open 24 0 #
   set $D1 0
  copyIn 24 # copies 24 bytes from $R1:$D1 to $R0:$D0
  read $D0 0
  mul $D0 $D0
  write 0 $D0
  read 8 $D0
  read 16 $D1
  mul $D0 $D1
  read 0 $D1
  add $D0 $D1
  write 0 $D0
  read 0 $R0
  close
  return


When compiling, these are reduced to bytes. Module commands are replaced with Calls. Language extensions add commands. The total number of commands is fixed, so that determines how many bytes a call requires.
So "open" sets $R0 to the new value, $R1 to the previous $R0 value and if the new block has at least one reference, it is set to the previous $R1 value. Close does the opposite, sets $R0 to $R1, and sets $R1 to either the first reference value or 0.


== Module Loading ==
== Module Loading ==
Line 55: Line 128:


I'll write the compiler as a package and include a command line so it can be used anywhere.
I'll write the compiler as a package and include a command line so it can be used anywhere.
== GC ==
The GC will actually be a language extension, but a core one. It has to be configured at runtime. It only runs against read-write memory blocks.
The header of each block contains a mark-and-sweep flag and the number of references. So it just does a mark and sweep.


== Memoization ==
== Memoization ==
A detail, but interesting. The base language should be fully deterministic. Language extensions can set a non-deterministic bit. This way, we can run a deterministic script and record the starting state and the output and memoize the whole process.
A detail, but interesting. The base language should be fully deterministic. Language extensions can set a non-deterministic bit. This way, we can run a deterministic script and record the starting state and the output and memoize the whole process.
== UTF-8 Style Numeric encoding ==
* 127: 1 byte 0xxxxxxx
* 16,384: 2 bytes 10xxxxxx xxxxxxxx
* 2,097,152 : 3 bytes 110xxxxx xxxxxxxx xxxxxxxx
and so on. So the information density is 7/8 bits.
[[Category:Code]]
[[Category:Projects]]

Latest revision as of 15:35, 23 August 2018

Maybe just do this: https://github.com/perlin-network/life

I've already done a first pass of this with the dist.ribut.us VM but the idea fascinates me so I may try and do a more advanced version.

Being able to take arbitrary commands and execute them safely and bounded is interesting. The language should have no means to escape it's bounds. It should have base constructs that are guaranteed, language extension modules that can be provided by the environment and library modules.

I think the correct approach is a byte code. The lowest level is primitive but can be run fast.

A level up is a scripting language.

  • Data structures
  • associative array
  • array

VM Safety

This is kind of the whole point, so it's important. Most critical is that the script is fully sandboxed - no access to disk or network and memory access is controlled.

Memory is use bounded. So is computation.

VM Definition

The spec recognizes 3 types; data, references and code. Code is byte code that can run by a VM instance.

A VM is primarily defined by it's commands. It can also be configured to limit memory use, running time, running steps and so on.

A running instance has scopes for code and memory. A memory scope has data, references and a write flag. There is a pointer to the current execution scope and index; 2 reference registers and 2 data registers. The call stack holds scope and index values, but does not store register values. There are two data registers to allow for binary operations. There are two reference registers because that is necessary to copy between scopes. But most operations use the scope in $R0.

I'm not sure about concurrency. I like it, but it adds complexity. I think the right approach would be to do a V1 with no concurrency, then possibly do a V2 with concurrency.

Simplicity

I would eventually like this implemented in many languages. For that reason, it should be easy to implement.

Assembly

Assembly is really the core. The byte code needs to be assembled per machine (because the commands are not fixed values).

There are variables and conditions, but no loops. This way we can define modules that wrap commands - If A is not available as command define A as a function.

An assembly first defines the number of memory blocks it wants. Each memory block starts with a data length, reference length and a type; populated ram, unpopulated ram or rom. For rom it can set literal values or request them from the VM (this is how modules get linked).

All operations internal to the scope use data values as ints. So a jump command uses an int.

Let's walk through this; I want one piece of code to call x -> x^2 + 1 with x=5. Well call the initial scope A and the function scope B.

Register version

 -A-
 set $D0 5
 call B.foo
 -B-
 foo:
 mul $D0 $D0
 add $D0 1
 return

Lets do (x,y,z)->x^2+y*z with x=6, y=7 and z=8

Have A setup the memory

 -A-
 open 24 0 # 24 data bytes and 0 references, sets $R0 to new scope
 write 0 6 # writes to the address in the scope at $R0
 write 8 7
 write 16 8
 call B.foo
 -B-
 foo:
 read $D0 0
 mul $D0 $D0
 write 0 $D0
 read 8 $D0 
 read 16 $D1 
 mul $D0 $D1
 read 0 $D1
 add $D0 $D1
 write 0 $D0
 return

Have B setup the memory

 -A-
 # $D0 is the current memory location
 write $D0 6
 add $D0 8
 write $D0 7
 add $D0 8
 write $D0 8
 add $D0 -16
 call B.foo
 -B-
 foo:
 open 24 0 # 
 set $D1 0
 copyIn 24 # copies 24 bytes from $R1:$D1 to $R0:$D0
 read $D0 0
 mul $D0 $D0
 write 0 $D0
 read 8 $D0 
 read 16 $D1 
 mul $D0 $D1
 read 0 $D1
 add $D0 $D1
 write 0 $D0
 read 0 $R0
 close
 return

So "open" sets $R0 to the new value, $R1 to the previous $R0 value and if the new block has at least one reference, it is set to the previous $R1 value. Close does the opposite, sets $R0 to $R1, and sets $R1 to either the first reference value or 0.

Module Loading

There should be standard collections of packages. A server can quickly download an index. When a script requests that a module be included the server can go get it. The risk this avoids is being able to define arbitrary package names and having the runtime make a network request searching for that which becomes identifiable.

Scripting

  • Data Structs
    • array
    • associative array
  • base types
    • signed int
    • unsigned int
    • byte
    • 64bit float
    • string

Script is strongly typed. Probably OO very similar to Go.

Operators

  • if
  • for : continue, break
  • func : return
  • exit

The exit operator causes the script to terminate. I think I'm going to leave it up the context to determine if the script was successful or not.

I'll write the compiler as a package and include a command line so it can be used anywhere.

GC

The GC will actually be a language extension, but a core one. It has to be configured at runtime. It only runs against read-write memory blocks.

The header of each block contains a mark-and-sweep flag and the number of references. So it just does a mark and sweep.

Memoization

A detail, but interesting. The base language should be fully deterministic. Language extensions can set a non-deterministic bit. This way, we can run a deterministic script and record the starting state and the output and memoize the whole process.

UTF-8 Style Numeric encoding

  • 127: 1 byte 0xxxxxxx
  • 16,384: 2 bytes 10xxxxxx xxxxxxxx
  • 2,097,152 : 3 bytes 110xxxxx xxxxxxxx xxxxxxxx

and so on. So the information density is 7/8 bits.