Xây dựng trình biên dịch Ada bằng vòng lặp phát triển AI tự chủ

Gần đây, tôi đang phát triển adac, một trình biên dịch Ada được viết bằng Ada.

Hiện tại, trình biên dịch này chỉ hỗ trợ một tập con rất nhỏ của Ada 2022. Nó có thể biên dịch một thủ tục tối thiểu qua các giai đoạn tách biệt gồm frontend, cây cú pháp trừu tượng, phân tích ngữ nghĩa, biểu diễn trung gian tùy chỉnh và backend native. Backend native hiện tạo chương trình cho x86-64 Linux và FreeBSD.

Mô tả đó chính xác về mặt kỹ thuật, nhưng vẫn bỏ sót phần khiến tôi quan tâm nhất trong dự án này.

Đối với tôi, adac không chỉ là một nỗ lực xây dựng thêm một trình biên dịch Ada. Nó đồng thời là một thử nghiệm về phát triển phần mềm có AI hỗ trợ theo hướng tự chủ.

Câu hỏi trung tâm là:

Khi chính kho mã định nghĩa kiến trúc, quy tắc kỹ thuật, yêu cầu kiểm chứng, chính sách lựa chọn công việc và điều kiện dừng, một tác nhân AI có thể tạo ra tiến triển dài hạn nhất quán trong một dự án hệ thống hay không?

Điều này khác với việc yêu cầu AI tạo ra một trình biên dịch từ một prompt lớn. Nó cũng khác với việc cứ chấp nhận mã được sinh ra cho đến khi thứ gì đó có vẻ hoạt động.

Mục tiêu không phải là loại bỏ kỷ luật kỹ thuật. Mục tiêu là mã hóa đủ nhiều kỷ luật đó vào kho mã để một tác nhân AI có thể làm việc bên trong các ràng buộc ấy.

Kho mã như một hợp đồng vận hành

Hầu hết các kho phần mềm đều chứa mã nguồn, kiểm thử và đôi khi có cả lộ trình. Trong adac, kho mã còn định nghĩa cả cách công việc phát triển phải được tiến hành.

Tài liệu dự án mô tả ranh giới kiến trúc, quy tắc sở hữu, nhóm lỗi, nền tảng đích được hỗ trợ, yêu cầu kiểm chứng và tiêu chí hoàn thành mốc.

Tệp AGENTS.md định nghĩa một vòng lặp phát triển tự chủ. Một chỉ thị triển khai tổng quát có thể khởi động một chuỗi công việc gần giống như sau:

  1. Kiểm tra cây làm việc, các commit gần đây, lộ trình và tài liệu liên quan.
  2. Chạy toàn bộ kiểm tra của kho mã để thiết lập một trạng thái chuẩn đã biết.
  3. Tìm điều kiện tiên quyết sớm nhất trong lộ trình vẫn chưa được đáp ứng.
  4. Chọn hạng mục công việc hoàn chỉnh nhỏ nhất có thể thúc đẩy điều kiện đó.
  5. Xác định hợp đồng, tiêu chí thành công, các ranh giới bị ảnh hưởng và phần refactor cần thiết.
  6. Cập nhật tài liệu trước khi triển khai nếu một hợp đồng thay đổi.
  7. Triển khai thay đổi như một lát cắt dọc hoàn chỉnh.
  8. Thêm kiểm thử dương và kiểm thử âm.
  9. Chạy kiểm thử tập trung, sau đó chạy toàn bộ bộ kiểm chứng.
  10. Xem xét toàn bộ diff và chỉ commit một thay đổi nhất quán đã vượt qua kiểm chứng.
  11. Đánh giá lại lộ trình và tiếp tục với hạng mục công việc tiếp theo.

Khi việc triển khai đã được cho phép, tác nhân không được dừng lại sau khi chỉ tạo ra kế hoạch. Nó tiếp tục cho đến khi gặp một điều kiện dừng đã được ghi trong tài liệu.

Các điều kiện dừng cũng được nêu rõ. Chẳng hạn, công việc phải dừng khi không thể suy ra an toàn một quyết định kiến trúc từ các hợp đồng hiện có, khi một thao tác phá hủy cần được phê duyệt, khi thay đổi của người dùng chồng lấn với nhiệm vụ đã chọn, hoặc khi môi trường không thể kiểm chứng kết quả.

