Vous êtes sur la page 1sur 78

Fundamentals of Perfect

Developer
A one-day hands-on tutorial

www.escherte

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

www.escherte

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

www.escherte

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 < b, b < c
assert a < c;

Save the file


Save the project and click the Verify tool

www.escherte

Some predefined classes

bool

set of X

char

bag of X

int

seq of X

real

map of (X -> 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)
www.escherte

Expressions
All the usual arithmetic operators
a < b < c means what you would expect it to
a / b (integer) and a % b have precondition b > 0
a / b (integer) rounds towards -
a .. b yields the sequence {a, a+1, a+2 b}
#c yields the cardinality or length of the collection c
a # c yields the number of occurrences of a in c

www.escherte

Booleans, Conditionals etc.


Boolean connectives:
& | ==>

<==

<==>

Conditional expression:
( [a > 0]: a, [b > 0]: b, []: 0)

Let-declaration:
( let t ^= a - b; t * t )

Embedded assertion:
( assert a > 0, b > 0; a / b )

The whole shebang:


( let t ^= a - b; assert t > 0; [t > 10]: 1, []: 10 / t )

www.escherte

Constructor calls
A constructor call yields a value of the specified type
Without parameters:
seq of int{}
MyClass{}

With parameters:
seq of int{a, b, c}
MyClass{42}

Some constructors have preconditions that must be


met
int{s} is ok when s = "123" but not when s = "lemon" !
[precondition is: ~s.empty & (forall c::s :- c.isDigit)]

www.escherte

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 x::c :- p(x) bool


forall x: T :- p(x)
exists x::c :- p(x) bool
exists x: T :- p(x)
that x::c :- p(x)
any x::c :- p(x)

those x::c :- p(x) set / seq / bag of T


for x::c yield v(x)
set / seq / bag of type of v
for those x::c :- p(x) yield v(x)

www.escherte

Declaring constants and


functions
Declaring a constant:
const four: int ^= 2 + 2;
const smallPrimes: seq of int
^= those x::2..100
:- ~(exists y::2..<x :- x % y = 0);
const two ^= 2;
Type can be omitted in simple
cases

Declaring a function:

function half(x: int): int


pre x > 0
Precondition (if needed)
^= x / 2;

www.escherte

Declaring properties
Use property declarations to express theorems:
property
assert half(four) = two;
Implicit universal quantification over the
parameters
property (x: int)
Givens to be
pre x > 0,
assumed
x % 2 = 0
assert half(x) < x,
(let h ^= half(x); h + h = x);
Consequences to be
proved

www.escherte

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
www.escherte

Declaring enumerations
class Currency ^= enum
unspecified, euro, pound, USdollar
end;
const localCurrency ^= pound@Currency;
localCurrency.toString

www.escherte

Declaring a Class
class Money ^=
abstract variables,
invariants,
methods,
constructors

interna
l
confined
interface

end;

www.escherte

[never mind for now]


[never mind for now]
access redeclarations,
methods,
constructors,
properties

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

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@Currency;

Restriction on the values of amt and ccy


www.escherte

Functions and Operators


Nam
e

No () if no
parameters

function worthHaving: bool


^= amt > 0;
function plus(m: Money): Money
pre m.ccy = ccy
^= Currency{amt + m.amt, ccy}
assert result.ccy = ccy;
operator (n: int) * : Money
^= Currency{amt * n, ccy};

Return type

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

www.escherte

Declaring Schemas
No self object

No () if no
parameters

Parameter is modified

nonmember schema swap(a!, b!: Money)


pre a.ccy = b.ccy
post change a,b satisfy a=b, b=a
assert a.plus(b) = b.plus(a);
schema !inflate(howMuch: int)
pre 0 < howMuch < 200
post amt! = (amt * howMuch)/100;

Postcondition
includes
frame
This one
modifies
instance
variables

Short for: change amt satisfy amt= (amt * howMuch)/100

www.escherte

Declaring Constructors
Note parameter list in
{}
build{a: int, c: Currency}
pre a > 0
post amt! = a, ccy! = c;
build{!amt: int}
post ccy! = euro@Currency;
build{}
^= Money
{0,
unspecified@Currency};
This constructor is
defined in terms of
another one

www.escherte

Postcondition must
initialise all instance
variables*
Initialise instance
variable directly from
the parameter
Short for:
change amt
satisfy amt= a
We do use {} if
no parameters
*except for variables whose
when-guards are false

Using access redeclarations


abstract variables may be redeclared in the interface:
function v1;
selector v2;

makes v1 readable
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!

www.escherte

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

www.escherte

Using Perfect with a graphical


UI
Java front-end
class MyApp
implements
ActionListener
MyApp()
Button 1
Button 2

Perfect back-end

constructor calls
when pressed
calls
when pressed
calls

class Application
^=
interface
build{}
function f ()
schema ! s ()

Best to avoid preconditions


in methods called from Java!

www.escherte

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

www.escherte

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]
www.escherte

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!

www.escherte

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

www.escherte

Recursive and templated


functions
Indicates that T can be any
type
function reverse(s: seq of class T): seq of T
decrease #s
^= ( [#s <= 1]:
s,
[]:
reverse(s.front).prepend(s.last)
);
Recursion variant

www.escherte

Recursive call

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


www.escherte

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!)

www.escherte

Lunch break!

www.escherte

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

www.escherte

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
www.escherte

Some simple refinements


function square(x: int): int
^= x^2
via
value x*x
end;
schema swap(a!, b!: class X)
post a!= b, b!= a
via
let temp ^= a;
a! = b;
b! = temp
end;

