Skip to content
This repository was archived by the owner on Oct 12, 2026. It is now read-only.

Commit 8968ea6

Browse files
committed
feat: signal side effects in interpreter
1 parent f32d264 commit 8968ea6

5 files changed

Lines changed: 124 additions & 35 deletions

File tree

‎examples/list.lbd‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
a: List = (list "Hello")
1+
a: List = (list)
22
(print a)
33

44
(list_append a "World")

‎include/lbd/intp/interpreter.h‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,14 @@ namespace intp::interp {
5151
friend std::ostream &operator<<(std::ostream &os, const Value &value);
5252
};
5353

54+
struct ResultOptions {
55+
bool side_effects = false;
56+
57+
void interpolate(const ResultOptions &result_options_);
58+
};
59+
5460
struct NativeFunction {
55-
using Impl = std::function<Value(
61+
using Impl = std::function<std::pair<Value, ResultOptions>(
5662
const std::vector<std::shared_ptr<Thunk> > &, const std::shared_ptr<Env> &)>;
5763

5864
int arity;
@@ -112,6 +118,7 @@ namespace intp::interp {
112118
struct Result {
113119
std::shared_ptr<Env> global_env;
114120
Value value;
121+
ResultOptions options = {};
115122
};
116123

117124
Result interpret(fe::ast::Program &program, std::optional<std::shared_ptr<Env> > global_env = std::nullopt,

‎src/intp/builtins.cpp‎

Lines changed: 74 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,68 +7,73 @@ namespace intp::interp {
77
static NativeFunction make_print() {
88
const std::string name = "print";
99
return {
10-
-1, name, [](const std::vector<std::shared_ptr<Thunk> > &args, const std::shared_ptr<Env> &) -> Value {
10+
-1, name, [](const std::vector<std::shared_ptr<Thunk> > &args,
11+
const std::shared_ptr<Env> &) -> std::pair<Value, ResultOptions> {
1112
for (auto &arg: args) {
1213
const Value &value = arg->force();
1314
std::cout << value;
1415
}
15-
return Value{static_cast<double>(0)};
16+
return std::make_pair(Value{static_cast<double>(0)}, ResultOptions{.side_effects = true});
1617
}
1718
};
1819
}
1920

2021
static NativeFunction make_add() {
2122
const std::string name = "add";
2223
return {
23-
2, name, [name](const std::vector<std::shared_ptr<Thunk> > &args, const std::shared_ptr<Env> &) -> Value {
24+
2, name, [name](const std::vector<std::shared_ptr<Thunk> > &args,
25+
const std::shared_ptr<Env> &) -> std::pair<Value, ResultOptions> {
2426
const Value &value1 = args[0]->force();
2527
const Value &value2 = args[1]->force();
2628
if (!std::holds_alternative<double>(value1) || !std::holds_alternative<double>(value2)) {
2729
options_v.logger.error({}, "runtime error: wrong arguments provided to native function ", name,
2830
"\n", name, " signature: Float -> Float -> Float");
2931
}
3032
const double result = std::get<double>(value1) + std::get<double>(value2);
31-
return Value{result};
33+
return std::make_pair(Value{result}, ResultOptions{});
3234
}
3335
};
3436
}
3537

3638
static NativeFunction make_sub() {
3739
const std::string name = "sub";
3840
return {
39-
2, name, [name](const std::vector<std::shared_ptr<Thunk> > &args, const std::shared_ptr<Env> &) -> Value {
41+
2, name, [name](const std::vector<std::shared_ptr<Thunk> > &args,
42+
const std::shared_ptr<Env> &) -> std::pair<Value, ResultOptions> {
4043
const Value &value1 = args[0]->force();
4144
const Value &value2 = args[1]->force();
4245
if (!std::holds_alternative<double>(value1) || !std::holds_alternative<double>(value2)) {
4346
options_v.logger.error({}, "runtime error: wrong arguments provided to native function ", name,
4447
"\n", name, " signature: Float -> Float -> Float");
4548
}
4649
const double result = std::get<double>(value1) - std::get<double>(value2);
47-
return Value{result};
50+
return std::make_pair(Value{result}, ResultOptions{});
4851
}
4952
};
5053
}
5154

5255
static NativeFunction make_mul() {
5356
const std::string name = "mul";
5457
return {
55-
2, name, [name](const std::vector<std::shared_ptr<Thunk> > &args, const std::shared_ptr<Env> &) -> Value {
58+
2, name, [name](const std::vector<std::shared_ptr<Thunk> > &args,
59+
const std::shared_ptr<Env> &) -> std::pair<Value, ResultOptions> {
5660
const Value &value1 = args[0]->force();
5761
const Value &value2 = args[1]->force();
5862
if (!std::holds_alternative<double>(value1) || !std::holds_alternative<double>(value2)) {
5963
options_v.logger.error({}, "runtime error: wrong arguments provided to native function ", name,
6064
"\n", name, " signature: Float -> Float -> Float");
6165
}
6266
const double result = std::get<double>(value1) * std::get<double>(value2);
63-
return Value{result};
67+
return std::make_pair(Value{result}, ResultOptions{});
6468
}
6569
};
6670
}
6771

6872
static NativeFunction make_cmp() {
6973
const std::string name = "cmp";
7074
return {
71-
2, name, [name](const std::vector<std::shared_ptr<Thunk> > &args, const std::shared_ptr<Env> &) -> Value {
75+
2, name, [name](const std::vector<std::shared_ptr<Thunk> > &args,
76+
const std::shared_ptr<Env> &) -> std::pair<Value, ResultOptions> {
7277
const Value &value1 = args[0]->force();
7378
const Value &value2 = args[1]->force();
7479
if (!std::holds_alternative<double>(value1) || !std::holds_alternative<double>(value2)) {
@@ -78,15 +83,16 @@ namespace intp::interp {
7883
const double num1 = std::get<double>(value1);
7984
const double num2 = std::get<double>(value2);
8085
const int result = num1 < num2 ? -1 : num1 > num2 ? 1 : 0;
81-
return Value{static_cast<double>(result)};
86+
return std::make_pair(Value{static_cast<double>(result)}, ResultOptions{});
8287
}
8388
};
8489
}
8590

8691
static NativeFunction make_if_zero() {
8792
const std::string name = "if_zero";
8893
return {
89-
3, name, [name](const std::vector<std::shared_ptr<Thunk> > &args, const std::shared_ptr<Env> &) -> Value {
94+
3, name, [name](const std::vector<std::shared_ptr<Thunk> > &args,
95+
const std::shared_ptr<Env> &) -> std::pair<Value, ResultOptions> {
9096
const Value &cond_value = args[0]->force();
9197
if (!std::holds_alternative<double>(cond_value)) {
9298
options_v.logger.error({}, "runtime error: wrong arguments provided to native function ", name,
@@ -96,9 +102,9 @@ namespace intp::interp {
96102
}
97103
// Lazy branching: only force the chosen clause
98104
if (const double cond = std::get<double>(cond_value); cond == 0.0) {
99-
return args[1]->force();
105+
return std::make_pair(Value{args[1]->force()}, ResultOptions{});
100106
}
101-
return args[2]->force();
107+
return std::make_pair(Value{args[2]->force()}, ResultOptions{});
102108
}
103109
};
104110
}
@@ -108,8 +114,9 @@ namespace intp::interp {
108114
}
109115

110116
static Value list_get(const std::shared_ptr<List> &list_v, size_t index) {
111-
if (index >= list_v->elements.size())
112-
throw std::out_of_range("list index out of range");
117+
if (index >= list_v->elements.size()) {
118+
options_v.logger.error({}, "runtime error: list index out of range, index is ", index);
119+
}
113120
return list_v->elements[index];
114121
}
115122

@@ -120,20 +127,66 @@ namespace intp::interp {
120127
static NativeFunction make_list() {
121128
const std::string name = "list";
122129
return {
123-
-1, name, [](const std::vector<std::shared_ptr<Thunk> > &args, const std::shared_ptr<Env> &) -> Value {
130+
-1, name, [](const std::vector<std::shared_ptr<Thunk> > &args,
131+
const std::shared_ptr<Env> &) -> std::pair<Value, ResultOptions> {
124132
std::vector<Value> values;
125133
for (auto &arg: args) {
126134
values.push_back(arg->force());
127135
}
128-
return make_list_obj(values);
136+
return std::make_pair(Value{make_list_obj(values)}, ResultOptions{});
137+
}
138+
};
139+
}
140+
141+
static NativeFunction make_list_size() {
142+
const std::string name = "list_size";
143+
return {
144+
1, name, [name](const std::vector<std::shared_ptr<Thunk> > &args,
145+
const std::shared_ptr<Env> &) -> std::pair<Value, ResultOptions> {
146+
const Value &arg0 = args[0]->force();
147+
if (!std::holds_alternative<std::shared_ptr<List> >(arg0)) {
148+
options_v.logger.error({}, "runtime error: wrong arguments provided to native function ", name,
149+
"\n", name,
150+
" signature: List -> Float""\n"
151+
"runtime error: expected <List> got ", arg0);
152+
}
153+
const auto list_v = std::get<std::shared_ptr<List> >(arg0);
154+
return std::make_pair(Value{static_cast<double>(list_v->elements.size())}, ResultOptions{});
155+
}
156+
};
157+
}
158+
159+
static NativeFunction make_list_get() {
160+
const std::string name = "list_get";
161+
return {
162+
// TODO: Add type checker to replace this manual approach
163+
2, name, [name](const std::vector<std::shared_ptr<Thunk> > &args,
164+
const std::shared_ptr<Env> &) -> std::pair<Value, ResultOptions> {
165+
const Value &arg0 = args[0]->force();
166+
if (!std::holds_alternative<std::shared_ptr<List> >(arg0)) {
167+
options_v.logger.error({}, "runtime error: wrong arguments provided to native function ", name,
168+
"\n", name,
169+
" signature: List -> Float -> List""\n"
170+
"runtime error: expected <List> got ", arg0);
171+
}
172+
const Value &arg1 = args[1]->force();
173+
if (!std::holds_alternative<double>(arg1)) {
174+
options_v.logger.error({}, "runtime error: wrong arguments provided to native function ", name,
175+
"\n", name,
176+
" signature: List -> Float -> List""\n"
177+
"runtime error: expected <Float> got ", arg1);
178+
}
179+
return std::make_pair(Value{list_get(std::get<std::shared_ptr<List> >(arg0), std::get<double>(arg1))},
180+
ResultOptions{});
129181
}
130182
};
131183
}
132184

133185
static NativeFunction make_list_append() {
134186
const std::string name = "list_append";
135187
return {
136-
2, name, [name](const std::vector<std::shared_ptr<Thunk> > &args, const std::shared_ptr<Env> &) -> Value {
188+
2, name, [name](const std::vector<std::shared_ptr<Thunk> > &args,
189+
const std::shared_ptr<Env> &) -> std::pair<Value, ResultOptions> {
137190
const Value &arg0 = args[0]->force();
138191
if (!std::holds_alternative<std::shared_ptr<List> >(arg0)) {
139192
options_v.logger.error({}, "runtime error: wrong arguments provided to native function ", name,
@@ -143,7 +196,7 @@ namespace intp::interp {
143196
}
144197
auto list_v = std::get<std::shared_ptr<List> >(arg0);
145198
list_append(list_v, args[1]->force());
146-
return list_v;
199+
return std::make_pair(Value{list_v}, ResultOptions{});
147200
}
148201
};
149202
}
@@ -158,6 +211,8 @@ namespace intp::interp {
158211
{make_cmp()},
159212
{make_if_zero()},
160213
{make_list()},
214+
{make_list_size()},
215+
{make_list_get()},
161216
{make_list_append()}
162217
};
163218
}

‎src/intp/interpreter.cpp‎

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
namespace intp::interp {
88
static options::Options options_v;
9+
static ResultOptions global_result_options;
910

1011
[[nodiscard]] std::string Closure::to_string() const {
1112
std::ostringstream oss;
@@ -52,6 +53,10 @@ namespace intp::interp {
5253
return os << value.to_string();
5354
}
5455

56+
void ResultOptions::interpolate(const ResultOptions &result_options_) {
57+
side_effects |= result_options_.side_effects;
58+
}
59+
5560
std::ostream &operator<<(std::ostream &os, const List &list) {
5661
return os << list.to_string();
5762
}
@@ -210,18 +215,33 @@ namespace intp::interp {
210215
// If there are no frames left, that's unexpected (shouldn't happen), bail.
211216
if (frames.empty()) {
212217
options_v.logger.error(call_loc, "internal error: no function frame to apply");
213-
std::exit(EXIT_FAILURE);
214218
}
219+
// Work on the top-most frame
220+
Value current_fn = frames.back();
221+
std::optional<Value> resultant_value;
215222
// If we've consumed all available Argument Thunks:
216223
if (idx >= work_args.size()) {
217224
// If top frame is a function, and we have no more args to feed it,
218225
// return that function (partial application) or its value as-is.
219-
// If it's not a function (shouldn't happen), return it as value.
220-
return frames.back();
226+
if (std::holds_alternative<std::shared_ptr<NativeFunction> >(current_fn)) {
227+
// Allow arity==0 and arity==-1 (variadic) to execute with zero args.
228+
if (const auto &native_fn = *std::get<std::shared_ptr<NativeFunction> >(current_fn);
229+
native_fn.arity == 0 || native_fn.arity == -1) {
230+
std::vector<std::shared_ptr<Thunk> > slice; // empty
231+
auto [value, result_options] = native_fn.impl(slice, call_site_env);
232+
global_result_options.interpolate(result_options);
233+
return value;
234+
}
235+
// Non-zero arity native function but no args left: partial application (return the function)
236+
return current_fn;
237+
}
238+
if (std::holds_alternative<Closure>(current_fn)) {
239+
// Closures require one argument; with none left, this is a partial application (return the closure).
240+
return current_fn;
241+
}
242+
// It's not a function (shouldn't happen), return it as value.
243+
return current_fn;
221244
}
222-
// Work on the top-most frame
223-
Value current_fn = frames.back();
224-
std::optional<Value> resultant_value;
225245
// Closure case: Closure consumes exactly one Argument (its Param)
226246
if (std::holds_alternative<Closure>(current_fn)) {
227247
const auto [param, body, env] = std::get<Closure>(current_fn);
@@ -232,8 +252,8 @@ namespace intp::interp {
232252
}
233253
// Native Function case: Consumes its arity-many Argument Thunks
234254
else if (std::holds_alternative<std::shared_ptr<NativeFunction> >(current_fn)) {
235-
const auto &[arity, name, impl] = *std::get<std::shared_ptr<NativeFunction> >(current_fn);
236-
if (arity != -1) {
255+
if (const auto &[arity, name, impl] = *std::get<std::shared_ptr<NativeFunction> >(current_fn);
256+
arity != -1) {
237257
if (work_args.size() - idx < arity) {
238258
options_v.logger.error(call_loc, "runtime error: native function ", name, " expects ", arity,
239259
" argument(s), found ", work_args.size() - idx);
@@ -243,15 +263,19 @@ namespace intp::interp {
243263
for (size_t i = 0; i < arity; ++i) {
244264
slice.push_back(work_args[idx + i]);
245265
}
246-
resultant_value = impl(slice, call_site_env);
266+
auto [resultant_value_, result_options] = impl(slice, call_site_env);
267+
resultant_value = resultant_value_;
268+
global_result_options.interpolate(result_options);
247269
idx += arity;
248270
} else {
249271
std::vector<std::shared_ptr<Thunk> > slice;
250272
slice.reserve(args.size() - idx);
251273
for (size_t i = 0; i < args.size() - idx; ++i) {
252274
slice.push_back(work_args[idx + i]);
253275
}
254-
resultant_value = impl(slice, call_site_env);
276+
auto [resultant_value_, result_options] = impl(slice, call_site_env);
277+
resultant_value = resultant_value_;
278+
global_result_options.interpolate(result_options);
255279
idx += args.size() - idx;
256280
}
257281
} else {
@@ -278,7 +302,6 @@ namespace intp::interp {
278302
if (idx < work_args.size()) {
279303
options_v.logger.error(call_loc, "runtime error: too many arguments applied to non-function value ",
280304
concrete);
281-
std::exit(EXIT_FAILURE);
282305
}
283306
// No Thunks remaining then concrete Value is the result
284307
return concrete;
@@ -327,7 +350,7 @@ namespace intp::interp {
327350
}
328351
}, value);
329352
}
330-
return {*global_env, result_value};
353+
return {*global_env, result_value, global_result_options};
331354
}
332355

333356
void install_builtins(const std::shared_ptr<Env> &env) {

‎src/repl.cpp‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ namespace repl {
3131
fe::parser::Parser parser(tokens);
3232
const std::optional<std::shared_ptr<intp::interp::Env> > temp_env = shared_env;
3333
// Merge loaded_env into shared_env
34-
if (const auto [loaded_env, _] = intp::interp::interpret(parser.program, temp_env, {.own_expr = true});
34+
if (const auto [loaded_env, _, _result_options] = intp::interp::interpret(
35+
parser.program, temp_env, {.own_expr = true});
3536
loaded_env) {
3637
if (!shared_env) {
3738
shared_env = loaded_env;
@@ -146,8 +147,11 @@ namespace repl {
146147
}
147148

148149
// Interpret
149-
const auto [global_env, value] = intp::interp::interpret(
150+
const auto [global_env, value, result_options] = intp::interp::interpret(
150151
parser_v.program, shared_global_env, options_v);
152+
if (result_options.side_effects) {
153+
std::cout << std::endl;
154+
}
151155
std::cout << colors::GREEN << "=> " << value << colors::RESET << std::endl;
152156
shared_global_env = global_env;
153157
} catch (const ControlledExit &) {

0 commit comments

Comments
 (0)