diff --git a/README.md b/README.md index 8c8a535..6eb5d5c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,28 @@ # nim-extensions Extensions for the nim programming language. + +These extensions aim to improve the usability of nim in practical applications. + +Extensions so far include: + +## OOP macro + usage: + + ```nim + class BaseObject: # inherits from RootObj + # attributes/properties + var + x: int + y: float + + method override_me(argx: float): int= + result = int(argx) + self.x + + class ClassName of BaseObject: + method method_name(arg1: int, arg2: float=0.3): float= + ## do stuff here + result = arg2 + + method override_me(argx: float): int= + result = int(self.y) + int(argx) + self.x + ``` diff --git a/oop_macro.nim b/oop_macro.nim new file mode 100644 index 0000000..2551c60 --- /dev/null +++ b/oop_macro.nim @@ -0,0 +1,233 @@ +import macros + +{.hint[XDeclaredButNotUsed]: off.} + +macro new*(obj: expr): expr {.immediate.}= + ## Replaces the new keyword to call an init + ## function if it exists + quote do: + var init_obj = `obj` + when compiles(init_obj.init()): + init_obj.init() + init_obj + +macro class*(head: expr, body: stmt): stmt {.immediate.} = + # The macro is immediate so that it doesn't + # resolve identifiers passed to it + + # object reference name inside methods. + # ie: self, self + let obj_reference = "self" + + var typeName, baseName: PNimrodNode + + if head.kind == nnkIdent: + # `head` is expression `typeName` + # echo head.treeRepr + # -------------------- + # Ident !"Animal" + typeName = head + + elif head.kind == nnkInfix and $head[0] == "of": + # `head` is expression `typeName of baseClass` + # echo head.treeRepr + # -------------------- + # Infix + # Ident !"of" + # Ident !"Animal" + # Ident !"RootObj" + typeName = head[1] + baseName = head[2] + + else: + quit "Invalid node: " & head.lispRepr + + # echo treeRepr(body) + # -------------------- + # StmtList + # VarSection + # IdentDefs + # Ident !"name" + # Ident !"string" + # Empty + # IdentDefs + # Ident !"age" + # Ident !"int" + # Empty + # MethodDef + # Ident !"vocalize" + # Empty + # Empty + # FormalParams + # Ident !"string" + # Empty + # Empty + # StmtList + # StrLit ... + # MethodDef + # Ident !"age_human_yrs" + # Empty + # Empty + # FormalParams + # Ident !"int" + # Empty + # Empty + # StmtList + # DotExpr + # Ident !"self" + # Ident !"age" + + # create a new stmtList for the result + result = newStmtList() + + # var declarations will be turned into object fields + var recList = newNimNode(nnkRecList) + + # add a super function to simulate OOP + # inheritance tree (Doesn't do what is expected because of dynamic binding) + #if not isNil(`baseName`): + # var super = quote do: + # proc super(self: `typeName`): `baseName`= + # return `baseName`(self) + # result.add(super) + + # Make forward declarations so that function order + # does not matter, just like in real OOP! + for node in body.children: + case node.kind: + of nnkMethodDef, nnkProcDef: + # inject `self: T` into the arguments + let n = copyNimTree(node) + n.params.insert(1, newIdentDefs(ident(obj_reference), typeName)) + # clear the body so we only get a + # declaration + n.body = newEmptyNode() + result.add(n) + + # forward declare the inheritable method + let n2 = copyNimTree(n) + let proc_name = $(n2.name.toStrLit()) + let type_name = $(typeName.toStrLit()) + let new_name = ident(proc_name & type_name) + n2.name = new_name + result.add(n2) + else: + discard + + # Iterate over the statements, adding `self: T` + # to the parameters of functions + for node in body.children: + case node.kind: + of nnkMethodDef, nnkProcDef: + # inject `self: T` into the arguments + let n = copyNimTree(node) + n.params.insert(1, newIdentDefs(ident(obj_reference), typeName)) + + # Copy the proc or method for inheritance + # ie: procName_ClassName() + let n2 = copyNimTree(node) + n2.params.insert(1, newIdentDefs(ident(obj_reference), typeName)) + let proc_name = $(n2.name.toStrLit()) + let type_name = $(typeName.toStrLit()) + let new_name = ident(proc_name & type_name) + n2.name = new_name + result.add(n2) + + # simply call the class method from here + # proc procName= + # procName_ClassName() + var p: seq[PNimrodNode] = @[] + for i in 1..n.params.len-1: + p.add(n.params[i][0]) + n.body = newStmtList(newCall(proc_name & type_name, p)) + + result.add(n) + + of nnkVarSection: + # variables get turned into fields of the type. + for n in node.children: + recList.add(n) + else: + result.add(node) + + # The following prints out the AST structure: + # + # import macros + # dumptree: + # type X = ref object of Y + # z: int + # -------------------- + # TypeSection + # TypeDef + # Ident !"X" + # Empty + # RefTy + # ObjectTy + # Empty + # OfInherit + # Ident !"Y" + # RecList + # IdentDefs + # Ident !"z" + # Ident !"int" + # Empty + + var type_decl: PNimrodNode + if baseName == nil: + type_decl = quote do: + type `typeName` = ref object of RootObj + else: + type_decl = quote do: + type `typeName` = ref object of `baseName` + + # Inspect the tree structure: + # + # echo type_decl.treeRepr + # -------------------- + # StmtList + # TypeSection + # TypeDef + # Ident !"Animal" + # Empty + # RefTy + # ObjectTy + # Empty + # OfInherit + # Ident !"RootObj" + # Empty <= We want to replace self + type_decl[0][0][2][0][2] = recList + result.insert(0, type_decl) + + +class Animal of RootObj: + var name: string + var age: int + method stuff(s:string): string = s + method vocalize: string = "..." + method age_human_yrs: int = self.age # `self` is injected + +class Dog of Animal: + method vocalize: string = "woof" + method age_human_yrs: int = self.age * 7 + +class Cat of Animal: + method vocalize: string = + # call the base class method + self.vocalize_animal() & "meow" + +class Tiger of Cat: + method init()= + echo "I am a new tiger" + method vocalize: string = + # no need for super.super! + self.vocalize_animal() & "Rawr!" + +if isMainModule: + var animals: seq[Animal] = @[] + animals.add(new(Dog(name: "Sparky", age: 10))) + animals.add(new(Cat(name: "Mitten", age: 10))) + animals.add(new(Tiger(name: "Jean", age: 2))) + + for a in animals: + echo a.name, " says ", a.vocalize() + echo a.age_human_yrs()