Fundamentals of Perfect Developer - PowerPoint PPT Presentation

About This Presentation
Title:

Fundamentals of Perfect Developer

Description:

head tail front back append(x) prepend(x) (s) # (x)in. take(n) drop(n) slice(offset, length) ... reverse(s.front).prepend(s.last) Indicates that T can be any ... – PowerPoint PPT presentation

Number of Views:64
Avg rating:3.0/5.0
Slides: 79
Provided by: davidc144
Category:

less

Transcript and Presenter's Notes

Title: Fundamentals of Perfect Developer


1
Fundamentals of Perfect Developer
  • A one-day hands-on tutorial

2
Outline
  • Session 1 Getting Started
  • Configuring Perfect Developer and creating a
    project
  • Expressions, types, constants, functions,
    properties
  • Classes, data, invariants, functions, schemas,
    constructors
  • Session 2 Going Further
  • Interfacing to a Java front-end
  • Sequences and recursion
  • Session 3 Refining methods and data
  • Statement lists, statement types, loops, nested
    refinement.
  • Internal data, retrieve functions
  • Session 4 Inheritance
  • Derived and deferred classes
  • Defining and redefining inherited methods

3
Configuration
  • Configure Perfect Developer to use the chosen
    editor
  • Load the Project Manager
  • Select OptionsEditor
  • Browse to the editor executable file
  • Select the editor type
  • Configure the editor to recognise Perfect
    Developer files
  • See instructions in the Editor Customizations
    folder of the Perfect Developer installation

4
Creating a project
  • Click on the New toolbar icon
  • Browse to a writable folder, enter a project
    name, click Save and then OK
  • Create a new file called Examples
  • Add text property (a, b, c int)
    pre a lt b, b lt c assert a lt c
  • Save the file
  • Save the project and click the Verify tool

5
Some predefined classes
  • bool
  • char
  • int
  • real
  • set of X
  • bag of X
  • seq of X
  • map of (X -gt Y)

Set, bag, sequence and map types are finite
collections. See Quick Reference or Language
Reference Manual for more details (e.g.
commonly-used members)
6
Expressions
  • All the usual arithmetic operators
  • a lt b lt c means what you would expect it to
  • a / b (integer) and a b have precondition
    b gt 0
  • a / b (integer) rounds towards -?
  • a .. b yields the sequence a, a1, a2 b
  • c yields the cardinality or length of the
    collection c
  • a c yields the number of occurrences of a in c

7
Booleans, Conditionals etc.
  • Boolean connectives
  • gt lt ltgt
  • Conditional expression
  • ( a gt 0 a, b gt 0 b, 0)
  • Let-declaration
  • ( let t a - b t t )
  • Embedded assertion
  • ( assert a gt 0, b gt 0 a / b )
  • The whole shebang
  • ( let t a - b assert t gt 0 t gt 10 1,
    10 / t )

8
Constructor calls
  • A constructor call yields a value of the
    specified type
  • Without parameters
  • seq of int
  • MyClass
  • With parameters
  • seq of inta, b, c
  • MyClass42
  • Some constructors have preconditions that must be
    met
  • ints is ok when s "123" but not when s
    "lemon" !
  • precondition is s.empty (forall cs
    - c.isDigit)

9
Quantifiers etc.
  • If c is of type set of T, bag of T or seq of
    T,and p(x) is any Boolean expression involving
    x
  • Expression Return type
  • forall xc - p(x) bool
  • forall x T - p(x)
  • exists xc - p(x) bool
  • exists x T - p(x)
  • that xc - p(x) T
  • any xc - p(x)
  • those xc - p(x) set / seq / bag of T
  • for xc yield v(x) set / seq / bag of type of
    v
  • for those xc - p(x) yield v(x)

10
Declaring constants and functions
  • Declaring a constant
  • const four int 2 2
  • const smallPrimes seq of int
  • those x2..100 - (exists
    y2..ltx - x y 0)
  • const two 2
  • Declaring a function
  • function half(x int) int
  • pre x gt 0 x / 2

Type can be omitted in simple cases
Precondition (if needed)
11
Declaring properties
  • Use property declarations to express theorems
    property
  • assert half(four) two
  • property (x int)
  • pre x gt 0,
  • x 2 0
  • assert half(x) lt x,
  • (let h half(x) h h x)

