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 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
@@ -0,0 +1,9 @@
1
+ [workspace]
2
+ members = ["ext/monty"]
3
+ resolver = "2"
4
+
5
+ [profile.release]
6
+ strip = true
7
+
8
+ [profile.dev]
9
+ strip = true
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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rb_sys/mkmf"
5
+
6
+ create_rust_makefile("monty/monty")
@@ -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
+ }