Skip to content

Instantly share code, notes, and snippets.

@fowlmouth
Created June 13, 2015 06:22
Show Gist options
  • Save fowlmouth/665bedf61e6f85ae7673 to your computer and use it in GitHub Desktop.
Save fowlmouth/665bedf61e6f85ae7673 to your computer and use it in GitHub Desktop.
## this is an component composition framework, it is a basic object model
## founded on composition rather than inheritance.
##
## changes since entoody:
## * removed unicast/multicast messages
## send() sends a message to the first component that applies,
## multicast() is an iterator that sends a message to all components that apply
##
## * removed static component ordering.
## order matters! higher components can intercept messages before
## lower ordered components
##
## * components are geared towards behavior, nice data structs have _accessors_
##
## What the hell is this though?
## * components are chunks of data and/or behavior. isolation of the two is preferred
## except when the data is a primitive (nim type)
## components can be primitive data or dynamic slotted objects
## their memory layout is final after creation
## * aggregates are collections of components, new behavior is bolted on and overrides
## old behavior. aggregate types may not add data after they are created, this would
## invalidate all of their children. a way around this is shown in stdobj.nim
## * objects are instances of aggregates, components here can be added and removed at
## will only require reallocation if data components are added/removed
##
## Example components:
## Position, Rotation,
## TextureRef,
## HealthPoints, Energy, MaxLifetime
##
##
##
##
##
##
import typetraits,tables,macros,strutils
export macros
type
NimDataTypeID* = int
ComponentKind* {.pure.}= enum
Static, Dynamic
Component* = ref object
bytes*: int
name*: string
messages*: Table[string,Object]
case kind*: ComponentKind
of ComponentKind.Static:
nimType*: NimDataTypeID
destructor, initializer: ObjectDestructor
aggr*: AggregateType
of ComponentKind.Dynamic:
slots*: seq[string]
AggregateType* = ref object
bytes*: int
components*: seq[(int,Component)]
UncheckedArray* {.unchecked.}[T] = array[1,T]
Object* = ref object
ty*: AggregateType
dat*: UncheckedArray[byte]
ObjectDestructor* = proc(self:Object, componentData: pointer){.nimcall.}
var data_type_counter{.global.} = 1
var staticComponents{.global.}: seq[Component] = @[]
proc castTo (fro,to:NimNode): NimNode {.compileTime.}=
newTree(nnkCast, to, fro)
proc genDecrefStmts (ty:NimNode, data:NimNode): NimNode {.compileTime.} =
result = newStmtList()
let stmts = newStmtList()
var skipCast = false
case ty.typekind
of ntyInt:
skipCast = true
of ntyString:
let data = data.castTo(newTree(nnkPtrTy, ty))
result = quote do:
`data`[] = nil
# let `strdata` =
# if not `data`[].isNil: GCunref(`data`[])
of ntyObject:
let data = data.castTo(newTree(nnkPtrTy, ty))
result = quote do:
reset `data`[]
of ntyRef, ntySequence:
let data = data.castTo(newTree(nnkPtrTy, ty[0]))
let dsym = genSym(nskLet, "data")
return quote do:
let `dsym` = `data`
if not `dsym`[].isNil: GCunref(`dsym`[])
of ntyProc:
# stmts.add(quote do: `data`[] = nil)
# ty_sym = ty[0]
let data = data.castTo(newTree(nnkPtrTy, ty[0]))
return quote do:
`data`[] = nil
of ntyDistinct:
# TODO fix later, remove the need for an extra function call
echo treerepr ty
let ty = if ty.kind == nnkSym: getType(ty) else: ty
let subt = ty[1]
return genDecrefStmts(subt, data)
of ntyCstring:
discard
else:
echo "what do for ", ty.typekind, "???"
echo treerepr ty
echo treerepr(getType(ty))
quit 0
proc destroyComponent* [t: any] (self:Object; data:pointer) {.nimcall.} =
# todo gcunref all non-nil refs (provide a default destructor)
macro decRefs : stmt =
let x: NimNode = (quote do: t)[0]
echo x.treerepr
var ty = getType(x)
var tyk = ty.typekind
if tyk == ntyTypedesc:
ty = ty[1]
tyk = ty.typekind
var ty_sym = ty
var stmts = genDecrefStmts(ty_sym, ident"data")
result = stmts
# if skipCast:
# result = stmts
# else:
# let pt = newTree(nnkPtrTy, ty_sym)
# echo repr pt
# result = newStmtList(quote do:
# let `data` = cast[`pt`](`data`))
# result.add stmts
when defined(debug):
echo "destroyComponent macro result: " , repr result
when defined(debug):
echo "destroyComponent invoked ", name(t)
static:
echo name( t)
decRefs()
proc next_type_id (t:typedesc): NimDataTypeID =
#mixin destroyComponent
result = data_type_counter.NimDataTypeID
var c = Component(
name: name(t),
bytes: sizeof(t),
messages: initTable[string,Object](4),
kind: ComponentKind.Static,
nimType: result,
destructor: ObjectDestructor(destroyComponent[t])
)
if staticComponents.len < c.nimType+1:
staticComponents.setLen c.nimType+1
staticComponents[c.nimType] = c
data_type_counter += 1
proc data_type_id* (t:typedesc): NimDataTypeID =
## accessor used to check if a component is a data type
# static: echo name(t)
# echo name(t)
var id{.global.} = next_type_id(t)
return id
#proc gt*(n: typedesc): NimNode {.magic: "NGetType", noSideEffect.}
#template getType* (t:typedesc): NimNode = getType(t)
proc dataComponent* (i:int): Component =
staticComponents[i]
proc typeComponent* (t:typedesc): Component =
dataComponent(dataTypeID(t))
proc `$` (co: Component): string =
"(Component $#:$#)".format(
if co.nimType == 0.NimDataTypeID: "0x"& $toHex(cast[int](co), sizeof(int)*2) else: $co.nimType,
co.name
)
## aggregate
proc aggregate* (cos: varargs[Component]): AggregateType =
# create a new type out of multiple components
result = AggregateType(components: newSeq[(int,Component)](cos.len))
var bytes = 0
for i in 0 .. high(cos):
let c = cos[high(cos)-i]
result.components[i] = (bytes,c)
bytes += c.bytes
result.bytes = bytes
proc rfind* (a: seq, b: any): int {.inline.}=
result = high(a)
while result > low(a):
if a[result] == b: return
dec result
proc findComponentIndex* (ty: AggregateType; co: Component): int =
proc `==` (a: (int,Component), b: Component): bool =
a[1] == b
ty.components.rfind(co)
proc findComponentOffset* (ty: AggregateType; co: Component): int =
result = ty.findComponentIndex co
if result == -1: return
result = ty.components[result][0]
## Object
var
aggx_undef*: AggregateType # global aggregate type used for nil
# todo add some safety that no components on here have data. 'twould be bad
template safeType* (obj: Object): AggregateType =
(if obj.isNil: aggx_undef else: obj.ty)
proc printComponentNames* (ty: AggregateType; sep = ", "): string =
result = ""
for i in countdown(ty.components.high, 0, 1):
result.add ty.components[i][1].name
if i > 0:
result.add sep
when defined(Debug):
var objBeingFreed*: proc(o:Object)
proc freeObject* (obj: Object) =
#obj.sendMessage("beingFreed")
echo "Object free'd: 0x",strutils.tohex(cast[int](obj), sizeof(pointer)*2)
echo " (", printComponentNames(obj.ty), ")"
when defined(Debug):
if not objBeingFreed.isNil:
objBeingFreed(obj)
for ofs,component in obj.ty.components.items:
case component.kind
of ComponentKind.Static:
if not component.destructor.isNil:
let component_data = obj.dat[ofs].addr
component.destructor obj, component_data
of ComponentKind.Dynamic:
let my_data = cast[ptr UncheckedArray[Object]](obj.dat[ofs].addr)
for idx in 0 .. high(component.slots):
template slot: Object = my_data[idx]
if not slot.isNil:
echo "slot ", component.slots[idx], " ref count: ", slot.getRefCount
GCunref slot
obj.ty = nil
when true or defined(nim_fix_unsaferef_free):
proc x =
var e: Object
new e, freeObject
e.ty = AggregateType(components: @[])
block: x()
GC_fullcollect()
proc instantiate* (ty: AggregateType): Object =
unsafeNew result, ty.bytes + sizeof(AggregateType)
result.ty = ty
# todo run initializers
proc dataPtr* (obj:Object; ty:typedesc): ptr ty =
let idx = obj.safeType.findComponentOffset(typeComponent(ty))
if idx == -1: return
return cast[ptr ty](obj.dat[idx].addr)
proc dataVar* (obj:Object; ty:typedesc): var ty =
obj.findData(ty)[]
proc findData* (obj:Object; ty:typedesc): ptr ty =
dataPtr(obj,ty)
proc findDataM* (obj:Object; ty:typedesc): var ty =
dataVar(obj,ty)
template printComponents* (e:Object): stmt =
let ty = e.safeType
for offs,c in items(ty.components):
echo " ", c.name
for k in keys(c.messages):
stdout.write k
stdout.write ", "
stdout.write '\L'
stdout.flushfile
echo ty.components.len , " total"
# proc send* (co:Component; msg:string; args:varargs[Object]): Object =
# let m = co.messages[msg]
# if not m.isNil:
# var bm: BoundComponent
# bm.comp = co
# bm.offs = -1
# result = m(bm, args)
## dynamic components
# proc readSlot (idx:int): MessageImpl =
# return proc(this:BoundComponent; args:varargs[Object]):Object =
# let offs = this.offs + (idx * sizeof(pointer))
# return cast[ptr Object](this.self.dat[offs].addr)[]
# proc writeSlot (idx:int): MessageImpl =
# return proc(this:BoundComponent; args:varargs[Object]):Object =
# let offs = this.offs + (idx * sizeof(pointer))
# cast[var Object](this.self.dat[offs].addr) = args[0]
# proc dynaComponent* (name: string; slots: varargs[string]): Component =
# result = Component(
# bytes: slots.len * sizeof(pointer),
# name: name,
# messages: initTable[string,MessageImpl](),
# kind: ComponentKind.Dynamic,
# slots: @slots
# )
# for i in 0 .. high(slots):
# let
# m_reader = slots[i]
# m_writer = m_reader&":"
# result.rawDefine m_reader, readSlot(i)
# result.rawDefine m_writer, writeSlot(i)
## aggregate behavior modifying
proc dup* (ty:AggregateType): AggregateType =
AggregateType(bytes: ty.bytes, components: ty.components)
proc isBehavior* (co:Component): bool = co.bytes == 0
## component stores no state
proc addBehavior* (ty:AggregateType; behav:Component): bool =
## modify an aggregate type by adding behavior, all instances of
## the type will be affected!
result = behav.isBehavior
if not result: return false
ty.components.add((0,behav))
proc addBehavior* (obj:Object; behav:Component): bool =
## derive a new aggregate type for obj with behav added.
## no other instances of the current type will be affected.
result = behav.isBehavior
if not result: return false
let new_ty = obj.ty.dup
new_ty.components.add((-1,behav))
obj.ty = new_ty
proc dropBehavior* (ty:AggregateType; behav:Component): bool =
## modifies an aggregate type, all instances will be affected!
result = behav.isBehavior
if not result: return false
let idx = ty.findComponentIndex(behav)
if idx == -1: return false
ty.components.delete idx
proc dropBehavior* (obj:Object; behav:Component): bool =
## derive a new type for the object without `behav`. this does not
## modify other objects, instead the object is assigned a new copy of type.
result = behav.isBehavior
if not result: return false
let idx = obj.ty.findComponentIndex(behav)
if idx == -1: return false
let new_ty = obj.ty.dup
new_ty.components.delete idx
obj.ty = new_ty
proc insertBehavior* (ty:AggregateType; behavior:Component; index:Natural): bool=
## insert a behavior into a specific slot
## index should range from 0 .. high(ty.components)
## index 0 inserts at the end of the components since dispatch looks in reverse
result = behavior.isBehavior
if not result: return
ty.components.insert(
(-1,behavior),
ty.components.len - index)
import tables, strutils, sequtils
import glossolalia
type NK* {.pure.} = enum
Ident, Symbol,
IntLiteral,
Message, Array, Block
type
Node* = ref NodeObj
NodeObj* {.acyclic.} = object
case kind*: NK
of NK.Ident, NK.Symbol: str*: string
of NK.IntLiteral: i*: int
of NK.Message .. NK.Array:
sub*: seq[Node]
of NK.Block:
args*,locals*:seq[string]
stmts*: seq[Node]
const valid_operator = {'+','-','/','*','&','|','%','$','@','='}
proc `[]`* (n:Node; idx:Positive): Node =
n.sub[idx]
proc `$` * (n:Node): string =
case n.kind
of NK.Message:
assert n.sub.len >= 2
result = "("
result.add($n.sub[0].str)
for i in 1 .. high(n.sub):
result.add ' '
result.add($n.sub[i])
result.add ')'
of NK.Block:
result = "["
for arg in n.args:
result.add ':'
result.add arg
result.add ' '
for local in n.locals:
result.add local
result.add ' '
result.add '|'
for i,statement in n.stmts:
result.add "\n"
result.add($statement)
if i < high(n.stmts):
result.add '.'
result.add ']'
of NK.Array:
result = "{"
for idx,node in pairs(n.sub):
result.add($node)
if idx < n.sub.high:
result.add ". "
result.add '}'
of NK.IntLiteral: return $n.i
of NK.Ident: return n.str
of NK.Symbol: return "#"&n.str
proc parens* (n: Rule[Node]): Rule[Node] =
charMatcher[Node]({'('}) and n and charMatcher[Node]({')'})
proc Ident* (m:string): Node = Node(kind:NK.Ident, str:m)
proc save_expr_keyword (nodes: seq[Node]): Node =
var ident = Node(kind:NK.Ident, str:"")
result = Node(kind: NK.Message, sub: @[ident])
var i = 0
if (nodes.len and 1) != 0:
result.sub.add nodes[0]
i = 1
else:
result.sub.add Ident("thisContext")
for j in countup(i,high(nodes),2):
ident.str.add nodes[j].str
result.sub.add nodes[j+1]
proc save_expr_binary (nodes: seq[Node]): Node =
result = nodes[0]
for i in countup(1, nodes.high, 2):
result = Node(kind: NK.Message, sub: @[nodes[i], result, nodes[i+1]])
proc UnaryExpr* (recv:Node; msgs:seq[Node]): Node =
result = recv
for i in 0 .. high(msgs):
result = Node(kind: NK.Message, sub: @[msgs[i], result])
# result = Node(kind: NK.Message, sub: @[recv])
# result.sub.add msgs
proc p (r:Rule[Node]):Rule[Node] =
Rule[Node](
m: proc(input:var InputState): Match[Node] =
result = r.m(input)
echo input.pos
)
proc echoP ():Rule[Node] =
Rule[Node](
m: proc(input:var InputState): Match[Node] =
echo input.str[input.pos]
result = Match[Node](kind:mUnrefined, pos:input.pos,len: -1)
)
proc Expression* : Rule[Node] =
grammar(Node):
ws := +(newline or chr({' ','\t'}))
newline := str("\r\L") or chr({'\L'})
colon := chr(':')
ident := ident_str.save(Ident)
ident_str := chr({'A'..'Z', 'a'..'z', '_'}) and *chr({'A'..'Z','a'..'z','0'..'9','_'})
keywd := keyword_str.save(Ident)
keyword_str := ident_str and colon
symbol := chr('#') and (+keyword_str or ident_str).save do (str: string)->Node: Node(kind:NK.Symbol, str:str)
binary_op :=
(+chr(valid_operator)).save(Ident)
literal_int := chr(strutils.Digits).repeat(1).save do(m:string)->Node: Node(kind:NK.IntLiteral, i:parseInt(m))
stmt_separator :=
?ws and chr('.') and ?ws
argument := colon and ident
anyChar := chr({char.low .. char.high})
proc saveArrBlank (r:Rule[Node]):Rule[Node] =
(r.save do (ns:seq[Node])->Node: Node(kind:NK.Array, sub:ns))
.saveBlank do ->Node: Node(kind:NK.Array, sub:nil)
arg_list :=
(argument.join(?ws) or anyChar.present).saveArrBlank and
?ws and
(ident.join(ws) or anyChar.present).saveArrBlank and
chr('|')
# ( colon and ?ws and ident.join(?ws) and ?ws and chr('|') and ?ws
# ).repeat(0,1)
# .save((ns:seq[Node])->Node=> Node(kind:NK.Array, sub:ns))
# .save((start:cstring,len:int)->Node=> Node(kind:NK.Array, sub: @[]))
block_literal :=
( chr('[') and ?ws and ?(arg_list and ?ws) and
expr_keyword.join(stmt_separator).saveArrBlank and
?ws and chr(']')
).save do (nodes:seq[Node])->Node:
result = Node(
kind: NK.Block)
if nodes.len == 1:
result.stmts = nodes[0].sub
result.args = @[]
result.locals = @[]
else:
template strs (n): seq[string] =
(if n.kind == NK.Array: (if n.sub.isNil: @[] else: n.sub.mapIt(string, it.str)) else: @[])
result.args = nodes[0].strs
result.locals= nodes[1].strs
result.stmts = nodes[2].sub
expr_terminal :=
(literal_int and absent(chr({'A'..'Z','a'..'z'}))) or
block_literal or
parens(?ws and expr_keyword and ?ws)
expr_unary :=
((expr_terminal and +(ws and ident and colon.absent)).save do (ns:seq[Node])->Node:
UnaryExpr(ns[0], ns[1 .. ^1])
) or
((ident and colon.absent).join(ws).save do (ns: seq[Node])->Node:
UnaryExpr(Ident("thisContext"), ns)
) or
expr_terminal
expr_binary :=
( expr_unary.join(?ws and binary_op and ?ws)
).save(save_expr_binary) or
expr_unary
expr_keyword :=
(
(expr_binary and +(?ws and keywd and ?ws and expr_binary)) or
(keywd and ?ws and expr_binary).join(?ws)
).save(save_expr_keyword) or
expr_binary
result = expr_keyword
export glossolalia
when isMainModule:
template test(str,rule): stmt =
do_assert rule.match(str), "[FAIL] "& astToStr(rule) &" for "&str
test "1", expr_unary
test "1 + 1", expr_binary
test "1 at: 2 put: 3", expr_keyword
test "1+2 at: 1", expr_keyword
test "[1]", block_literal
test "[1+2]", block_literal
test "[ 1+2 at: 1 ]", block_literal
test "[a b]", Expression
echo Expression.match("[false. true]").nodes[0]
echo Expression.match("[:n a b| n send: a with: b ]")
import cmodel2,macros
proc slotsComponent* (name: string; slots: varargs[string]): Component
## depends on opcodes
var
cxObj* = slotsComponent("Object")
cxTrue* = slotsComponent("True")
obj_true* = aggregate(cxTrue, cxObj).instantiate
cxFalse* = slotsComponent("False")
obj_false* = aggregate(cxFalse, cxObj).instantiate
cxUndef* = slotsComponent("Undef")
cmodel2.aggxUndef = aggregate(cxUndef, cxObj)
type
NimTerminalObject* = concept X
asObject(X) is Object
template defPrimitiveComponent* (n:untyped, comp:typed): stmt =
{.line: instantiationInfo().}:
static: assert `comp` isNot NimTerminalObject
let `cx n`* {.inject} = typeComponent(comp)
let `aggx n`* {.inject} = aggregate(typeComponent(comp), cx_obj)
`cx n`.aggr = `aggx n`
`cx n`.name = astToStr(n)
proc `obj n`* (some: `comp`): Object =
result = `aggx n`.instantiate
result.dataVar(comp) = some
proc asObject* (some: `comp`): Object{.inline.}=
`obj n`(some)
static: assert `comp` is NimTerminalObject
defineMessage(`cx n`, "as"&astToStr(n))
do: return self
proc `as n`* (some:Object): var `comp` {.inline.} =
let dp = some.dataPtr(`comp`)
if not dp.isNil: return dp[]
# try to call asX on the object
let o = some.send("as"&astToStr(n))
let dp2 = o.dataPtr(`comp`)
if not dp2.isNil: return dp2[]
## message sending
type
BoundComponent* = object
self*: Object
comp*: Component
idx*: int
## BoundComponent
## contains three pieces needed to access an instance of a component:
## the object that owns it, the component itself and the data offset in object.
## BoundComponent is valid so long as you do not prepend the object type
proc asPtr* (c:BoundComponent; t:typedesc): ptr t =
# take unsafe reference to component data.
do_assert c.comp.nimType == data_type_id(t)
let offs = c.self.ty.components[c.idx][0]
cast[ptr t](c.self.dat[offs].addr)
proc asVar* (c:BoundComponent; t:typedesc): var t =
c.asPtr(t)[]
proc getComponent* (some: Object; comp: Component): BoundComponent {.inline.} =
BoundComponent(
self: some,
comp: comp,
idx: some.safeType.findComponentIndex(comp)
)
proc getComponent* (some: Object; n: int): BoundComponent {.inline.} =
do_assert n in 0 .. some.safeType.components.high
BoundComponent(self:some, idx:n, comp:some.safeType.components[n][1])
proc isValid* (bc: BoundComponent): bool = bc.idx > -1
proc slotPtr* (bc: BoundComponent; idx: int): ptr Object =
let offs = bc.self.safeType.components[bc.idx][0]
return
addr cast[ptr UncheckedArray[Object]](bc.self.dat[offs].addr)[idx]
proc slotVar* (bc: BoundComponent; idx: int): var Object =
bc.slotPtr(idx)[]
type
MessageSearch* = object
continueFrom*: int
msg*: Object
bound*: BoundComponent
import tables
proc findMessage* (ty: AggregateType; msg: string; res: var MessageSearch): bool =
for i in countdown(res.continueFrom, 0, 1):
let
c = ty.components[i]
m = c[1].messages[msg]
if not m.isNil:
res.continueFrom = i-1
res.msg = m #cast[MessageImpl](m)
res.bound.comp = c[1]
res.bound.idx = i
result = true
return
# proc bindMessage* (e:Object; msg:string): Object =
# # creates a context
# let ty = e.safeType
# var ms: MessageSearch
# ms.continueFrom = high(ty.components)
# if ty.findMessage(msg, ms):
# ms.bound.self = e
# # create its context
# result = createContext(ms.msg, ms.bound)
# #result = ms.msg(ms.bound, args)
# else:
# echo "failed to find `",msg.repr,"` on entity ", cast[int](e)
# print_components(e)
proc findMessage* (obj:Object; msg:string): (BoundComponent,Object) =
let ty = obj.safeType
var ms: MessageSearch
ms.continueFrom = high(ty.components)
if ty.findMessage(msg, ms):
ms.bound.self = obj
result = (ms.bound, ms.msg)
return
result[0].idx = -1
let
cxBoundComponent* = typeComponent(BoundComponent)
type
Block* = object
ipStart*, ipEnd*: int
meth*: Object
let cxBlock* = typeComponent(Block)
type
CompiledMethod* = object
bytecode*: seq[byte]
args,locals: seq[string]
contextCreator*: Component ## component with slots for args and locals to hold state in the context object
let
cxCompiledMethod* = typeComponent(CompiledMethod)
aggxCompiledMethod* = aggregate(cxCompiledMethod, cxObj)
proc initCompiledMethod* (bytecode:seq[byte]; args,locals:openarray[string]=[]): CompiledMethod =
result = CompiledMethod(
bytecode:bytecode,
args: @args,
locals: @locals
)
result.contextCreator = slotsComponent("locals", result.args & result.locals)
type
PrimitiveCB = proc(context: Object; this: BoundComponent): Object{.nimcall.}
PrimitiveMethod* = object
fn*: PrimitiveCB
code*,name*: string
let
cxPrimitiveMethod* = typeComponent(PrimitiveMethod)
aggxPrimitiveMethod* = aggregate(cxPrimitiveMethod, cxCompiledMethod, cxObj)
cxPrimitiveMethod.aggr = aggxPrimitiveMethod
proc newPrimitiveMessage* (args:openarray[string]; name,src:string; fn:PrimitiveCB): Object
proc rawDefine* (co:Component; msg:string; obj:Object) =
co.messages[msg] = obj
macro defineMessage* (co,msg,body:untyped):stmt =
#co:Component; msg:string; body:untyped{nkDo}
let cs = callsite()
# let co = cs[1]
# let msg = cs[2]
# let body = cs[3]
let params = [
ident"Object",
newIdentDefs(ident"context", ident"Object"),
newIdentDefs(ident"this", ident"BoundComponent")
]
## the body param is a nnkDo, lets scan the params and turn them into
## templates.
let new_body = newStmtList()
var arg_names = newseq[NimNode]()
new_body.add quote do:
template self(): Object = this.self
template thisComponent(): Component = this.comp
var arg_idx = 0
for fp1 in 1 .. len(body.params)-1:
let p = body.params[fp1]
for fp2 in 0 .. len(p)-3:
let name = p[fp2]
let strname = repr(name)
new_body.add(quote do:
let `name` = context.send(`strname`)
#template `name`: Object = args[`arg_idx`])
)
arg_idx += 1
arg_names.add(newLit($name))
## copy the rest of the function into the new stmt list
body.body.copyChildrenTo new_body
let new_proc = newProc(
params = params,
body = new_body,
proc_type = nnkLambda)
new_proc.pragma = newTree(nnkPragma, ident"nimcall")
var co_safe = co
# if co_safe.typekind == ntyTypedesc:
# co_safe = quote do: typeComponent(`co`)
## call rawDefine() to store it
let argNamesNode = newTree(nnkBracket, arg_names)
# let src = repr(callsite())
# echo src
echo treerepr cs
result = quote do:
let m = newPrimitiveMessage(`arg_names_node`, `msg`, "", `new_proc`)
rawDefine(`co_safe`, `msg`, m)
when defined(Debug):
echo repr result
# template defineMessage* (co:Component; msg:string; body:untyped):stmt {.immediate,dirty.} =
# getAst(defineMessage2(co,msg,body))
proc send* (recv:Object; msg:string; args:varargs[Object]): Object
# not used in the vm
defPrimitiveComponent(Int, int)
defPrimitiveComponent(String, string)
import tables
type
Instr* {.pure.} = enum
NOP, PushNIL, PushPOD, PushBLOCK, PushThisContext,
Pop, Dup, Send, GetSlot, SetSlot, ExecPrimitive, Return
iseq = seq[byte]
InstrBuilder* = object
iset*: iseq
index*: int
labels*: Table[string,int]
proc initInstrBuilder* : InstrBuilder =
newSeq result.iset, 0
result.labels = initTable[string,int]()
type
RawBytes* = cstring
Serializable* = concept obj
var builder: InstrBuilder
serialize(obj, builder)
var source: RawBytes
unserialize(obj, source)
proc ensureLen* (some: var seq; len: int) {.inline.}=
if some.len < len: some.setLen len
template addByte* (i: var InstrBuilder; b: untyped): stmt =
let byt = byte(b)
i.iset.ensureLen i.index+1
i.iset[i.index] = byt
i.index += 1
template addNullBytes* (i: var InstrBuilder; n: int): stmt =
i.iset.ensureLen i.index+n
i.index += n
proc pushPOD* (i: var InstrBuilder; obj: Serializable) =
mixin serialize
let ty = typeComponent(type(obj))
let id = ty.nim_type
do_assert id < 127, "FIX ASAP" # make this two bytes if it gets big
do_assert id > 0, "invalid id "& $id & "::"& ty.name
i.addByte Instr.PushPOD
i.addByte id
serialize(obj, i)
import endians
proc pushBlock* (i:var InstrBuilder; args,locals:openarray[string]; iseq: var iseq) =
i.addByte Instr.PushBLOCK
i.addByte args.len
for idx in 0 .. high(args):
let start = i.index
i.addNullBytes args[idx].len+1
copyMem i.iset[start].addr, args[idx].cstring, args[idx].len
i.addByte locals.len
for idx in 0 .. high(locals):
let start = i.index
i.addNullBytes locals[idx].len+1
copyMem i.iset[start].addr, locals[idx].cstring, locals[idx].len
block:
let start = i.index
i.addNullBytes 4
var len = iseq.len.uint32
bigEndian32(i.iset[start].addr, len.addr)
let instrs_start = i.index
i.addNullBytes len.int
copyMem i.iset[instrs_start].addr, iseq[0].addr, len
proc pushThisContext* (i: var InstrBuilder) =
## stack effect ( -- object )
i.addByte Instr.PushThisContext
proc pushNIL* (i: var InstrBuilder) =
## stack effect ( -- nil )
i.addByte Instr.PushNIL
proc dup* (i: var InstrBuilder) =
## stack effect ( object -- object object )
i.addByte Instr.Dup
proc pop* (i: var InstrBuilder) =
## stack effect ( object -- )
i.addByte Instr.Pop
proc send* (i: var InstrBuilder; msg:string; args:int) =
let L = msg.len
i.addByte Instr.Send
i.addByte args
i.addByte L
let IDX = i.index
i.addNullBytes L
copyMem i.iset[IDX].addr, msg.cstring, L
proc getSlot* (i:var InstrBuilder; slot:int) =
## stack effect ( -- object )
assert slot in 0 .. 127 # arbitrary limit
i.addByte Instr.GetSlot
i.addByte slot
proc setSlot* (i:var InstrBuilder; slot:int) =
## stack effect ( object -- )
assert slot in 0 .. 127
i.addByte Instr.SetSlot
i.addByte slot
proc execPrimitive* (i:var InstrBuilder) =
i.addByte Instr.ExecPrimitive
proc done* (i: var InstrBuilder): seq[byte] =
i.iset.setLen i.index
result = i.iset
proc newPrimitiveMessage* (args:openarray[string]; name,src:string; fn:PrimitiveCB): Object =
result = aggxPrimitiveMethod.instantiate
result.dataPtr(PrimitiveMethod).fn = fn
result.dataPtr(PrimitiveMethod).code = src
result.dataPtr(PrimitiveMethod).name = name
var ibuilder = initInstrBuilder()
ibuilder.execPrimitive
let cm = result.dataPtr(CompiledMethod)
cm[] = initCompiledMethod(ibuilder.done, args=args, locals=[])
## dynamic components
var readers: seq[Object] = @[]
proc readSlot (idx:int): Object =
# returns a CompiledMethod to read slot idx from a component instance
if idx < 0: return
if idx < readers.len:
result = readers[idx]
if not result.isNil: return
elif readers.high < idx:
readers.setLen idx+1
result = aggxCompiledMethod.instantiate
readers[idx] = result
var ibuilder = initInstrBuilder()
ibuilder.getSlot(idx)
result.dataVar(CompiledMethod) =
initCompiledMethod(
ibuilder.done,
args=[] )
var writers: seq[Object] = @[]
proc writeSlot (idx:int): Object =
# return proc(this:BoundComponent; args:varargs[Object]):Object =
# let offs = this.offs + (idx * sizeof(pointer))
# cast[var Object](this.self.dat[offs].addr) = args[0]
if idx < 0: return
if idx < writers.len:
result = writers[idx]
if not result.isNil: return
elif writers.high < idx:
writers.setLen idx+1
result = aggxCompiledMethod.instantiate
writers[idx] = result
var ib = initInstrBuilder()
ib.pushThisContext
ib.send "val", 0
ib.setSlot(idx)
result.dataVar(CompiledMethod) =
initCompiledMethod(
ib.done, args=["val"] )
proc slotsComponent (name: string; slots: varargs[string]): Component =
result = Component(
bytes: slots.len * sizeof(pointer),
name: name,
messages: initTable[string,Object](),
kind: ComponentKind.Dynamic,
slots: @slots
)
for i in 0 .. high(slots):
let
m_reader = slots[i]
m_writer = m_reader&":"
result.rawDefine m_reader, readSlot(i)
result.rawDefine m_writer, writeSlot(i)
type Stack* = distinct seq[Object]
let cxStack* = typeComponent(Stack)
let aggxStack = aggregate(cxStack, cxObj)
proc len* (some: ptr Stack): int =
result =
if some.isNil: 0
elif seq[Object](some[]).isNil: 0
else: seq[Object](some[]).len
proc pop* (some: ptr Stack): Object =
result =
if some.len == 0: nil
else: seq[Object](some[]).pop
proc push* (some: ptr Stack; val:Object) =
if some.isNil: return
if seq[Object](some[]).isNil: newSeq seq[Object](some[]), 0
seq[Object](some[]).add val
proc top* (some: ptr Stack): Object =
let some = (ptr seq[Object])(some)
if some.isNil or some[].isNil or some[].len == 0: return
return some[][some[].high]
type
Context* = object
parent*: Object # lexical parent context (where the block was instantiated from)
# for methods this may be a link to global state? not sure yet
caller*: Object #
instrs*: Object ## CompiledMethod or Block
exec*: Object
ip*, highIP*: int
let cxContext* = typeComponent(Context)
proc createContext* (compiledMethod:Object; bound:BoundComponent): Object =
## allocates a context for a method
## does not set Context.caller or .parent
let cm = compiledMethod.dataPtr(CompiledMethod)
#echo cm.contextCreator.isNil
result = instantiate aggregate(
cm.contextCreator,
cxBoundComponent,
cxStack, cxContext
)
result.dataVar(Context).highIP = cm.bytecode.high
result.dataVar(Context).instrs = compiledMethod
result.dataVar(BoundComponent) = bound
proc createBlockContext* (): Object =
nil
# defineMessage(cxCompiledMethod, "createContext") do:
# let cm = this.asPtr(CompiledMethod)
# if cm.contextCreator.isNil: return
# result = aggregate(cxBoundComponent, cm.contextCreator, cxContextInstance)
# .instantiate
# discard context.send(result, "parent:", context)
type Exec* = object
activeContext*, rootContext*: Object
bytePtr*: Object
result*: Object
let cxExec* = typeComponent(Exec)
let aggxExec* = aggregate(cxExec, cxObj)
proc isActive* (some: ptr Exec): bool =
not some.activeContext.isNil
proc contextIsFinished* (some: ptr Exec): bool =
let ac = some.activeContext
if ac.isNil: return true
let ctx = ac.dataPtr(Context)
if ctx.isNil: return true
if ctx.ip > ctx.highIP:
return true
proc ptrToBytecode* (some: ptr Exec): ptr UncheckedArray[byte] =
let ctx = some.activeContext.dataPtr(Context)
let cm = ctx.instrs
# result = cast[ptr UncheckedArray[byte]](
# cm.dataPtr(CompiledMethod).bytecode[ctx.ip].addr
# )
result = cast[ptr UncheckedArray[byte]](
cm.dataPtr(CompiledMethod).bytecode[0].addr
)
proc setActiveContext* (someExec, ctx: Object) =
someExec.dataVar(Exec).activeContext = ctx
ctx.dataVar(Context).exec = someExec
let cxRawBytes* = typeComponent(cstring)
let aggxRawBytes* = aggregate(cxRawBytes, cxObj)
import strutils
template echoCode* (xpr:expr): stmt =
echo astToStr(xpr),": ",xpr
template echoCodeI* (i=2; xpr:expr): stmt =
echo repeat(' ',i), astToStr(xpr), ": ", xpr
template wdd * (body:stmt):stmt =
when defined(Debug): body
const ShowInstruction =
defined(Debug) or defined(ShowInstruction)
proc tick* (self: Object) =
let exe = self.dataPtr(Exec)
assert(not exe.activeContext.isNil)
let activeContext = exe.activeContext
let thisContext = activeContext.dataPtr(Context)
let thisStack = activeContext.dataPtr(Stack)
wdd:
echo thisContext.ip, "/", thisContext.highIP
if exe.contextIsFinished:
wdd: echo "Leaving context - finished"
let val = thisStack.pop
let next = exe.activeContext.dataPtr(Context).caller
if next.isNil or next.dataPtr(Context).exec == self:
exe.result = val
exe.activeContext = nil
return
next.dataPtr(Stack).push val
self.setActiveContext next
return
# exe.activeContext = next
# if not next.isNil:
# next.dataPtr(Stack).push val
# else:
# exe.result = val
# return
template top (): expr =
thisStack.top
template push (o): stmt =
thisStack.push o
template pop (): expr =
thisStack.pop
let iset = exe.ptrToBytecode
var idx = thisContext.ip
let op = iset[idx]
when ShowInstruction:
echo op.Instr ," @ ", thisContext.ip,"/",thisContext.highIP
case op.Instr
of Instr.NOP:
idx += 1
of Instr.Dup:
idx += 1
push top()
of Instr.Pop:
idx += 1
discard pop()
of Instr.PushNIL:
idx += 1
push nil.Object
of Instr.PushThisContext:
idx += 1
push activeContext
of Instr.ExecPrimitive:
idx += 1
# execute the primitive attached to the currently running context
let pm = thisContext.instrs.dataPtr(PrimitiveMethod)
when ShowInstruction:
echo " ExecPrimitive($#)".format(pm.name)
if not pm.isNil:
let bc = activeContext.dataPtr(BoundComponent)
if bc.isNil:
echo "NO BOUND COMPONENT!"
push nil
else:
if pm.fn.isNil:
echo "PRIMITIVE IS NIL??"
#activeContext.printcomponents
push nil
else:
let res = pm.fn(activeContext, bc[])
push res
else:
push nil
of Instr.GetSlot:
idx += 1
let slot = iset[idx]
when ShowInstruction:
echo "GetSlot(", slot, ")"
idx += 1
let bm = activeContext.dataPtr(BoundComponent)
if not bm.isNil:
# found bound method, trying to get slot "slot"
do_assert slot.int in 0 .. high(bm.comp.slots)
let obj = bm[].slotVar(slot.int)
wdd: obj.printcomponents
push obj
# let offs = bm.self.ty.components[bm.idx][0]
# do_assert offs != -1
# let dp = cast[ptr UncheckedArray[Object]](bm.self.dat[offs].addr)
# push dp[slot]
else:
echo "BoundComponent not found!"
push nil.Object
of Instr.SetSlot:
idx += 1
let slot = iset[idx]
idx += 1
when ShowInstruction:
echo "SetSlot(", slot, ")"
let val = pop()
let bc = activeContext.dataPtr(BoundComponent)
if not bc.isNil:
do_assert slot.int in 0 .. high(bm.comp.slots)
do_assert bm[].isValid
bc[].slotVar(slot.int) = val
# let offs = bm.self.ty.components[bm.idx][0]
# do_assert offs != -1
# let dp = cast[ptr UncheckedArray[Object]](bm.self.dat[offs].addr)
# dp[slot] = val
else:
echo "BoundComponent not found for set slot!!!"
of Instr.PushPOD:
idx += 1
let id = iset[idx].int
let ty = dataComponent(id)
idx += 1
let cstr = cast[cstring](iset[idx].addr)
#let src = objRawBytes(cstr)
if exe.bytePtr.isNil:
exe.bytePtr = aggxRawBytes.instantiate
let src = exe.bytePtr
src.dataVar(RawBytes) = cstr
when defined(Debug):
echoCodeI 2, ty.name
let obj = ty.aggr.instantiate()
if obj.isNil:
echo "NEW POD ", ty.name, " FAILED TO LOAD"
else:
wdd: obj.printcomponents
let o2 = obj.send("loadFromRaw:", src)
let L = o2.dataPtr(int)
if L.isNil: echo "LOADFROMRAW: RESULT IS NOT INT!"
else:
idx += L[]
push obj
echo " PushPOD($#)" % obj.send("print").asString
of Instr.PushBLOCK:
idx += 1
let nArgs = iset[idx].int
var args = newSeq[string](nArgs)
idx += 1
for arg in 0 .. <nArgs:
var start = idx
var str: array[129,char]
for i in 0 .. 128:
str[i] = iset[start+i].char
if str[i] == '\00':
idx += i
break
args[arg] = $ str[0].addr.cstring
idx += 1
let nLocals = iset[idx].int
var locals = newSeq[string](nLocals)
idx += 1
for arg in 0 .. <nLocals:
var start = idx
var str: array[129,char]
for i in 0 .. 128:
str[i] = iset[start+i].char
if str[i] == '\00':
idx += i
break
locals[arg] = $ str[0].addr.cstring
idx += 1
var bytecode_len: uint32
bigEndian32(bytecode_len.addr, iset[idx].addr)
idx += 4
let bc_start = idx
idx += bytecode_len.int
let bc_end = idx
## create an object that can create this block's context
let obj = aggregate(cxBlock, cxObj).instantiate
let bp = obj.dataPtr(Block)
bp.ipStart = bc_start
bp.ipEnd = bc_end
bp.meth = thisContext.instrs
push obj
echo " * bytecode_len = ", bytecode_len
echo " * end up at ", idx
of Instr.Send:
idx += 1
let argN = iset[idx].int
idx += 1
let L = iset[idx].int
idx += 1
var str = newString(L)
copyMem str[0].addr, iset[idx].addr, L
idx += L
when ShowInstruction:
echo "Send(", str, ", ", argN, ")"
echo "-- entering ", str
var args = newSeq[Object](argN)
#echoCode str
let H = <argN
for i in 0 .. H:
args[H-i] = pop()
let recv = pop()
#echoCode frame.sp
let (bc,msg) = recv.findMessage(str)
if msg.isNil:
recv.printcomponents
echo "msg is nil ($#)" % str
else:
## create the context for the message, fill in its arguments
## link the context to the current context, make the new context
## active
## when the new context exits it will return to this context
## and push its result on the stack
let ctx = createContext(msg, bc)
ctx.dataPtr(Context).caller = activeContext
let bc = ctx.getComponent(ctx.ty.components.high)
for i in 0 .. H:
bc.slotVar(i) = args[i]
# let arg_locals = cast[ptr UncheckedArray[Object]](
# ctx.dat[ctx.ty.components[ctx.ty.components.high][0]].addr
# )
# for i in 0 .. H:
# arg_locals[i] = args[i]
self.setActiveContext ctx
else:
echo "unknown opcode ", op.ord, " (", op.Instr , ")"
quit 1
thisContext.ip = idx
echo op.Instr, " next IP here: ", thisContext.ip, "/", thisContext.highIP
when defined(Debug) or defined(ShowStack):
echo "stack.len = ", thisStack.len
proc executorForContext* (ctx:Object): Object =
result = aggxExec.instantiate
result.dataPtr(Exec).rootContext = ctx
result.dataPtr(Exec).activeContext = ctx
proc send* (recv:Object; msg:string; args:varargs[Object]): Object =
let (bc,msg) = recv.findMessage(msg)
if not bc.isValid: return
# validate that msg has args.len args
if msg.dataPtr(CompiledMethod).args.len != args.len:
return nil
# instantiate the context
let ctx = createContext(msg, bc)
if ctx.isNil:
echo "ctx is nil??"
# set the argument in the slots of the context ._.
if args.len > 0:
let idx = ctx.ty.components.high
let (offs, comp) = ctx.ty.components[idx]
let nSlots = comp.slots.len
if nSlots < args.len: return nil
# not enough slots to hold arguments? check here could be smarter
let slots = cast[ptr UncheckedArray[Object]](ctx.dat[offs].addr)
for i in 0 .. args.high:
slots[i] = args[i]
# instantiate and execute a vm
var ticks = 0
let o = executorForContext(ctx)
let exe = o.dataPtr(Exec)
while exe.isActive:
o.tick
ticks += 1
result = exe.result
wdd: echo "TICKS: ", ticks
defineMessage(cxInt, "+") do (other):
let other_int = other.dataVar(int)
result = asObject(this.asVar(int) + other_int)
defineMessage(cxInt, "print") do:
result = asObject($ this.asVar(int))
defineMessage(cxString, "print") do:
result = self
defineMessage(cxObj, "asString") do:
result = self.send("print")
import cmodel2, stdobj2
defineMessage(cxExec, "tick") do:
# let exe = this.asPtr(Exec)
# exe.tick
tick(self)
## wrap InstrBuilder here
type Compiler* = object
let
cxCompiler* = typeComponent(Compiler)
aggxCompiler* = aggregate(cxCompiler, cxObj)
cxCompiler.aggr = aggxCompiler
defineMessage(cxCompiler, "compile:") do (src):
discard
import endians
proc serialize* (some:int; builder:var InstrBuilder) =
let i = builder.index
builder.addNullBytes sizeof(some)
let dest = builder.iset[i].addr
var some = some
when sizeof(some) == 4:
bigEndian32(dest, some.addr)
elif sizeof(some) == 8:
bigEndian64(dest, some.addr)
else:
static:
assert false, "wat the size of int is on your masheen omg fix"
proc unserialize* (some:var int; source:RawBytes): int =
when sizeof(some) == 4:
bigEndian32(some.addr, source)
result = 4
elif sizeof(some) == 8:
bigEndian64(some.addr, source)
result = 8
defineMessage(cxInt, "loadFromRaw:")
do (byteSrc):
let dat = byteSrc.dataPtr(RawBytes)
let my_int = this.asPtr(int)
let len_read = my_int[].unserialize dat[]
assert len_read == sizeof(int)
result = objInt(len_read)
import kwdgrammar
defPrimitiveComponent(ASTNode,Node)
defineMessage(cxASTNode, "print")
do:
asObject($ this.asVar(Node))
proc compileNode (builder: var InstrBuilder; node: Node) =
case node.kind
of NK.IntLiteral:
builder.pushPOD node.i
of NK.Message:
# (msg recv arg0 arg1 ...)
let msg = node.sub[0].str
# recv
builder.compileNode node.sub[1]
for i in 2 .. high(node.sub):
# args
builder.compileNode node.sub[i]
builder.send msg, len(node.sub)-2
of NK.Block:
var new_builder = initInstrBuilder()
for i,s in node.stmts:
new_builder.compileNode s
if i < high(node.stmts): new_builder.pop
var iseq = new_builder.done
builder.pushBlock node.args, node.locals, iseq
else:
echo "not ready to compile ", node
quit 1
proc compileExpr* (str:string): Object =
let match = Expression().match(str)
if match.kind != mNodes:
echo match
return
assert match.nodes.len == 1
let node = match.nodes[0]
var builder = initInstrBuilder()
builder.compileNode node
# creates a CompiledMethod ready to be executed
result = aggxCompiledMethod.instantiate
result.dataVar(CompiledMethod) = initCompiledMethod(builder.done, args=[], locals=[])
# result = aggregate(
# dynaComponent("context", "parent", "code"),
# cxBlock, cxObj
# ).instantiate
# result.dataVar(Block).iset = iset
# discard result.send("code:", asObject(node))
proc execute* (expresion:string): Object =
let meth = compileExpr(expresion)
if meth.isNil:
echo "failed to compile ", expresion
return
let ctx = createContext(meth, BoundComponent(self: nil, idx: 1))
let o_exe = executorForContext(ctx)
let exe = o_exe .dataPtr(Exec)
while exe.isActive:
#exe.tick
tick(o_exe)
result = exe.result
proc createBlockContext (blck, caller:Object): Object =
result = instantiate aggregate(
cxStack, cxContext
)
let bl = blck.dataPtr(Block)
let ctx = result.dataPtr(Context)
ctx.caller = caller
ctx.ip = bl.ipStart
ctx.highIP = bl.ipEnd
ctx.instrs = bl.meth
#blck.printComponents
defineMessage(cxBlock, "value") do:
let ctx = createBlockContext(self, context)
# let ctx = createContext(msg, bc)
context.dataPtr(Context).exec.setActiveContext(ctx)
# type
# Context* = object
# sender*, pc*,sp*: Object
# blck*: Object # compiledmethod or block
## pc points to bytecode inside the enclosing method
## limit is the highest bytecode
# type
# Frame* = object
# context*: Object
# blck*: Object
# ip, sp*: int
# entry*: Object
# defPrimitiveComponent Frame,Frame
# type
# Process* = object
# #stack*: seq[Object]
# callstack*: seq[Object]
# let cxProcess* = typeComponent(Process)
# let aggxProcess* = aggregate(cxProcess, cxObj)
# defineMessage(cxProcess, "init") do:
# this.asVar(Process).stack.newSeq 0
# this.asVar(Process).callstack.newSeq 0
# proc activeFrame* (some: Process): Object = some.callstack[^1]
# proc safeActiveFrame* (some: Process): Object =
# (if some.callstack.len > 0: some.callstack[some.callstack.high] else: nil)
# defineMessage(cxProcess, "send:to:with:")
# do (msg,obj,args):
# defineMessage(cxProcess, "activeFrame") do:
# this.asVar(Process).safeActiveFrame
# defineMessage(cxProcess, "activateBlock:") do (blck):
# let context = blck.send("instantiate")
# let parent = self.asVar(Process).safeActiveFrame
# var fr = Frame(
# context: parent,
# ip: 0,
# sp: if parent.isNil: 0 else: parent.asVar(Frame).sp
# )
# defineMessage(cxProcess, "pushFrame:") do (ctx):
# this.asVar(Process).callstack.add fr.asObject
# proc newProcessFromBlock* (blck:Object):Object =
# result = aggxProcess.instantiate
# discard result.send("init")
# discard result.send("activateBlock:", blck)
# var ms: MessageSearch
# let ty = e.safeType
# ms.continueFrom = high(ty.components)
# if ty.findMessage(msg, ms):
# ms.bound.self = e
# result = ms.msg(ms.bound, args)
# else:
# echo "failed to find `",msg.repr,"` on entity ", cast[int](e)
# print_components(e)
# iterator multicast* (e:Object; msg:string; args:varargs[Object]): Object =
# var ms: MessageSearch
# let ty = e.safeType
# ms.continueFrom = high(ty.components)
# while ty.findMessage(msg, ms):
# ms.bound.self = e
# yield ms.msg(ms.bound, args)
import cmodel2, vm2, stdobj2, tables
import kwdgrammar
const test2 = "[1] value"
let ob = execute(test2)
ob.printcomponents
echo "---------------------------------"
let ob2 = ob.send("print")
ob2.printcomponents
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment