Skip to content

Da-Buche/skill-best-practices

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

45 Commits
 
 

Repository files navigation

General Coding Advice

The following pieces of advice are not specific to SKILL in particular, but I believe, it is always good to remind them.

Programming Mottos

  • Less code less errors.
    (i.e. from a CAD engineer point-of-view: less code less support!)
  • Keep it simple.
  • Be consistent.

Programming Mindset

Write code to make it understandable by anyone.

Your future self (and your coworkers) will be thankful!

Programs must be written for people to read, and only incidentally for machines to execute.
-- Harold Abelson, Structure and Interpretation of Computer Programs, MIT

Note

SKILL is a high level language, most scripts are called by human interaction (GUI, click, ...).

A few seconds, sometimes even minutes, to run are often acceptable.
So [always] prefer clarity over efficiency. (Effiency is often achieved while clarifying your code.)

Warning

Still, avoid square or exponential complexities...
If you are unaware about complexity, you should learn the following before coding more...
Wikipedia: Computational complexity

Be explicit.

Avoid defining global things with constructed names. If you do so, document it very clearly.
When debugging or discovering new code, it is very disturbing not being able to grep what you are looking for.

Caution

Here is a bad SKILL example of smart (or dynamic) definitions.

;; Define predicates to check if objects are simple geometric shapes.
(foreach obj_type '( rect polygon ellipse )
  (funcall 'defun (concat 'is_ obj_type ?) '( obj )
    (lsprintf  "Check if OBJ is a %s" obj_type)
    `(eq ,obj_type obj->objType)
    ))

This might confuse most developers that will have a hard time looking where is_rect? is defined.

So use explicit definitions. Choose explicit functions and variables names. Write explicit error messages. Anyone reading and debugging your code will be thankful.

Keep things together.

Instead of having external documentation that will become outdated, put comments at the right location.
Or even better, write docstrings inside the code.

Avoid external mapping files.

Follow common practices.

If you write specific, tricky or weird code, document it properly!

Warning

If every other developer is having a hard time understanding your code.
Your way is probably not the best, or it hasn't been clearly explicited why it is the good solution!

Always be in control, no place for random.

Avoid global variables, it makes your code context dependent.
(At least prefix them, so they have less chances to be overwritten.)

Avoid dependencies (when reasonable).

If you have to develop a server to communicate with another language to use one or two features.
Make sure it is not simpler to implement what's missing.
Or that you cannot develop what you need directly in the other language.

At least, make sure your dependencies are well tested, well documented and well maintained.

Programming Methodology

Check what exists already.

Before coding in a rush, make sure there are no solutions available.

Note

In Python, the community is active and many open-soucre modules are already there.
In SKILL, although most code is proprietary, many scripts are available here:

The support search is often returning good and interesting results.
It contains many examples with code snippets, you can probably re-use directly or adapt!

Use standard functions.

As much as possible, try to not reinvent the wheel.

Write tests and specs before coding.

If you know what to expect in most cases, you will get the big picture before you start coding.
This will help you write clearer, simpler code and avoid rushing headfirst into a wall.
It will also help debugging as the more you test, the less you break things and the easier you spot errors.

Add docstrings to functions.

The minimum requirement is to list the arguments and the output.
It is also nice to explain the purpose and detail the behavior.

Docstrings are better than regular comments as they are easier to manipulate,
when generating documentation, for instance.

Basic SKILL/SKILL++ Advice

Simple Improvements

The following advice are low hanging fruits but can drastically improve code quality and robustness.

Lint your files.

Follow lint recommendations and try to reach 100% score.
A high score is reassuring, it means the author made effort cleaning his code.

It often avoids bugs and errors even before running the code.

Prefix unused variables with underscores.

Prefixing a variable with '_' is the standard syntax to declare it unused.
It is common to define functions that must accept unused arguments.

Tip

Unused variables will decrease Lint score, unless they have '_' prefix.

For instance:

;; Display a simple form with a string field and a button.
;; When the button is clicked, it prints the value of the string field.
(hiDisplayForm
  (hiCreateLayoutForm (gensym 'custom_form) "Custom Form"
    (hiCreateFormLayout 'main_layout ?items (list
        (hiCreateStringField 
          ?name  'input_field
          ?prompt "Input"
          )
        (hiCreateButton 
          ?name       'print_button
          ?buttonText "Print"
          ;; Here the _field value has to be taken in account, but is never used...
          ?callback   (lambda ( _field form ) (println form->input_field->value))
          )
        ))
    ?buttonLayout 'Close
    ))

Use (car (exists ...)) instead of (car (setof ...))

I have seen this one many times, SKILL developers love setof... Of course, it is super useful.
But if you only need the first element of a filtered list, then you probably don't need to parse it entirely.

In 99% cases, you can blindlessly replace (car (setof ...)) by (car (exists ...)).
The output will stay the same but it will run faster.

;; Generate a list of random values, just for the sake of this example
(let ( dividers
       results
       )
  (for _ 0 99 (push (random 100) dividers))

  ;; Here we browse all dividers and keep only the ones that are zero
  ;; Then we take the first one of those zeros
  (when (car (setof number dividers (zerop number)))
    (error "One of the dividers is zero: %N" dividers))

  ;; The following will give the same result
  ;; But it stops as soon as one divider equal to zero is found
  (when (car (exists number dividers (zerop number)))
    (error "One of the dividers is zero: %N" dividers))

  results = (foreach mapcar number dividers 1.0/number)
  (println results)

  ;; The worst-case complexity is still identical, if there are no zero in dividers
  ;; the list will be browsed entirely in both cases.
  ;; But the average complexity is way improved for `exists`.

  );let

Caution

The 1% cases where this is not valid are the cases where the loop contains destructive (or constructive) code.
i. e. when operations inside setof modify the environment.

This is not a good practice though.

;; Let's re-use the previous example
;; But this time we also want to count the occurences of each generated number
(let ( ( occurences_by_number (makeTable 'occurences 0) )
       dividers
       results
       )
  (for _ 0 99 (push (random 100) dividers))

  ;; Here we cannot replace `setof` by `exists`
  ;; Otherwise the occurences count will be wrong as soon as zero is encountered
  (when (car (setof number dividers
               (progn occurences_by_number[number]++ (zerop number))))
    (error "One of the dividers is zero: %N" dividers))

  results = (foreach mapcar number dividers 1.0/number)
  (println results)

  ;; Print values with associated occurences
  (foreach number occurences_by_number
    (info "%d : %d\n" number occurences_by_number[number])
    )

  );let
A clearer solution could be:

But it does not use exists nor setof and one could argue that the previous code is more efficient.

(let ( ( occurences_by_number (makeTable 'occurences 0) )
       dividers
       results
       )
  (for _ 0 99 (push (random 100) dividers))

  ;; Count occurences of each number
  (foreach number dividers
    occurences_by_number[number]++
    )

  ;; Make sure zero is not present
  (assert (zerop occurences_by_number[0]) 
    "One of the dividers is zero: %N" dividers)

  results = (foreach mapcar number dividers 1.0/number)
  (println results)

  ;; Print values with associated occurences
  (foreach number occurences_by_number
    (info "%d : %d\n" number occurences_by_number[number])
    )

  );let

Avoid nested if calls.

Nested if calls are hard to understand. Ther are also complicated to maintain.
Here are a few solutions to avoid them:

Use cond.

cond is the default Lisp statement to group conditions. It is somewhat equivalent to if ... elif ... else in other languages.

(defun fibonacci (n)
  "Return the Nth Fibonacci number."
  (if (zerop n)
      0
    (if (onep n)
        1
      (fibonnaci n-1)+(fibonnaci n-2)
      ))
  )

;; The nested `if`s can be replaced by a single `cond`
(defun fibonacci (n)
  "Return the Nth Fibonacci number."
  (cond
    ( (zerop n) 0                               )
    ( (onep  n) 1                               )
    ( t         (fibonnaci n-1)+(fibonnaci n-2) )
    ))

Use case or caseq.

When comparing the result of an expression to several values (as done just before), then case is more suitable.
It is somewhat equivalent to switch statements in other languages.

(defun fibonacci (n)
  "Return the Nth Fibonacci number."
  (case n
    ( 0 0                               )
    ( 1 1                               )
    ( t (fibonnaci n-1)+(fibonnaci n-2) )
    ))

;; It is also possible to group values which return the same output
(defun fibonacci (n)
  "Return the Nth Fibonacci number."
  (case n
    ( ( 0 1 ) n                               )
    ( t       (fibonnaci n-1)+(fibonnaci n-2) )
    ))

Tip

If you want to check if the value is exactly t or a list. You can use the following

(case variable
 ( ( ( 12 27 ) ) (info "variable is equal to '( 12 27 )\n" ) )
 ( ( t         ) (info "variable is exactly 't\n"          ) )
 ( t             (info "variable can be anything...\n"     ) )
 )

Note

caseq is similar to case but it uses eq instead of equal for the comparison.
It is faster but only works when comparing value to symbols or integers.
You can stick to case as it will work in all cases and the performance gain is often negligible.

Use error or assert.

In many cases, raising errors is the proper things to do.
It avoids nesting statements as it exits the current stack.

One common use-case, is to check arguments:

(defun box_width ( box )
  "Return BOX width."
  (if (isBBox box)
      (rightEdge box)-(leftEdge box)
      ))

;; The previous function does not guarantee that the output would be a valid number.
;; If anything else than a box is passed as argument, it will return nil.
;; It is cleaner to raise a meaningful error message as soon as the erratic input is found.
(defun box_width ( box )
  "Return BOX width."
  (unless (isBBox box) (error "box_width - input is not a valid bounding box: %N" box))
  (rightEdge box)-(leftEdge box)
  )

Note

(unless <predicate> (error <msg> <msg_inputs...>))
;; This is completely equivalent to:
(assert <predicate> <msg> <msg_inputs...>)

Use prog.

prog disrupts with Lisp mindset but it remains a very useful statement.
Like error it allows to exit the current stack thanks to return calls.
It might be useful to raise warnings instead of errors.

Caution

I do not advise to use go statements inside prog. It is often cleaner to write dedicated functions.

Use or and and.

Boolean operatons might be useful to group several conditions:

(when (and (stringp obj)
           (not (blankstrp obj))
           )
  (info "obj is a non-blank string!: %N\n" obj)
  )

Use advanced predicates.

There are many available predicates. Knowing them can avoid grouping conditions. Here are some examples of useful native predicates:

Predicate Description
dplp Object is a Disembodied Property List [DPL].
blankstrp String is empty or contains only whitespace.
isBBox Object is a bounding box.
atom Object is an atom (i.e. nil or not a list).

Write your own predicates, if you are often repeating the same checks:

(defun nonblankstring? (obj)
  "Return t if OBJ is a non-blank string.
Meaning a string that contains at least one character which is not whitespace."
  (and (stringp obj)
       (not (blankstrp obj))
       ))

Use (printf "%s" str) instead of (printf str)

When using most printing functions (lsprintf, printf, fprintf, info, warn, error, ...), avoid using a variable as first argument. (This is even more true when you are unaware of the variable content. It can be the result of a command, the text of a file or a user input.)

;; The following will break
(foreach str '( "This is a dummy example string\n"
                "Another one but with a percentage sign %\n"
                )
  (info str)
  )

;; This is much safer
(foreach str '( "This is a dummy example string\n"
                "Another one but with a percentage sign %\n"
                )
  (info "%s" str)
  )

Shape data before comparison and report unsupported cases

Instead of writing advanced conditions for many possible values. Shape your data so it can only ends in a few different baskets.

;; Let's assume that you have a function `(read_user_input)` which can return verbosity levels
;; like "Verbose" "debug" or "None". Here is a small example to exploit it properly:
(let ( ( verbosity (read_user_input) )
       )
  (case (lowerCase )
    ( "verbose"
      ...
      )
    ( "debug"
      ...
      )
    ( "none"
      ...
      )
    ;; Important, when case is not supported, raise a meaningful error!
    ( t
      (error "Verbose level should be \"vebose\", \"debug\" or \"none\": %N\n" verbosity)
      )
    ))

Construct Graphical User Interfaces [GUIs] with layout forms

Warning

Forms with hardcoded coordinates are hard to modify and maintain.

(defun create_custom_form ()
  "Create and return an example form."
  (hiCreateAppForm
    ?name      (gensym 'custom_form)
    ?formTitle "Example Form"
    ?fields
    (list
      ;; String
      (list
        (hiCreateStringField
          ?name   'string_field
          ?prompt "string"
          )
        0:0 300:30 100
        )
      ;; Combo
      (list
        (hiCreateComboField
          ?name   'combo_field
          ?prompt "combo"
          ?items '( "Choice A" "Choice B" "Other" )
          )
        0:30 300:30 100
        )
      ));list ;hiCreateAppForm
  );defun

(hiDisplayForm (create_custom_form))

Tip

Creating a similar form using layouts is simpler.
The coordinates are calculated automatically.
The form will be more responsive (it will adapt when resized, etc.).

(defun create_custom_form ()
  "Create and return an example form."
  (hiCreateLayoutForm (gensym 'custom_form) "Example Form"
    (hiCreateFormLayout 'main_layout
      ?items
      (list
        ;; String
        (hiCreateStringField
          ?name   'string_field
          ?prompt "string"
          )
        ;; Combo
        (hiCreateComboField
          ?name   'combo_field
          ?prompt "combo"
          ?items '( "Choice A" "Choice B" "Other" )
          )
        ));list ;hiCreateFormLayout
    ));hiCreateLayoutForm ;defun

(hiDisplayForm (create_custom_form))

Layout forms are very flexible, you just need to know the right functions

Useful functions, macros & syntax forms

Here are some useful statements that are worth knowing while coding in SKILL or SKILL++.

Type conversion

Function Description
concat Concatenate any number of strings, symbols and integers into a symbol.
strcat Concatenate any number of symbols and strings into a string.
atoi Convert a string into an integer.
atof Convert a string into a floating-point number.
aelNumber Convert an integer, a floating-point number or a string into a number. It supports scientifc or metric notations.
aelEngNotation Convert a number into a string using engineering notation.
aelSuffixNotation Convert a number into a string using metric suffix notation.

Tip

concat & strcat can also be used to simply convert a string into a symbol or a symbol into a string.
They are faster to type and to execute than stringToSymbol & symbolToString. So you can forget about those two last ones.

Formatting

Formatting functions behave like Unix printf utility:

  • lsprintf
  • printf
  • fprintf
  • info
  • warn
  • error
  • sprintf

It is possible to padd printed values using a minimum number of characters to the right or left.
It is also possible to specify the number of decimals when printing a floating-point number.

Read Cadence SKILL Language Reference IC23.1 - fprintf its documentation is well detailled.

Tip

Here is an example of advanced printing:

(defun padd ( str padd_num "Sx")
  "Return STR left-padded with spaces so its minimum length is PADD_NUM."
  (lsprintf (lsprintf "%%%ds" padd_num) str)
  )

(let ( ( constants_tuples '( ( pi                "π"  3.14159 )
                             ( "Euler's Number"  "e"  2.71828 )
                             ( phi               "φ"  1.61803 )
                             ( "2's square root" "√2" 1.41421 )
                             ) )
       ( max_len          0    )
       )
  ;; Calculate maximum name length
  (foreach tuple constants_tuples (setq max_len (max max_len (length (strcat (car tuple))))))

  ;; Print formatted constants
  (foreach tuple constants_tuples
    (destructuringBind ( name symbol value ) tuple
      (info "%s %-4s %8.4f\n" (padd name max_len) symbol value)
      ))
  )

instring and outstring

Sometimes you need to imitate ports. For instance, if you want to redirect poport and get everything that is printed to it as a string.

A solution would be to create a temporary file. Open it as an outfile port and read it afterwards...
There is a much cleaner solution:

(let ( ( out_port (outstring) )
       ( _poport  poport      )
       str )
  (unwindProtect
    (progn
      (setq poport out_port)
      (info "This is a dummy info message!\n")
      (setq str (getOutstring out_port))
      )
    (progn 
      (setq poport _poport)
      (close out_port)
      )
    )
  ;; Print message catched in string
  (println str)
  )

instring follows the opposite behavior, it takes a string as argument and will emulate an infile port whose text is the content of the string.

List splitting

I know combinations of car and cdr are very Lispy, but they are often hard to read. You can often use nth and nthcdr which are easier to read. Or even better, use destructuringBind.

Warning

;; Bad example of how to split a bounding box into coordinates
(letseq ( ( cellview (geGetEditCellView) )
          ( box      cellview->bBox      )
          ( x0       (caar   box)        )
          ( y0       (cadar  box)        )
          ( x1       (caadr  box)        )
          ( y1       (cadadr box)        )
          )
  ...
  )

;; Better solution but it can still be improved
(letseq ( ( cellview (geGetEditCellView) )
          ( box      cellview->bBox      )
          ( x0       (leftEdge   box)    )
          ( y0       (bottomEdge box)    )
          ( x1       (rightEdge  box)    )
          ( y1       (topEdge    box)    )
          )
  ...
  )

Tip

It is much cleaner using destructuringBind:

(destructuringBind ( ( x0 y0 ) ( x1 y1 ) ) (geGetEditCellView)->bBox
  ...
  )

Note

Lisp is meant to manipulate lists using functions. destructuringBind is simply a macro that defines local functions and apply them to a list.

(destructuringBind ( a b @key c @rest _ ) mylist
  ...
  )
;; is completely equivalent to
(apply (lambda ( a b @key c @rest _ ) ...) mylist)

Using predicates with lists

The following functions are very useful to apply a predicate to list elements:

Function Description
setof Return elements that pass the predicate.
exists Return first sublist whose first element passes the predicate.
forall Return t if all elements pass the predicate, nil otherwise.
;; Get all rectangle shapes from cellview
(setof shape (geGetEditCellView)->shapes (equal "rect" shape->objType))

;; Check if cellview contain an ellipse
(when (exists shape (geGetEditCellView)->shapes (equal "ellipse" shape->objType))
  (warn "Cellview contains an ellipse"))

;; Check if all shapes are polygons
(when (forall shape (geGetEditCellView)->shapes (equal "polygon" shape->objType))
  (warn "All shapes are polygons"))

Setting anything

"setters" and "getters" are very common.
They are functions to access and manage properties.
However their syntax can be confusing, for instance:

;; Set property 'a to 12 in a DPL
(let ( ( dpl (list nil) )
     )
  ;; `putprop` arguments order is : object, value, key
  (putprop dpl 12 'a)
  (get dpl 'a)
  )
  
;; Set property 'a to 12 in an instance
(defclass dummy () ( ( a ) ))
(let ( ( obj (makeInstance 'dummy) )
       )
  ;; `setSlotValue` arguments order is : object, key, value...
  (setSlotValue obj 'a 12)
  (slotValue obj 'a)
  )

This is where setf comes in handy.
If we know how to access a property, it should be equally simple to set it.
setf is a macro meant for that, it abstracts setter syntax.

Let's update the previous example:

;; Set property 'a to 12 in a DPL
(let ( ( dpl (list nil) )
     )
  (setf (get dpl 'a) 12)
  (get dpl 'a)
  )
  
;; Set property 'a to 12 in an instance
(defclass dummy () ( ( a ) ))
(let ( ( obj (makeInstance 'dummy) )
       )
  ;; `setSlotValue` arguments order is : object, key, value...
  (setf (slotValue obj 'a) 12)
  (slotValue obj 'a)
  )

It is simpler, easier to remember and more consistent!

Note

setf works with almost every getter: get, getShellEnvVar, status, car, nth, ...

And in the few cases where it does not work, it is always possible to define it.

;; `(setf (envGetVal ...) ...)` seems to be missing, let's define it
(defun setf_envGetVal ( value tool name type )
  "`setf` helper for `envGetVal`."
  (envSetVal tool name type value)
  )

;; Now we can use the following syntax to set the number of input lines in the CIW
(setf (envGetVal "ui" "ciwCmdInputLines" 'int) 10)

Layout form functions

To fully exploit User Interfaces creation in SKILL, you should have a look at the following functions:

  • hiCreateFormLayout
  • hiCreateLayoutForm
  • hiCreateVerticalBoxLayout
  • hiCreateHorizontalBoxLayout
  • hiCreate...Field (All field creation functions)

You can also create interactive Library, Cell, View fields using:

  • ddHiCreateLibraryComboField
  • ddHiCreateCellComboField
  • ddHiCreateViewComboField
  • ddHiLinkFields

You can create a layer selection field using:

  • leCreateLayerField

hiSetFieldMinSize can be useful to align several fields or headers.
It can also be used to reset the height of a layer field created with leCreateLayerField which appears bigger than other fields by default.

Tip

You can use (hiDisplayForm custom_form -1:-1) to display a form under the mouse.

Regular expressions

If you are unaware about regular expressions. You should definitely have a look at the following resources:

Site Description
Regex One Interactive courses to learn regular expressions from zero.
Regular Expressions 101 Test and explain regular expressions interactively.

In SKILL, instead of rex... functions, I advise using pcre... ones.
They are following Perl Compatible Regular Expressions [PCRE] standards.

  • pcreCompile
  • pcreMatchp
  • pcreReplace
  • pcreSubstitute
  • pcreGenCompileOptBits

Comparison of strings containing numbers

alphalessp is limited when comparing strings with numbers. This is not the case of its lesser known cousin alphaNumCmp which compares numbers inside strings in a natural way:

(let ( ( file_names '( "video_0.mp4"
                       "video_12.mp4"
                       "video_27.mp4"
                       "video_10.mp4"
                       "video_1.mp4"
                       "video_111.mp4"
                       ) )
       )
  (println (sort (copy file_names) 'alphalessp))
  ;; > ("video_0.mp4" "video_1.mp4" "video_10.mp4" "video_111.mp4" "video_12.mp4" "video_27.mp4")
  
  (println (sort (copy file_names) (lambda ( str0 str1 ) (negativep (alphaNumCmp str0 str1)))))
  ;; > ("video_0.mp4" "video_1.mp4" "video_10.mp4" "video_12.mp4" "video_27.mp4" "video_111.mp4")
  )

Resolving shell variables and symlinks in paths

Instead of playing with getShellEnvVar, strcat and sprintf.
simplifyFilename is the right function to expand shell variables in paths.
It works like Unix realpath (or readlink -f).

;; Return the path of the SKILL interpreter utility
(simplifyFilename "$CDS_INST_DIR/tools.lnx86/dfII/bin/skill")

Geometry

Use nearlyEqual

Warning

When working with floating-point numbers, which happens often when working in Layout or when calculating geometrical values. You cannot properly rely on equal:

(let ( ( num 0.0 )
       )
  (for _ 1 1000000
    num += 0.0000001
    )

  (printf " num: %N\n (equal 0.1 num): %N\n (nearlyEqual 0.1 num): %N\n"
    num (equal 0.1 num) (nearlyEqual 0.1 num))
  ; > num: 0.1
  ; > (equal 0.1 num): nil
  ; > (nearlyEqual 0.1 num): t
  )

Even if num value is represented as 0.1, its actual value is extremely close, but still slightly different.

Tip

When working with floating-point numbers nearlyEqual is often the right function to compare numbers.

Use complex numbers

When working on geometrical functions, use complex numbers. They easily solve most problems regarding angles.

Function Description
complex Build a complex number, usually from x:y coordinates.
real Return the real part of a complex number (its x coordinate).
imag Return the imaginary part of a complex number (its y coordinate).
abs Return the modulus of a complex number.
phase Retunr the phase (or angle) of a complex number.

Here is a simple example of a function to round the corners of a rectangle:

(defun round_rectangle ( rect radius points "dnx" )
  "Round each corners of RECT using RADIUS and a number of POINTS."
  (assert (eq "rect" rect->objType) "round_rectangle - first argument should be a rectangle")
  (letseq ( ( pi      (acos -1)              )
            ( step    (pi/2.0)/(sub1 points) )
            ( new_pts ()                     )
            )
    (destructuringBind ( ( x0 y0 ) ( x1 y1 ) ) rect->bBox
      ;; Make sure that radius fits in rectangle
      (assert radius <= (x1-x0)/2.0 "round-rectangle - radius %N > half width %N"  radius (x1-x0)/2.)
      (assert radius <= (y1-y0)/2.0 "round-rectangle - radius %N > half height %N" radius (y1-y0)/2.)
      ;; Browse each corner
      ;; Each corner is represented by center of the circle used for rounding and the start angle
      (foreach tuple (list
                       (list (complex x1-radius y1-radius) 0      ) ; top-right
                       (list (complex x0+radius y1-radius) pi/2   ) ; top-left
                       (list (complex x0+radius y0+radius) pi     ) ; bottom-left
                       (list (complex x1-radius y0+radius) pi+pi/2) ; bottom-right
                       )
        (destructuringBind ( z angle ) tuple
          (for _ 1 points
            ;; Use complex exponential representation
            (push z+radius*(exp (complex 0 angle)) new_pts)
            (setq angle angle+step)
            ))
        ))
    ;; Transform rectangle using calculated points
    (dbConvertRectToPolygon rect)
    (setf rect->points (foreach mapcar z new_pts (real z):(imag z)))
    ))
    
;; The previous function is just an example, it is [almost] always better to use native functions
(defun round_rectangle ( rect radius points "dnx" )
  "Round each corners of RECT using RADIUS and a number of POINTS."
  (leModifyCorner rect '(t t t t) nil radius (sub1 points))
  )

Tricky errors that confuse beginners

sort is destructive

Be careful when using sort, it one of the very few destructive functiosn available in SKILL.
It is a tricky one because its desctructive behavior is implicit.
The output might be what you expect, but the input list is often messed up in the process...

Caution

;; The following code is not sorting the list but destructs it instead
(let ( ( dummy_list '( 12 27 42 3 2 1 101 ))
       )
  (sort dummy_list 'lessp)
  (println dummy_list)
  )

There are several solutions depending on the use-case:

  1. You actually want to modify the sorted list.

    (setq dummy_list (sort dummy_list 'lessp))
  2. You use the sorted list but need to keep the original intact.

    (println (sort (copy dummy_list) 'lessp))
  3. You are sorting a list which is generated on-the-fly, then you don't care about destroying it.

    (println (sort (setof num dummy_list (evenp num)) 'lessp))

quote or ' returns a fixed pointer

If you return a symbol or a list constructed using ', the value is actually an object at a fixed address in memory.
Here is an example to show how this can mess up your code:

Caution

;; The following function seems perfectly valid and harmless
(defun new_dpl ()
  "Build an empty disembodied property list [DPL] and return it."
  '( nil )
  )

;; Let's use it to create a DPL and set its properties
(let ( ( dpl (new_dpl) )
       )
  (setf dpl->a 12)
  (setf dpl->b 27)

  ;; This will print (nil b 27 a 12) as expected
  (println dpl)
  )

;; However the behavior of our function seems broken
(println (new_dpl))
; > (nil b 27 a 12)
;; The return value is not an empty DPL anymore!

;; What happened is that when evaluating '( nil ) the SKIL interpreter built a list
;; and stored its pointer directly inside the function

;; You can confirm this behavior with the following statement
;; (You will see the updated DPL directly inside the function code)
(pp new_dpl)

Tip

Instead of constructing list using ', prefer using list.
This will reconstruct the list everytime the function is called and avoid unwillingly caching values.

;; This is a much safer and valid approach
(defun new_dpl ()
  "Build an empty disembodied property list [DPL] and return it."
  (list nil)
  )

;; Everytime `new_dpl` is called, it will construct a new list containing nil and return nil
;; This will avoid unwanted behaviors

;; You can check it with the following statement
(equal (new_dpl) (new_dpl))
; > t
(eq (new_dpl) (new_dpl))
; > nil

eq returns nil, this means that even if objects have the same value (as stated by equal), they are different.
The previous implementation using '( nil ) instead of (list nil) will return t for both equal and eq.

nil as a symbol

The SKILL Language user guide states that:

Both nil and t always evaluate to themselves and must never be used as the name of a variable.
-- Cadence SKILL Language User Guide IC25.1 - Atoms

I fully agree that they must never be used as the name of a variable.
But the first part stating that nil always evaluate to itself is not completely true...

This is very unlikely, but you can obtain nil as a symbol.
This can lead to unexpected behavior:

(let ( ( nil_var (concat "nil") )
       )
  (println nil_var)
  ; > nil
  (eq nil_var nil)
  ; > nil
  (equal nil_var nil)
  ; > nil
  )

As stated before, this should not happen in regular code.
But you might encounter issues due to this if you read a string from a user input and use concat on it.

rexMagic is a vey nasty switch

If you want to mess with a Virtuoso session, you can type:

(rexMagic nil)

This will break things like rexMatchp, rexReplace which is expected.
But it will also subtly break unexpected functions like pcreSubstitute or simplifyFilename.

Warning

(defun split_email ( email )
  "Split EMAIL string into first name, last name and website.
Return nil when EMAIL cannot be parsed properly."
  (when (pcreMatchp "([^.@]+)\\.?(.*?)@(.+)" email)
    (mapcar 'pcreSubstitute '( "\\1" "\\2" "\\3" ))
    ))
    
(rexMagic t)
(println (split_email "[email protected]"))
; > ("first_name" "last_name" "example.com")

(rexMagic nil)
(println (split_email "[email protected]"))
; > ("\\1" "\\2" "\\3")

Tip

You should always set rexMagic when using one of the functions mentioned above.
Here is a clean way to do so:

(defun split_email ( email )
  "Split EMAIL string into first name, last name and website.
Return nil when EMAIL cannot be parsed properly."
  ;; Properly set and re-set `rexMagic`
  (let ( ( magic (rexMagic) )
         )
    (unwindProtect
      (progn
        (rexMagic t)
        (when (pcreMatchp "([^.@]+)\\.?(.*?)@(.+)" email)
          (mapcar 'pcreSubstitute '( "\\1" "\\2" "\\3" ))
          ))
      (rexMagic magic)
      ))
  )

Debugging Advice

Regarding all other errors, reading the following article is strongly advised:
Debugging Virtuoso Session – Basic Tips and Frequently Asked Questions

Any CAD engineer supporting Virtuoso should be familiar with it.

Advanced SKILL & SKILL++

Advanced mapping functions

Mapping functions are very useful, you probably use mapc often without realising it.

(foreach elt '( 1 2 3 ) (println elt))
;; is completely equivalent to
(mapc (lambda (elt) (println elt)) '( 1 2 3 ))

But mapc is only one of six useful statements.
To better understand them, let's define the following function:

(defun print_and_return_twice (object)
  "Print OBJECT value and return it twice in a list."
  (println object)
  (list object object)
  )

We can now try the following six mapping functions:

(mapc    'print_and_return_twice '( 1 2 3 ))
(mapcar  'print_and_return_twice '( 1 2 3 ))
(mapcan  'print_and_return_twice '( 1 2 3 ))
(map     'print_and_return_twice '( 1 2 3 ))
(maplist 'print_and_return_twice '( 1 2 3 ))
(mapcon  'print_and_return_twice '( 1 2 3 ))

We get the following results:

Function Printed values Returned values Description
mapc 1 2 3 (1 2 3) Browse elements, return input
mapcar 1 2 3 ((1 1) (2 2) (3 3)) Browse elements, return output
mapcan 1 2 3 (1 1 2 2 3 3) Browse elements, return concatenated output
map (1 2 3) (2 3) (3) (1 2 3) Browse sublists, return input
maplist (1 2 3) (2 3) (3) ((1 1) (2 2) (3 3)) Browse sublists, return output
mapcon (1 2 3) (2 3) (3) (1 1 2 2 3 3) Browse sublists, return concatenated output

It can be summarized in the following table:

Browse \ Return Input Output Concatenated Output
Elements mapc mapcar mapcan
Sublists map maplist mapcon

Those mappings can be used within foreach thanks to the following unfamiliar syntax:

(foreach <mapping_function> <var> <list> <body>...)

Tip

foreach mapcar is the more intuitive function.
It is very useful to get the results of a loop in a list.

(defun table_elements ( table "o" )
  "Return the list of elements contained inside TABLE."
  (foreach mapcar key table[?] table[key])
  )

Tip

foreach mapcan is also very useful.
You can use it to filter elements like setof but return values different from the browsed elements.

(defun even_values ( table )
  "Return all the even values contained in TABLE."
  (foreach mapcan key table[?]
    (let ( ( val table[key] )
           )
       (when (evenp val)
         (list val)
         ))))

Tip

foreach can take multiple variables as first argument. It allows to browse several lists in parallel.
WARNING: All the lists should have the same length.

This is especially useful to browse a list of elements two by two:

(let ( ( polygon (car (exists shape (geGetEditCellView)->shapes)) )
       )
  ;; Print polygon edges
  (foreach ( pt0 pt1 ) polygon->points (cdr polygon->points)
    (printf "%N %N\n" pt0 pt1)
    ))

This can also be done using foreach map.

(let ( ( polygon (car (exists shape (geGetEditCellView)->shapes)) )
       )
  ;; Print polygon edges
  (foreach map pts polygon->points
    (let ( ( pt0 (car  pts) )
           ( pt1 (cadr pts) )
           )
      (when pt1
        (printf "%N %N\n" pt0 pt1)
        ))
    ))

Use @key and @rest arguments

If you want your code to be easier to maintain in the future.
You should define [almost] all your global functions using only @key and @rest arguments.

If you need to introduce another argument afterwards, or take in account one that you might support,
you will be thankful to have used @key and @rest arguments.

Use _backquote `

_backquote is a macro used to simplify the creation of lists where some elements are evaluated but others are not.

_backquote or (`) works like quote (or '), except that you can use _comma (or ,) and _commaAt (or ,@) to evaluate certain parts.

;; Here is a dummy example to show how `_backquote` can be used to build a DPL.
(defun build_dpl ( a b @rest args )
  "Build and return a DPL containing A, B and ARGS."
  `( nil
     a    ,a
     b    ,b
     a_b  ( ,a ,b )
     args ,@args
     ))
     
;; It is equivalent to nested calls of `list`, `quote` and `constar`.
(defun build_dpl ( a b @rest args )
  (constar nil 
    'a   a 
    'b   b 
    'a_b (list a b) 
    'args args
    ))

_backquote ` is very useful to Define macros.

Define macros

Macros have a steep learning curve but they are extremely useful if you want to manipulate unevaluated code or improve the syntax of statements. It the proper way to address design patterns.

wrap example

For instance, unwindProtect styntax is heavy and prone to error.
This is due to the closing form being placed at the end of the statement.

Here is a different approach where the opening and closing statements are grouped together.
(It is inspired by Python's with context manager.)

(defmacro wrap ( in out @rest body )
  "Wrapper to group IN and OUT statements inside `unwindProtect`."
  `(unwindProtect (progn ,in ,@body) ,out)
  )