Điều này quan trọng vì phát triển tự chủ mà không có quy tắc dừng thì không thực sự là kỹ thuật tự chủ. Nó chỉ là sửa đổi không có giám sát.

Không phải là “tạo ra một trình biên dịch”

Trình biên dịch là một đối tượng phù hợp cho thử nghiệm này vì nó khó chấp nhận những triển khai hời hợt.

Việc parser chấp nhận một vài cú pháp là chưa đủ. Một tính năng ngôn ngữ có thể phải đi qua nhiều giai đoạn tách biệt:

source
  -> lexer
  -> parser
  -> AST
  -> semantic analysis
  -> custom IR
  -> backend
  -> executable

Một tính năng chưa hoàn chỉnh nếu nó chỉ tồn tại trong parser.

Nó còn phải được biểu diễn đúng, kiểm chứng, hạ xuống biểu diễn thấp hơn, phát sinh đầu ra và kiểm thử. Khi chương trình nguồn không hợp lệ, nó phải bị từ chối đúng cách. Lỗi nội bộ của trình biên dịch phải vẫn phân biệt được với chẩn đoán nguồn thông thường. Đích không được hỗ trợ phải thất bại trước khi tạo ra đầu ra gây hiểu nhầm. Sản phẩm build một phần cũng không được công bố như một kết quả thành công.

Các yêu cầu này khiến tác nhân khó tạo ra ảo giác tiến triển bằng những đoạn mã rời rạc.

Kho mã yêu cầu rõ ràng các lát cắt tính năng theo chiều dọc. Mỗi tính năng được triển khai phải đi xuyên qua mọi giai đoạn trình biên dịch mà nó cần, cùng với chẩn đoán và kiểm thử.

Quy tắc này nhằm ngăn chặn một dạng thất bại phổ biến trong mã hệ thống do AI sinh ra: nhiều thành phần trông có vẻ hợp lý nhưng không tạo thành một hệ thống đáng tin cậy.

Công việc gần đây: hạ tầng trước tính năng

Tập con Ada có thể quan sát được vẫn được chủ ý giữ ở quy mô nhỏ.

Hiện tại, adac có thể biên dịch một thủ tục tối thiểu chứa các câu lệnh đơn giản như nullreturn. Điều này hoàn toàn chưa đủ cho các chương trình Ada thực tế, và tôi không giới thiệu nó như một trình biên dịch đa dụng có thể sử dụng.

Thay vào đó, công việc gần đây tập trung vào hạ tầng cần thiết để mở rộng an toàn hơn.

Các công việc đó bao gồm:

  • ngữ cảnh biên dịch tường minh;
  • trạng thái chẩn đoán do ngữ cảnh sở hữu;
  • định danh ổn định cho nút cú pháp, ký hiệu và thực thể ngữ nghĩa;
  • phạm vi nguồn được gắn vào đối tượng cú pháp;
  • kiểm chứng tại ranh giới giữa các giai đoạn trình biên dịch;
  • đầu vào nguồn có giới hạn;
  • số lượng nút AST được tạo có giới hạn;
  • mức tăng ký hiệu interned có giới hạn;
  • từ chối có kiểm soát đối với đầu vào sai hoặc quá lớn;
  • kiểm thử có tính tất định;
  • quy tắc khôi phục công việc tự chủ bị gián đoạn.

Đây không phải là các tính năng hấp dẫn đối với người dùng. Chúng cũng không tạo nên một chương trình trình diễn ấn tượng.

Tuy vậy, chúng xử lý các vấn đề sẽ trở nên đắt đỏ về sau: quyền sở hữu không rõ ràng, trạng thái toàn cục của tiến trình, tăng trưởng bộ nhớ không giới hạn, biểu diễn nội bộ không hợp lệ, đầu ra một phần bị công bố ngoài ý muốn, và phiên phát triển không thể tiếp tục an toàn sau khi bị gián đoạn.

Đối với một hệ thống được phát triển tự chủ, nền tảng này có thể quan trọng hơn việc nhanh chóng mở rộng phạm vi cú pháp được hỗ trợ.

