Nguồn hoàn chỉnh trên GitHub
Triển khai hoàn chỉnh của ngôn ngữ lập trình Stoffle có sẵn trên GitHub. Hãy mở issue nếu bạn tìm thấy lỗi hoặc có thắc mắc.
Trong bài viết này, chúng ta sẽ tiếp tục triển khai trình thông dịch cho Stoffle, một ngôn ngữ lập trình mô hình được xây dựng hoàn toàn bằng Ruby. Chúng ta đã bắt đầu trình thông dịch trong bài viết trước. Bạn có thể đọc thêm về dự án này trong phần đầu tiên của series.
Trong bài trước, chúng ta đã xem cách triển khai các tính năng đơn giản hơn của Stoffle: biến, điều kiện, toán tử một ngôi và hai ngôi, kiểu dữ liệu, và in ra console. Bây giờ là lúc chúng ta gỡ bỏ áo khoác và xử lý các phần còn lại thách thức hơn: định nghĩa hàm, gọi hàm, phạm vi biến, và vòng lặp.
Giống như trước, chúng ta sẽ sử dụng cùng một chương trình ví dụ từ đầu đến cuối bài viết. Chúng ta sẽ đi qua từng dòng, khám phá các triển khai cần thiết tại trình thông dịch để biến từng cấu trúc khác nhau trong chương trình Stoffle mẫu thành hiện thực. Cuối cùng, chúng ta sẽ thấy trình thông dịch hoạt động và chạy chương trình bằng CLI đã tạo trong bài trước của series.
Gauss Quay Lại
Nếu bạn có trí nhớ tốt, bạn có thể nhớ rằng ở phần hai của series, chúng ta đã thảo luận cách xây dựng Lexer. Trong bài đó, chúng ta đã xem một chương trình tính tổng các số trong một dãy để minh họa cú pháp của Stoffle. Ở cuối bài này, chúng ta cuối cùng sẽ có thể chạy chương trình đó! Dưới đây là chương trình lại:
fn sum_integers: first_integer, last_integer
i = first_integer
sum = 0
while i <= last_integer
sum = sum + i
i = i + 1
end
println(sum)
end
sum_integers(1, 100)
Cây cú pháp trừu tượng (AST) cho chương trình tính tổng số nguyên của chúng ta như sau:

Nhà Toán học Đã Cảm Hứng Chương Trình Mẫu Stoffle Của Chúng Ta
Carl Friedrich Gauss được cho là tự tìm ra công thức tính tổng dãy số khi mới 7 tuổi.
Như bạn có thể nhận ra, chương trình của chúng ta không sử dụng công thức của Gauss. Vì ngày nay chúng ta có máy tính, chúng ta có đặc quyền giải quyết bài toán này theo cách "thô sơ". Hãy để những người bạn silicium làm việc nặng nhọc cho chúng ta.
Định Nghĩa Hàm
Điều đầu tiên chúng ta làm trong chương trình là định nghĩa hàm sum_integers. Việc khai báo hàm nghĩa là gì? Như bạn có thể đoán, đó là hành động tương tự như gán giá trị cho một biến. Khi định nghĩa hàm, chúng ta liên kết một tên (tức tên hàm, một định danh) với một hoặc nhiều biểu thức (tức thân hàm). Chúng ta cũng đăng ký tên nào mà các giá trị truyền vào khi gọi hàm sẽ được gán. Các định danh này trở thành biến cục bộ trong quá trình thực thi hàm và được gọi là tham số. Các giá trị được truyền khi gọi hàm (và được gán cho tham số) là đối số.
Hãy xem #interpret_function_definition:
def interpret_function_definition(fn_def)
env[fn_def.function_name_as_str] = fn_def
end
Rất đơn giản, phải không? Như bạn có thể nhớ từ bài trước trong series, khi trình thông dịch được khởi tạo, chúng ta tạo một môi trường (environment). Đây là nơi lưu trữ trạng thái chương trình, và trong trường hợp của chúng ta, nó đơn giản là một Hash của Ruby. Ở bài trước, chúng ta đã thấy cách biến và giá trị gán cho chúng được lưu trong env. Định nghĩa hàm cũng sẽ được lưu ở đó. Khóa là tên hàm, giá trị là node AST dùng để định nghĩa hàm (Stoffle::AST::FunctionDefinition). Dưới đây là lại node AST này:
class Stoffle::AST::FunctionDefinition < Stoffle::AST::Expression
attr_accessor :name, :params, :body
def initialize(fn_name = nil, fn_params = [], fn_body = nil)
@name = fn_name
@params = fn_params
@body = fn_body
end
def function_name_as_str
name.name
end
def ==(other)
children == other&.children
end
def children
[name, params, body]
end
end
Việc có tên hàm liên kết với Stoffle::AST::FunctionDefinition có nghĩa là chúng ta có thể truy cập tất cả thông tin cần thiết để thực thi hàm. Ví dụ, chúng ta có sẵn số lượng đối số mong đợi và có thể dễ dàng phát ra lỗi nếu lời gọi hàm không cung cấp đúng. Chi tiết này và các chi tiết khác chúng ta sẽ thấy khi khám phá code chịu trách nhiệm thông dịch lời gọi hàm.
Gọi Hàm
Tiếp tục đi qua ví dụ của chúng ta, hãy tập trung vào lời gọi hàm. Sau khi định nghĩa hàm sum_integers, chúng ta gọi nó với đối số là số 1 và 100:
fn sum_integers: first_integer, last_integer
i = first_integer
sum = 0
while i <= last_integer
sum = sum + i
i = i + 1
end
println(sum)
end
sum_integers(1, 100)
Việc thông dịch lời gọi hàm xảy ra tại #interpret_function_call:
def interpret_function_call(fn_call)
return if println(fn_call)
fn_def = fetch_function_definition(fn_call.function_name_as_str)
stack_frame = Stoffle::Runtime::StackFrame.new(fn_def, fn_call)
assign_function_args_to_params(stack_frame)
# Thực thi thân hàm.
call_stack << stack_frame
value = interpret_nodes(fn_def.body.expressions)
call_stack.pop
value
end
Đây là một hàm phức tạp, nên chúng ta sẽ cần thời gian để phân tích. Như đã giải thích trong bài trước, dòng đầu tiên chịu trách nhiệm kiểm tra xem hàm được gọi có phải là println không. Nếu là hàm do người dùng định nghĩa, trường hợp ở đây, chúng ta tiếp tục và lấy định nghĩa của nó bằng #fetch_function_definition. Như dưới đây, hàm này khá đơn giản, chúng ta chỉ cần lấy node AST Stoffle::AST::FunctionDefinition đã lưu trước đó trong môi trường hoặc phát ra lỗi nếu hàm không tồn tại.
def fetch_function_definition(fn_name)
fn_def = env[fn_name]
raise Stoffle::Error::Runtime::UndefinedFunction.new(fn_name) if fn_def.nil?
fn_def
end
Quay lại #interpret_function_call, mọi thứ bắt đầu trở nên thú vị hơn. Khi nghĩ về hàm trong ngôn ngữ mô hình đơn giản của chúng ta, chúng ta có hai mối quan tâm đặc biệt. Thứ nhất, chúng ta cần chiến lược để theo dõi các biến cục bộ của hàm. Thứ hai, chúng ta phải xử lý biểu thức return. Để giải quyết những thách thức này, chúng ta sẽ khởi tạo một đối tượng mới, gọi là frame (khung ngăn xếp), mỗi khi hàm được gọi. Ngay cả khi cùng một hàm được gọi nhiều lần, mỗi lần gọi mới sẽ khởi tạo một frame mới. Đối tượng này sẽ giữ tất cả các biến cục bộ của hàm. Vì một hàm có thể gọi hàm khác và cứ thế, chúng ta phải có cách để biểu diễn và theo dõi luồng thực thi của chương trình. Để làm vậy, chúng ta sẽ sử dụng cấu trúc dữ liệu stack (ngăn xếp), gọi là call stack (ngăn xếp gọi). Trong Ruby, một mảng tiêu chuẩn với phương thức #push và #pop sẽ làm việc tốt như một triển khai stack.
Call Stack và Stack Frame
Hãy nhớ rằng chúng ta sử dụng thuật ngữ call stack và stack frame một cách lỏng lẻo. Bộ xử lý và các ngôn ngữ lập trình cấp thấp thường cũng có call stacks và stack frames, nhưng chúng không hoàn toàn giống với những gì chúng ta có ở đây trong ngôn ngữ mô hình.
Nếu các khái niệm này kích thích sự tò mò của bạn, tôi khuyên bạn nên tìm hiểu về call stacks và stack frames. Nếu bạn thực sự muốn tiếp cận gần phần cứng hơn, tôi gợi ý đặc biệt tìm hiểu về call stacks của bộ xử lý.
Dưới đây là code triển khai Stoffle::Runtime::StackFrame:
module Stoffle
module Runtime
class StackFrame
attr_reader :fn_def, :fn_call, :env
def initialize(fn_def_ast, fn_call_ast)
@fn_def = fn_def_ast
@fn_call = fn_call_ast
@env = {}
end
end
end
end
Quay lại #interpret_function_call, bước tiếp theo là gán các giá trị truyền trong lời gọi hàm cho các tham số tương ứng, chúng sẽ có thể truy cập như biến cục bộ bên trong thân hàm. #assign_function_args_to_params chịu trách nhiệm cho bước này:
def assign_function_args_to_params(stack_frame)
fn_def = stack_frame.fn_def
fn_call = stack_frame.fn_call
given = fn_call.args.length
expected = fn_def.params.length
if given != expected
raise Stoffle::Error::Runtime::WrongNumArg.new(fn_def.function_name_as_str, given, expected)
end
# Áp dụng các giá trị truyền trong lời gọi hàm này cho các tham số đã định nghĩa.
if fn_def.params != nil
fn_def.params.each_with_index do |param, i|
if env.has_key?(param.name)
# Một biến toàn cục đã được định nghĩa. Chúng ta gán giá trị truyền vào cho nó.
env[param.name] = interpret_node(fn_call.args[i])
else
# Không có biến toàn cục nào cùng tên. Chúng ta tạo biến cục bộ mới.
stack_frame.env[param.name] = interpret_node(fn_call.args[i])
end
end
end
end
Trước khi khám phá triển khai #assign_function_args_to_params, cần thảo luận ngắn gọn về phạm vi biến (variable scoping). Đây là chủ đề phức tạp và tinh tế. Đối với Stoffle, hãy rất thực dụng và áp dụng giải pháp đơn giản. Trong ngôn ngữ nhỏ của chúng ta, chỉ có cấu trúc hàm tạo ra phạm vi mới. Hơn nữa, biến toàn cục luôn ưu tiên trước. Hệ quả là tất cả biến được khai báo (tức lần sử dụng đầu tiên) bên ngoài hàm là biến toàn cục và được lưu trong env. Biến bên trong hàm là cục bộ cho chúng và được lưu trong env của stack frame được tạo khi thông dịch lời gọi hàm. Tuy nhiên có một ngoại lệ: tên biến trùng với biến toàn cục đã tồn tại. Nếu va chạm xảy ra, biến cục bộ sẽ không được tạo, và chúng ta sẽ đọc/gán cho biến toàn cục hiện có.
Được rồi, bây giờ chiến lược phạm vi biến đã rõ, hãy quay lại #assign_function_args_to_params. Trong đoạn đầu của phương thức, chúng ta lấy node định nghĩa hàm và node lời gọi hàm từ đối tượng stack frame được truyền vào. Có những thứ này trên tay, dễ dàng kiểm tra số lượng đối số cung cấp có khớp với số lượng tham số hàm mong đợi không. Chúng ta phát ra lỗi khi có sự không khớp giữa đối số cho và tham số mong đợi. Ở phần cuối của #assign_function_args_to_params, chúng ta gán các đối số (tức giá trị) cung cấp khi gọi hàm cho các tham số tương ứng (tức biến cục bộ bên trong hàm). Lưu ý rằng chúng ta kiểm tra xem tên tham số có va chạm với biến toàn cục hiện có không. Như đã giải thích, trong trường hợp này, chúng ta không tạo biến cục bộ trong stack frame của hàm mà chỉ áp dụng giá trị truyền vào cho biến toàn cục hiện có.
Quay lại #interpret_function_call, cuối cùng chúng ta đẩy stack frame vừa tạo vào call stack. Sau đó, gọi lại người bạn cũ #interpret_nodes để bắt đầu thông dịch thân hàm:
def interpret_function_call(fn_call)
return if println(fn_call)
fn_def = fetch_function_definition(fn_call.function_name_as_str)
stack_frame = Stoffle::Runtime::StackFrame.new(fn_def, fn_call)
assign_function_args_to_params(stack_frame)
# Thực thi thân hàm.
call_stack << stack_frame
value = interpret_nodes(fn_def.body.expressions)
call_stack.pop
value
end
Thông Dịch Thân Hàm
Bây giờ chúng ta đã thông dịch lời gọi hàm, đến lúc thông dịch thân hàm:
fn sum_integers: first_integer, last_integer
i = first_integer
sum = 0
while i <= last_integer
sum = sum + i
i = i + 1
end
println(sum)
end
sum_integers(1, 100)
Hai dòng đầu của hàm sum_integers là gán biến. Chúng ta đã đề cập chủ đề này trong bài trước của series. Tuy nhiên, bây giờ chúng ta có phạm vi biến, và do đó code xử lý gán đã thay đổi một chút. Hãy khám phá nó:
def interpret_var_binding(var_binding)
if call_stack.length > 0
# Chúng ta đang bên trong một hàm. Nếu tên trỏ đến biến toàn cục, gán giá trị cho nó.
# Ngược lại, tạo và/hoặc gán cho biến cục bộ.
if env.has_key?(var_binding.var_name_as_str)
env[var_binding.var_name_as_str] = interpret_node(var_binding.right)
else
call_stack.last.env[var_binding.var_name_as_str] = interpret_node(var_binding.right)
end
else
# Chúng ta không ở trong hàm. Do đó, tạo và/hoặc gán cho biến toàn cục.
env[var_binding.var_name_as_str] = interpret_node(var_binding.right)
end
end
Bạn nhớ khi chúng ta đẩy stack frame được tạo cho lời gọi hàm vào call_stack? Điều này tiện lợi bây giờ vì chúng ta có thể kiểm tra xem có đang bên trong hàm không bằng cách xác thực call_stack có độ dài lớn hơn không (tức là có ít nhất một stack frame). Nếu đang trong hàm, trường hợp ở code chúng ta đang thông dịch, chúng ta kiểm tra xem đã có biến toàn cục cùng tên với biến đang cố gắng gán giá trị chưa. Như bạn đã biết, nếu va chạm xảy ra, chúng ta sẽ chỉ gán giá trị cho biến toàn cục hiện có, và biến cục bộ sẽ không được tạo. Khi tên chưa được sử dụng, chúng ta tạo biến cục bộ mới và gán giá trị dự định cho nó. Vì call_stack là stack (cấu trúc dữ liệu last in first out), chúng ta biết biến cục bộ này nên được lưu trong env của frame được stack cuối cùng (tức frame được tạo cho hàm hiện tại đang xử lý). Cuối cùng, phần cuối của #interpret_var_binding xử lý gán xảy ra bên ngoài hàm. Vì chỉ hàm tạo phạm vi mới trong Stoffle, không có gì thay đổi ở đây, vì biến tạo bên ngoài hàm luôn là toàn cục và lưu tại biến instance env.
Quay lại chương trình của chúng ta, bước tiếp theo là thông dịch vòng lặp chịu trách nhiệm tính tổng các số nguyên. Hãy làm mới bộ nhớ và nhìn lại AST của chương trình Stoffle:

Node biểu diễn vòng lặp là Stoffle::AST::Repetition:
class Stoffle::AST::Repetition < Stoffle::AST::Expression
attr_accessor :condition, :block
def initialize(cond_expr = nil, repetition_block = nil)
@condition = cond_expr
@block = repetition_block
end
def ==(other)
children == other&.children
end
def children
[condition, block]
end
end
Lưu ý node AST này cơ bản tập hợp các khái niệm chúng ta đã khám phá trong các bài trước. Đối với điều kiện, chúng ta sẽ có biểu thức thường có gốc (nghĩ về node gốc của AST biểu thức) là Stoffle::AST::BinaryOperator (ví dụ: '>', 'or', v.v.). Đối với thân vòng lặp, chúng ta sẽ có Stoffle::AST::Block. Điều này hợp lý, phải không? Dạng cơ bản nhất của vòng lặp là một hoặc nhiều biểu thức (một block) được lặp lại trong khi biểu thức là truthy (tức khi điều kiện đánh giá thành giá trị truthy).
Phương thức tương ứng tại trình thông dịch là #interpret_repetition:
def interpret_repetition(repetition)
while interpret_node(repetition.condition)
interpret_nodes(repetition.block.expressions)
end
end
Ở đây, bạn có thể ngạc nhiên về sự đơn giản (và, dám nói, vẻ đẹp) của phương thức này. Chúng ta có thể triển khai thông dịch vòng lặp bằng cách kết hợp các phương thức đã khám phá trong các bài trước. Bằng cách sử dụng vòng lặp while của Ruby, chúng ta đảm bảo tiếp tục thông dịch các node cấu thành vòng lặp Stoffle (bằng cách gọi liên tục #interpret_nodes) trong khi đánh giá điều kiện là true. Việc đánh giá điều kiện dễ dàng như gọi kẻ nghiêm trọng thường thấy, phương thức #interpret_node.
Trả Về Từ Hàm
Chúng ta gần đến đích! Sau vòng lặp, chúng ta tiến hành in kết quả tổng ra console. Chúng ta sẽ không đi qua lại vì đã đề cập ở phần cuối series. Nhắc lại nhanh: nhớ rằng hàm println do Stoffle cung cấp và bên trong trình thông dịch chúng ta chỉ dùng puts của Ruby.
Để kết thúc bài này, chúng ta phải xem lại #interpret_nodes. Phiên bản cuối cùng của nó hơi khác so với phiên bản trước. Bây giờ nó bao gồm code xử lý trả về từ hàm và mở rộng call stack. Đây là phiên bản hoàn chỉnh của #interpret_nodes trong vinh quang của nó:
def interpret_nodes(nodes)
last_value = nil
nodes.each do |node|
last_value = interpret_node(node)
if return_detected?(node)
raise Stoffle::Error::Runtime::UnexpectedReturn unless call_stack.length > 0
self.unwind_call_stack = call_stack.length # Lưu cấp độ stack hiện tại để biết khi nào dừng trả về.
return last_value
end
if unwind_call_stack == call_stack.length
# Vẫn còn bên trong hàm đã return, nên tiếp tục bong lên từ các cấu trúc của nó (ví dụ: điều kiện, vòng lặp, v.v.).
return last_value
elsif unwind_call_stack > call_stack.length
# Đã trả về từ hàm, nên reset "chỉ số unwind".
self.unwind_call_stack = -1
end
end
last_value
end
Như bạn đã biết, #interpret_nodes được dùng mỗi khi cần thông dịch một loạt biểu thức. Nó dùng để bắt đầu thông dịch chương trình và mọi lúc gặp node có block liên kết (như Stoffle::AST::FunctionDefinition). Cụ thể khi xử lý hàm, có hai kịch bản: thông dịch hàm và gặp biểu thức return hoặc thông dịch hàm đến cuối mà không gặp return. Trong trường hợp thứ hai, nghĩa là hàm không có return tường minh hoặc đường đi code không có return.
Hãy làm mới ký ức trước khi tiếp tục. Như bạn có thể nhớ từ vài đoạn trước, #interpret_nodes được gọi khi bắt đầu thông dịch hàm sum_integers (tức khi nó được gọi trong chương trình). Lại đây là source code chương trình chúng ta đang đi qua:
fn sum_integers: first_integer, last_integer
i = first_integer
sum = 0
while i <= last_integer
sum = sum + i
i = i + 1
end
println(sum)
end
sum_integers(1, 100)
Chúng ta đang ở cuối thông dịch hàm. Như bạn có thể đoán, hàm của chúng ta không có return tường minh. Đây là đường đi dễ nhất của #interpret_nodes. Chúng ta cơ bản lặp qua tất cả node của hàm, trả về giá trị của biểu thức cuối cùng được thông dịch (nhắc nhở nhanh: Stoffle có implicit return). Điều này đưa chúng ta đến đích, kết thúc thông dịch chương trình.
Mặc dù chương trình đã được thông dịch hoàn toàn, mục đích chính của bài này là giải thích triển khai trình thông dịch, nên hãy dành thêm chút thời gian để xem trình thông dịch xử lý trường hợp gặp return bên trong hàm như thế nào.
Trước tiên, biểu thức return được đánh giá ngay đầu hoạt động. Giá trị của nó sẽ là đánh giá của cái đang được trả về. Đây là code cho Stoffle::AST::Return:
class Stoffle::AST::Return < Stoffle::AST::Expression
attr_accessor :expression
def initialize(expr)
@expression = expr
end
def ==(other)
children == other&.children
end
def children
[expression]
end
end
Sau đó, chúng ta có một điều kiện đơn giản phát hiện node AST return. Sau khi làm vậy, chúng ta trước tiên kiểm tra an toàn để xác thực đang bên trong hàm. Để làm vậy, chỉ cần kiểm tra độ dài call stack. Độ dài lớn hơn không nghĩa là thực sự bên trong hàm. Trong Stoffle, không cho phép dùng biểu thức return bên ngoài hàm, nên phát lỗi nếu kiểm tra thất bại. Trước khi trả về giá trị do lập trình viên dự định, chúng ta trước tiên ghi lại độ dài call stack hiện tại, lưu tại biến instance unwind_call_stack. Để hiểu tại sao điều này quan trọng, hãy xem lại #interpret_function_call:
def interpret_function_call(fn_call)
return if println(fn_call)
fn_def = fetch_function_definition(fn_call.function_name_as_str)
stack_frame = Stoffle::Runtime::StackFrame.new(fn_def, fn_call)
assign_function_args_to_params(stack_frame)
# Thực thi thân hàm.
call_stack << stack_frame
value = interpret_nodes(fn_def.body.expressions)
call_stack.pop
value
end
Ở đây, cuối #interpret_function_call, lưu ý rằng chúng ta pop stack frame khỏi call stack sau khi thông dịch hàm. Hệ quả, độ dài call stack sẽ giảm đi một. Vì chúng ta đã bảo toàn độ dài stack tại thời điểm phát hiện return, chúng ta có thể so sánh độ dài ban đầu này mỗi lần thông dịch node mới tại #interpret_nodes. Hãy xem đoạn code làm điều này bên trong iterator node của #interpret_nodes:
def interpret_nodes(nodes)
# ...
nodes.each do |node|
# ...
if unwind_call_stack == call_stack.length
# Vẫn còn bên trong hàm đã return, nên tiếp tục bong lên từ các cấu trúc của nó (ví dụ: điều kiện, vòng lặp v.v.).
return last_value
elsif unwind_call_stack > call_stack.length
# Đã trả về từ hàm, nên reset "chỉ số unwind".
self.unwind_call_stack = -1
end
# ...
end
# ...
end
Điều này có thể hơi khó nắm bắt lúc đầu. Tôi khuyến khích bạn xem triển khai đầy đủ trên GitHub và thử nghiệm nếu nghĩ nó có thể giúp hiểu phần cuối này của trình thông dịch. Điểm quan trọng cần nhớ ở đây là chương trình điển hình có nhiều cấu trúc lồng sâu. Do đó, thực thi #interpret_nodes thường dẫn đến gọi mới #interpret_nodes, có thể dẫn đến thêm gọi #interpret_nodes và cứ thế! Khi gặp return bên trong hàm, chúng ta có thể đang bên trong cấu trúc lồng sâu. Ví dụ, return ở bên trong điều kiện là một phần của vòng lặp. Để trả về từ hàm, chúng ta phải trả về từ tất cả các lời gọi #interpret_nodes cho đến khi trả về từ lời gọi được khởi tạo bởi #interpret_function_call (tức lời gọi #interpret_nodes bắt đầu thông dịch thân hàm).
Ở đoạn code trên, chúng ta làm nổi bật chính xác cách chúng ta làm điều này. Bằng cách có giá trị dương tại @unwind_call_stack và giá trị đó bằng độ dài call stack hiện tại, chúng ta chắc chắn rằng đang bên trong hàm đã return và vẫn chưa return từ lời gọi gốc được khởi tạo bởi #interpret_function_call. Khi cuối cùng điều này xảy ra, @unwind_call_stack sẽ lớn hơn độ dài call stack hiện tại; do đó, chúng ta biết đã thoát khỏi hàm đã return, và không còn cần tiếp tục quá trình bong lên. Sau đó, chúng ta reset @unwind_call_stack. Chỉ để làm rõ việc dùng @unwind_call_stack, đây là các giá trị có thể có:
- -1, giá trị ban đầu và trung lập, chỉ ra không ở trong hàm nào đã return.
- giá trị dương bằng độ dài call stack, chỉ ra vẫn còn bên trong hàm đã return.
- giá trị dương lớn hơn độ dài call stack, chỉ ra không còn bên trong hàm đã return.
Chạy Chương Trình Của Chúng Ta Bằng Stoffle CLI
Trong bài trước của series, chúng ta đã tạo CLI đơn giản để thông dịch chương trình Stoffle dễ dàng hơn. Bây giờ đã khám phá triển khai trình thông dịch, hãy xem nó hoạt động, chạy chương trình. Như hiển thị ở nhiều phần khác nhau, code của chúng ta định nghĩa rồi gọi hàm sum_integers với đối số 1 và 100. Nếu trình thông dịch hoạt động đúng, chúng ta nên thấy 5050.0 (tổng tập số nguyên từ 1 đến 100) in ra console:

Kết Luận
Trong bài này, chúng ta triển khai các phần cuối cần thiết để hoàn thiện trình thông dịch. Chúng ta giải quyết định nghĩa hàm, gọi hàm, phạm vi biến, và vòng lặp. Bây giờ, chúng ta có một ngôn ngữ lập trình đơn giản nhưng hoạt động!
Trong phần tiếp theo và cuối cùng của series, tôi sẽ chia sẻ một số tài nguyên tôi coi là lựa chọn tuyệt vời cho những ai muốn tiếp tục học triển khai ngôn ngữ lập trình. Tôi cũng sẽ đề xuất một số thách thức bạn có thể thực hiện để tiếp tục học hỏi đồng thời cải tiến phiên bản Stoffle của bạn. Hẹn gặp lại sau; tạm biệt!