Thanks to wrap redefine an improved split_email (the one, which was created in rexMagic is a vey nasty switch) :

(defun split_email ( email )
  "Split EMAIL string into first name, last name and website.
Return nil when EMAIL cannot be parsed properly."
  ;; Properly set and re-set `rexMagic`
  (let ( ( magic (rexMagic) )
         )
    (wrap (rexMagic t    )
          (rexMagic magic)
      ;; Inside `wrap` we guarantee `rexMagic` value
      (when (pcreMatchp "([^.@]+)\\.?(.*?)@(.+)" email)
        (mapcar 'pcreSubstitute '( "\\1" "\\2" "\\3" ))
        ))
    ))

Use expandMacro' and expandMacroDeep' to test your macros

When writing macros, or testing some code, expandMacro and expandMacroDeep are very useful to understand what is going on under the hood.

Use tconc structures

Lisp is meant to manipulate lists. It is fine in most cases. But sometimes you need to add elements at the tail of the list instead of the head.

(defun enum ( n "x" )
  "Return the list of integers between 0 and N-1 (included)."
  (assert (plusp n) "enum - n should be a positive integer: %N" n)
  (let ( ( res (tconc nil nil) )
         )
    (for i 0 (sub1 n)
      (tconc res i)
      )
    ;; This is required to convert `tconc` structure into the list of concatenated numbers
    (cdar res)
    ))