Tác nhân AI có thể thêm tính năng rất nhanh. Vấn đề khó hơn là bảo đảm rằng sau mỗi lần bổ sung, kho mã vẫn ở trạng thái mà một lần chạy tác nhân khác có thể kiểm tra, hiểu, kiểm chứng và tiếp tục.

Giới hạn tài nguyên cũng là một phần của tính đúng đắn

Một lĩnh vực phát triển gần đây là giới hạn tài nguyên tường minh.

Trình biên dịch hiện có các chính sách giới hạn cho số ký tự nguồn, số nút AST và số ký hiệu interned khác nhau. Các giới hạn này thuộc về ngữ cảnh biên dịch thay vì là hành vi toàn cục bị che giấu.

Với một trình biên dịch nhỏ như vậy, điều này có thể nghe giống như gia cố quá sớm, nhưng nó phục vụ nhiều mục đích.

Thứ nhất, mã nguồn phải được xem là đầu vào không đáng tin cậy. Một tệp nguồn sai hoặc đối nghịch không được phép làm các cấu trúc nội bộ tăng lên không giới hạn.

Thứ hai, cạn kiệt tài nguyên cần có một đường thất bại được định nghĩa rõ. Nó không được biến thành crash của bộ cấp phát, trạng thái trình biên dịch không hợp lệ hoặc tệp đầu ra chỉ được công bố một phần.

Thứ ba, giới hạn tường minh khiến hành vi có thể kiểm thử được. Kiểm thử có thể dùng ngân sách cố ý rất nhỏ và xác minh rằng trình biên dịch thất bại đúng tại ranh giới mà không làm hỏng các kho nội bộ.

Cuối cùng, hợp đồng tài nguyên cung cấp cho tác nhân tự chủ một khuôn khổ rõ ràng hơn. Thay vì thêm các kiểm tra phòng thủ mơ hồ ở mọi nơi có vẻ có thể thất bại, nó phải duy trì một chính sách đã được ghi trong tài liệu, có quyền sở hữu rõ ràng và hành vi quan sát được.

Con người vẫn chịu trách nhiệm

Gọi đây là phát triển tự chủ không có nghĩa là con người biến mất.

Tôi vẫn xác định định hướng dài hạn. Tôi quyết định mục tiêu sản phẩm nào quan trọng. Tôi xem xét lựa chọn kiến trúc, điều chỉnh các hợp đồng trong kho mã và dừng hoặc chuyển hướng công việc khi cần thiết.

Thử nghiệm này không nhằm kiểm tra liệu AI có thể tự quyết định phần mềm nào nên tồn tại.

Nó nhằm kiểm tra liệu AI có thể thực hiện một phần có ý nghĩa của vòng lặp kỹ thuật mà không cần con người lựa chọn và giám sát từng chỉnh sửa riêng lẻ hay không.

Một tác nhân tự chủ hữu ích phải có khả năng:

  • kiểm tra trạng thái hiện tại thay vì dựa vào ngữ cảnh hội thoại đã cũ;
  • chọn công việc từ các điều kiện tiên quyết đã được ghi trong tài liệu;
  • nhận ra khi refactor là cần thiết về mặt cấu trúc;
  • cập nhật hợp đồng và triển khai cùng nhau;
  • kiểm chứng toàn bộ kết quả;
  • chỉ commit các thay đổi nhất quán;
  • phục hồi sau gián đoạn;
  • dừng khi không có thẩm quyền đưa ra quyết định.

Đây là một tuyên bố hẹp hơn so với “AI có thể tự xây dựng phần mềm”, nhưng cũng cụ thể và có thể kiểm chứng hơn.

Tại sao chọn Ada?

Ada là một ngôn ngữ thú vị cho dự án này vì nhiều lý do.

Nó khuyến khích giao diện tường minh, kiểu mạnh, biểu diễn có ràng buộc và suy luận cẩn trọng về tính đúng đắn. Những đặc tính đó hữu ích trong việc triển khai trình biên dịch, nhưng cũng hữu ích khi cố gắng ràng buộc các thay đổi do AI sinh ra.

Ada cũng có đặc tả ngôn ngữ trưởng thành và một cộng đồng thường quan tâm đến ngữ nghĩa chính xác, tính khả chuyển, an toàn và khả năng bảo trì lâu dài.

