Skip to content

Commit c0ef93b

Browse files
authored
Merge pull request #145 from chr1st0scli/release-1.2.0
Add extension methods for easier consumption by .NET systems.
2 parents 47649ec + 573b7b9 commit c0ef93b

File tree

9 files changed

+438
-54
lines changed

9 files changed

+438
-54
lines changed

CHANGELOG.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,15 @@ Semantic versioning is followed.
1616

1717
### Removed
1818

19-
## [1.1.0] - 2024-01-28
19+
## [1.2.0] - 2024-02-18
2020

2121
### Added
22-
- Ceiling and floor primitive procedures.
22+
- Add extension methods in the `RainLisp.DotNetIntegration` namespace for easier consumption by .NET systems.
2323

24-
### Fixed
25-
26-
### Changed
24+
## [1.1.0] - 2024-01-28
2725

28-
### Removed
26+
### Added
27+
- Ceiling and floor primitive procedures.
2928

3029
## [1.0.0] - 2023-07-20
3130

RainLisp/Docs/dotnet-integration.md

Lines changed: 54 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,42 @@ Your .NET system can specify an overall computational infrastructure and call Ra
1111
1212
> The .NET examples below are written in C# but any .NET language can be used.
1313
14+
## Evaluating RainLisp Expressions
15+
`IInterpreter` defines `Evaluate` method overloads for evaluating RainLisp expressions, i.e. a program. These methods typically return an `IEnumerable<EvaluationResult>`
16+
and the actual evaluation of each RainLisp expression occurs on a per request basis, i.e. while the enumeration is enumerated.
17+
18+
Let's see a simple example.
19+
20+
```csharp
21+
using RainLisp;
22+
using RainLisp.DotNetIntegration;
23+
24+
const string RAIN_LISP_CODE = @"
25+
(+ 1 1)
26+
(- 10 2)
27+
(* 4 7)
28+
9";
29+
30+
var interpreter = new Interpreter();
31+
var results = interpreter.Evaluate(RAIN_LISP_CODE);
32+
33+
foreach (var result in results)
34+
{
35+
Console.WriteLine(result.Number());
36+
}
37+
```
38+
39+
`RAIN_LISP_CODE` above contains the RainLisp expressions, each on a new line. Each expression is evaluated at runtime while `results` are enumerated in the `foreach` loop.
40+
41+
> Note that all subsequent examples use the `Execute` overloads which mirror the `Evaluate` ones. `Execute` methods evaluate all RainLisp expressions at once and
42+
return the `EvaluationResult` of the last one.
43+
1444
## One-off Call
1545

1646
Let's suppose there is a custom RainLisp code that specifies a log file's name for a .NET system.
1747
It first prints a message to the standard output and finally returns the log file name by concatenating some strings.
18-
Conventionally, the RainLisp coder knows that the last thing they should return is a string.
48+
Conventionally, the RainLisp coder knows that the last thing they should return is a string and this effectively reflects
49+
the programming contract between the two systems.
1950

2051
```scheme
2152
(define dt (utc-now))
@@ -29,21 +60,17 @@ The above code can be stored in a database or some other configuration medium. F
2960
Then the .NET system can evaluate it as follows.
3061

3162
```csharp
32-
using System.Linq;
3363
using RainLisp;
34-
using RainLisp.Evaluation.Results;
64+
using RainLisp.DotNetIntegration;
3565

3666
var interpreter = new Interpreter();
3767

38-
var result = interpreter.Evaluate(RAIN_LISP_CODE).Last();
39-
string logFileName = ((StringDatum)result).Value;
68+
var result = interpreter.Execute(RAIN_LISP_CODE);
69+
string logFileName = result.String();
4070

