monty-rb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Cargo.toml +9 -0
- data/README.md +251 -0
- data/ext/monty/Cargo.toml +17 -0
- data/ext/monty/extconf.rb +6 -0
- data/ext/monty/src/errors.rs +130 -0
- data/ext/monty/src/lib.rs +20 -0
- data/ext/monty/src/monty_object.rs +211 -0
- data/ext/monty/src/monty_run.rs +243 -0
- data/ext/monty/src/resource_limits.rs +88 -0
- data/ext/monty/src/run_progress.rs +362 -0
- data/lib/monty/run.rb +150 -0
- data/lib/monty/version.rb +5 -0
- data/lib/monty.rb +14 -0
- metadata +114 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2fb3f19f561ec5c1c423e4f285717613ee92d9007cfb7ca0e9a87c00f912449b
|
|
4
|
+
data.tar.gz: 2ecf1a8636e3945f82677cbed4a942f47026c89be25ace25466fd969ab016ad2
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ebbe528825cd7b82728b125a28b303134526c3dbc3edb9afeec90f95041dafb36d542e4946bc112c92659c46a447babdc93548cd1082db6b4e124f63fbb7a0a4
|
|
7
|
+
data.tar.gz: 7e094af7174ecceea7dbe88f4a208a446e39446d2a313bbe48819db263448687eb7b189308a2ece9e5e8652752dab6c0cdb1f1e0143ae65f6dac3781e590b92b
|
data/Cargo.toml
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# Monty Ruby
|
|
2
|
+
|
|
3
|
+
Ruby bindings for [Monty](https://github.com/pydantic/monty), a minimal and secure Python interpreter written in Rust by [Pydantic](https://pydantic.dev). Built with [Magnus](https://github.com/matsadler/magnus).
|
|
4
|
+
|
|
5
|
+
## What is Monty?
|
|
6
|
+
|
|
7
|
+
[Monty](https://github.com/pydantic/monty) is a Python interpreter designed specifically for AI agents. Unlike CPython, it prioritizes security and embeddability over compatibility with the full Python ecosystem.
|
|
8
|
+
|
|
9
|
+
Key properties of the Monty interpreter:
|
|
10
|
+
|
|
11
|
+
- **Sub-microsecond startup** — no VM boot, no module loading, ready instantly
|
|
12
|
+
- **Strict sandboxing** — no filesystem, no network, no environment variable access by default
|
|
13
|
+
- **CPython-comparable performance** — not a toy interpreter
|
|
14
|
+
- **External function mediation** — all I/O is controlled by the host application
|
|
15
|
+
- **Snapshot/resume** — serialize execution state mid-flight and restore it later
|
|
16
|
+
- **Resource limits** — cap memory, allocations, execution time, and recursion depth
|
|
17
|
+
|
|
18
|
+
Monty supports a practical Python subset: functions, closures, async/await, type hints, dataclasses, list/dict/set comprehensions, and exceptions. It does not support arbitrary classes (coming soon), match statements, or the Python standard library beyond `sys`, `typing`, `asyncio`, `dataclasses`, and `json`.
|
|
19
|
+
|
|
20
|
+
## Why Ruby bindings?
|
|
21
|
+
|
|
22
|
+
If you're building AI agents, tool-use pipelines, or code evaluation features in Ruby, Monty lets you safely execute LLM-generated Python without:
|
|
23
|
+
|
|
24
|
+
- **Containers or VMs** — no Docker, no subprocess spawning, no cold starts
|
|
25
|
+
- **Security risks** — the interpreter can't touch the filesystem, network, or env vars unless you explicitly allow it via external functions
|
|
26
|
+
- **Language mismatch** — your orchestration stays in Ruby while computation happens in Python
|
|
27
|
+
|
|
28
|
+
### Use cases
|
|
29
|
+
|
|
30
|
+
- **AI agent tool execution** — LLMs generate Python code; your Ruby app runs it safely and returns the result
|
|
31
|
+
- **Computed fields and expressions** — let users write Python expressions that your app evaluates (e.g. spreadsheet formulas, data transformations, scoring functions)
|
|
32
|
+
- **Sandboxed scripting** — embed a user-facing scripting language in your Ruby application with deterministic resource limits
|
|
33
|
+
- **Polyglot data pipelines** — bridge Python's data manipulation idioms (list comprehensions, dict operations) into a Ruby-native workflow
|
|
34
|
+
- **Code evaluation APIs** — build "run this code" endpoints without worrying about arbitrary code execution
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
> [!NOTE] Experimental gem
|
|
38
|
+
|
|
39
|
+
This gem was created as an experiment to see how Opus 4.6 and ChatGPT Codex 5.3 would be able to take the Monty crate from the Pydantic team and bind it to Ruby. All of the code in this gem has been generated by AI Agents.
|
|
40
|
+
|
|
41
|
+
## How the bindings work
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
┌─────────────────────────────────────────────────────┐
|
|
45
|
+
│ Ruby application │
|
|
46
|
+
│ │
|
|
47
|
+
│ Monty::Run.new(python_code, inputs: ["x"]) │
|
|
48
|
+
│ run.call(42) # => result │
|
|
49
|
+
│ │
|
|
50
|
+
├─────────────────────────────────────────────────────┤
|
|
51
|
+
│ Ruby wrappers (lib/monty/) │
|
|
52
|
+
│ - Keyword arguments, blocks, idiomatic Ruby API │
|
|
53
|
+
│ - call_with_externals { |fn| ... } │
|
|
54
|
+
│ │
|
|
55
|
+
├─────────────────────────────────────────────────────┤
|
|
56
|
+
│ Magnus FFI layer (ext/monty/src/) │
|
|
57
|
+
│ - #[magnus::wrap] structs → Ruby classes │
|
|
58
|
+
│ - MontyObject ↔ Ruby value conversion │
|
|
59
|
+
│ - Error mapping: MontyException → Monty::Error │
|
|
60
|
+
│ │
|
|
61
|
+
├─────────────────────────────────────────────────────┤
|
|
62
|
+
│ Monty interpreter (Rust crate) │
|
|
63
|
+
│ - MontyRun: parse once, execute many times │
|
|
64
|
+
│ - Sandboxed VM with resource tracking │
|
|
65
|
+
│ - External function call / snapshot / resume │
|
|
66
|
+
└─────────────────────────────────────────────────────┘
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The native extension is compiled as a C-compatible dynamic library (`cdylib`) via [rb-sys](https://github.com/oxidize-rb/rb-sys) and [Magnus](https://github.com/matsadler/magnus). No tokio runtime is needed — Monty's execution is synchronous from Rust's perspective (Python `async/await` is handled internally by the interpreter's cooperative state machine).
|
|
70
|
+
|
|
71
|
+
**Value conversion** happens at the boundary: Ruby objects are converted to `MontyObject` variants on the way in, and converted back to Ruby objects on the way out. This means there's no shared mutable state between Ruby and Python — each call is a clean value-in, value-out exchange.
|
|
72
|
+
|
|
73
|
+
**External functions** allow Python code to call back into Ruby. When the interpreter hits an external function call, it pauses execution and returns a `Monty::FunctionCall` to the host. The host resolves the call in Ruby and resumes the interpreter with the result. This is how you give sandboxed Python controlled access to I/O, databases, APIs, or anything else.
|
|
74
|
+
|
|
75
|
+
## Installation
|
|
76
|
+
|
|
77
|
+
Add to your Gemfile:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
gem "monty-rb"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Or install directly:
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
gem install monty-rb
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Requires Rust 1.90+ for compilation.
|
|
90
|
+
|
|
91
|
+
## Usage
|
|
92
|
+
|
|
93
|
+
### Simple Execution
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
require "monty"
|
|
97
|
+
|
|
98
|
+
# Evaluate a Python expression
|
|
99
|
+
run = Monty::Run.new("x + y", inputs: ["x", "y"])
|
|
100
|
+
run.call(1, 2) # => 3
|
|
101
|
+
|
|
102
|
+
# Python functions
|
|
103
|
+
code = <<~PYTHON
|
|
104
|
+
def factorial(n):
|
|
105
|
+
if n <= 1:
|
|
106
|
+
return 1
|
|
107
|
+
return n * factorial(n - 1)
|
|
108
|
+
|
|
109
|
+
factorial(n)
|
|
110
|
+
PYTHON
|
|
111
|
+
|
|
112
|
+
run = Monty::Run.new(code, inputs: ["n"])
|
|
113
|
+
run.call(10) # => 3628800
|
|
114
|
+
|
|
115
|
+
# Reusable — parse once, call many times
|
|
116
|
+
run = Monty::Run.new("x * 2", inputs: ["x"])
|
|
117
|
+
run.call(5) # => 10
|
|
118
|
+
run.call(21) # => 42
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Data Type Conversion
|
|
122
|
+
|
|
123
|
+
Ruby values are automatically converted to Python and back:
|
|
124
|
+
|
|
125
|
+
| Ruby | Python | Ruby |
|
|
126
|
+
|------|--------|------|
|
|
127
|
+
| `nil` | `None` | `nil` |
|
|
128
|
+
| `true` / `false` | `True` / `False` | `true` / `false` |
|
|
129
|
+
| `Integer` | `int` | `Integer` |
|
|
130
|
+
| `Float` | `float` | `Float` |
|
|
131
|
+
| `String` | `str` | `String` |
|
|
132
|
+
| `Array` | `list` | `Array` |
|
|
133
|
+
| `Hash` | `dict` | `Hash` |
|
|
134
|
+
| `Symbol` | `str` | `String` |
|
|
135
|
+
|
|
136
|
+
Tuples are returned as frozen Arrays. Nested structures are converted recursively.
|
|
137
|
+
|
|
138
|
+
### Capturing Output
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
run = Monty::Run.new("print('hello')\n42")
|
|
142
|
+
result = run.call(capture_output: true)
|
|
143
|
+
result[:result] # => 42
|
|
144
|
+
result[:output] # => "hello\n"
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Resource Limits
|
|
148
|
+
|
|
149
|
+
Prevent runaway code from consuming unbounded resources:
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
run = Monty::Run.new("x ** x ** x", inputs: ["x"])
|
|
153
|
+
run.call(100, limits: {
|
|
154
|
+
max_duration: 2.0, # seconds
|
|
155
|
+
max_memory: 10_485_760, # bytes
|
|
156
|
+
max_allocations: 100_000,
|
|
157
|
+
max_recursion_depth: 500
|
|
158
|
+
})
|
|
159
|
+
# Raises Monty::ResourceError if any limit is exceeded
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### External Function Calls
|
|
163
|
+
|
|
164
|
+
Monty scripts can call external functions that you implement in Ruby. This is the primary mechanism for giving sandboxed Python controlled access to external resources:
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
code = <<~PYTHON
|
|
168
|
+
response = fetch("https://api.example.com/data")
|
|
169
|
+
response.upper()
|
|
170
|
+
PYTHON
|
|
171
|
+
|
|
172
|
+
run = Monty::Run.new(code, external_functions: ["fetch"])
|
|
173
|
+
|
|
174
|
+
# Block-based API (recommended)
|
|
175
|
+
result = run.call_with_externals do |call|
|
|
176
|
+
case call.function_name
|
|
177
|
+
when "fetch"
|
|
178
|
+
Net::HTTP.get(URI(call.args[0]))
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Capture output and apply limits
|
|
183
|
+
result = run.call_with_externals(capture_output: true, limits: { max_duration: 2.0 }) do |call|
|
|
184
|
+
case call.function_name
|
|
185
|
+
when "fetch"
|
|
186
|
+
Net::HTTP.get(URI(call.args[0]))
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
# result[:result] => Python return value
|
|
190
|
+
# result[:output] => captured stdout
|
|
191
|
+
|
|
192
|
+
# Manual step-through API
|
|
193
|
+
run = Monty::Run.new(code, external_functions: ["fetch"])
|
|
194
|
+
progress = run.start
|
|
195
|
+
|
|
196
|
+
while progress.is_a?(Monty::FunctionCall)
|
|
197
|
+
result = handle_function(progress.function_name, progress.args)
|
|
198
|
+
progress = progress.resume(result)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
final_value = progress.value
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Serialization
|
|
205
|
+
|
|
206
|
+
`Monty::Run` instances can be serialized for caching or storage. Since parsing is separated from execution, you can parse once and reuse across requests:
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
run = Monty::Run.new("x * 2", inputs: ["x"])
|
|
210
|
+
bytes = run.dump
|
|
211
|
+
|
|
212
|
+
# Later, or in another process...
|
|
213
|
+
restored = Monty::Run.load(bytes)
|
|
214
|
+
restored.call(21) # => 42
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Error Handling
|
|
218
|
+
|
|
219
|
+
```ruby
|
|
220
|
+
# Monty::Error - base error class (< StandardError)
|
|
221
|
+
# Monty::SyntaxError - Python syntax errors
|
|
222
|
+
# Monty::ResourceError - resource limit exceeded
|
|
223
|
+
# Monty::ConsumedError - using a consumed Run/FunctionCall
|
|
224
|
+
|
|
225
|
+
begin
|
|
226
|
+
run = Monty::Run.new("1 / 0")
|
|
227
|
+
run.call
|
|
228
|
+
rescue Monty::Error => e
|
|
229
|
+
puts e.message
|
|
230
|
+
end
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Development
|
|
234
|
+
|
|
235
|
+
```sh
|
|
236
|
+
bundle install
|
|
237
|
+
bundle exec rake compile # build native extension
|
|
238
|
+
bundle exec rake spec # run tests
|
|
239
|
+
bundle exec rake # compile + test
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## References
|
|
243
|
+
|
|
244
|
+
- [Monty](https://github.com/pydantic/monty) — the Rust interpreter this gem wraps
|
|
245
|
+
- [Magnus](https://github.com/matsadler/magnus) — Rust ↔ Ruby bindings framework
|
|
246
|
+
- [rb-sys](https://github.com/oxidize-rb/rb-sys) — Ruby native extension build system for Rust
|
|
247
|
+
- [Pydantic](https://pydantic.dev) — the team behind Monty
|
|
248
|
+
|
|
249
|
+
## License
|
|
250
|
+
|
|
251
|
+
MIT
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "monty"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
publish = false
|
|
6
|
+
license = "MIT"
|
|
7
|
+
description = "Ruby bindings for Monty Python interpreter"
|
|
8
|
+
|
|
9
|
+
[lib]
|
|
10
|
+
name = "monty"
|
|
11
|
+
crate-type = ["cdylib"]
|
|
12
|
+
|
|
13
|
+
[dependencies]
|
|
14
|
+
monty-lang = { package = "monty", git = "https://github.com/pydantic/monty.git" }
|
|
15
|
+
magnus = { version = "0.8", features = ["rb-sys"] }
|
|
16
|
+
rb-sys = { version = "0.9", features = ["stable-api-compiled-fallback"] }
|
|
17
|
+
num-bigint = "0.4"
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
use magnus::{Error, ExceptionClass, Module, Ruby};
|
|
2
|
+
use std::cell::RefCell;
|
|
3
|
+
|
|
4
|
+
thread_local! {
|
|
5
|
+
static MONTY_ERROR: RefCell<Option<ExceptionClass>> = const { RefCell::new(None) };
|
|
6
|
+
static SYNTAX_ERROR: RefCell<Option<ExceptionClass>> = const { RefCell::new(None) };
|
|
7
|
+
static RESOURCE_ERROR: RefCell<Option<ExceptionClass>> = const { RefCell::new(None) };
|
|
8
|
+
static CONSUMED_ERROR: RefCell<Option<ExceptionClass>> = const { RefCell::new(None) };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
pub fn define_exceptions(ruby: &Ruby, module: &magnus::RModule) -> Result<(), Error> {
|
|
12
|
+
let standard_error = ruby.exception_standard_error();
|
|
13
|
+
|
|
14
|
+
let monty_error = module.define_error("Error", standard_error)?;
|
|
15
|
+
MONTY_ERROR.with(|cell| {
|
|
16
|
+
*cell.borrow_mut() = Some(monty_error);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
let syntax_error = module.define_error("SyntaxError", monty_error)?;
|
|
20
|
+
SYNTAX_ERROR.with(|cell| {
|
|
21
|
+
*cell.borrow_mut() = Some(syntax_error);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
let resource_error = module.define_error("ResourceError", monty_error)?;
|
|
25
|
+
RESOURCE_ERROR.with(|cell| {
|
|
26
|
+
*cell.borrow_mut() = Some(resource_error);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
let consumed_error = module.define_error("ConsumedError", monty_error)?;
|
|
30
|
+
CONSUMED_ERROR.with(|cell| {
|
|
31
|
+
*cell.borrow_mut() = Some(consumed_error);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
Ok(())
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
pub fn monty_error(message: String) -> Error {
|
|
38
|
+
MONTY_ERROR.with(|cell| {
|
|
39
|
+
let class = cell.borrow();
|
|
40
|
+
match class.as_ref() {
|
|
41
|
+
Some(cls) => Error::new(*cls, message),
|
|
42
|
+
None => {
|
|
43
|
+
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
44
|
+
Error::new(ruby.exception_runtime_error(), message)
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
pub fn syntax_error(message: String) -> Error {
|
|
51
|
+
SYNTAX_ERROR.with(|cell| {
|
|
52
|
+
let class = cell.borrow();
|
|
53
|
+
match class.as_ref() {
|
|
54
|
+
Some(cls) => Error::new(*cls, message),
|
|
55
|
+
None => {
|
|
56
|
+
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
57
|
+
Error::new(ruby.exception_runtime_error(), message)
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
pub fn resource_error(message: String) -> Error {
|
|
64
|
+
RESOURCE_ERROR.with(|cell| {
|
|
65
|
+
let class = cell.borrow();
|
|
66
|
+
match class.as_ref() {
|
|
67
|
+
Some(cls) => Error::new(*cls, message),
|
|
68
|
+
None => {
|
|
69
|
+
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
70
|
+
Error::new(ruby.exception_runtime_error(), message)
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
pub fn consumed_error() -> Error {
|
|
77
|
+
CONSUMED_ERROR.with(|cell| {
|
|
78
|
+
let class = cell.borrow();
|
|
79
|
+
match class.as_ref() {
|
|
80
|
+
Some(cls) => Error::new(
|
|
81
|
+
*cls,
|
|
82
|
+
"this object has been consumed and can no longer be used",
|
|
83
|
+
),
|
|
84
|
+
None => {
|
|
85
|
+
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
86
|
+
Error::new(
|
|
87
|
+
ruby.exception_runtime_error(),
|
|
88
|
+
"this object has been consumed and can no longer be used",
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
pub fn map_monty_exception(exc: monty_lang::MontyException) -> Error {
|
|
96
|
+
let summary = exc.summary();
|
|
97
|
+
|
|
98
|
+
// Check if it's a syntax error
|
|
99
|
+
if exc.exc_type() == monty_lang::ExcType::SyntaxError {
|
|
100
|
+
return syntax_error(summary);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
monty_error(summary)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
pub fn map_resource_error(err: monty_lang::ResourceError) -> Error {
|
|
107
|
+
let message = match err {
|
|
108
|
+
monty_lang::ResourceError::Allocation { limit, count } => {
|
|
109
|
+
format!("allocation limit exceeded: {count} allocations (limit: {limit})")
|
|
110
|
+
}
|
|
111
|
+
monty_lang::ResourceError::Time { limit, elapsed } => {
|
|
112
|
+
format!(
|
|
113
|
+
"time limit exceeded: {:.2}s elapsed (limit: {:.2}s)",
|
|
114
|
+
elapsed.as_secs_f64(),
|
|
115
|
+
limit.as_secs_f64()
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
monty_lang::ResourceError::Memory { limit, used } => {
|
|
119
|
+
format!("memory limit exceeded: {used} bytes used (limit: {limit})")
|
|
120
|
+
}
|
|
121
|
+
monty_lang::ResourceError::Recursion { limit, depth } => {
|
|
122
|
+
format!("recursion limit exceeded: depth {depth} (limit: {limit})")
|
|
123
|
+
}
|
|
124
|
+
monty_lang::ResourceError::Exception(exc) => {
|
|
125
|
+
return map_monty_exception(exc);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
resource_error(message)
|
|
130
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
use magnus::{Error, Ruby};
|
|
2
|
+
|
|
3
|
+
#[allow(dead_code)]
|
|
4
|
+
mod errors;
|
|
5
|
+
mod monty_object;
|
|
6
|
+
mod monty_run;
|
|
7
|
+
mod resource_limits;
|
|
8
|
+
mod run_progress;
|
|
9
|
+
|
|
10
|
+
#[magnus::init]
|
|
11
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
12
|
+
let module = ruby.define_module("Monty")?;
|
|
13
|
+
|
|
14
|
+
errors::define_exceptions(ruby, &module)?;
|
|
15
|
+
resource_limits::define_resource_limits_class(ruby, &module)?;
|
|
16
|
+
monty_run::define_run_class(ruby, &module)?;
|
|
17
|
+
run_progress::define_progress_classes(ruby, &module)?;
|
|
18
|
+
|
|
19
|
+
Ok(())
|
|
20
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
use magnus::value::ReprValue;
|
|
2
|
+
use magnus::{Error, RArray, RHash, Ruby, TryConvert, Value};
|
|
3
|
+
use monty_lang::MontyObject;
|
|
4
|
+
|
|
5
|
+
/// Convert a Ruby value to a MontyObject
|
|
6
|
+
pub fn ruby_to_monty(val: Value) -> Result<MontyObject, Error> {
|
|
7
|
+
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
8
|
+
|
|
9
|
+
if val.is_nil() {
|
|
10
|
+
return Ok(MontyObject::None);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Check for booleans by class name
|
|
14
|
+
if let Some(b) = detect_bool(val) {
|
|
15
|
+
return Ok(MontyObject::Bool(b));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Integer
|
|
19
|
+
if val.is_kind_of(ruby.class_integer()) {
|
|
20
|
+
// Try i64 first, fall back to BigInt via string
|
|
21
|
+
if let Ok(i) = i64::try_convert(val) {
|
|
22
|
+
return Ok(MontyObject::Int(i));
|
|
23
|
+
}
|
|
24
|
+
// Large integer: convert via string representation
|
|
25
|
+
let s: String = val.funcall("to_s", ())?;
|
|
26
|
+
let big = s
|
|
27
|
+
.parse::<num_bigint::BigInt>()
|
|
28
|
+
.map_err(|e| Error::new(ruby.exception_arg_error(), format!("invalid integer: {e}")))?;
|
|
29
|
+
return Ok(MontyObject::BigInt(big));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Float
|
|
33
|
+
if val.is_kind_of(ruby.class_float()) {
|
|
34
|
+
let f: f64 = f64::try_convert(val)?;
|
|
35
|
+
return Ok(MontyObject::Float(f));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// String
|
|
39
|
+
if val.is_kind_of(ruby.class_string()) {
|
|
40
|
+
let s: String = String::try_convert(val)?;
|
|
41
|
+
return Ok(MontyObject::String(s));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Symbol -> String
|
|
45
|
+
if val.is_kind_of(ruby.class_symbol()) {
|
|
46
|
+
let s: String = val.funcall("to_s", ())?;
|
|
47
|
+
return Ok(MontyObject::String(s));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Array -> List
|
|
51
|
+
if val.is_kind_of(ruby.class_array()) {
|
|
52
|
+
let arr: RArray = RArray::try_convert(val)?;
|
|
53
|
+
let mut items = Vec::with_capacity(arr.len());
|
|
54
|
+
for i in 0..arr.len() {
|
|
55
|
+
let item: Value = arr.entry(i as isize)?;
|
|
56
|
+
items.push(ruby_to_monty(item)?);
|
|
57
|
+
}
|
|
58
|
+
return Ok(MontyObject::List(items));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Hash -> Dict
|
|
62
|
+
if val.is_kind_of(ruby.class_hash()) {
|
|
63
|
+
let hash: RHash = RHash::try_convert(val)?;
|
|
64
|
+
let pairs = hash_to_pairs(hash)?;
|
|
65
|
+
return Ok(MontyObject::dict(pairs));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
Err(Error::new(
|
|
69
|
+
ruby.exception_type_error(),
|
|
70
|
+
format!(
|
|
71
|
+
"cannot convert {} to a Python object",
|
|
72
|
+
val.class().inspect()
|
|
73
|
+
),
|
|
74
|
+
))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Convert a MontyObject to a Ruby value
|
|
78
|
+
pub fn monty_to_ruby(obj: MontyObject) -> Result<Value, Error> {
|
|
79
|
+
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
80
|
+
|
|
81
|
+
match obj {
|
|
82
|
+
MontyObject::None => Ok(ruby.qnil().as_value()),
|
|
83
|
+
MontyObject::Bool(b) => {
|
|
84
|
+
if b {
|
|
85
|
+
Ok(ruby.qtrue().as_value())
|
|
86
|
+
} else {
|
|
87
|
+
Ok(ruby.qfalse().as_value())
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
MontyObject::Int(i) => Ok(ruby.integer_from_i64(i).as_value()),
|
|
91
|
+
MontyObject::BigInt(bi) => {
|
|
92
|
+
let s = bi.to_string();
|
|
93
|
+
let ruby_str = ruby.str_new(&s);
|
|
94
|
+
ruby_str.funcall("to_i", ())
|
|
95
|
+
}
|
|
96
|
+
MontyObject::Float(f) => Ok(ruby.float_from_f64(f).as_value()),
|
|
97
|
+
MontyObject::String(s) => Ok(ruby.str_new(&s).as_value()),
|
|
98
|
+
MontyObject::Bytes(b) => {
|
|
99
|
+
let s = ruby.str_from_slice(&b);
|
|
100
|
+
s.funcall::<_, _, Value>("force_encoding", ("ASCII-8BIT",))?;
|
|
101
|
+
Ok(s.as_value())
|
|
102
|
+
}
|
|
103
|
+
MontyObject::List(items) => {
|
|
104
|
+
let arr = ruby.ary_new_capa(items.len());
|
|
105
|
+
for item in items {
|
|
106
|
+
let val = monty_to_ruby(item)?;
|
|
107
|
+
arr.push(val)?;
|
|
108
|
+
}
|
|
109
|
+
Ok(arr.as_value())
|
|
110
|
+
}
|
|
111
|
+
MontyObject::Tuple(items) => {
|
|
112
|
+
let arr = ruby.ary_new_capa(items.len());
|
|
113
|
+
for item in items {
|
|
114
|
+
let val = monty_to_ruby(item)?;
|
|
115
|
+
arr.push(val)?;
|
|
116
|
+
}
|
|
117
|
+
arr.funcall::<_, _, Value>("freeze", ())?;
|
|
118
|
+
Ok(arr.as_value())
|
|
119
|
+
}
|
|
120
|
+
MontyObject::NamedTuple {
|
|
121
|
+
field_names,
|
|
122
|
+
values,
|
|
123
|
+
..
|
|
124
|
+
} => {
|
|
125
|
+
let hash = ruby.hash_new();
|
|
126
|
+
for (name, value) in field_names.into_iter().zip(values.into_iter()) {
|
|
127
|
+
let key = ruby.str_new(&name);
|
|
128
|
+
let val = monty_to_ruby(value)?;
|
|
129
|
+
hash.aset(key, val)?;
|
|
130
|
+
}
|
|
131
|
+
Ok(hash.as_value())
|
|
132
|
+
}
|
|
133
|
+
MontyObject::Dict(pairs) => {
|
|
134
|
+
let hash = ruby.hash_new();
|
|
135
|
+
for (k, v) in pairs.into_iter() {
|
|
136
|
+
let key = monty_to_ruby(k)?;
|
|
137
|
+
let val = monty_to_ruby(v)?;
|
|
138
|
+
hash.aset(key, val)?;
|
|
139
|
+
}
|
|
140
|
+
Ok(hash.as_value())
|
|
141
|
+
}
|
|
142
|
+
MontyObject::Set(items) | MontyObject::FrozenSet(items) => {
|
|
143
|
+
let arr = ruby.ary_new_capa(items.len());
|
|
144
|
+
for item in items {
|
|
145
|
+
let val = monty_to_ruby(item)?;
|
|
146
|
+
arr.push(val)?;
|
|
147
|
+
}
|
|
148
|
+
Ok(arr.as_value())
|
|
149
|
+
}
|
|
150
|
+
MontyObject::Dataclass { attrs, .. } => {
|
|
151
|
+
let hash = ruby.hash_new();
|
|
152
|
+
for (k, v) in attrs.into_iter() {
|
|
153
|
+
let key = monty_to_ruby(k)?;
|
|
154
|
+
let val = monty_to_ruby(v)?;
|
|
155
|
+
hash.aset(key, val)?;
|
|
156
|
+
}
|
|
157
|
+
Ok(hash.as_value())
|
|
158
|
+
}
|
|
159
|
+
MontyObject::Ellipsis => {
|
|
160
|
+
let sym = ruby.to_symbol("ellipsis");
|
|
161
|
+
Ok(sym.as_value())
|
|
162
|
+
}
|
|
163
|
+
MontyObject::Type(t) => {
|
|
164
|
+
let repr = format!("{t:?}");
|
|
165
|
+
Ok(ruby.str_new(&repr).as_value())
|
|
166
|
+
}
|
|
167
|
+
MontyObject::BuiltinFunction(f) => {
|
|
168
|
+
let repr = format!("{f:?}");
|
|
169
|
+
Ok(ruby.str_new(&repr).as_value())
|
|
170
|
+
}
|
|
171
|
+
MontyObject::Path(s) => Ok(ruby.str_new(&s).as_value()),
|
|
172
|
+
MontyObject::Repr(s) => Ok(ruby.str_new(&s).as_value()),
|
|
173
|
+
MontyObject::Cycle(_, s) => Ok(ruby.str_new(&s).as_value()),
|
|
174
|
+
MontyObject::Exception { exc_type, arg } => {
|
|
175
|
+
let msg = arg.unwrap_or_else(|| format!("{exc_type:?}"));
|
|
176
|
+
Err(crate::errors::monty_error(msg))
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// Convert a Ruby Array of values to Vec<MontyObject>
|
|
182
|
+
pub fn ruby_array_to_monty_vec(arr: RArray) -> Result<Vec<MontyObject>, Error> {
|
|
183
|
+
let mut result = Vec::with_capacity(arr.len());
|
|
184
|
+
for i in 0..arr.len() {
|
|
185
|
+
let item: Value = arr.entry(i as isize)?;
|
|
186
|
+
result.push(ruby_to_monty(item)?);
|
|
187
|
+
}
|
|
188
|
+
Ok(result)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// Detect Ruby true/false by querying the class name
|
|
192
|
+
fn detect_bool(val: Value) -> Option<bool> {
|
|
193
|
+
let class_val: Value = val.funcall("class", ()).ok()?;
|
|
194
|
+
let name: String = class_val.funcall("name", ()).ok()?;
|
|
195
|
+
match name.as_str() {
|
|
196
|
+
"TrueClass" => Some(true),
|
|
197
|
+
"FalseClass" => Some(false),
|
|
198
|
+
_ => None,
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
fn hash_to_pairs(hash: RHash) -> Result<Vec<(MontyObject, MontyObject)>, Error> {
|
|
203
|
+
let keys: RArray = hash.funcall("keys", ())?;
|
|
204
|
+
let mut pairs = Vec::with_capacity(keys.len());
|
|
205
|
+
for i in 0..keys.len() {
|
|
206
|
+
let key: Value = keys.entry(i as isize)?;
|
|
207
|
+
let val: Value = hash.aref(key)?;
|
|
208
|
+
pairs.push((ruby_to_monty(key)?, ruby_to_monty(val)?));
|
|
209
|
+
}
|
|
210
|
+
Ok(pairs)
|
|
211
|
+
}
|