Implicit universal quantification over the
parameters
Givens to be assumed
Consequences to be proved
12
Exercise 1 express the following
  • All the integers from 0 to 100 inclusive, in
    ascending order. Verify that your solution
    contains 42 but does not contain 101.
  • The integer j (which is known to be greater than
    0) divides the integer i exactly.
  • The squares of all the prime numbers from 2 to
    100 inclusive.
  • The highest integer in a set S of integers that
    is known to be non-empty

13
Declaring enumerations
class Currency enum unspecified, euro,
pound, USdollarendconst localCurrency
pound_at_Currency localCurrency.toString
14
Declaring a Class
class Money
Variables declared here form the abstract data
model of the class Invariants here constrain the
data These methods and constructors are for use
by confined and/or interface methods and
constructors Access redeclarations allow abstract
variables to be directly accessed from outside
the class These methods and constructors can be
called from outside the class Only the interface
section is mandatory
abstract
variables, invariants, methods, constructors
internal
never mind for now
confined
never mind for now
interface
access redeclarations, methods, constructors, prop
erties
end
15
Declaring data and invariants
Variable amt is of type int
Variable ccy is of type Currency
abstract var amt int, ccy Currency
invariant amt 0 ccy unspecified_at_Currency
Restriction on the values of amt and ccy
16
Functions and Operators
No () if no parameters
Name
Return type
function worthHaving bool amt gt
0function plus(m Money) Money pre m.ccy
ccy Currencyamt m.amt, ccy assert
result.ccy ccyoperator (n int) Money
Currencyamt n, ccy
Result expression
Optional precondition
Optional postassertion
Operator declarations are the same as function
declarations apart from the header
Use nonmember prefix for methods properties
with no self object
17
Declaring Schemas
Parameter is modified
No self object
No () if no parameters
nonmember schema swap(a!, b! Money) pre a.ccy
b.ccy post change a,b satisfy ab, ba
assert a.plus(b) b.plus(a) schema
!inflate(howMuch int) pre 0 lt howMuch lt 200
post amt! (amt howMuch)/100
Postcondition includes frame
This one modifies instance variables
Short for change amt satisfy amt (amt
howMuch)/100
18
Declaring Constructors
Note parameter list in
Postcondition must initialise all instance
variables
builda int, c Currency pre a gt 0 post
amt! a, ccy! cbuild!amt int post ccy!
euro_at_Currencybuild Money 0,
unspecified_at_Currency
Initialise instance variable directly from the
parameter
Short forchange amt satisfy amt a
We do use if no parameters
This constructor is defined in terms of another
one
except for variables whose when-guards are
false
19
Using access redeclarations
  • abstract variables may be redeclared in the
    interface
  • function v1 makes v1 readable
  • selector v2 makes v2 readable and writable
  • Making a variable writable is generally a bad
    idea
  • Except for convenience classes, e.g. class pair
  • Making a variable of a complicated type readable
    is generally a bad idea
  • Because we cant then change its representation
    easily
  • Constants may be redeclared as nonmember
    functions
  • Use access redeclarations sparingly!

20
Exercise 2 Specification(followed by coffee
break)
  • You have been provided with a Perfect
    specification of class Money
  • Try to verify it (there will be 3 verification
    errors)
  • Fix the specification to remove the verification
    errors
  • Verify that multiplying a Money object by 2 is
    equivalent to adding it to itself
  • Declare a operator that works like the plus
    function except that if the amount of either
    operand is zero, we dont care what the
    corresponding currency is

21
Using Perfect with a graphical UI
Java front-end
Perfect back-end
class MyAppimplements ActionListener
class Application interface
constructor calls
build
MyApp()
when pressed calls
function f ()
Button 1
when pressed calls
schema ! s ()
Button 2
Best to avoid preconditionsin methods called
from Java!
22
Using Perfect with a graphical UI
  • Declare a Perfect class Application
  • Declare interface functions/schemas to call from
    Java
  • Declare an interface constructor to call from
    Java
  • In the graphical interface code
  • Import Application.java
  • Instantiate a single Application object during
    initialisation
  • Call member functions/schemas when buttons are
    pressed
  • Convert parameter types as necessary
  • We have provided you with a sample
  • In file TutorialExample.java

23
Exercise 3 Build the sample program
  • Open project TutorialExample.pdp
  • Verify the project
  • Click the Build tool icon
  • Check for error messages
  • Locate and run output\App.jar
  • Try making your own changes to Application.pd
  • e.g. print some of the expressions you wrote
    earlier
  • tips follow