4171
Console.WriteLine($"Calculated log file name: {logFileName}.");
4272
```
4373

44-
Notice that only the last evaluation result (LINQ's Last() method call) is taken into consideration, which is expected to be a `StringDatum`. This effectively reflects
45-
the programming contract between the two systems.
46-
4774
> Note that exception handling in the C# example is omitted for brevity.
4875
4976
## Consecutive Calls
@@ -66,41 +93,32 @@ and the second one will call it with an appropriate value.
6693
Once again, assume that the above code is loaded into `RAIN_LISP_CODE`.
6794

6895
```csharp
69-
using System.Linq;
7096
using RainLisp;
97+
using RainLisp.DotNetIntegration;
7198
using RainLisp.Evaluation;
72-
using RainLisp.Evaluation.Results;
7399

74100
var interpreter = new Interpreter();
75101

76102
// Create the procedure.
77103
IEvaluationEnvironment? environment = null;
78-
_ = interpreter.Evaluate(RAIN_LISP_CODE, ref environment)
79-
.Last();
104+
_ = interpreter.Execute(RAIN_LISP_CODE, ref environment);
80105

81106
// Call it for month February.
82-
var result = interpreter.Evaluate("(get-monthly-ratio 2)", ref environment)
83-
.Last();
107+
var result = interpreter.Execute("(get-monthly-ratio 2)", ref environment);
84108

85-
double ratio = ((NumberDatum)result).Value;
109+
double ratio = result.Number();
86110

87111
// Write the result to the standard output.
88112
Console.WriteLine($"The calculation ratio for February is: {ratio}.");
89113
```
90114

91115
In order for one evaluation to take into consideration and build onto another, a common `IEvaluationEnvironment` is used.
92116

93-
The first call to `Evaluate` creates the `get-monthly-ratio` procedure.
117+
The first call to `Execute` creates the `get-monthly-ratio` procedure.
94118

95-
> `Evaluate` returns an `IEnumerable<EvaluationResult>`. Therefore, notice that the LINQ `Last` method is called to force the enumeration
96-
and therefore actually evaluate the code. If this is not done, RainLisp code will actually never be executed.
97-
98-
The second time `Evaluate` is called the February's ratio is asked `(get-monthly-ratio 2)`. Note that the existing `environment` is used so that
119+
The second time `Execute` is called the February's ratio is asked `(get-monthly-ratio 2)`. Note that the existing `environment` is used so that
99120
the previous evaluation, the procedure creation, is still in effect.
100121

101-
> Once again, the `Last` method is used to force the evaluation, even though other LINQ techniques can apply. For this particular example,
102-
`First` could also work since `(get-monthly-ratio 2)` is the only call that is made.
103-
104122
In this example, the contract is that the RainLisp code implements a procedure called `get-monthly-ratio` which takes a month number
105123
as a parameter and returns a numeric ratio.
106124

@@ -110,7 +128,7 @@ a possible malicious code injection.
110128

111129
### Improving Performance
112130

113-
The two `Evaluate` method flavors we have seen so far, have respective overloads accepting a `RainLisp.AbstractSyntaxTree.Program` instance,
131+
The two `Execute` method flavors we have seen so far, have respective overloads accepting a `RainLisp.AbstractSyntaxTree.Program` instance,
114132
which is an abstract syntax tree, instead of the code as a `string`. When you know that the RainLisp code is unlikely to change, you can
115133
use these calls to speed up the evaluation. For example, you can cache the result of the lexical and grammar syntax analysis and skip these steps
116134
in consecutive calls by calling the aforementioned overloads. If your code is simple, you can even always skip the analysis phases by specifying an
@@ -119,11 +137,10 @@ abstract syntax tree directly, effectively treating code as data.
119137
Let's see this in action.
120138

121139
```csharp
122-
using System.Linq;
123140
using RainLisp;
124141
using RainLisp.AbstractSyntaxTree;
142+
using RainLisp.DotNetIntegration;
125143
using RainLisp.Evaluation;
126-
using RainLisp.Evaluation.Results;
127144
using RainLisp.Parsing;
128145
using RainLisp.Tokenization;
129146