Use SKILL++

SKILL++ is actually safer and simpler to use than SKILL.
Although the opposite is not true (because SKILL++ offers more capabilities than SKILL),
most SKILL scripts are valid in SKILL++.

The only cases were SKILL scripts are not valid in SKILL++ are the ones using global variables.

To use SKILL++, you can use the statement inScheme. Or simply rename your files with .ils or .scm prefix.

Functions

In SKILL++, functions are atoms (like strings, symbols, integers, ...). They can be assigned to variables and behave like other objects.

Note

In SKILL, functions have a dedicated namespace.
You can have a function called var but also a variable named var and both will live in parallel.

In Scheme, there is only one namespace for both, this is less prone to errors as one symbol can only contain one value.

This implies that functions can be local, this is actually the default behavior. You can use putd, defglobalfun or globalProc to define global functions.

(let ()

  ;; This function is local, it cannot be accessed outside the parent `let`
  (defun factorial_rec ( n acc )
    "Tail-recursive helper for `factorial`"
    (if (plusp n)
      (factorial_rec n-1 (times n acc))
      acc))
      
  ;; This function is global, it can be used everywhere
  (defglobalfun factorial ( n "x" )
    "Return N factorial (N!)."
    (let ( ( tail_call_opt (status optimizeTailCall) )
           )
      (sstatus optimizeTailCall t)
      (unwindProtect
        (factorial_rec n 1)
        (sstatus optimizeTailCall tail_call_opt)
        )
      ))

  )