24
Tips
  • To use constants and functions from Examples.pd
  • Add file Examples.pd to the project
  • Add to Application.pd import "Examples.pd"
  • You can convert any expression to a string
  • By calling .toString on it
  • To make your application robust
  • Verify your version of Application.pd
  • Dont add any preconditions to the methods called
    from Java!

25
Sequences and Strings
  • The standard collection types are
  • set of X, bag of X, seq of X (where X is any
    type you like)
  • string ? seq of char
  • Members of class seq of X include
  • head tail front back append(x) prepend(x)
    (s) (x)in
  • take(n) drop(n) slice(offset, length)
  • isndec isninc permndec permninc isOrdered(cp)
    !sort(cp)
  • findFirst(x) findLast(x)
  • Useful global functions include
  • flatten(ss seq of seq of X) interleave(ss, s)
  • See the Library Reference for details

26
Recursive and templated functions
Indicates that T can be any type
function reverse(s seq of class T) seq of T
decrease s ( s lt 1 s,
reverse(s.front).prepend(s.last)
)
Recursion variant
Recursive call
27
Recursion variants
  • General form is
  • decrease e1, e2, e3
  • e1, e2 are of int, bool, char or an enumeration
    type
  • The variant must decrease on each recursive call
  • Either e1 must decrease
  • Or e1 stays the same and e2 decreases
  • Or e1 and e2 stay the same and e3 decreases
  • Integer components must not go negative

28
Exercise 4 Sequences
  • Specify the following functions
  • numLeadingSpaces(s string) nat
  • returns the index of the first character in s
    that is not a space, or the length of s if it is
    all spaces
  • removeLeadingSpaces(s string) string
  • returns s with any leading spaces removed
  • firstWord(s string) string
  • returns the leading characters of s up to but not
    including the first space character in s
  • splitIntoWords(s string) seq of string
  • splits the sentence s into individual
    words(hint use recursion!)

29
Lunch break!
30
Refinement
  • There are three types of refinement in Perfect
  • Refining result expressions to statement lists
  • Refining postconditions to statement lists
  • Refining abstract data to implementation data
  • When you refine the abstract data of a class, you
    normally need to refine the method specifications
    as well
  • So we will start with refining specifications

31
Refining specifications
  • Specification refinement serves these purposes
  • To implement a specification where Perfect
    Developer fails to
  • To implement a specification more efficiently
  • To take account of data refinement in affected
    methods
  • You can refine these sorts of specification
  • Expressions that follow the symbol
  • Postconditions
  • To refine a specification, append to it
  • via statement-list end

32
Some simple refinements
function square(x int) int x2via value
xxend schema swap(a!, b! class X)post a! b,
b! avia let temp a a! b b!
tempend
value statement returns a value from the via..end
Semicolon separates and sequences the statements
A postcondition can be used as a statement
33
Nested Refinements
  • You can refine not just method specifications but
    also
  • Postcondition statements
  • Let-declarations in statement lists

function fourth(x int) int x4 via
let x2 x2 via value xx
end value x2x2 end
value yielded by the inner via..end
value yielded by the outer via..end
34
Types of Statement
  • Let-declaration
  • Assertion
  • Variable declaration
  • Postcondition
  • pass statement
  • If-statement
  • value and done statements
  • Loop statement
  • Block statement

Exactly the same as in bracketed expressions
Same as in postconditions
Omit the post keyword!
Does nothing
35
If-statement
Guard
if c in a..z isAletter! true
valid! true c in 0..9 isAletter!
false valid! true valid! false fi
Statement list
Optional else part
fi means the same as pass fi
36
value statement
function max(a,b,c class X) X satisfy result
gt a result gt b result gt c
(resulta resultb resultc) via
if a gt b value max(a, c)
value max(b, c) fi end
Every path in an expression refinement must end
at a value statement
37
done statement
schema max(a!,b,c class X) post change a
satisfy a gt a a gt b
a gt c (a a a b a c)
via if a gt b a! max(a, c)
done fi a! max(b, c) end
A postcondition refinement may contain one or
more done statements
Implicit done statement here
38
Loop statement
Start of loop statement
// Calculate ab var rslt int! 1 loop var
j nat! 0 change rslt keep rslt aj
until j b decrease b - j rslt! b, j!
1 end
Loop variable declaration
List of what the loop can change
Loop invariant list
Termination condition
Loop variant
Loop body
End of loop statement
39
Loop statement
  • loop
  • local variable declarations (optional)
  • change list (optional)
  • invariant
  • termination condition (optional)
  • variant
  • body statements
  • end
  • If no change list is given, only the local
    variables can be changed
  • If no termination condition is given, the loop
    terminates when the variant can decrease no more