@@ -149,8 +166,7 @@ var program = parser.Parse(tokens);
149166
var interpreter = new Interpreter();
150167
IEvaluationEnvironment? environment = null;
151168
// We evaluate the abstract syntax tree instead of source code.
152-
_ = interpreter.Evaluate(program, ref environment)
153-
.Last();
169+
_ = interpreter.Execute(program, ref environment);
154170

155171
// For the actual call to get-monthly-ratio, we are taking a different approach.
156172
// Since it is just a simple procedure call (application), we build the abstract syntax tree ourselves, effectively treating code as data.
@@ -164,10 +180,9 @@ var procedureCallProgram = new RainLisp.AbstractSyntaxTree.Program
164180
};
165181

166182
// We evaluate the manually built abstract syntax tree
167-
var result = interpreter.Evaluate(procedureCallProgram, ref environment)
168-
.Last();
183+
var result = interpreter.Execute(procedureCallProgram, ref environment);
169184

170-
double ratio = ((NumberDatum)result).Value;
185+
double ratio = result.Number();
171186

172187
// Write the result to the standard output.
173188
Console.WriteLine($"The calculation ratio for February is: {ratio}.");
@@ -241,28 +256,25 @@ Below, it is demonstrated how the .NET system could use the above code and acces
241256
Once again, assume the above code is stored in `RAIN_LISP_CODE`.
242257

243258
```csharp
244-
using System.Linq;
245259
using RainLisp;
260+
using RainLisp.DotNetIntegration;
246261
using RainLisp.Evaluation;
247-
using RainLisp.Evaluation.Results;
248262

249263
// Install the necessary RainLisp code for payroll calculation.
250264
var interpreter = new Interpreter();
251265
IEvaluationEnvironment? environment = null;
252-
_ = interpreter.Evaluate(RAIN_LISP_CODE, ref environment)
253-
.Last();
266+
_ = interpreter.Execute(RAIN_LISP_CODE, ref environment);
254267

255268
// Calculate the payroll details for an unmarried employee that gets a 5000 gross income.
256269
// For simplicity, we are using RainLisp code for the calls but we could build our own AST, as we have seen before.
257-
_ = interpreter.Evaluate("(define payroll (calculate-payroll 5000 false))", ref environment)
258-
.Last();
270+
_ = interpreter.Execute("(define payroll (calculate-payroll 5000 false))", ref environment);
259271

260272
// Get the calculated payroll details.
261-
double tax = ((NumberDatum)interpreter.Evaluate("(get-tax payroll)", ref environment).First()).Value;
262-
double insurance = ((NumberDatum)interpreter.Evaluate("(get-insurance payroll)", ref environment).First()).Value;
263-
double netIncome = ((NumberDatum)interpreter.Evaluate("(get-net-income payroll)", ref environment).First()).Value;
264-
bool isMarried = ((BoolDatum)interpreter.Evaluate("(get-marital-status payroll)", ref environment).First()).Value;
265-
DateTime payDate = ((DateTimeDatum)interpreter.Evaluate("(get-paydate payroll)", ref environment).First()).Value;
273+
double tax = interpreter.Execute("(get-tax payroll)", ref environment).Number();
274+
double insurance = interpreter.Execute("(get-insurance payroll)", ref environment).Number();
275+
double netIncome = interpreter.Execute("(get-net-income payroll)", ref environment).Number();
276+
bool isMarried = interpreter.Execute("(get-marital-status payroll)", ref environment).Bool();
277+
DateTime payDate = interpreter.Execute("(get-paydate payroll)", ref environment).DateTime();
266278

267279
// Write them to the standard output.
268280
Console.WriteLine($"Bob is {(!isMarried ? "not" : "")} married, pays {tax} tax, {insurance} insurance and is getting paid {netIncome} on {payDate}.");

0 commit comments

Comments
 (0)