sai-language
Version:
An object-oriented language designed to afford code comprehension and maintenance. Transpiles in-place to Javascript.
1,848 lines (1,622 loc) • 24.5 kB
Plain Text
reference:
{
FS from require 'fs'
G: ct 0
}
object SAILib singleton
// instance (member) variables
instance:
// change debugFunction to alter where strings from the "debug" verb go
{
debugFunction ~console.log
// internal caches
_protocache blank
_singletoncache blank
_coveragecache blank
// Instantiate
//
// called when the object is created. Nothing to do here.
//
}
Instantiate task
{
inc G.ct
if
{
debug "re-Instantiating over ${@serial_number}"
}
set G.ct
// debug "sai-lib serial ${@serial_number}"
set
// canIterate (utility)
//
// returns true if the candidate seems to be a true iterator, or at least acts like one
//
}
canIterate task given i
{
unless i
{
return false
}
if i[~Symbol.iterator]
{
return true
}
if 'function' is typeof i
{
return true
}
if 'function' is typeof i.next
{
return true
}
return false
// @mustIterate (utility)
//
// returns true if the candidate MUST be iterated
//
}
mustIterate task given i
{
unless i
{
return false
}
if 'function' is typeof i.next
{
return true
}
if 'function' is typeof i
{
return true
}
return false
// @isObject (utility)
//
// returns true if an actual Javascript object
//
}
isObject task given i
{
if i is null
{
return false
}
if 'object' is typeof i
{
return true
}
return false
// isArray (utility)
//
// returns true if an actual Javascript array
//
}
isArray task given i
{
return from ~Array.isArray i
// @isMergable (utility)
//
// returns true if it has items, attributes, or is an iterator
//
}
isMergable task given i
{
return or or
// @isCollection (utility)
//
// returns true if an array or an object
//
}
isCollection task given i
{
return or
// coverage housekeeping
//
//
}
coverage task given test, description
{
with '${description} - ${test}'
{
unless [it]
{
debug 'Covered ${it}'
set [it] true
// assert
//
// throw an error if the test is false
//
}
}
}
assert task as test, message
{
unless test
{
throw new Error 'SAI: failed assertion: ${msg default ''}'
// debug
//
// display debug information
//
}
}
debug_op task as o
{
try
{
set o default 'undefined'
if 'function' is typeof o.next
{
set o '{possible iterator via .next}'
}
else if 'function' is typeof o
{
set o '{function}'
}
o
}
catch
{
"SAILib.debug exception: ${error.message}"
o
// iterator
//
// If the object seems an iterator ALREADY under iteration, return it.
// Otherwise, attempt to invoke the generator to create an iteration
// with blank parameters.
//
}
}
iterator_op task given i
{
unless i
{
return i
}
if 'function' is typeof i.next
{
return i
}
if 'function' is typeof i
{
return from i
}
if i[~Symbol.iterator]
{
return from i[~Symbol.iterator]
}
return i
// iterate
//
// given an object, create and invoke an iterator for it.
//
}
iterate_op task given a
{
if undefined is a
{
return a
}
if
{
return from a
}
if a[~Symbol.iterator]
{
return from a[~Symbol.iterator]
}
if
{
return from process
{
ply a
{
yield it
}
}
}
if
{
return from process
{
each a
{
yield: key, it
}
}
}
return from process
{
yield a
// kviterate (key value iterate)
//
// given any object or value, create and invoke an iterator for it.
// for every available element, the iterater will return an array with key, value
// This is used by the keyword EVERY
//
}
}
kviterate_op task given a
{
unless a
{
return from process
{
nop
}
}
if
{
return from process
{
iterate a
{
yield: key, it
}
}
}
if a[~Symbol.iterator]
{
return from process
{
iterate a
{
yield: key, it
}
}
}
if
{
return from process
{
ply a
{
yield: key, it
}
}
}
if
{
return from process
{
each a
{
yield: key, it
}
}
}
return from process
{
yield: 0, a
// collect
//
// If an object must be iterated, return all of its yielded values.
// Otherwise return it unchanged.
//
}
}
collect_op task given i
{
if undefined is i
{
return i
}
unless
{
return i
}
set a empty
iterate i
{
a.push it
}
return a
// drain
//
// drain a generator, throwing values away
//
}
drain_op task given i
{
if undefined is i
{
return i
}
unless
{
return i
}
iterate i
{
nop
// sort
//
// Given a value, force it into an array, then sort it.
//
}
}
sort_op task given a, fn
{
if
{
return a.slice(0).sort(fn)
}
if
{
return .sort(fn)
}
if
{
return .sort(fn)
}
return a
// enlist
//
// Given any object, array, iterator or value, return an
// array with all of its values.
//
// undefined -> undefined
// value -> [value]
// array -> array
// object -> [[key,value],[key,value],...]
// iterator -> [yielded values]
//
}
enlist_op task given a
{
if undefined is a .. return a
if .. return a
set out empty
if
{
iterate a
{
out.push it
}
}
else if
{
each a
{
out.push: key, it
}
}
else
{
out.push a
}
return out
// entrait
//
// given any object, array, iterator or value, return an
// object with all of its values.
//
// undefined -> undefined
// value -> {value: true}
// array -> {[0][0]:[0][1], [1][0]:[1][1], ...}
// object -> object
// iterator -> {y0[0]:y0[1], y1[0]:y1[1], ...}
//
}
entrait_op task given a
{
if undefined is a .. return a
set out empty
set assign to task given k, v
{
if (k isnt undefined) and (v isnt undefined)
{
set out[k] v
}
}
if
{
iterate a
{
assign .0, .1
}
}
else if
{
ply a
{
assign .0, .1
}
}
else if
{
return a
}
else
{
set out[a] true
}
return out
// alter
//
// return the value of a function
//
}
alter_op task as a, fn
{
return fn(a)
// observe
//
// execute a function, leaving the object unaltered
//
}
observe_op task given a, fn
{
fn a
return a
// audit
//
// Execute a function on every element of an array/list/generator
// But DOES NOT produce the results or alter the original array
// Returns the original array.
//
}
audit_op task given a, fn
{
if
{
ply a
{
fn it, key
}
}
else if
{
return from process
{
iterate a
{
fn it, key
yield it
}
}
}
else if
{
each a
{
fn it, key
}
}
return a
// concat
//
// Create an array by concatenating two arrays.
// Forces things that are not arrays to be arrays.
// Returns a new array, unless "inplace" is set,
// then will modify the first array inplace if possible.
//
// [1, 2] concat [3, 4] -> [1, 2, 3, 4]
// [1, 2] concat [[3, 4], [5, 6]] -> [1, 2, [3, 4], [5, 6]]
// [1, 2] concat 3 -> [1, 2, 3]
// [1, 2] concat {c:3, d:4} -> [1, 2, {c:3, d:4}]
// [1, 2] concat undef -> [1, 2]
// 1 concat [3, 4] -> [1, 3, 4]
// 1 concat 3 -> [1, 3]
// 1 concat {c:3, d:4} -> [1, {c:3, d:4}]
// 1 concat undef -> [1]
// {a:1, b:2} concat [3, 4] -> [{a:1, b:2}, 3, 4]
// {a:1, b:2} concat 3 -> [{a:1, b:2}, 3]
// {a:1, b:2} concat {c:3, d:4} -> [{a:1, b:2}, {c:3, d:4}]
// {a:1, b:2} concat undef -> [{a:1, b:2}]
// undef concat undef -> undef
// undef concat 3 -> [3]
// undef concat {c:3, d:4} -> [{c:3, d:4}]
// undef concat [3, 4] -> [3, 4]
//
}
concat_op task given a, b, inplace
{
if undefined is a
{
if undefined is b .. return undefined
if or .. return b
return array b
}
if undefined is b
{
if or .. return a
return array a
}
if
{
if
{
return from process
{
iterate a .. yield it
iterate b .. yield it
}
}
if
{
set b copy b
return from process
{
iterate a .. yield it
ply b .. yield it
}
}
return from process
{
iterate a .. yield it
yield b
}
}
if not
{
set a array a
}
else unless inplace
{
set a copy a
}
if
{
iterate b .. a.push it
}
else if
{
set a a.concat(b)
}
else
{
a.push b
}
return a
// map
//
// execute a function on every element of an array/object/iterator
// returning a new array/object/iterator with the product of that
// repeatedly invoked function
//
}
map_op task given a, fn
{
if undefined is a .. return a
if
{
set r empty
ply a .. set r[key] fn(it,key)
return r
}
if
{
return from process
{
iterate a .. yield fn(it,key)
}
}
if
{
set r blank
each a .. set r[key] fn(it,key)
return r
}
return fn(a,undefined)
// filter
//
// Evaluats a function on every element of an array/object/iterator
// and returns a new array/object/iterator with only the elements
// the function returned "true" on.
//
}
filter_op task given a, fn
{
if undefined is a .. return a
if
{
set r empty
ply a .. if fn(it,key) .. r.push it
return r
}
if
{
return from process
{
iterate a .. if fn(it,key) .. yield it
}
}
if
{
set r blank
each a .. if fn(it,key) .. set r[key] it
return r
}
return fn(a,undefined) ?? a :: undefined
// reduce
//
// With a starting value (accumulator) that persists through invocations,
// invoke a function on every element of the provided array/object/iterator
// and return the accumulator.
//
}
reduce_op task given a, fn, accum
{
if undefined is a .. return a
if
{
set l a.length
unless l .. return accum
set k 0
if undefined is accum
{
set accum a[k]
inc k
}
while k < l
{
set accum fn(accum,a[k],k)
inc k
}
return accum
}
if
{
return from process
{
set a a iterate // activate iterator if necessary
set step a.next()
unless step.done
{
set k 0
if undefined is accum
{
set accum step.value
set step a.next()
inc k
}
until step.done
{
set accum fn(accum, step.value, k)
set step a.next()
inc k
}
}
yield accum
}
}
if
{
set init undefined is accum
each a
{
if init
{
set accum it
set init false
}
else
{
set accum fn(accum,it,key)
}
}
return accum
// wrap in array and try again
}
return
// slice
//
// return a subset of elements of the given list/iterator
//
// Singletons are wrapped in an array first.
//
// rules
//
// slice undef, -y last y rows
// slice undef, 0 nothing
// slice undef, undef everything
// slice undef, +y first y rows
//
// slice -x, -y everything except last y rows, starting x from end of list
// slice -x, 0 nothing
// slice -x, undef last x rows
// slice -x, +y y rows starting x from end of list
//
// slice 0, -y everything except last y rows
// slice 0, 0 nothing
// slice 0, undef everything
// slice 0, +y first y rows
//
// slice +x, -y everything except last y rows, starting at x
// slice +x, 0 nothing
// slice +x, undef everything starting at x
// slice +x, +y y rows starting at x
}
slice_op task given a, st, ct
{
if undefined is a .. return a
// zero count
if ct is 0 // *, 0
{
if
{
return process .. nop
}
else
{
return empty
// must be an array or an iterator
}
}
unless or
{
set a array a
// return everything // u/0, u
}
if ct is undefined and (st is undefined or st is 0)
{
if
{
return copy a
}
else
{
return a
// return first ct rows // u/0, +ct
}
}
if ct > 0 and (st is undefined or st is 0) // return first ct rows
{
if
{
return a.slice(0,ct)
}
else
{
return from process
{
iterate a
{
if key < ct .. yield it
else .. break
}
}
}
}
if
{
set len a.length
if undefined is st and ct<0 // u, -ct
{
return from a.slice len+ct, len
}
else if 0 is st and ct<0 // 0, -ct
{
return from a.slice 0, len+ct
}
else if st>0
{
if ct<0 // +st, -ct
{
return from a.slice st, len+ct
}
else if ct>0 // +st, +ct
{
return from a.slice st, st+ct
}
else // +st, u
{
return from a.slice st, len
}
}
else if st<0
{
if ct<0 // -st, -ct
{
return from a.slice len+st, len+ct
}
else if ct>0 // -st, +ct
{
return from a.slice len+st, len+st+ct
}
else
{
return from a.slice len+st, len // -st, u
}
}
throw new ~Error 'Impossible code path in slice_op'
// generator
}
set
{
skip 0 // items to skip
crop 0 // items to discard off end
size 0 // cache size
only undefined // stop after collecting this many
keep true
}
if undefined is st and ct<0 // u, -
{
set keep false
set size to -ct
}
else if st<0 // -, *
{
set keep false
set size (-st)
if ct<0 // -, -
{
set crop to -ct
}
else if ct>0 // -, +
{
set crop size-ct
}
}
else if st>0 // +, *
{
set skip st
if ct<0 // +, -
{
set size to -ct
set crop to -ct
}
else if ct>0 // +, +
{
set only ct
}
}
else // 0, -
{
set size to -ct
set crop to -ct
}
set a self iterate
return from process
{
set cache empty
set v a.next()
until skip <= 0 or v.done
{
dec skip
set v a.next()
}
until v.done
{
cache.push v.value
if cache.length > size
{
with cache.shift()
{
if keep
{
if only <= 0 .. return
dec only // this code relies on behaviour of "undefined"
yield it
}
}
}
set v a.next()
}
while cache.length > crop
{
yield cache.shift()
// element
//
// returns a single element from an array/iterator
//
}
}
}
element_op task given a, index
{
if undefined is a .. return a
if
{
return a[index]
}
if
{
return .next().value
}
if index is 0 or index is -1
{
return a
}
return undefined
// clone
//
// create a shallow copy of an array or object
//
}
clone_op task given a
{
if
{
return a.slice(0)
}
if
{
set b blank
each a
{
unless undefined is it
{
set b[key] it
}
}
return b
}
return a
// overlay
//
// creates a new collection with the left collection overlaid by the right collection
//
// [1, 2, 3] overlay [4, undefined, 6] -> [4, 2, 6]
// {a:1, b:2} overlay {c:3, b:4, a:undefined} -> {a:1, b:4, c:3}
//
}
overlay_op task given l, r
{
if undefined is l
{
set l blank
}
if not
{
throw new ~Error "SAI: Attempt to OVERLAY onto something that's not a collection/iterable."
}
if not
{
throw new ~Error "SAI: Attempt to OVERLAY with something that's not a collection/iterable."
}
unless
{
set l copy self
if // left static, right iterate
{
return from process
{
set r self iterate
set v r.next()
ply l
{
unless v.done
{
yield v.value default it
set v r.next()
}
else
{
yield it
}
}
}
}
if // left static, right object
{
each r
{
if it isnt undefined
{
set l[key] it
}
}
}
else // left static, right array
{
ply r
{
if it isnt undefined
{
set l[key] it
}
}
}
return l
}
set l self iterate
if // left iterate, right iterate
{
set r self iterate
return from process
{
set vl l.next()
set vr r.next()
until vr.done
{
yield vr.value is undefined ?? vl.value :: vr.value
set vl l.next()
set vr r.next()
}
yielding l
}
}
set r copy self // left iterate, right static
return from process
{
set i 0
set v l.next()
until v.done
{
yield r[i] is undefined ?? v.value :: r[i]
inc i
set v l.next()
// select
//
// get only elements of src enumerated by keys
//
// ['Apple', 'Banana', 'Cherry', 'Durian'] select [2, 0] -> ['Cherry', 'Apple']
// {a:1, b:2, c:3, d:4} select ["a","c"] -> {a:1, c:3}
//
}
}
}
select_op task given src, manifest
{
unless
{
throw new ~Error "SAI: Left argument to SELECT must be list/traits/iterable."
}
unless
{
if undefined is manifest .. return manifest
set manifest array manifest
}
if
{
if // array, array
{
set result empty
ply manifest
{
result.push src[it]
}
return result
}
else if // array, iterator
{
set src copy src
return from process
{
iterate manifest
{
yield src[it]
}
}
}
set result empty // array, traits
each manifest
{
result.push src[key]
}
return result
}
else if
{
if // iterator, iterator
{
set src self iterate
return from process
{
set l src.next()
set buffer empty
iterate manifest
{
while not l.done and buffer.length <= it
{
buffer.push l.value
set l src.next()
}
yield buffer[it]
}
}
}
if // iterator, array
{
set manifest copy manifest
set src self iterate
return from process
{
set l src.next()
set buffer empty
ply manifest
{
while not l.done and buffer.length <= it
{
buffer.push l.value
set l src.next()
}
yield buffer[it]
}
}
}
set manifest (self keys thru number .) by asc // iterator, traits
return from process
{
set i 0
iterate src
{
if key is manifest[i]
{
yield it
inc i
if i>=manifest.length .. return
}
}
}
}
if // traits, iterator
{
set src copy src
return from process
{
iterate manifest
{
if it isnt undefined
{
yield src[it]
}
}
}
}
set result blank
if // traits, array
{
ply manifest
{
set result[it] src[it]
}
return result
}
if // traits, traits
{
each manifest
{
set result[key] src[key]
}
}
else // traits, value
{
set result[manifest] src[manifest]
}
return result
// update
//
// Updates a collection of traits in-place.
//
// [1, 2, 3] update [4, undefined, 6] -> [4, 2, 6]
// {a:1, b:2} update {c:3, b:4, a:undefined} -> {a:1, b:4, c:3}
//
}
update_op task as dest, manifest
{
if undefined is dest
{
set dest blank
}
unless or
{
throw new ~Error "Attempt to UPDATE into something that's not a list or traits."
}
unless
{
throw new ~Error "Attempt to UPDATE from something that's not a list or traits."
}
if
{
iterate manifest
{
if it isnt undefined .. set dest[key] it
}
}
else if
{
ply manifest
{
if it isnt undefined .. set dest[key] it
}
}
else
{
each manifest
{
if it isnt undefined .. set dest[key] it
}
}
return dest
// delete
//
// Delete, in place, specific items from a collectios
//
// [1, 2, 3, 4] delete [1, 2] -> [1, 3]
// [a:1, b:2, c:3] delete ["a"] -> [b:2, c:3]
// [a:1, b:2, c:3] delete {b:5} -> [a:1, c:3]
//
}
delete_op task as dest, manifest
{
if
{
throw new ~Error "SAI: Attempt to DELETE from an iterator, which is not presently supported."
}
unless
{
throw new ~Error "SAI: Attempt to DELETE from something that's not a list or traits."
}
set
{
splicer task given index
{
unless index is undefined
{
dest.splice index,1
}
}
deleter task given ignore, index
{
unless index is undefined
{
delete dest[index]
}
}
deletel task given index, ignore
{
unless index is undefined
{
delete dest[index]
}
}
}
if
{
unless
{
splicer manifest
}
else if
{
ply manifest using splicer
}
else if
{
iterate manifest using splicer
}
else
{
each manifest
{
local index ~parseInt(key)
unless isNaN index
{
dest.splice index,1
}
}
}
}
else // it's not an array
{
unless
{
delete dest[manifest]
}
else if
{
ply manifest using deletel
}
else if
{
iterate manifest using deletel
}
else if
{
each manifest using deleter
}
else
{
delete dest[manifest]
}
}
return dest
// deepFreeze
//
// Freeze an object and all of its properties (ensure they cannot be changed)
//
}
deepFreeze task given o
{
~Object.freeze o
each o
{
if not o.hasOwnProperty(key) .. continue
if 'object' isnt typeof it .. continue
if ~Object.isFrozen(it) .. continue
it
// xor
//
// return true if a XOR b
//
//
}
}
xor_op task given a, b
{
return a ?? (b??false::a) :: (b??b::false)
// min
//
// return the lower value
//
}
min_op task given a, b
{
return (a<b) ?? a :: b
// max
//
// return the higher value
//
}
max_op task given a, b
{
return (a>b) ?? a :: b
// compare
//
// return -1 if a is less than b
// return 1 if a is greater than b
// otherwise return 0
//
}
compare_op task given a, b
{
if a<b .. return -1
if a>b .. return 1
return 0
// keys
//
// return the keys (item identifiers) from an object in an array.
//
}
keys_op task given a
{
set result empty
if
{
count a.length
{
result.push counter
}
}
else if
{
iterate a
{
result.push key
}
}
else if
{
each a
{
result.push key
}
}
return result
// count
//
// return how many items are in a collection
//
}
count_op task given a
{
if
{
return a.length
}
set i 0
if
{
iterate a
{
inc i
}
return i
}
if
{
each a
{
inc i
}
return i
}
if a is undefined
{
return 0
}
return 1
// values
//
// return a list of all of the item values in a collectios
//
}
values_op task given a
{
if
{
return copy a
}
if
{
return
}
set result empty
if
{
each a
{
result.push it
}
return result
}
if undefined isnt a
{
result.push a
}
return result
// newerror
//
// prepare an error object for throwing
//
}
newerror task given line, file, parameters
{
set e new ~Error parameters.message, file, line
each parameters
{
set e[key] to it
}
return e
// number
//
// convert a value into a number, or zero if that's not possible.
//
}
number_op task given x
{
set n ~parseFloat(x)
return (isNaN n) ?? 0 :: n
// expects
//
// (see 20.Keywords.md for description of purpose)
//
}
expects_op task given params, prototype
{
set result empty
set tester task given param, name, type
{
if type is typeof param
{
nop // good
}
else if param.isof and param.isof[type]
{
nop // good
}
else // bad
{
if param.isa
{
result.push: trait name, 'expects' type, found param.isa
}
else
{
result.push: trait name, 'expects' type, found typeof param
}
}
}
each prototype as type
{
if key is '_root'
{
tester params, 'Root', type
}
else unless params[key]
{
result.push: trait key, 'expects' type, found 'undefined'
}
else if type isnt true
{
tester params[key], key, type
}
}
return result
// expectsThrow
//
// Verify parameters match a prototype, throwing an exception if they don't.
//
}
expectsThrow task given params, prototype, name
{
set x
unless x.length .. return
set err empty
each x
{
err.push '${it.trait} should be ${it.expects} but is instead ${it.found}'
}
throw new ~Error 'SAI: parameter exception in ${name}\n${err.join('\n')}'
// FinalizePrototype
//
// Lock and freeze prototype attributes as needed.
// Verify contracts are fulfilled
// Bind and build an instantiation function.
//
}
finalizePrototype task given proto
{
each proto.__tobelocked
{
~Object.defineProperty proto, it, :configurable false
}
delete proto.__tobelocked
each proto.__tobefrozen
{
proto[it]
}
delete proto.__tobefrozen
if proto.__unverified
{
each proto.__contracts
{
if undefined is proto[it]
{
throw new ~Error "SAI: Contractually required trait ${it} does not exist in object ${proto.isa}."
}
}
delete proto.__unverified
delete proto.__contracts
}
set proto.constructor task
{
set obj ~Object.create(proto)
if obj.Constructor .. obj.Constructor
if obj.Instantiate .. obj.Instantiate.apply obj, ~arguments
return obj
// create_op_base
//
// Function called by compiled SAI to instantiate new SAI objects by name.
// The following code is overridden when running SAI in managed mode.
//
}
}
create_op_base task given name, parameters
{
set proto [name]
unless proto
{
set
{
fn name + '.js'
src FS.readFileSync(fn,'utf8')
src '(function(exports, require, module, __filename, __dirname) {${src}});'
mod ~eval(src)
proto mod(blank,~require,blank,fn,~__dirname)
[name] proto
}
}
unless proto
{
throw new ~Error 'SAI.Create: Do not know how to create SAI object ${name}.';
}
set obj ~Object.create(proto)
if obj.Constructor .. obj.Constructor
if obj.Instantiate .. obj.Instantiate.apply obj, parameters
return obj
// singleton
//
// Function called by compiled SAI to instantiate singleton SAI objects by name.
//
}
singleton_op task given name, parameters
{
set [name] default from name, parameters
return [name]
}