40
Designing loop invariants
  • Variables in loop invariants may be primed or
    unprimed
  • Primed current values at the start of an
    iteration
  • Unprimed value before the loop started
  • The invariant is the only source of information
    about current values of changing variables
  • The state when the loop completes is given by
  • invariant until-part
  • The invariant should comprise
  • A generalisation of the state that the loop is
    meant to achieve
  • Additional constraints needed to make the
    invariant, until-part, variant and body
    well-formed

41
Example of invariant design
  • Given s seq of int we wish to achieve total
    over s
  • Generalise this to tot over s.take(j) for
    some loop counter j
  • When j s then the generalisation becomes the
    required state because s.take(s) s
  • This generalisation forms part of the invariant
  • But s.take(j) has precondition 0 lt j lt s
  • So we must either add this precondition as an
    earlier invariant
  • Or as a type constraint in the declaration of j

42
Loop example (incorrect)
var totl int! 0 loop var j int! 0
change totl keep totl over s.take(j)
until j s decrease s - j totl! sj,
j! 1 end
Problem! These expressions are not universally
well-formed
43
Loop example (version 1)
var totl int! 0 loop var j int! 0
change totl keep 0 lt jlt s, totl
over s.take(j) until j s decrease s -
j totl! sj, j! 1 end
Added this extra invariant at the start
This is now well-formed
This is also well-formed (provided the
until-condition is false)
44
Loop example (version 2)
Added this type constraint
var totl int! 0 loop var j (int in 0..s)!
0 change totl keep totl over s.take(j)
until j s decrease s - j totl!
sj, j! 1 end
This is now well-formed
This is also well-formed (provided the
until-condition is false)
45
Refining recursion to loops
function rev(s seq of int) seq of int
decrease s (s.empty s,
rev(s.tail).append(s.head)) via var rslt seq
of int! seq of int loop var j (nat in
0..s)! 0 change rslt keep rslt
rev(s.take(j)) until j s decrease s
- j rslt! rslt.prepend(sj), j! 1
end value rsltend
46
Refining recursion to loops
  • Is the preceding example correct?
  • Probably!
  • But Perfect Developer cannot verify it!
  • The definition builds the result from front to
    back
  • Using append
  • The implementation builds the result from back to
    front
  • Using prepend
  • They are equivalent only because of associativity
  • (a b) c a (b c)
  • reverse(x.tail).append(x.head)
    reverse(x.front).prepend(x.last)
  • To prove this we need an inductive prover!

47
Refining recursion to loops
function rev(s seq of int) seq of int
decrease s (s.empty s,
rev(s.tail).append(s.head)) via var rslt seq
of int! seq of int loop var j (nat in
0..s)! s change rslt keep rslt
rev(s.drop(j)) until j 0 decrease
j j! - 1, rslt! rslt.append(sj)
end value rsltend
48
Loops a summary
  • Getting the invariant correct is critical
  • It must describe the relationships between all
    variables changed by the loop (including the
    local loop variables)
  • Its main part is a generalisation of the desired
    state after the loop
  • When the until condition is satisfied, the
    generalisation must reduce to the desired state
  • You may also need to include constraints on
    variables
  • To make expressions in the loop well-formed
  • If refining a recursive definition, make sure
    that the loop builds the result in the same order
    as the definition

49
Exercise 5 Method refinement
  • Refine the following specifications
  • function min2(x, y int) intsatisfy result lt
    x, result lt y, result x
    result y
  • function findFirst(s seq of int, x int)
    intsatisfy 0 lt result lt s, result
    s sresult x, forall j0..ltresult
    - sj x
  • Function numLeadingSpaces from exercise 4
  • Function splitIntoWords from exercise 4