Scoping

The main difference between SKILL & SKILL++ is the scoping.

SKILL scoping is dynamic, it means variable are evaluated in the current environment.

;; This is SKILL
(inSkill
  (let ( ( custom_var 12 )
         )
    (defglobalfun get_custom_var ()
      "Return then value of custom_var."
      custom_var
      )
    )
  
  (let ( ( custom_var 27 )
         )
    (println (get_custom_var))
    ; > 27
    )
    
  (errset (println (get_custom_var)) t)
  ; ERROR> unbound variable - custom_var
  )

SKILL++ scoping is lexical, variables are evaluated in the environment they were defined in.

;; This is SKILL++
(inScheme
  (let ( ( custom_var 12 )
         )
    (defglobalfun get_custom_var ()
      "Return then value of custom_var."
      custom_var
      )
    )
  
  (let ( ( custom_var 27 )
         )
    (println (get_custom_var))
    ; > 12
    )
    
  (errset (println (get_custom_var)) t)
  ; > 12
  )

SKILL++ is more reliable, the things you define are not context-dependent.

Closures

In SKILL++, you can write closures. I.e. functions in a restricted lexical environment (see scoping above).

It is possible to hide functions and variables that are only shared within a restricted environment:

(inScheme
  (let ( ( counter 0 )
         )
  
    (defglobalfun get_counter ()
      "Return hidden counter."
      counter
      )
      
    (defglobalfun increment_counter ( @optional ( step 1 ) "n" )
      "Increment hidden counter by STEP."
      (setq counter (plus counter step))
      )
      
    (defglobalfun decrement_counter ( @optional ( step 1 ) "n" )
      "Decrement hidden counter by STEP."
      (setq counter (difference counter step))
      )

  ))