Điều đó khiến việc ẩn mình sau một ví dụ đồ chơi thành công trở nên khó khăn. Nếu adac phát triển thành một trình biên dịch hữu ích, nó sẽ cần nhiều hơn nhận dạng cú pháp. Nó sẽ cần phân tích ngữ nghĩa đáng tin cậy, biểu diễn trung gian đã được kiểm chứng, hành vi runtime đúng, ranh giới ABI được định nghĩa rõ và hỗ trợ có kỷ luật cho nhiều đích.

Vì vậy, Ada đặc biệt phù hợp với thử nghiệm phát triển này.

Định hướng dài hạn

Mục tiêu dài hạn là một trình biên dịch Ada đa nền tảng, có thể tự host và có biểu diễn trung gian độc lập với đích của riêng mình.

Lộ trình bao gồm:

  • phạm vi hỗ trợ frontend Ada 2022 rộng hơn;
  • kiểu vô hướng, biểu thức, luồng điều khiển và lời gọi chương trình con;
  • biểu diễn trung gian có kiểu và biểu diễn trung gian luồng điều khiển;
  • backend native đáng tin cậy;
  • package và biên dịch tách biệt;
  • kiểu tổng hợp và mô hình lưu trữ;
  • ngoại lệ, generic và tagged type;
  • tự host theo từng giai đoạn;
  • các đích bổ sung, bao gồm WebAssembly;
  • backend LLVM tùy chọn;
  • công cụ như bộ kiểm tra phong cách và bộ định dạng.

Lộ trình dựa trên năng lực chứ không dựa trên ngày tháng. Một mốc chỉ hoàn thành khi hợp đồng, triển khai, bộ kiểm chứng, chẩn đoán và kiểm thử của nó đáp ứng các tiêu chí kết thúc đã được ghi trong tài liệu.

Sự phân biệt này rất quan trọng. Phát triển tự chủ không nên tối ưu cho việc tạo ra thật nhiều commit hoặc đánh dấu xong những tên tính năng bề mặt. Nó nên tối ưu cho năng lực đã được kiểm chứng.

Điều tôi thực sự đang kiểm chứng

Vẫn còn quá sớm để khẳng định phương pháp này hoạt động.

Trình biên dịch vẫn còn nhỏ, và nhiều tính năng ngôn ngữ khó nhất vẫn ở phía trước. Phân giải tên, phân giải overload, package, generic, ngoại lệ, quản lý lưu trữ, biên dịch tách biệt, chi tiết ABI và tự host sẽ tạo áp lực lớn hơn nhiều lên cả kiến trúc lẫn quy trình phát triển.

Những thất bại đáng chú ý có thể không xuất hiện dưới dạng lỗi sinh mã rõ ràng.

Chúng có thể xuất hiện như:

  • kiến trúc dần trôi lệch qua nhiều commit riêng lẻ đều có vẻ hợp lý;
  • tài liệu không còn khớp với triển khai;
  • kiểm thử xác minh ví dụ thay vì hợp đồng;
  • sửa lỗi cục bộ làm tổn hại khả năng bảo trì lâu dài;
  • refactor quá mức khi chưa có đủ bằng chứng;
  • refactor chưa đủ chỉ để tránh một diff lớn;
  • vòng lặp tự chủ tiếp tục khi lẽ ra phải dừng;
  • tác nhân không thể phân biệt công việc chưa hoàn thành với công việc đã hoàn thành.

Đó chính xác là những vấn đề mà tôi muốn quy trình do kho mã định nghĩa phải làm lộ ra.

Vì vậy, adac đồng thời là hai dự án.

Một là trình biên dịch Ada.

Dự án còn lại là một nỗ lực khám phá xem có thể biểu đạt bao nhiêu phán đoán kỹ thuật dưới dạng trạng thái kho mã, kiểm chứng có thể thực thi và các quy tắc vận hành tường minh cho một tác nhân AI.

Trình biên dịch sẽ là kết quả nhìn thấy được.

Lịch sử phát triển có thể mới là hiện vật thú vị hơn.

Mã nguồn có sẵn trên GitHub:

github.com/hodong-kim/adac