50
Data refinement
  • When designing a class, we should always use the
    simplest possible abstract data model
  • Avoid redundancy!
  • Dont be concerned with efficiency at this stage!
  • The methods are specified in terms of this model
  • This keeps the specifications simple!
  • The data should not be directly accessible from
    outside
  • So we can change the implementation of the data
    without changing the class interface
  • Except for very simple classes like pair

51
Data Refinement (contd)
  • Perfect supports two sorts of data refinement
  • Replacing abstract variables by internal
    variables
  • Use a retrieve function to indicate that a
    variable is replaced
  • Examples see Dictionary.pd and Queue.pd
    inC\Program Files\Escher Technologies\Perfect
    Developer\Examples\Refinement
  • Supplementing abstract variables by internal
    variables
  • The new internal data adds no new information
  • Declare internal invariants to specify the
    relationship
  • Example add an index to a data structure
  • The internal data may be changed even within a
    function

52
Data Refinement Example
  • We have a class that maintains a list of numbers
  • Its constructor creates an empty list
  • We provide a method to append a number to the
    list
  • We provide a method to return the sum of all the
    numbers in the list

53
List of integers class
function sum(s seq of int) int decrease s
(s.empty 0, sum(s.front)
s.last) class ListOfNumbers abstract var
list seq of int interface function list
build post list! seq of int
schema !addNumber(n int) post list!
list.append(n) function getSum int
sum(list) end
54
Data Refinement Example
  • Suppose that method sum is called frequently
  • Save time by caching the sum of the list

55
Refined list of integers class
class ListOfNumbers abstract var list seq
of int internal var totl int invariant
totl sum(list) interface function list
build post list! seq of int via
list! seq of int, totl! 0 end
56
Refined list of integers class
schema !addNumber(n int) post list!
list.append(n) via list!
list.append(n), totl! n end function
getSum int sum(list) via value
totl end end
57
Exercise 6 Data Refinement
  • Write a recursive function longest which, given
    a list of strings, returns the longest string in
    the list (or the empty string if the list is
    empty, or the latest one of several equal-length
    longest strings)
  • Write a class that maintains a list of strings.
    You should provide
  • A constructor, which sets the list to an empty
    list
  • A member schema to append a new string to the
    list
  • A member function to return the longest string
    in the list
  • Refine the class to make the implementation of
    the longest member function more efficient

58
Inheritance
  • When declaring a class you can inherit another
    class
  • Declare class UniversityMember
  • Then class Student inherits
    UniversityMember
  • And class StaffMember inherits
    UniversityMember
  • And class Professor inherits StaffMember
  • A derived class inherits the variables of its
    parent
  • But they are not normally visible in the
    derived class
  • A derived class inherits the methods of its
    parent
  • But only confined and interface members of the
    parent are visible

59
The confined section
  • The confined section behaves like the interface
    section
  • You can put the same types of declaration in it
  • i.e. Methods, operators, constructors, access
    redeclarations
  • Not constants, variables or invariants
  • But confined declarations are only visible within
    the current class and its descendents
  • Not to the public at large!
  • cf. protected in Java and C

60
Redefining methods
  • Functions, selectors, schemas and operators that
    are inherited from a parent class may be
    redefined
  • This must be indicated using the redefine keyword
  • The parameter and result types in the
    redefinition must be identical to those in the
    overridden function

61
Example of overriding
class UniversityMember abstract var
firstNames seq of string, lastName
string interface function getSalary Money
0 end class StaffMember inherits
UniversityMemberabstract var salary
Money interface redefine function getSalary
Money salary end
62
from types and Dynamic Binding
  • You may declare a variable (or parameter, or
    result) to be of type from C where C is a class
  • e.g. var member from UniversityMember
  • The variable may be assigned a value of type C or
    any of its descendants
  • So member may be assigned a value of type
    Student, Professor
  • from C actually means the union of all
    non-deferred classes in the set comprising C and
    its direct and indirect descendents
  • When calling a member function on such a
    variable, the redefinition appropriate to the
    actual type is called
  • e.g. the relevant version of getSalary

63
Deferred methods
  • You can also declare a method in a class deferred
  • The method is left undefined in that class
  • This avoids the risk of classes inheriting what
    may be an unsuitable definition
  • The class itself must also be flagged deferred
    and may not be instantiated
  • Descendent classes may define the method using
    the define keyword
  • Any descendent class that does not define it is
    also a deferred class