www.escherte

value statement
returns a value from
the via..end
Semicolon separates
and sequences the
statements
A postcondition can
be used as a
statement

Nested Refinements
You can refine not just method specifications but
also:
Postcondition statements
Let-declarations in statement lists
function fourth(x: int): int
^= x^4
via
let x2 ^= x^2
via
value x*x
end;
value x2*x2
end;

www.escherte

value yielded by
the inner via..end

value yielded by
the outer via..end

Types of Statement
Let-declaration
Assertion

Exactly the same as


in bracketed
expressions

Variable declaration Same as in postconditions


Postcondition

Omit the post keyword!

pass statement

Does nothing

If-statement
value and done statements
Loop statement
Block statement
www.escherte

If-statement
if
[c in `a`..`z`]:
isAletter! = true;
valid! = true;

Guard

Statement list

[c in `0`..`9`]:
isAletter! = false;
valid! = true;
[]:
valid! = false
fi

www.escherte

Optional else part


[] fi means the same
as
[]: pass fi

value statement
function max(a,b,c: class X): X
satisfy result >= a
& result >= b
& result >= c
& (result=a | result=b |
result=c)
via
if
[a > b]:
value max(a, c);
[]:
value max(b, c)
fi
end;

www.escherte

Every path in
an expression
refinement
must end at a
value
statement

done statement
schema max(a!,b,c: class X)
post change a
satisfy a >= a
& a >= b
& a >= c
& (a= a | a= b | a= c)
via
if
[a > b]:
a!= max(a, c);
done;
[] fi;
a!= max(b, c)
end;

www.escherte

A postcondition
refinement may
contain one or
more done
statements
Implicit done
statement
here

Loop statement
// Calculate a^b
var rslt: int! = 1;
loop
var j: nat! = 0;
change rslt
keep rslt = a^j
until j= b
decrease b - j;
rslt! * b,
j! + 1
end;

Start of loop
statement
Loop variable
declaration
List of what the loop can
change
Loop invariant list
Termination
condition
Loop variant
Loop body

End of loop
statement

www.escherte

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

www.escherte

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, untilpart, variant and body well-formed

www.escherte

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 <= j <= #s
So we must either add this precondition as an earlier
invariant
Or as a type constraint in the declaration of j

www.escherte

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! + s[j],
j! + 1
end;

www.escherte

Problem! These
expressions are
not universally
well-formed

Loop example (version 1)


var totl: int! = 0;
loop
var j: int! = 0;
change totl
keep 0 <= j<= #s,
totl= + over s.take(j)
until j = #s
decrease #s - j;
totl! + s[j],
j! + 1
end;

www.escherte

Added this extra


invariant at the start
This is now wellformed

This is also wellformed (provided


the until-condition is
false)

Loop example (version 2)


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! + s[j],
j! + 1
end;

www.escherte

Added this type


constraint

This is now wellformed


This is also wellformed (provided
the until-condition is
false)

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(s[j]),
j! + 1
end;
value rslt
end;

www.escherte

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!

www.escherte

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(s[j])
end;
value rslt
end;

www.escherte

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

www.escherte

Exercise 5: Method refinement


Refine the following specifications
function min2(x,
satisfy result
result
result

y: int): int
<= x,
<= y,
= x | result = y;

function findFirst(s: seq of int, x: int): int


satisfy 0 <= result <= #s,
result = #s | s[result] = x,
forall j::0..<result :- s[j] ~= x;
Function numLeadingSpaces from exercise 4
Function splitIntoWords from exercise 4

www.escherte

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]

www.escherte

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 in
C:\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

www.escherte

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

www.escherte

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;

www.escherte

Data Refinement Example


Suppose that method sum is called frequently
Save time by caching the sum of the list

www.escherte

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;

www.escherte

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;

www.escherte

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

www.escherte

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

www.escherte

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++

www.escherte

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

www.escherte

Example of overriding
class UniversityMember ^=
abstract
var firstNames: seq of string, lastName: string;
interface
function getSalary: Money
^= 0;

end;
class StaffMember ^= inherits UniversityMember
abstract
var salary: Money;
interface
redefine function getSalary: Money
^= salary;

end;

www.escherte

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 [re]definition appropriate to the actual type is
called
e.g. the relevant version of getSalary

www.escherte

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

www.escherte

Example of deferred method


deferred class UniversityMember ^=
abstract
var firstNames: seq of string, lastName: string;
interface
deferred function getSalary: Money;

end;
class StaffMember ^= inherits UniversityMember
abstract
var salary: Money;
interface
define function getSalary: Money
^= salary;

end;

www.escherte

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 ^=

www.escherte

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

www.escherte

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
www.escherte

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

www.escherte

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 UniversityMember
abstract
var salary: int;
interface
define function isPaid: bool
^= true;
define function getSalary: Money
^= salary;

www.escherte

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?

www.escherte

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 UniversityMember
interface
define function isPaid: bool
^= false;
absurd function getSalary;

www.escherte

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

www.escherte

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

www.escherte

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
www.escherte

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_example.pdf

www.escherte

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 -> Y)
How to solve verification problems
Serialization
Declaring axioms

www.escherte

Further Reading
Perfect Developer 3.0 Language Reference
Manual
Start -> Programs -> Perfect Developer ->
Documentation
Or click on the book tool in the Project Manager
Also available at www.eschertech.com

Online tutorial
via Support -> Self-help section of the web site

Teaching materials
via Support -> Self-help -> Teaching materials

www.escherte

Thank you for participating!

www.escherte

Vous aimerez peut-être aussi