Sequential definitions of arguments

In SKILL++ arguments are defined one by one like letseq would do.

This means that argument default values can depend on the previous arguments.

;; Dummy example of function which can be used either by providing
;; ?source_cv or ?source_lib ?source_cell and ?source_view
(defun copy_cellview
  ( @key
    ( source_cv   (geGetEditCellView) )
    ( source_lib  source_cv->libName  )
    ( source_cell source_cv->cellName )
    ( source_view source_cv->viewName )
    ( target_lib  target_cv->libName  )
    ( target_cell target_cv->cellName )
    ( target_view target_cv->viewName )
    @rest _ )
  "Copy SOURCE_LIB/SOURCE_CELL/SOURCE_VIEW to TARGET_LIB/TARGET_CELL/TARGET_VIEW."
  (assert (or (nequal source_lib  target_lib )
              (nequal source_cell target_cell)
              (nequal source_view target_view)
              )
    "Source and target are identical: %N/%N/%N" source_lib source_cell source_view)
  (let ( ( cv (dbOpenCellViewByType source_lib source_cell source_view) )
         )
    (unwindProtect
      (dbCopyCellView cv target_lib target_cell target_view)
      (dbClose cv)
      )
    ))

About

Cadence SKILL/SKILL++ coding advice.

Topics

Resources

Stars

Watchers

Forks