64
Example of deferred method
deferred class UniversityMember abstract var
firstNames seq of string, lastName
string interface deferred function getSalary
Money end class StaffMember inherits
UniversityMemberabstract var salary
Money interface define function getSalary
Money salary end
65
Final classes and methods
  • A method can be declared final to prevent it from
    being redefined in a descendent class
  • final function getSalary Money
  • You can also declare a method final when defining
    or redefining it
  • define final function getSalary Money
  • redefine final function getSalary Money
  • You can declare a class final to mean that no
    other class may inherit it
  • final class Professor

66
Some consequences
  • If D is a deferred class
  • var x D is illegal
  • But you can use var x from D
  • If F is a final class
  • var x from F is illegal
  • But you can use var x F
  • If C is a non-final class, f is a final method
    and g is a non-final method, and given var x
    from C
  • In x.f the prover can assume the full
    postcondition of f
  • In x.g the prover can assume only the
    postassertion of g

67
Preconditions and inheritance
  • When a method is inherited, by default the
    precondition is inherited too
  • You may override the inherited precondition by
    giving a new one in the method definition or
    redefinition
  • The new precondition must be satisfied whenever
    the old one would have been satisfied
  • i.e. you may only weaken the precondition
  • To get round this, have the precondition call a
    method that you can redefine

68
Inherited precondition example
  • Suppose we declare a deferred method isPaid in
    class UniversityMember
  • define this to return false for class Student,
    true for class StaffMember
  • Add precondition pre isPaid to method getSalary

69
Inherited precondition example
deferred class UniversityMember abstract var
firstNames seq of string, lastName
string interface deferred function isPaid
bool deferred function getSalary Money
pre isPaid end class StaffMember inherits
UniversityMemberabstract var salary
int interface define function isPaid bool
true define function getSalary Money
salary
70
Inherited precondition example
  • That worked OK for class StaffMember, but what
    about class Student?
  • Method getSalary can never be called on class
    Student because its precondition is always false
  • How should we declare it?

71
Absurd method example
deferred class UniversityMember abstract var
firstNames seq of string, lastName
string interface deferred function isPaid
bool deferred function getSalary Money
pre isPaid end class Student inherits
UniversityMemberinterface define function
isPaid bool false absurd function
getSalary
72
Absurd methods
  • Declaring a method absurd means that its
    precondition is always false
  • Repeat the parameter list of the method but not
    its return type
  • An absurd method declaration has these
    consequences
  • The method is defined or redefined such that
    calling it will raise an exception
  • A verification condition is generated (i.e. that
    the precondition is always false)
  • It avoids the Given false, so proof is trivial
    warnings you will otherwise see

73
Inheritance and postassertions
  • When defining or redefining an inherited method,
    by default the postassertion is inherited
  • You may override the postassertion by giving a
    new one
  • The old postassertion must be satisfied whenever
    the new one is
  • i.e. you may only strengthen the postassertion
  • You can also use assert , q
  • This means assert the inherited postassertion and
    then q

74
Inheritance tips
  • When using inheritance, declare methods final
    where possible
  • This is not necessary in leaf classes which are
    declared final
  • For non-final methods of non-final classes,
    postassertions are very important
  • Because the prover needs them when the methods
    are called on from types
  • Does not apply to defining methods like isPaid
  • Only use from types where you really need to
    allow different types at run-time

75
Inheritance exercises
  • Design a UniversityMember or Employee class
    hierarchy that reflects the properties and
    privileges of members of your organisation
  • Specify a family of shopping scanners, as
    outlined at the end of the Shopping Scanner
    worked example at
  • http//www.eschertech.com/teaching/scanner_exampl
    e.pdf

76
What we didnt cover
  • Lots!
  • after expressions
  • over expressions
  • Guarded variable declarations
  • Selectors
  • Members of classes set and bag
  • Declaring templated classes
  • Other library classes, e.g. map of (X -gt Y)
  • How to solve verification problems
  • Serialization
  • Declaring axioms

77
Further Reading
  • Perfect Developer 3.0 Language Reference Manual
  • Start -gt Programs -gt Perfect Developer -gt
    Documentation
  • Or click on the book tool in the Project Manager
  • Also available at www.eschertech.com
  • Online tutorial
  • via Support -gt Self-help section of the web site
  • Teaching materials
  • via Support -gt Self-help -gt Teaching materials

78
Thank you for participating!
Write a Comment
User Comments (0)
About PowerShow.com