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

Commit dd7847d

Browse files
committed
feat: add support for runtime type checking of native function signatures
1 parent fba858b commit dd7847d

9 files changed

Lines changed: 79 additions & 59 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,4 +125,5 @@ lbd [options]
125125
- TODO: CI/CD for testing.
126126
- TODO: Fix tests output in README.
127127
- TODO: Step debug kind of functionality in REPL. Inside REPL, while loading a file, load one expression at a time and
128-
provide user with some feedback and actions to perform.
128+
provide user with some feedback and actions to perform.
129+
- TODO: Make a pre-commit-hook which does not allow to push to master, if the tests are failing.

‎include/lbd/runtime/builtins.h‎

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,21 @@
11
#pragma once
22

3+
#include <lbd/options.h>
34
#include <lbd/runtime/interpreter.h>
45
#include <lbd/runtime/type.h>
5-
#include <lbd/options.h>
66

77
namespace runtime::builtins {
88
extern context::Options optionsValue;
99

1010
/// Helper functions to make defining native-functions easier.
1111
using TypePointer = std::shared_ptr<type::Type>;
1212

13-
inline TypePointer simpleType(type::TypeTag tag) {
14-
return std::make_shared<type::SimpleType>(tag);
15-
}
16-
17-
inline TypePointer listType() {
18-
return std::make_shared<type::ListType>();
19-
}
13+
TypePointer simpleType(type::TypeTag tag, bool hardCheck = true);
2014

21-
inline std::shared_ptr<type::FunctionType> functionType(const std::vector<TypePointer> &argumentTypes,
22-
const TypePointer &returnType,
23-
bool isVariadic = false) {
24-
return std::make_shared<type::FunctionType>(argumentTypes, returnType, isVariadic);
25-
}
15+
TypePointer listType();
2616

27-
// TODO: Move the definitions to source-file.
17+
std::shared_ptr<type::FunctionType> functionType(const std::vector<TypePointer> &argumentTypes,
18+
const TypePointer &returnType, bool isVariadic = false);
2819

2920
// TODO: Generate the SIGNATURE documentation on-demand, rather than editing it manually.
3021

‎include/lbd/runtime/interpreter.h‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ namespace runtime {
5858
struct ResultOptions {
5959
bool sideEffects = false;
6060

61-
// TODO: _ may not be needed here
6261
void interpolate(const ResultOptions &resultOptions);
6362
};
6463

@@ -128,7 +127,7 @@ namespace runtime {
128127

129128
std::vector<Value> forceArguments(const std::vector<std::shared_ptr<Thunk> > &arguments);
130129

131-
Value applyFunctionApplication(Value functionName, const std::vector<std::shared_ptr<Thunk> > &arguments,
130+
Value applyFunctionApplication(const Value &functionName, const std::vector<std::shared_ptr<Thunk> > &arguments,
132131
const std::shared_ptr<Environment> &callSiteEnvironment,
133132
const std::optional<frontend::Location> &callLocation = std::nullopt);
134133

‎include/lbd/runtime/type.h‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,16 @@ namespace runtime::type {
3939
[[nodiscard]] virtual std::string toString() const = 0;
4040

4141
[[nodiscard]] virtual bool isCallable() const = 0;
42+
43+
[[nodiscard]] bool allowsHardCheck() const;
44+
45+
protected:
46+
/// Do not force the thunk for type-checking if disabled, check using the thunk's unevaluated-expression.
47+
bool hardCheck = true;
4248
};
4349

4450
struct SimpleType final : Type {
45-
explicit SimpleType(TypeTag tag);
51+
explicit SimpleType(TypeTag tag, bool hardCheck = true);
4652

4753
~SimpleType() override = default;
4854

@@ -72,7 +78,6 @@ namespace runtime::type {
7278
};
7379

7480
struct FunctionType final : Type {
75-
// TODO: Add support for void return-type. Maybe if return-type is nullptr: then it is void.
7681
explicit FunctionType(const std::vector<std::shared_ptr<Type> > &argumentTypes,
7782
const std::shared_ptr<Type> &returnType, bool isVariadic = false);
7883

@@ -82,7 +87,7 @@ namespace runtime::type {
8287

8388
[[nodiscard]] bool matches(const Value &value) const override;
8489

85-
[[nodiscard]] bool matchesArgumentTypes(const std::vector<Value> &values) const;
90+
[[nodiscard]] bool matchesArgumentTypes(const std::vector<std::shared_ptr<Thunk> > &thunks) const;
8691

8792
[[nodiscard]] bool matchesReturnType(const Value &value) const;
8893

‎src/runtime/builtin-modules/builtin_module_core.cpp‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,11 @@ namespace runtime::builtins {
112112
const std::string name = "if_zero";
113113
// TODO: The return-type should be either argument-1 or argument-2.
114114
const auto signature = functionType(
115-
{simpleType(type::TypeTag::Float), simpleType(type::TypeTag::Any), simpleType(type::TypeTag::Any)},
115+
{
116+
simpleType(type::TypeTag::Float),
117+
simpleType(type::TypeTag::Any, false),
118+
simpleType(type::TypeTag::Any, false)
119+
},
116120
simpleType(type::TypeTag::Any)
117121
);
118122
return {
@@ -121,8 +125,7 @@ namespace runtime::builtins {
121125
const Value &conditionValue = arguments[0]->force();
122126
if (!std::holds_alternative<double>(conditionValue)) {
123127
optionsValue.logger.error({}, "runtime error: wrong arguments provided to native function ", name,
124-
"\n", name,
125-
" signature: Float -> A -> B -> A|B\n"
128+
"\n", name, " signature: Float -> A -> B -> A|B\n"
126129
"runtime error: expected <double> got ", conditionValue);
127130
}
128131
// Lazy branching: only force the chosen clause.

‎src/runtime/builtin-modules/builtin_module_list.cpp‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -339,13 +339,13 @@ namespace runtime::builtins {
339339
NativeFunction makeFoldRight() {
340340
const std::string name = "foldr";
341341
// TODO: Implement indexed any-type, as the type should ideally be:
342-
// (Any1 -> Any2 -> Any2) -> [Any1] -> Any2 -> Any2
342+
// (Any1 -> Any2 -> Any2) -> Any2 -> [Any1] -> Any2
343343
const auto signature = functionType(
344344
{
345345
functionType({simpleType(type::TypeTag::Any), simpleType(type::TypeTag::Any)},
346346
simpleType(type::TypeTag::Any)),
347-
listType(),
348-
simpleType(type::TypeTag::Any)
347+
simpleType(type::TypeTag::Any),
348+
listType()
349349
},
350350
simpleType(type::TypeTag::Any)
351351
);

‎src/runtime/builtins.cpp‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,19 @@
88
namespace runtime::builtins {
99
context::Options optionsValue;
1010

11+
TypePointer simpleType(type::TypeTag tag, bool hardCheck) {
12+
return std::make_shared<type::SimpleType>(tag, hardCheck);
13+
}
14+
15+
TypePointer listType() {
16+
return std::make_shared<type::ListType>();
17+
}
18+
19+
std::shared_ptr<type::FunctionType> functionType(const std::vector<TypePointer> &argumentTypes,
20+
const TypePointer &returnType, bool isVariadic) {
21+
return std::make_shared<type::FunctionType>(argumentTypes, returnType, isVariadic);
22+
}
23+
1124
std::vector<NativeFunction> getBuiltins(const context::Options options_) {
1225
optionsValue = options_;
1326
return {

‎src/runtime/interpreter.cpp‎

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,8 @@ namespace runtime {
214214
for (const auto &argument: functionApplication.arguments) {
215215
argumentThunks.push_back(std::make_shared<Thunk>(argument.get(), environment));
216216
}
217-
return applyFunctionApplication(functionValue, argumentThunks, environment, functionApplication.location);
217+
frontend::Location newLocation = functionApplication.location;
218+
return applyFunctionApplication(functionValue, argumentThunks, environment, newLocation);
218219
}
219220

220221
Value evalExpression(const frontend::Expression &expression, std::shared_ptr<Environment> environment) {
@@ -241,24 +242,15 @@ namespace runtime {
241242
return thunk;
242243
}
243244

244-
std::vector<Value> forceArguments(const std::vector<std::shared_ptr<Thunk> > &arguments) {
245-
std::vector<Value> values;
246-
values.reserve(arguments.size());
247-
for (const auto &argument: arguments) {
248-
values.push_back(argument->force());
249-
}
250-
return values;
251-
}
252-
253-
Value applyFunctionApplication(Value functionName, const std::vector<std::shared_ptr<Thunk> > &arguments,
245+
Value applyFunctionApplication(const Value &functionName, const std::vector<std::shared_ptr<Thunk> > &arguments,
254246
const std::shared_ptr<Environment> &callSiteEnvironment,
255247
const std::optional<frontend::Location> &callLocation) {
256248
// Local mutable copy of the args, for inserting evaluated values as Thunks when needed.
257249
std::vector<std::shared_ptr<Thunk> > workArguments = arguments;
258250
// Frame Stack: Functions to which we are currently applying Arguments.
259251
// Start with the initial Function.
260252
std::vector<Value> frames;
261-
frames.push_back(std::move(functionName));
253+
frames.push_back(functionName);
262254
size_t index = 0; // Index of next Thunk to consume from work_args
263255
while (true) {
264256
// If there are no frames left, that's unexpected (shouldn't happen), bail.
@@ -316,17 +308,15 @@ namespace runtime {
316308
for (size_t i = 0; i < arity; ++i) {
317309
slice.push_back(workArguments[index + i]);
318310
}
319-
// // ReSharper disable once CppTooWideScopeInitStatement
320-
// std::vector<Value> releasedValues = forceArguments(slice);
321-
// // Type-Check the function-arguments.
322-
// if (!signature.matchesArgumentTypes(releasedValues)) {
323-
// optionsValue.logger.error(callLocation,
324-
// "runtime error: wrong arguments provided to native-function ",
325-
// nativeFunction.getName(),
326-
// "\n", nativeFunction.getName(), " signature: ", signature.toString());
327-
// // TODO: Also print exactly which argument caused the error.
328-
// // Print the signature got, with the incorrect argument color differently.
329-
// }
311+
// Type-Check the function-arguments.
312+
if (!signature.matchesArgumentTypes(slice)) {
313+
optionsValue.logger.error(callLocation,
314+
"runtime error: wrong arguments provided to native-function ",
315+
nativeFunction.getName(),
316+
"\n", nativeFunction.getName(), " signature: ", signature.toString());
317+
// TODO: Also print exactly which argument caused the error.
318+
// Print the signature got, with the incorrect argument color differently.
319+
}
330320
auto [resultantValue_, resultOptions] = nativeFunction.getImplementation()(
331321
slice, callSiteEnvironment);
332322
if (!signature.matchesReturnType(resultantValue_)) {

‎src/runtime/type.cpp‎

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,22 @@ namespace runtime::type {
4343
}
4444
}
4545

46-
SimpleType::SimpleType(const TypeTag tag) : tag(tag) {
46+
bool Type::allowsHardCheck() const {
47+
return hardCheck;
48+
}
49+
50+
SimpleType::SimpleType(const TypeTag tag, const bool hardCheck) : tag(tag) {
51+
this->hardCheck = hardCheck;
4752
}
4853

4954
bool SimpleType::matches(const Value &value) const {
5055
return tag == TypeTag::Any || tag == typeTagFromValue(value);
5156
}
5257

5358
bool SimpleType::equals(const Type &otherType) const {
59+
if (tag == TypeTag::Any) {
60+
return true;
61+
}
5462
if (const auto *otherSimpleType = dynamic_cast<const SimpleType *>(&otherType)) {
5563
return tag == otherSimpleType->tag;
5664
}
@@ -99,21 +107,31 @@ namespace runtime::type {
99107

100108

101109
bool FunctionType::matches(const Value &value) const {
102-
// Only type-check function-applications, cannot type-check
103-
// closures/lambda-expressions due to lazy-evaluation.
110+
// TODO: Revisit this.
111+
// Cannot type-check closures due to lazy-evaluation, thus assuming it to be equal.
112+
if (std::holds_alternative<Closure>(value)) {
113+
return true;
114+
}
104115
if (!std::holds_alternative<std::shared_ptr<NativeFunction> >(value)) {
105116
return false;
106117
}
107118
return equals(*std::get<std::shared_ptr<NativeFunction> >(value)->getSignature());
108119
}
109120

110-
bool FunctionType::matchesArgumentTypes(const std::vector<Value> &values) const {
121+
bool FunctionType::matchesArgumentTypes(const std::vector<std::shared_ptr<Thunk> > &thunks) const {
111122
// Perform size-check on non-variadic-functions.
112-
if (!isVariadic && argumentTypes.size() != values.size()) {
123+
if (!isVariadic && argumentTypes.size() != thunks.size()) {
113124
return false;
114125
}
115126
for (size_t index = 0; index < argumentTypes.size(); index++) {
116-
if (!argumentTypes[index] || !argumentTypes[index]->matches(values[index])) {
127+
if (!argumentTypes[index]) {
128+
return false;
129+
}
130+
if (!argumentTypes[index]->allowsHardCheck()) {
131+
// TODO: Perform soft-check using thunk's unevaluated expression.
132+
continue;
133+
}
134+
if (!argumentTypes[index]->matches(thunks[index]->force())) {
117135
return false;
118136
}
119137
}
@@ -154,15 +172,15 @@ namespace runtime::type {
154172
std::ostringstream outputStringStream;
155173
for (auto &argumentType: argumentTypes) {
156174
if (argumentType->isCallable()) {
157-
outputStringStream << "(" << argumentType->toString() << ") ";
175+
outputStringStream << "(" << argumentType->toString() << ") -> ";
158176
} else {
159-
outputStringStream << argumentType->toString() << " ";
177+
outputStringStream << argumentType->toString() << " -> ";
160178
}
161179
}
162180
if (returnType->isCallable()) {
163-
outputStringStream << "(" << returnType->toString() << ") ";
181+
outputStringStream << "(" << returnType->toString() << ")";
164182
} else {
165-
outputStringStream << returnType->toString() << " ";
183+
outputStringStream << returnType->toString();
166184
}
167185
return outputStringStream.str();
168186
}

0 commit comments

Comments
 (0)