diff options
Diffstat (limited to 'doc/tut2.rst')
-rw-r--r-- | doc/tut2.rst | 1009 |
1 files changed, 0 insertions, 1009 deletions
diff --git a/doc/tut2.rst b/doc/tut2.rst deleted file mode 100644 index 845feb6d1..000000000 --- a/doc/tut2.rst +++ /dev/null @@ -1,1009 +0,0 @@ -====================== -Nim Tutorial (Part II) -====================== - -:Author: Andreas Rumpf -:Version: |nimversion| - -.. contents:: - - -Introduction -============ - - "Repetition renders the ridiculous reasonable." -- Norman Wildberger - - -This document is a tutorial for the advanced constructs of the *Nim* -programming language. **Note that this document is somewhat obsolete as the** -`manual <manual.html>`_ **contains many more examples of the advanced language -features.** - - -Pragmas -======= - -Pragmas are Nim's method to give the compiler additional information/ -commands without introducing a massive number of new keywords. Pragmas are -enclosed in the special ``{.`` and ``.}`` curly dot brackets. This tutorial -does not cover pragmas. See the `manual <manual.html#pragmas>`_ or `user guide -<nimc.html#additional-features>`_ for a description of the available -pragmas. - - -Object Oriented Programming -=========================== - -While Nim's support for object oriented programming (OOP) is minimalistic, -powerful OOP techniques can be used. OOP is seen as *one* way to design a -program, not *the only* way. Often a procedural approach leads to simpler -and more efficient code. In particular, preferring composition over inheritance -is often the better design. - - -Objects -------- - -Like tuples, objects are a means to pack different values together in a -structured way. However, objects provide many features that tuples do not: -They provide inheritance and information hiding. Because objects encapsulate -data, the ``T()`` object constructor should only be used internally and the -programmer should provide a proc to initialize the object (this is called -a *constructor*). - -Objects have access to their type at runtime. There is an -``of`` operator that can be used to check the object's type: - -.. code-block:: nim - type - Person = ref object of RootObj - name*: string # the * means that `name` is accessible from other modules - age: int # no * means that the field is hidden from other modules - - Student = ref object of Person # Student inherits from Person - id: int # with an id field - - var - student: Student - person: Person - assert(student of Student) # is true - # object construction: - student = Student(name: "Anton", age: 5, id: 2) - echo student[] - -Object fields that should be visible from outside the defining module have to -be marked by ``*``. In contrast to tuples, different object types are -never *equivalent*. New object types can only be defined within a type -section. - -Inheritance is done with the ``object of`` syntax. Multiple inheritance is -currently not supported. If an object type has no suitable ancestor, ``RootObj`` -can be used as its ancestor, but this is only a convention. Objects that have -no ancestor are implicitly ``final``. You can use the ``inheritable`` pragma -to introduce new object roots apart from ``system.RootObj``. (This is used -in the GTK wrapper for instance.) - -Ref objects should be used whenever inheritance is used. It isn't strictly -necessary, but with non-ref objects assignments such as ``let person: Person = -Student(id: 123)`` will truncate subclass fields. - -**Note**: Composition (*has-a* relation) is often preferable to inheritance -(*is-a* relation) for simple code reuse. Since objects are value types in -Nim, composition is as efficient as inheritance. - - -Mutually recursive types ------------------------- - -Objects, tuples and references can model quite complex data structures which -depend on each other; they are *mutually recursive*. In Nim -these types can only be declared within a single type section. (Anything else -would require arbitrary symbol lookahead which slows down compilation.) - -Example: - -.. code-block:: nim - type - Node = ref NodeObj # a traced reference to a NodeObj - NodeObj = object - le, ri: Node # left and right subtrees - sym: ref Sym # leaves contain a reference to a Sym - - Sym = object # a symbol - name: string # the symbol's name - line: int # the line the symbol was declared in - code: Node # the symbol's abstract syntax tree - - -Type conversions ----------------- -Nim distinguishes between `type casts`:idx: and `type conversions`:idx:. -Casts are done with the ``cast`` operator and force the compiler to -interpret a bit pattern to be of another type. - -Type conversions are a much more polite way to convert a type into another: -They preserve the abstract *value*, not necessarily the *bit-pattern*. If a -type conversion is not possible, the compiler complains or an exception is -raised. - -The syntax for type conversions is ``destination_type(expression_to_convert)`` -(like an ordinary call): - -.. code-block:: nim - proc getID(x: Person): int = - Student(x).id - -The ``InvalidObjectConversionError`` exception is raised if ``x`` is not a -``Student``. - - -Object variants ---------------- -Often an object hierarchy is overkill in certain situations where simple -variant types are needed. - -An example: - -.. code-block:: nim - - # This is an example how an abstract syntax tree could be modelled in Nim - type - NodeKind = enum # the different node types - nkInt, # a leaf with an integer value - nkFloat, # a leaf with a float value - nkString, # a leaf with a string value - nkAdd, # an addition - nkSub, # a subtraction - nkIf # an if statement - Node = ref NodeObj - NodeObj = object - case kind: NodeKind # the ``kind`` field is the discriminator - of nkInt: intVal: int - of nkFloat: floatVal: float - of nkString: strVal: string - of nkAdd, nkSub: - leftOp, rightOp: Node - of nkIf: - condition, thenPart, elsePart: Node - - var n = Node(kind: nkFloat, floatVal: 1.0) - # the following statement raises an `FieldError` exception, because - # n.kind's value does not fit: - n.strVal = "" - -As can been seen from the example, an advantage to an object hierarchy is that -no conversion between different object types is needed. Yet, access to invalid -object fields raises an exception. - - -Methods -------- -In ordinary object oriented languages, procedures (also called *methods*) are -bound to a class. This has disadvantages: - -* Adding a method to a class the programmer has no control over is - impossible or needs ugly workarounds. -* Often it is unclear where the method should belong to: is - ``join`` a string method or an array method? - -Nim avoids these problems by not assigning methods to a class. All methods -in Nim are multi-methods. As we will see later, multi-methods are -distinguished from procs only for dynamic binding purposes. - - -Method call syntax ------------------- - -There is a syntactic sugar for calling routines: -The syntax ``obj.method(args)`` can be used instead of ``method(obj, args)``. -If there are no remaining arguments, the parentheses can be omitted: -``obj.len`` (instead of ``len(obj)``). - -This method call syntax is not restricted to objects, it can be used -for any type: - -.. code-block:: nim - - echo "abc".len # is the same as echo len("abc") - echo "abc".toUpper() - echo {'a', 'b', 'c'}.card - stdout.writeLine("Hallo") # the same as writeLine(stdout, "Hallo") - -(Another way to look at the method call syntax is that it provides the missing -postfix notation.) - -So "pure object oriented" code is easy to write: - -.. code-block:: nim - import strutils, sequtils - - stdout.writeLine("Give a list of numbers (separated by spaces): ") - stdout.write(stdin.readLine.split.map(parseInt).max.`$`) - stdout.writeLine(" is the maximum!") - - -Properties ----------- -As the above example shows, Nim has no need for *get-properties*: -Ordinary get-procedures that are called with the *method call syntax* achieve -the same. But setting a value is different; for this a special setter syntax -is needed: - -.. code-block:: nim - - type - Socket* = ref object of RootObj - host: int # cannot be accessed from the outside of the module due to missing star - - proc `host=`*(s: var Socket, value: int) {.inline.} = - ## setter of host address - s.host = value - - proc host*(s: Socket): int {.inline.} = - ## getter of host address - s.host - - var s: Socket - new s - s.host = 34 # same as `host=`(s, 34) - -(The example also shows ``inline`` procedures.) - - -The ``[]`` array access operator can be overloaded to provide -`array properties`:idx:\ : - -.. code-block:: nim - type - Vector* = object - x, y, z: float - - proc `[]=`* (v: var Vector, i: int, value: float) = - # setter - case i - of 0: v.x = value - of 1: v.y = value - of 2: v.z = value - else: assert(false) - - proc `[]`* (v: Vector, i: int): float = - # getter - case i - of 0: result = v.x - of 1: result = v.y - of 2: result = v.z - else: assert(false) - -The example is silly, since a vector is better modelled by a tuple which -already provides ``v[]`` access. - - -Dynamic dispatch ----------------- - -Procedures always use static dispatch. For dynamic dispatch replace the -``proc`` keyword by ``method``: - -.. code-block:: nim - type - PExpr = ref object of RootObj ## abstract base class for an expression - PLiteral = ref object of PExpr - x: int - PPlusExpr = ref object of PExpr - a, b: PExpr - - # watch out: 'eval' relies on dynamic binding - method eval(e: PExpr): int = - # override this base method - quit "to override!" - - method eval(e: PLiteral): int = e.x - method eval(e: PPlusExpr): int = eval(e.a) + eval(e.b) - - proc newLit(x: int): PLiteral = PLiteral(x: x) - proc newPlus(a, b: PExpr): PPlusExpr = PPlusExpr(a: a, b: b) - - echo eval(newPlus(newPlus(newLit(1), newLit(2)), newLit(4))) - -Note that in the example the constructors ``newLit`` and ``newPlus`` are procs -because it makes more sense for them to use static binding, but ``eval`` is a -method because it requires dynamic binding. - -In a multi-method all parameters that have an object type are used for the -dispatching: - -.. code-block:: nim - - type - Thing = ref object of RootObj - Unit = ref object of Thing - x: int - - method collide(a, b: Thing) {.inline.} = - quit "to override!" - - method collide(a: Thing, b: Unit) {.inline.} = - echo "1" - - method collide(a: Unit, b: Thing) {.inline.} = - echo "2" - - var a, b: Unit - new a - new b - collide(a, b) # output: 2 - - -As the example demonstrates, invocation of a multi-method cannot be ambiguous: -Collide 2 is preferred over collide 1 because the resolution works from left to -right. Thus ``Unit, Thing`` is preferred over ``Thing, Unit``. - -**Performance note**: Nim does not produce a virtual method table, but -generates dispatch trees. This avoids the expensive indirect branch for method -calls and enables inlining. However, other optimizations like compile time -evaluation or dead code elimination do not work with methods. - - -Exceptions -========== - -In Nim exceptions are objects. By convention, exception types are -suffixed with 'Error'. The `system <system.html>`_ module defines an -exception hierarchy that you might want to stick to. Exceptions derive from -``system.Exception``, which provides the common interface. - -Exceptions have to be allocated on the heap because their lifetime is unknown. -The compiler will prevent you from raising an exception created on the stack. -All raised exceptions should at least specify the reason for being raised in -the ``msg`` field. - -A convention is that exceptions should be raised in *exceptional* cases: -For example, if a file cannot be opened, this should not raise an -exception since this is quite common (the file may not exist). - - -Raise statement ---------------- -Raising an exception is done with the ``raise`` statement: - -.. code-block:: nim - var - e: ref OSError - new(e) - e.msg = "the request to the OS failed" - raise e - -If the ``raise`` keyword is not followed by an expression, the last exception -is *re-raised*. For the purpose of avoiding repeating this common code pattern, -the template ``newException`` in the ``system`` module can be used: - -.. code-block:: nim - raise newException(OSError, "the request to the OS failed") - - -Try statement -------------- - -The ``try`` statement handles exceptions: - -.. code-block:: nim - # read the first two lines of a text file that should contain numbers - # and tries to add them - var - f: File - if open(f, "numbers.txt"): - try: - let a = readLine(f) - let b = readLine(f) - echo "sum: ", parseInt(a) + parseInt(b) - except OverflowError: - echo "overflow!" - except ValueError: - echo "could not convert string to integer" - except IOError: - echo "IO error!" - except: - echo "Unknown exception!" - # reraise the unknown exception: - raise - finally: - close(f) - -The statements after the ``try`` are executed unless an exception is -raised. Then the appropriate ``except`` part is executed. - -The empty ``except`` part is executed if there is an exception that is -not explicitly listed. It is similar to an ``else`` part in ``if`` -statements. - -If there is a ``finally`` part, it is always executed after the -exception handlers. - -The exception is *consumed* in an ``except`` part. If an exception is not -handled, it is propagated through the call stack. This means that often -the rest of the procedure - that is not within a ``finally`` clause - -is not executed (if an exception occurs). - -If you need to *access* the actual exception object or message inside an -``except`` branch you can use the `getCurrentException() -<system.html#getCurrentException>`_ and `getCurrentExceptionMsg() -<system.html#getCurrentExceptionMsg>`_ procs from the `system <system.html>`_ -module. Example: - -.. code-block:: nim - try: - doSomethingHere() - except: - let - e = getCurrentException() - msg = getCurrentExceptionMsg() - echo "Got exception ", repr(e), " with message ", msg - - -Annotating procs with raised exceptions ---------------------------------------- - -Through the use of the optional ``{.raises.}`` pragma you can specify that a -proc is meant to raise a specific set of exceptions, or none at all. If the -``{.raises.}`` pragma is used, the compiler will verify that this is true. For -instance, if you specify that a proc raises ``IOError``, and at some point it -(or one of the procs it calls) starts raising a new exception the compiler will -prevent that proc from compiling. Usage example: - -.. code-block:: nim - proc complexProc() {.raises: [IOError, ArithmeticError].} = - ... - - proc simpleProc() {.raises: [].} = - ... - -Once you have code like this in place, if the list of raised exception changes -the compiler will stop with an error specifying the line of the proc which -stopped validating the pragma and the raised exception not being caught, along -with the file and line where the uncaught exception is being raised, which may -help you locate the offending code which has changed. - -If you want to add the ``{.raises.}`` pragma to existing code, the compiler can -also help you. You can add the ``{.effects.}`` pragma statement to your proc and -the compiler will output all inferred effects up to that point (exception -tracking is part of Nim's effect system). Another more roundabout way to -find out the list of exceptions raised by a proc is to use the Nim ``doc2`` -command which generates documentation for a whole module and decorates all -procs with the list of raised exceptions. You can read more about Nim's -`effect system and related pragmas in the manual <manual.html#effect-system>`_. - - -Generics -======== - -Generics are Nim's means to parametrize procs, iterators or types -with `type parameters`:idx:. They are most useful for efficient type safe -containers: - -.. code-block:: nim - type - BinaryTreeObj[T] = object # BinaryTree is a generic type with - # with generic param ``T`` - le, ri: BinaryTree[T] # left and right subtrees; may be nil - data: T # the data stored in a node - BinaryTree*[T] = ref BinaryTreeObj[T] # type that is exported - - proc newNode*[T](data: T): BinaryTree[T] = - # constructor for a node - new(result) - result.data = data - - proc add*[T](root: var BinaryTree[T], n: BinaryTree[T]) = - # insert a node into the tree - if root == nil: - root = n - else: - var it = root - while it != nil: - # compare the data items; uses the generic ``cmp`` proc - # that works for any type that has a ``==`` and ``<`` operator - var c = cmp(it.data, n.data) - if c < 0: - if it.le == nil: - it.le = n - return - it = it.le - else: - if it.ri == nil: - it.ri = n - return - it = it.ri - - proc add*[T](root: var BinaryTree[T], data: T) = - # convenience proc: - add(root, newNode(data)) - - iterator preorder*[T](root: BinaryTree[T]): T = - # Preorder traversal of a binary tree. - # Since recursive iterators are not yet implemented, - # this uses an explicit stack (which is more efficient anyway): - var stack: seq[BinaryTree[T]] = @[root] - while stack.len > 0: - var n = stack.pop() - while n != nil: - yield n.data - add(stack, n.ri) # push right subtree onto the stack - n = n.le # and follow the left pointer - - var - root: BinaryTree[string] # instantiate a BinaryTree with ``string`` - add(root, newNode("hello")) # instantiates ``newNode`` and ``add`` - add(root, "world") # instantiates the second ``add`` proc - for str in preorder(root): - stdout.writeLine(str) - -The example shows a generic binary tree. Depending on context, the brackets are -used either to introduce type parameters or to instantiate a generic proc, -iterator or type. As the example shows, generics work with overloading: the -best match of ``add`` is used. The built-in ``add`` procedure for sequences -is not hidden and is used in the ``preorder`` iterator. - - -Templates -========= - -Templates are a simple substitution mechanism that operates on Nim's -abstract syntax trees. Templates are processed in the semantic pass of the -compiler. They integrate well with the rest of the language and share none -of C's preprocessor macros flaws. - -To *invoke* a template, call it like a procedure. - -Example: - -.. code-block:: nim - template `!=` (a, b: expr): expr = - # this definition exists in the System module - not (a == b) - - assert(5 != 6) # the compiler rewrites that to: assert(not (5 == 6)) - -The ``!=``, ``>``, ``>=``, ``in``, ``notin``, ``isnot`` operators are in fact -templates: this has the benefit that if you overload the ``==`` operator, -the ``!=`` operator is available automatically and does the right thing. (Except -for IEEE floating point numbers - NaN breaks basic boolean logic.) - -``a > b`` is transformed into ``b < a``. -``a in b`` is transformed into ``contains(b, a)``. -``notin`` and ``isnot`` have the obvious meanings. - -Templates are especially useful for lazy evaluation purposes. Consider a -simple proc for logging: - -.. code-block:: nim - const - debug = true - - proc log(msg: string) {.inline.} = - if debug: stdout.writeLine(msg) - - var - x = 4 - log("x has the value: " & $x) - -This code has a shortcoming: if ``debug`` is set to false someday, the quite -expensive ``$`` and ``&`` operations are still performed! (The argument -evaluation for procedures is *eager*). - -Turning the ``log`` proc into a template solves this problem: - -.. code-block:: nim - const - debug = true - - template log(msg: string) = - if debug: stdout.writeLine(msg) - - var - x = 4 - log("x has the value: " & $x) - -The parameters' types can be ordinary types or the meta types ``expr`` -(stands for *expression*), ``stmt`` (stands for *statement*) or ``typedesc`` -(stands for *type description*). If the template has no explicit return type, -``stmt`` is used for consistency with procs and methods. - -If there is a ``stmt`` parameter it should be the last in the template -declaration. The reason is that statements can be passed to a template -via a special ``:`` syntax: - -.. code-block:: nim - - template withFile(f: expr, filename: string, mode: FileMode, - body: stmt): stmt {.immediate.} = - let fn = filename - var f: File - if open(f, fn, mode): - try: - body - finally: - close(f) - else: - quit("cannot open: " & fn) - - withFile(txt, "ttempl3.txt", fmWrite): - txt.writeLine("line 1") - txt.writeLine("line 2") - -In the example the two ``writeLine`` statements are bound to the ``body`` -parameter. The ``withFile`` template contains boilerplate code and helps to -avoid a common bug: to forget to close the file. Note how the -``let fn = filename`` statement ensures that ``filename`` is evaluated only -once. - - -Macros -====== - -Macros enable advanced compile-time code transformations, but they cannot -change Nim's syntax. However, this is no real restriction because Nim's -syntax is flexible enough anyway. Macros have to be implemented in pure Nim -code if the `foreign function interface (FFI) -<manual.html#foreign-function-interface>`_ is not enabled in the compiler, but -other than that restriction (which at some point in the future will go away) -you can write any kind of Nim code and the compiler will run it at compile -time. - -There are two ways to write a macro, either *generating* Nim source code and -letting the compiler parse it, or creating manually an abstract syntax tree -(AST) which you feed to the compiler. In order to build the AST one needs to -know how the Nim concrete syntax is converted to an abstract syntax tree -(AST). The AST is documented in the `macros <macros.html>`_ module. - -Once your macro is finished, there are two ways to invoke it: -(1) invoking a macro like a procedure call (expression macros) -(2) invoking a macro with the special ``macrostmt`` - syntax (statement macros) - - -Expression Macros ------------------ - -The following example implements a powerful ``debug`` command that accepts a -variable number of arguments: - -.. code-block:: nim - # to work with Nim syntax trees, we need an API that is defined in the - # ``macros`` module: - import macros - - macro debug(n: varargs[expr]): stmt = - # `n` is a Nim AST that contains a list of expressions; - # this macro returns a list of statements (n is passed for proper line - # information): - result = newNimNode(nnkStmtList, n) - # iterate over any argument that is passed to this macro: - for x in n: - # add a call to the statement list that writes the expression; - # `toStrLit` converts an AST to its string representation: - result.add(newCall("write", newIdentNode("stdout"), toStrLit(x))) - # add a call to the statement list that writes ": " - result.add(newCall("write", newIdentNode("stdout"), newStrLitNode(": "))) - # add a call to the statement list that writes the expressions value: - result.add(newCall("writeLine", newIdentNode("stdout"), x)) - - var - a: array[0..10, int] - x = "some string" - a[0] = 42 - a[1] = 45 - - debug(a[0], a[1], x) - -The macro call expands to: - -.. code-block:: nim - write(stdout, "a[0]") - write(stdout, ": ") - writeLine(stdout, a[0]) - - write(stdout, "a[1]") - write(stdout, ": ") - writeLine(stdout, a[1]) - - write(stdout, "x") - write(stdout, ": ") - writeLine(stdout, x) - - - -Statement Macros ----------------- - -Statement macros are defined just as expression macros. However, they are -invoked by an expression following a colon. - -The following example outlines a macro that generates a lexical analyzer from -regular expressions: - -.. code-block:: nim - - macro case_token(n: stmt): stmt = - # creates a lexical analyzer from regular expressions - # ... (implementation is an exercise for the reader :-) - discard - - case_token: # this colon tells the parser it is a macro statement - of r"[A-Za-z_]+[A-Za-z_0-9]*": - return tkIdentifier - of r"0-9+": - return tkInteger - of r"[\+\-\*\?]+": - return tkOperator - else: - return tkUnknown - - -Building your first macro -------------------------- - -To give a footstart to writing macros we will show now how to turn your typical -dynamic code into something that compiles statically. For the exercise we will -use the following snippet of code as the starting point: - -.. code-block:: nim - - import strutils, tables - - proc readCfgAtRuntime(cfgFilename: string): Table[string, string] = - let - inputString = readFile(cfgFilename) - var - source = "" - - result = initTable[string, string]() - for line in inputString.splitLines: - # Ignore empty lines - if line.len < 1: continue - var chunks = split(line, ',') - if chunks.len != 2: - quit("Input needs comma split values, got: " & line) - result[chunks[0]] = chunks[1] - - if result.len < 1: quit("Input file empty!") - - let info = readCfgAtRuntime("data.cfg") - - when isMainModule: - echo info["licenseOwner"] - echo info["licenseKey"] - echo info["version"] - -Presumably this snippet of code could be used in a commercial software, reading -a configuration file to display information about the person who bought the -software. This external file would be generated by an online web shopping cart -to be included along the program containing the license information:: - - version,1.1 - licenseOwner,Hyori Lee - licenseKey,M1Tl3PjBWO2CC48m - -The ``readCfgAtRuntime`` proc will open the given filename and return a -``Table`` from the `tables module <tables.html>`_. The parsing of the file is -done (without much care for handling invalid data or corner cases) using the -`splitLines proc from the strutils module <strutils.html#splitLines>`_. There -are many things which can fail; mind the purpose is explaining how to make -this run at compile time, not how to properly implement a DRM scheme. - -The reimplementation of this code as a compile time proc will allow us to get -rid of the ``data.cfg`` file we would need to distribute along the binary, plus -if the information is really constant, it doesn't make from a logical point of -view to have it *mutable* in a global variable, it would be better if it was a -constant. Finally, and likely the most valuable feature, we can implement some -verification at compile time. You could think of this as a *better unit -testing*, since it is impossible to obtain a binary unless everything is -correct, preventing you to ship to users a broken program which won't start -because a small critical file is missing or its contents changed by mistake to -something invalid. - - -Generating source code -++++++++++++++++++++++ - -Our first attempt will start by modifying the program to generate a compile -time string with the *generated source code*, which we then pass to the -``parseStmt`` proc from the `macros module <macros.html>`_. Here is the -modified source code implementing the macro: - -.. code-block:: nim - :number-lines: - - import macros, strutils - - macro readCfgAndBuildSource(cfgFilename: string): stmt = - let - inputString = slurp(cfgFilename.strVal) - var - source = "" - - for line in inputString.splitLines: - # Ignore empty lines - if line.len < 1: continue - var chunks = split(line, ',') - if chunks.len != 2: - error("Input needs comma split values, got: " & line) - source &= "const cfg" & chunks[0] & "= \"" & chunks[1] & "\"\n" - - if source.len < 1: error("Input file empty!") - result = parseStmt(source) - - readCfgAndBuildSource("data.cfg") - - when isMainModule: - echo cfglicenseOwner - echo cfglicenseKey - echo cfgversion - -The good news is not much has changed! First, we need to change the handling -of the input parameter (line 3). In the dynamic version the -``readCfgAtRuntime`` proc receives a string parameter. However, in the macro -version it is also declared as string, but this is the *outside* interface of -the macro. When the macro is run, it actually gets a ``PNimNode`` object -instead of a string, and we have to call the `strVal proc -<macros.html#strVal>`_ (line 5) from the `macros module <macros.html>`_ to -obtain the string being passed in to the macro. - -Second, we cannot use the `readFile proc <system.html#readFile>`_ from the -`system module <system.html>`_ due to FFI restriction at compile time. If we -try to use this proc, or any other which depends on FFI, the compiler will -error with the message ``cannot evaluate`` and a dump of the macro's source -code, along with a stack trace where the compiler reached before bailing out. -We can get around this limitation by using the `slurp proc -<system.html#slurp>`_ from the `system module <system.html>`_, which was -precisely made for compilation time (just like `gorge <system.html#gorge>`_ -which executes an external program and captures its output). - -The interesting thing is that our macro does not return a runtime `Table -<tables.html#Table>`_ object. Instead, it builds up Nim source code into -the ``source`` variable. For each line of the configuration file a ``const`` -variable will be generated (line 15). To avoid conflicts we prefix these -variables with ``cfg``. In essence, what the compiler is doing is replacing -the line calling the macro with the following snippet of code: - -.. code-block:: nim - const cfgversion= "1.1" - const cfglicenseOwner= "Hyori Lee" - const cfglicenseKey= "M1Tl3PjBWO2CC48m" - -You can verify this yourself adding the line ``echo source`` somewhere at the -end of the macro and compiling the program. Another difference is that instead -of calling the usual `quit proc <system.html#quit>`_ to abort (which we could -still call) this version calls the `error proc <macros.html#error>`_ (line -14). The ``error`` proc has the same behavior as ``quit`` but will dump also -the source and file line information where the error happened, making it -easier for the programmer to find where compilation failed. In this situation -it would point to the line invoking the macro, but **not** the line of -``data.cfg`` we are processing, that's something the macro itself would need -to control. - - -Generating AST by hand -++++++++++++++++++++++ - -To generate an AST we would need to intimately know the structures used by the -Nim compiler exposed in the `macros module <macros.html>`_, which at first -look seems a daunting task. But we can use as helper shortcut the `dumpTree -macro <macros.html#dumpTree>`_, which is used as a statement macro instead of -an expression macro. Since we know that we want to generate a bunch of -``const`` symbols we can create the following source file and compile it to -see what the compiler *expects* from us: - -.. code-block:: nim - import macros - - dumpTree: - const cfgversion: string = "1.1" - const cfglicenseOwner= "Hyori Lee" - const cfglicenseKey= "M1Tl3PjBWO2CC48m" - -During compilation of the source code we should see the following lines in the -output (again, since this is a macro, compilation is enough, you don't have to -run any binary):: - - StmtList - ConstSection - ConstDef - Ident !"cfgversion" - Ident !"string" - StrLit 1.1 - ConstSection - ConstDef - Ident !"cfglicenseOwner" - Empty - StrLit Hyori Lee - ConstSection - ConstDef - Ident !"cfglicenseKey" - Empty - StrLit M1Tl3PjBWO2CC48m - -With this output we have a better idea of what kind of input the compiler -expects. We need to generate a list of statements. For each constant the source -code generates a ``ConstSection`` and a ``ConstDef``. If we were to move all -the constants to a single ``const`` block we would see only a single -``ConstSection`` with three children. - -Maybe you didn't notice, but in the ``dumpTree`` example the first constant -explicitly specifies the type of the constant. That's why in the tree output -the two last constants have their second child ``Empty`` but the first has a -string identifier. So basically a ``const`` definition is made up from an -identifier, optionally a type (can be an *empty* node) and the value. Armed -with this knowledge, let's look at the finished version of the AST building -macro: - -.. code-block:: nim - :number-lines: - - import macros, strutils - - macro readCfgAndBuildAST(cfgFilename: string): stmt = - let - inputString = slurp(cfgFilename.strVal) - - result = newNimNode(nnkStmtList) - for line in inputString.splitLines: - # Ignore empty lines - if line.len < 1: continue - var chunks = split(line, ',') - if chunks.len != 2: - error("Input needs comma split values, got: " & line) - var - section = newNimNode(nnkConstSection) - constDef = newNimNode(nnkConstDef) - constDef.add(newIdentNode("cfg" & chunks[0])) - constDef.add(newEmptyNode()) - constDef.add(newStrLitNode(chunks[1])) - section.add(constDef) - result.add(section) - - if result.len < 1: error("Input file empty!") - - readCfgAndBuildAST("data.cfg") - - when isMainModule: - echo cfglicenseOwner - echo cfglicenseKey - echo cfgversion - -Since we are building on the previous example generating source code, we will -only mention the differences to it. Instead of creating a temporary ``string`` -variable and writing into it source code as if it were written *by hand*, we -use the ``result`` variable directly and create a statement list node -(``nnkStmtList``) which will hold our children (line 7). - -For each input line we have to create a constant definition (``nnkConstDef``) -and wrap it inside a constant section (``nnkConstSection``). Once these -variables are created, we fill them hierarchichally (line 17) like the -previous AST dump tree showed: the constant definition is a child of the -section definition, and the constant definition has an identifier node, an -empty node (we let the compiler figure out the type), and a string literal -with the value. - -A last tip when writing a macro: if you are not sure the AST you are building -looks ok, you may be tempted to use the ``dumpTree`` macro. But you can't use -it *inside* the macro you are writting/debugging. Instead ``echo`` the string -generated by `treeRepr <macros.html#treeRepr>`_. If at the end of the this -example you add ``echo treeRepr(result)`` you should get the same output as -using the ``dumpTree`` macro, but of course you can call that at any point of -the macro where you might be having troubles. - - -Compilation to JavaScript -========================= - -Nim code can be compiled to JavaScript. However in order to write -JavaScript-compatible code you should remember the following: -- ``addr`` and ``ptr`` have slightly different semantic meaning in JavaScript. - It is recommended to avoid those if you're not sure how they are translated - to JavaScript. -- ``cast[T](x)`` in JavaScript is translated to ``(x)``, except for casting - between signed/unsigned ints, in which case it behaves as static cast in - C language. -- ``cstring`` in JavaScript means JavaScript string. It is a good practice to - use ``cstring`` only when it is semantically appropriate. E.g. don't use - ``cstring`` as a binary data buffer. |