The following pieces of advice are not specific to SKILL in particular, but I believe, it is always good to remind them.
- Less code less errors.
(i.e. from a CAD engineer point-of-view: less code less support!) - Keep it simple.
- Be consistent.
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
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.
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.
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!
Avoid global variables, it makes your code context dependent.
(At least prefix them, so they have less chances to be overwritten.)
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.
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!
As much as possible, try to not reinvent the wheel.
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.
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.
The following advice are low hanging fruits but can drastically improve code quality and robustness.
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.
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
))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`.
);letCaution
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])
)
);letA 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])
)
);letNested if calls are hard to understand. Ther are also complicated to maintain.
Here are a few solutions to avoid them:
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) )
))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.
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...>)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.
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)
)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))
))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)
)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)
)
))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
Here are some useful statements that are worth knowing while coding in SKILL or SKILL++.
| 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 functions behave like Unix printf utility:
lsprintfprintffprintfinfowarnerrorsprintf
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)
))
)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.
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)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"))"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)To fully exploit User Interfaces creation in SKILL, you should have a look at the following functions:
hiCreateFormLayouthiCreateLayoutFormhiCreateVerticalBoxLayouthiCreateHorizontalBoxLayouthiCreate...Field(All field creation functions)
You can also create interactive Library, Cell, View fields using:
ddHiCreateLibraryComboFieldddHiCreateCellComboFieldddHiCreateViewComboFieldddHiLinkFields
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.
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.
pcreCompilepcreMatchppcreReplacepcreSubstitutepcreGenCompileOptBits
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")
)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")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.
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))
)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:
-
You actually want to modify the sorted list.
(setq dummy_list (sort dummy_list 'lessp)) -
You use the sorted list but need to keep the original intact.
(println (sort (copy dummy_list) 'lessp)) -
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))
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))
; > nileq 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.
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.
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)
))
)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.
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)
))
))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.
_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.
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.
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" ))
))
))When writing macros, or testing some code, expandMacro and expandMacroDeep are
very useful to understand what is going on under the hood.
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)
))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.
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)
)
))
)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.
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))
)
))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)
)
))