Để đảm bảo tính chính xác của kết quả phân tích, miền ngữ nghĩa trừu tượng phải thỏa mãn một số thuộc tính cơ bản sau đây [3]:
+ Tính đủ: tất cả các lỗi đều không được bỏ qua, nếu trong miền ngữ nghĩa cụ thể chứa thông tin về lỗi (giao với vùng vi phạm khác rỗng) thì miền ngữ nghĩa trừu tượng cũng phải chứa thông tin về lỗi đó; + Tính chính xác: quá trình trừu tượng hóa để tạo ra miền ngữ nghĩa
trừu tượng phải đủ chính xác để tránh cảnh báo sai (false alarm); + Đơn giản: Miền ngữ nghĩa trừu tượng phải đơn giản nhất có thể
(tránh hiện tượng bùng nổ tổ hợp)
Biểu diễn tính đủ trong hình 2.6: Miền ngữ nghĩa trừu tượng không đảm bảo tính đủ nên bỏ sót lỗi/miền trừu tượng vi phạm vùng cấm.
24
Hình 2.6: Trừu tượng hóa quĩ đạo hành vi không đảm bảo tính đủ
Biểu diễn tính chính xác trong hình 2.7: miền ngữ nghĩa trừu tượng không chính xác, vi phạm vào vùng cấm.
Hình 2.7: Lỗi trừu tượng hóa quĩ đạo hành vi không chính xác 2.3.7. Tiêu chuẩn trừu tượng hóa
Tiêu chuẩn trừu tượng hóa đóng vai trò như là một cơ sở cho việc thiết kế các phân tích tĩnh chương trình [3]:
+ Trừu tượng hóa dữ liệu chương trình.
+ Trừu tượng hóa các toán tử cơ bản chương trình.
+ Trừu tượng hóa các cấu trúc điều khiển chương trình như: cấu trúc rẽ nhánh, cấu trúc lặp, ...
25
Có thể được tham số hóa để cho phép thích nghi với miền ứng dụng.
Hình 2.8: Tiêu chuẩn trừu tượng hóa theo khoảng 2.4. Lý thuyết dàn
Thứ tự từng phần: Một tập thứ tự từng phần (Partially ordered set -
poset) là một cấu trúc toán học: ( , ⊑), với là một tập hợp và ⊑ là một quan hệ 2 ngôi trên thỏa mãn [11]:
+ Phản xạ (reflexivity): ∀ ∈ : ⊑
+ Phản đối xứng (anti-symmetry): ∀ , ∈ : ⊑ ∧ ⊑ ⇒ =
+ Bắc cầu (transitivity): ∀ , , ∈ : ⊑ ∧ ⊑ ⇒ ⊑
Nếu ( , ⊑) là một poset và ⊆ thì ( , ⊑) cũng là một poset.
Để biểu diễn poset ta sử dụng biểu đồ Hasse bằng cách sử dụng các đồ
thị định hướng với tập đỉnh chính là , tập cạnh và tập cung chính là các quan hệ 2 ngôi ⊑, nếu 〈 , 〉 ∈ ⊑ thì trên biểu đồ ta vẽ một cung hướng từ đỉnh
sang đỉnh y như trong hình 2.9 [3].
Cận trên: Cho ⊆ . Ta nói rằng ∈ là một cận trên của , kí hiệu
⊑ , nếu ta có ∀ ∈ : ⊑ .
Cận dưới: Tương tự ta cũng có ∈ được gọi là một cận dưới của , kí hiệu ⊑ , nếu ∀ ∈ ∶ ⊑ .
26
Cận trên nhỏ nhất và cận dưới lớn nhất: Cận trên nhỏ nhất của kí hiệu ⊔ , được định nghĩa bởi ⊑⊔ ∧ ∀ ∈ : ⊑ ⇒⊔ ⊑ . Tương tự ta có cận dưới lớn nhất kí hiệu là ⊓ , được định nghĩa bởi ⊓ ⊑ ∧
∀ ∈ : ⊑ ⇒ ⊑⊓ [2].
Định nghĩa 2.1: Dàn (Lattice) là một quan hệ thứ tự từng phần mà trong đó ⊔ và ⊓ tồn tại đối với tất cả ⊆ .
Một dàn được gọi là đầy đủ khi cận trên nhỏ nhất tồn tại [11].
Lưu ý rằng một dàn có duy nhất một phần tử lớn nhất ⊤ được định nghĩa là ⊤ =⊔ và duy nhất một phần tử nhỏ nhất ⊥ được định nghĩa là
⊥=⊓ (do tính chất phản xạ của quan hệ thứ tự từng phần). Ví dụ: ∀ ∈ ℤ: ⊥⊑⊥⊑ ⊑ ⊑ ⊤ ⊑ ⊤
Với mỗi tập hợp hữu hạn phần tử = {0, 1, 2, 3} định nghĩa một dàn
(2 , ⊆), trong đó ⊥= ∅, ⊤ = , ⊔ = ∪ , và ⊓ = ∩ (dàn đảo ngược (2 , ⊇) cũng được mô tả tượng tự) được mô tả trong Hình 2.9.
Hình 2.9: Biểu đồ Hasse của dàn (2 , ⊆)
Chiều cao của dàn: Chiều cao của một dàn được định nghĩa là độ dài đường đi dài nhất từ phần tử ⊥ tới phần tử ⊤. Ở ví dụ trên thì chiều cao của dàn là 4, nhìn chung thì ở các dàn (2 , ⊆) hoặc (2 , ⊇) có chiều cao chính là số phần tử | | [2].
27
Ngữ nghĩa của một ngôn ngữ lập trình sẽ xác định ngữ nghĩa của bất kỳ chương trình được viết trong ngôn ngữ đó. Ngữ nghĩa của một chương trình cung cấp một cách hình thức hóa mô hình toán học của tất cả các hành vi có thể của một hệ thống máy tính thực hiện chương trình này, trong quá trình tương tác với bất kỳ môi trường có thể.
Trong phần tiếp theo, chúng ta sẽ tiếp tục giải thích tại sao hình thức hóa ngữ nghĩa của một chương trình có thể được xác định là việc giải một hệ phương trình điểm cố định. Tiếp theo, để so sánh ngữ nghĩa, chúng ta sẽ thấy rằng tất cả các ngữ nghĩa của một chương trình có thể được tổ chức trong một hệ thống phân cấp bằng kỹ thuật trừu tượng hóa. Bằng cách quan sát các tính toán ở mức độ trừu tượng khác nhau, người ta có thể xấp xỉ điểm cố định
(approximate fixpoints) từ đó tổ chức các ngữ nghĩa của một chương trình
trên một dàn [7].
Ví dụ: Cho đoạn mã chương trình như sau int func(){ int x,y,z,a,b; z = a+b; y = a*b; while (y > a+b) { a = a+1; x = a+b; } }
Khi đó ta có 4 biểu thức khác nhau { + , ∗ , > + , + 1}, do đó biểu diễn các toán tử của chương trình trên dàn được mô tả như sau:
28
Hình 2.10: Biểu diễn ngữ nghĩa của một chương trình thành dàn 2.5. Lý thuyết điểm cố định
2.5.1. Điểm cố định
Một hàm : → là hàm đơn điệu khi ∀ , ∈ : ⊑ ⇒ ( ) ⊑ ( ). Lưu ý rằng với thuộc tích này không bao gồm là một hàm đồng biến (với mọi ∀ ∈ : ⊆ ( )). Ví dụ, tất cả các hàm hằng số là đơn điệu, hợp thành của hai hàm đơn điệu là một hàm đơn điệu
Định nghĩa 2.2 (điểm cố định): Cho một dàn L với chiều cao hữu hạn, x là một điểm cố định của thỏa mãn f(x)=x, mỗi hàm đơn điệu f có duy nhất
một điểm cố định nhỏ nhất (least fixed-point) định nghĩa là:
( ) = ⨆ (⊥) thỏa mãn điều kiện ( ) = ( )
Chứng minh: Chứng minh này là khá đơn giản
Nhận thấy rằng ⊥⊑ (⊥) do ⊥ là phần tử nhỏ nhất.
Do là hàm đơn điệu nên ta suy ra rằng (⊥) ⊑ (⊥) và bằng phương pháp quy nạp ta có: (⊥) ⊑ (⊥).
Vì thế, ta có chuỗi tăng: ⊥⊑ (⊥) ⊑ (⊥) …
Do dàn được giả định có chiều cao hữu hạn nên sẽ tồn tại sao cho
(⊥) = (⊥). Ta định nghĩa ( ) = (⊥) và từ ( ( )) =
29
Bây giờ ta giả sử là một điểm cố định khác. Từ đó ⊥⊑ ta suy ra rằng (⊥) ⊑ ( ) = . Vì là hàm đơn điệu và bằng quy nạp ta có
( ) = (⊥) ⊑ . Do đó, ( ) là điểm cố định nhỏ nhất, do tính chất phản đối xứng trên dàn nên nó là duy nhất [2].
Định lý Knaster-Tarski: Nếu : ⎯ là đơn điệu và là một dàn đầy đủ, các tập tất cả điểm cố định của cũng là một dàn đầy đủ [3].
2.5.2. Mở rộng ký hiệu điểm cố định + Ký hiệu: ( ) là điểm cố định nhỏ nhất, ta có ∀ ∈ : ( ( ) = + Ký hiệu: ( ) là điểm cố định nhỏ nhất, ta có ∀ ∈ : ( ( ) = ) ⟹ ( ( ) ⊑ ), ( ) là điểm cố định lớn nhất ∀ ∈ : ( ( ) = ) ⟹ ( ( ) ⊒ ). + Tập hợp các fixpoint: ( ) = {∀ ∈ | ( ) = } + Tập hợp các tiền fixpoint: ( ) = {∀ ∈ | ⊑ ( )} + Tập hợp các hậu fixpoint: ( ) = {∀ ∈ | ⊒ ( )} 2.5.3. Bước lặp
Cho là một hàm trên tập , lặp của bắt đầu từ ∈ được định nghĩa như sau: ( ) = và ( ) = ( ( )) ớ ∈ ℕ
Do đó = ∘ ∘ ∘∘∘
ầ
và ta cũng có
= ∘ , ∘ = , = ×
Nếu là một tập hữu hạn ∀ > | |: ∃ ≤ | |: ( ) = ( ) thì hoặc có thể là điểm cố định, hoặc bước lặp tạo thành một vòng tròn.
Định lý Kleene: Nếu : ⎯ là đơn điệu, là một dàn đầy đủ và bảo toàn tất cả các cận trên nhỏ nhất thì là giới hạn của chuỗi [3]:
=⊥
= ( )
Ví dụ về bước lặp hội tụ đến điểm cố định của hàm = được mô tả trong Hình 2.11
30
Hình 2.11: Biểu diễn điểm cố định của =
Độ phức tạp về thời gian để tính toán một điểm cố định phụ thuộc vào 3 yếu tố sau đây:
+ Chiều cao của dàn, cung cấp một ràng buộc đối với biến số k; + Chi phí tính toán của hàm ;
+ Chi phí cho việc kiểm tra kết quả có bằng nhau không sau mỗi lần lặp. Tính toán một điểm cố định có thể được minh họa là các bước đi lên trong một dàn bắt đầu từ điểm ⊥ (Hình 2.12) [2].
Hình 2.12: Tính toán điểm cố định trên dàn
Thuộc tính đóng: Nếu , , . . . , là các dàn có chiều cao hữu hạn thì tích × × . . .× = {( , , . . . , )| ∈ } với ⊑ được định nghĩa theo từng điểm (Point-wise). Cùng với ⊔ và ⊓ có thể được tính theo từng điểm như sau:
31
ℎ ℎ ( × × . . .× ) = ℎ ℎ ( ) + ⋯ + ℎ ℎ ( )
Tương tự ta cũng có phép toán cộng
+ + . . . + = {( , ) | ∈ \{⊥, ⊤}} ∪ {⊥, ⊤}
với ⊤ và ⊥ cho trước và ( , ) ⊑ ( , ) nếu và chỉ nếu = ∧ ⊑ . Lưu ý rằng ℎ ℎ ( 1 + . . . + ) = {ℎ ℎ ( )}. Phép toán cộng được minh họa như hình dưới đây [2]:
Hình 2.13: Phép cộng dàn
Nếu dàn có chiều cao hữu hạn, khi đó ( ) (phép nâng dàn), được minh họa như hình dưới đây:
Hình 2.14: Phép nâng dàn Và ta có: ℎ ℎ ( ( )) = ℎ ℎ ( ) + 1 [2],
32
Hình 2.15: Dàn phẳng
( ) là một dàn có chiều cao là 2. Nếu và được định nghĩa như trên, khi đó ta thu được một dàn ánh xạ (map) với chiều cao hữu hạn theo thứ tự từng điểm:
⟼ = {[ ⟼ , . . . , ⟼ ]| ∈ }
⊑ ⇔ ∀ : ( ) ⊑ ( ). Nếu là tập hữu hạn và có chiều cao hữu hạn thì ℎ ℎ ( ⟼ ) = | |. ℎ ℎ ( ) [2]
2.5.4. Phương trình và bất phương trình điểm cố định
Điểm cố định là điều rất đáng quan tâm, vì nó cho chúng ta các mối
liên hệ đến hệ thống phương trình để giải quyết vấn đề. Cho là một dàn với chiều cao hữu hạn. Một hệ phương trình được biểu diễn:
= ( , . . . , ) = ( , . . . , ) … .
= ( , . . . , )
Với xi là các biến của : ⟼ là tập hợp các hàm đơn điệu. Với mỗi hệ chỉ có nghiệm nhỏ nhất duy nhất, nó chính là điểm cố định nhỏ nhất của hàm ∶ → được định nghĩa như sau:
33
Chúng ta giải quyết tương tự nhưng đối với hệ bất phương trình:
⊑ ( , . . . , ) ⊑ ( , . . . , ) … .
⊑ ( , . . . , )
Ta có mối quan hệ ⊑ là tương đương với = ⊓ . Vì vậy các nghiệm này được bảo toàn bởi các thể hiện lại trong hệ phương trình:
= ⊓ ( , . . . , ) = ⊓ ( , . . . , ) … .
= ⊓ ( , . . . , )
Đây là hệ phương trình với hàm đơn điệu như đã nêu ở phần trên [2]. 2.5.4. Đồ thị luồng điều khiển (Control Flow Graphs - CFG)
Phân tích kiểu được bắt đầu với các cây cú pháp của một chương trình và các ràng buộc được xác định qua các biến được gán cho các nút. Phân tích làm việc theo cách này là một luồng không phân biệt, nghĩa là các kết quả giống nhau nếu một chuỗi câu lệnh 1 2 được hoán vị thành 2 1. Các phân tích này sử dụng luồng phân biệt là một đồ thị luồng điều khiển, đó là một biểu diễn khác của mã nguồn chương trình [2].
Một đồ thị luồng điều khiển (CFG) là một đồ thị có hướng, trong đó các nút (node) tương ứng với điểm chương trình và cạnh biểu diễn cho luồng
điều khiển. Một CFG luôn có một điểm đơn đầu vào ký hiệu là entry, và một điểm đơn đầu ra, ký hiệu exit.
Nếu v là một nút trong một CFG thì ta ký hiệu pred(v) biểu diễn tập hợp các nút kề trước và succ(v) tập hợp các nút kế tiếp.
Dưới đây chúng ta xem xét các lệnh đơn giản mà CFG có thể được xây dựng. Các CFG cho các phép gán (id = E;), output (printf(E);), lệnh
34
rời khỏi hàm (return E;), và khai báo biến (float E;) được xem xét như sau:
Hình 2.16: CFG cho các lệnh đơn giản
Đới với chuỗi lệnh tuần tự S1S2, ta bỏ các nút exit của lệnh S1 và nút entry của lệnh S 2 và gộp 2 lệnh này với nhau ta được:
Hình 2.17: CFG cho cấu trúc tuần tự
Tương tự như vậy, các cấu trúc điều khiển cũng được mô hình hóa bằng CFG như sau:
Cấu trúc rẽ nhánh dạng khuyết (if E S1;), dạng đủ (if E S1
else S2;)
Hình 2.18: CFG cho cấu trúc rẽ nhánh id =E; printf(E); return E; float E;
S1 S2
S1 S2
35
Cấu trúc lặp cho kiểm tra điều kiện trước (while(E) S1;), kiểm tra điều kiện sau (do S1 while E;) và vòng lặp for(E1;E2;E3) S1;)
Hình 2.19: CFG cho cấu trúc lặp Hình 2.20: CFG của hàm tính ! E S1 E1 E2 S1 S1 S1 E
36
2.5.5. Phân tích luồng dữ liệu (Data Flow Analysis)
Phân tích luồng dữ liệu trong khuôn khổ các hàm đơn điệu là bắt đầu một đồ thị CFG và một dàn có chiều cao hữu hạn. Dàn có thể được cố định cho tất cả các chương trình hoặc nó có thể được tham số hóa với các chương trình đã được xác định.
Với mỗi nút v trong CFG, chúng ta gán một biến ⟦ ⟧ khoảng cách, trên các phần tử của . Đối với mỗi cấu trúc trong các ngôn ngữ lập trình, sau đó chúng ta định nghĩa một ràng buộc luồng dữ liệu có liên quan tới giá trị của các biến của các nút tương ứng với những nút khác (thường là những nút kề trước và kề sau).
Đối với cách suy luận như vậy, chúng ta sẽ mô hình hóa sử dụng ký hiệu ⟦ ⟧cho ⟦ ⟧ nếu S là cú pháp liên quan đến .
Đối với một CFG đầy đủ, chúng ta có thể trích xuất có hệ thống một tập các ràng buộc trên các biến. Nếu tất cả những ràng buộc này đưa ra được phương trình hoặc bất phương trình với vế phải là hàm đơn điệu, thì chúng ta có thể sử dụng các thuật toán về điểm cố định để tính toán các nghiệm duy nhất nhỏ nhất.
Các ràng buộc luồng dữ liệu là đúng nếu tất cả các nghiệm tương ứng với thông tin đúng đắn về chương trình. Các phân tích được bảo toàn từ các nghiệm có thể nhiều hơn hoặc độ không chính xác là rất ít, nhưng việc tính toán các các nghiệm nhỏ nhất sẽ làm tăng độ phức tạp tỷ lệ với chiều dài ngữ nghĩa tính toán [2].
2.5.6. Thuật toán tìm điểm cố định
Nếu một CFG có các nút = { 1, 2, . . . , }, khi đó chúng ta làm việc với dàn . Giả sử rằng nút sinh ra phương trình dòng dữ liệu
⟦ ⟧ = (⟦ ⟧, … ⟦ ⟧), Chúng ta xây dựng các hàm ∶ → như đã
mô tả sẽ dễ hơn:
37
Dưới đây là một số thuật toán tính toán điểm cố định: Thuật toán naive để thính toán điểm cố định :
x = (⊥,...,⊥);
do {
t=x; x=F(x); }while (x ≠ t);
Một thuật toán tốt hơn, gọi là thuật toán lặp chaotic, thực tế là nó khai thác cấu trúc dàn để tính toán điểm cố định ( , . . . , )
x1 = ⊥; . . . xn = ⊥; do { t1 = x1; . . . tn = xn; x1 = F1(x1, . . . , xn); . . . xn = Fn(x1, . . . , xn); } while (x1≠t1 ∨ . . . ∨ xn≠tn);
Với cả hai thuật toán được trình bày ở trên, rõ ràng là lãng phí vì thông tin cho tất cả các nút được tính toán lại trong mỗi lần lặp, mặc dù chúng ta có thể biết là nó không thể thay đổi. Để có được một thuật toán tốt hơn, chúng ta phải tiếp tục nghiên cứu cấu trúc của các ràng buộc riêng biệt.
Trong trường hợp tổng quát, mỗi biến ⟦ ⟧ phụ thuộc vào các biến khác. Tuy nhiên, một ví dụ thực tế của sẽ đọc các giá trị của một vài biến khác. Chúng ta biểu diễn thông tin này như một hàm:
∶ → 2
Đối với mỗi nút v cho chúng ta biết tập con của các nút khác mà ⟦ ⟧ xuất hiện một cách không bình thường ở vế phải của phương trình luồng dữ liệu của nó. Đó là, ( ) là tập hợp các nút chứa thông tin có thể phụ thuộc
38
vào các thông tin của , chúng ta có thể trình bày các thuật toán giải quyết vấn đề này, đó là thuật toán work-list:
x1 = ⊥; . . . xn = ⊥; q = [v1, . . . , vn]; while (q ≠ []){ Giả sử q = [vi,...]; y=Fi(x1,...,xn); q=q.tail(); if(y≠xi){ for(v∈dep(vi))q.append(v); xi = y; } }
Với thuật toán trên q=q.tail(), q.append(v) là phép toán loại bỏ một phần tử, thêm một phần tử [2].
Để làm rõ hơn về một số ứng dụng của lý thuyết dàn và điểm cố định trong việc phân tích tĩnh chương trình (phân tích tính sống của biến, phân tích biểu thức bận rộn, phân tích biểu thức có sẵn …) được trình bày trong phần phụ lục A của luận văn.
Lý thuyết kỹ thuật giải thích trừu tượng xác định được rằng ngữ nghĩa của chương trình hoàn toàn có thể được biểu diễn dưới dạng điểm cố định [7]. 2.6. Tiếp cận kết nối Galois cho kỹ thuật giải thích trừu tượng
2.6.1. Kết nối Galois
Kết nối Galois trong kỹ thuật giải thích trừu tượng là hình thức hóa ý tưởng phương trình: = ( ) có thể đầu tiên được đơn giản hóa thành
= ( ). Với tập các hàm ∈ ⎯ là đơn điệu và (⊑,⊔) là một
poset, và sau đó được giải bằng hàm lặp bắt đầu từ phần tử cơ bản ⊥.
Kỹ thuật này được hiểu là một xấp xỉ rời rạc của và mở rộng khái niệm xấp xỉ này theo nhiều cách khác nhau, tới miền ngữ nghĩa