rubyduino 0.1.1 → 0.1.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aebef08a8012ff1994cfef5f943278222b9cc76c0147398d467d2a815121c37c
4
- data.tar.gz: d48692b143f9248dcae3cddc6db3b823ce9386e0a28e693133c88d28a4fea72d
3
+ metadata.gz: 1ec4285a969c2afed1449f750c1674b63f1ba5cb5b454994cfdb9356173595cb
4
+ data.tar.gz: 8e1dba9b348672d0aa96f4eb7645242da1e111fa36595c1f601ab9a1fa45acdf
5
5
  SHA512:
6
- metadata.gz: 3bbdc16bf822d3bc4ed389a64fbaa3fbc61d9dfa97604baf0c19d35a9ab0918040e1e2de5d1ccae958b2972338e18be6725eeefdd40291ab0b06fbeab067302b
7
- data.tar.gz: 54625b52317c6de6607655e81cbb1e9b19f493c9b1eb669f50edfe080c06b6a863ed07ab72d29e55af1a626a23d760103aa7142843f81499cd6aa7fb9879e989
6
+ metadata.gz: 736ec9d0f9c5665ba2f88f647c36d0dcd1010a3e4d405d1a49a6032d1eaefaa60aecbd64a2d9e103796f9bf7443a8163370dc5951fdcd14bb589938452cc5ca7
7
+ data.tar.gz: 203d97b5aed491505f46e681d04aa4128dce2349ac93513d8cf307f1b8e96ddeaaa31478788c5f9aa919a028fe276cbab1b10a6c54466b69cf7ee17f2a1abc35
data/CHANGELOG.md CHANGED
@@ -1,9 +1,19 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [0.1.0] - 2026-05-08
3
+ ## [0.1.3] - 2026-05-08
4
4
 
5
- - Initial release
5
+ - Add the built-in `ArduinoUNO` prelude with FFI-backed GPIO, analog read, and millisecond delay bindings
6
+ - Replace the legacy `system("pin13:*")` sketch API with top-level helpers like `pin_mode`, `digital_write`, and `delay_ms`
7
+ - Update the default hello example to use the new ArduinoUNO API
8
+
9
+ ## [0.1.2] - 2026-05-08
10
+
11
+ - Update vendored Spinel revision
6
12
 
7
13
  ## [0.1.1] - 2026-05-08
8
14
 
9
15
  - Fix avrdude discovery for installed command
16
+
17
+ ## [0.1.0] - 2026-05-08
18
+
19
+ - Initial release
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  <div align="center">
2
2
 
3
- # Rubyduino
3
+ # Rubyduino
4
+ [Experimental]
4
5
 
5
6
  <img width="250" height="250" alt="image copy" src="https://github.com/user-attachments/assets/156c7d41-ed42-43f3-a720-ed2e9c12b52c" />
6
7
 
@@ -15,14 +16,13 @@ Under the hood it uses [Spinel](https://github.com/matz/spinel), a Ruby AOT comp
15
16
  <img width="500" height="281" alt="IMG_0197" src="https://github.com/user-attachments/assets/d2b1cc69-647a-4f63-b090-31b13a21e5a8" />
16
17
 
17
18
  ```ruby
18
- system("pin13:output")
19
+ pin_mode(ArduinoUNO::LED_BUILTIN, ArduinoUNO::OUTPUT)
19
20
 
20
21
  loop do
21
- duration = 0.1
22
- system("pin13:high")
23
- sleep duration
24
- system("pin13:low")
25
- sleep duration
22
+ digital_write(ArduinoUNO::LED_BUILTIN, ArduinoUNO::HIGH)
23
+ delay_ms(100)
24
+ digital_write(ArduinoUNO::LED_BUILTIN, ArduinoUNO::LOW)
25
+ delay_ms(100)
26
26
  end
27
27
  ```
28
28
 
data/bin/rubyduino CHANGED
@@ -98,6 +98,7 @@ rubyduino_dir = File.join(root_dir, "lib/rubyduino")
98
98
  parse_bin = File.join(spinel_dir, "spinel_parse")
99
99
  codegen_rb = File.join(rubyduino_dir, "spinel_arduino_codegen.rb")
100
100
  entry_c = File.join(rubyduino_dir, "arduino_entry.c")
101
+ arduino_uno_rb = File.join(rubyduino_dir, "arduino_uno.rb")
101
102
 
102
103
  mcu = "atmega328p"
103
104
  f_cpu = "16000000UL"
@@ -137,6 +138,7 @@ abort "rubyduino: #{source}: No such file" unless File.file?(source)
137
138
  abort "rubyduino: missing Spinel checkout at #{spinel_dir}" unless Dir.exist?(spinel_dir)
138
139
  abort "rubyduino: missing #{codegen_rb}" unless File.file?(codegen_rb)
139
140
  abort "rubyduino: missing #{entry_c}" unless File.file?(entry_c)
141
+ abort "rubyduino: missing #{arduino_uno_rb}" unless File.file?(arduino_uno_rb)
140
142
  abort "rubyduino: avr-gcc not found" unless command?("avr-gcc")
141
143
  abort "rubyduino: avr-objcopy not found" unless command?("avr-objcopy")
142
144
 
@@ -164,9 +166,11 @@ avrdude = find_avrdude
164
166
  abort "rubyduino: avrdude not found" unless avrdude
165
167
 
166
168
  ast_tmp = File.join(Dir.tmpdir, "spinel_arduino_ast.#{$PROCESS_ID}.#{rand(1_000_000)}")
169
+ source_tmp = File.join(Dir.tmpdir, "rubyduino_source.#{$PROCESS_ID}.#{rand(1_000_000)}.rb")
167
170
  begin
168
171
  warn "Spinel: #{source} -> #{c_file}"
169
- run!(parse_bin, source, ast_tmp)
172
+ File.write(source_tmp, "#{File.read(arduino_uno_rb)}\n#{File.read(source)}")
173
+ run!(parse_bin, source_tmp, ast_tmp)
170
174
  run!(RbConfig.ruby, codegen_rb, ast_tmp, c_file)
171
175
 
172
176
  warn "AVR: #{c_file} -> #{hex_file}"
@@ -183,4 +187,5 @@ begin
183
187
  run!(avrdude, *conf_args, "-p#{mcu}", "-carduino", "-P#{port}", "-b#{baud}", "-D", "-Uflash:w:#{hex_file}:i")
184
188
  ensure
185
189
  FileUtils.rm_f(ast_tmp)
190
+ FileUtils.rm_f(source_tmp)
186
191
  end
data/examples/hello.rb CHANGED
@@ -1,9 +1,8 @@
1
- system("pin13:output")
1
+ pin_mode(ArduinoUNO::LED_BUILTIN, ArduinoUNO::OUTPUT)
2
2
 
3
3
  loop do
4
- duration = 0.1
5
- system("pin13:high")
6
- sleep duration
7
- system("pin13:low")
8
- sleep duration
4
+ digital_write(ArduinoUNO::LED_BUILTIN, ArduinoUNO::HIGH)
5
+ delay_ms(100)
6
+ digital_write(ArduinoUNO::LED_BUILTIN, ArduinoUNO::LOW)
7
+ delay_ms(100)
9
8
  end
@@ -0,0 +1,43 @@
1
+ module ArduinoUNO
2
+ LOW = 0
3
+ HIGH = 1
4
+
5
+ INPUT = 0
6
+ OUTPUT = 1
7
+ INPUT_PULLUP = 2
8
+
9
+ A0 = 14
10
+ A1 = 15
11
+ A2 = 16
12
+ A3 = 17
13
+ A4 = 18
14
+ A5 = 19
15
+
16
+ LED_BUILTIN = 13
17
+
18
+ ffi_func :pin_mode, [:uint8, :uint8], :int
19
+ ffi_func :digital_write, [:uint8, :uint8], :int
20
+ ffi_func :digital_read, [:uint8], :int
21
+ ffi_func :analog_read, [:uint8], :int
22
+ ffi_func :delay_ms, [:uint32], :void
23
+ end
24
+
25
+ def pin_mode(pin, mode)
26
+ ArduinoUNO.pin_mode(pin, mode)
27
+ end
28
+
29
+ def digital_write(pin, value)
30
+ ArduinoUNO.digital_write(pin, value)
31
+ end
32
+
33
+ def digital_read(pin)
34
+ ArduinoUNO.digital_read(pin)
35
+ end
36
+
37
+ def analog_read(pin)
38
+ ArduinoUNO.analog_read(pin)
39
+ end
40
+
41
+ def delay_ms(ms)
42
+ ArduinoUNO.delay_ms(ms)
43
+ end
@@ -3,6 +3,7 @@
3
3
 
4
4
  #include <stdint.h>
5
5
  #include <stdlib.h>
6
+ #include <string.h>
6
7
  #include <avr/io.h>
7
8
  #include <util/delay.h>
8
9
  #include <util/delay_basic.h>
@@ -46,6 +47,7 @@ static void sp_gc_mark(void *ptr) {
46
47
 
47
48
  typedef struct {
48
49
  mrb_int *data;
50
+ mrb_int start;
49
51
  mrb_int len;
50
52
  mrb_int cap;
51
53
  } sp_IntArray;
@@ -69,6 +71,7 @@ static sp_Range sp_range_new(mrb_int first, mrb_int last) {
69
71
 
70
72
  static sp_IntArray *sp_IntArray_new(void) {
71
73
  sp_IntArray *array = (sp_IntArray *)malloc(sizeof(sp_IntArray));
74
+ array->start = 0;
72
75
  array->len = 0;
73
76
  array->cap = 4;
74
77
  array->data = (mrb_int *)malloc(sizeof(mrb_int) * array->cap);
@@ -80,7 +83,7 @@ static void sp_IntArray_push(sp_IntArray *array, mrb_int value) {
80
83
  array->cap *= 2;
81
84
  array->data = (mrb_int *)realloc(array->data, sizeof(mrb_int) * array->cap);
82
85
  }
83
- array->data[array->len] = value;
86
+ array->data[array->start + array->len] = value;
84
87
  array->len++;
85
88
  }
86
89
 
@@ -89,7 +92,7 @@ static mrb_int sp_IntArray_length(sp_IntArray *array) {
89
92
  }
90
93
 
91
94
  static mrb_int sp_IntArray_get(sp_IntArray *array, mrb_int index) {
92
- return array->data[index];
95
+ return array->data[array->start + index];
93
96
  }
94
97
 
95
98
  static sp_FloatArray *sp_FloatArray_new(void) {
@@ -117,30 +120,140 @@ static mrb_float sp_FloatArray_get(sp_FloatArray *array, mrb_int index) {
117
120
  return array->data[index];
118
121
  }
119
122
 
120
- static int sp_streq(const char *a, const char *b) {
121
- while (*a && *b && *a == *b) {
122
- a++;
123
- b++;
123
+ static int rd_uno_valid_pin(uint8_t pin) {
124
+ return pin <= 19;
125
+ }
126
+
127
+ static volatile uint8_t *rd_uno_ddr(uint8_t pin) {
128
+ if (pin <= 7) {
129
+ return &DDRD;
124
130
  }
125
- return *a == *b;
131
+ if (pin <= 13) {
132
+ return &DDRB;
133
+ }
134
+ if (pin <= 19) {
135
+ return &DDRC;
136
+ }
137
+ return NULL;
126
138
  }
127
139
 
128
- static int sp_arduino_system(const char *cmd) {
129
- if (sp_streq(cmd, "pin13:output")) {
130
- DDRB |= _BV(DDB5);
140
+ static volatile uint8_t *rd_uno_port(uint8_t pin) {
141
+ if (pin <= 7) {
142
+ return &PORTD;
143
+ }
144
+ if (pin <= 13) {
145
+ return &PORTB;
146
+ }
147
+ if (pin <= 19) {
148
+ return &PORTC;
149
+ }
150
+ return NULL;
151
+ }
152
+
153
+ static volatile uint8_t *rd_uno_pin_reg(uint8_t pin) {
154
+ if (pin <= 7) {
155
+ return &PIND;
156
+ }
157
+ if (pin <= 13) {
158
+ return &PINB;
159
+ }
160
+ if (pin <= 19) {
161
+ return &PINC;
162
+ }
163
+ return NULL;
164
+ }
165
+
166
+ static uint8_t rd_uno_bit(uint8_t pin) {
167
+ if (pin <= 7) {
168
+ return pin;
169
+ }
170
+ if (pin <= 13) {
171
+ return pin - 8;
172
+ }
173
+ return pin - 14;
174
+ }
175
+
176
+ int pin_mode(uint8_t pin, uint8_t mode) {
177
+ volatile uint8_t *ddr = rd_uno_ddr(pin);
178
+ volatile uint8_t *port = rd_uno_port(pin);
179
+ uint8_t mask;
180
+
181
+ if (!ddr || !port) {
182
+ return 1;
183
+ }
184
+
185
+ mask = (uint8_t)(1 << rd_uno_bit(pin));
186
+
187
+ if (mode == 1) {
188
+ *ddr |= mask;
131
189
  return 0;
132
190
  }
133
- if (sp_streq(cmd, "pin13:high")) {
134
- PORTB |= _BV(PORTB5);
191
+
192
+ if (mode == 0) {
193
+ *ddr &= (uint8_t)~mask;
194
+ *port &= (uint8_t)~mask;
135
195
  return 0;
136
196
  }
137
- if (sp_streq(cmd, "pin13:low")) {
138
- PORTB &= (uint8_t)~_BV(PORTB5);
197
+
198
+ if (mode == 2) {
199
+ *ddr &= (uint8_t)~mask;
200
+ *port |= mask;
139
201
  return 0;
140
202
  }
203
+
141
204
  return 1;
142
205
  }
143
206
 
207
+ int digital_write(uint8_t pin, uint8_t value) {
208
+ volatile uint8_t *port = rd_uno_port(pin);
209
+ uint8_t mask;
210
+
211
+ if (!port) {
212
+ return 1;
213
+ }
214
+
215
+ mask = (uint8_t)(1 << rd_uno_bit(pin));
216
+
217
+ if (value) {
218
+ *port |= mask;
219
+ } else {
220
+ *port &= (uint8_t)~mask;
221
+ }
222
+
223
+ return 0;
224
+ }
225
+
226
+ int digital_read(uint8_t pin) {
227
+ volatile uint8_t *reg = rd_uno_pin_reg(pin);
228
+
229
+ if (!reg) {
230
+ return -1;
231
+ }
232
+
233
+ return ((*reg & (uint8_t)(1 << rd_uno_bit(pin))) != 0) ? 1 : 0;
234
+ }
235
+
236
+ int analog_read(uint8_t pin) {
237
+ uint8_t channel = pin;
238
+
239
+ if (pin >= 14 && pin <= 19) {
240
+ channel = pin - 14;
241
+ }
242
+
243
+ if (channel > 5 || !rd_uno_valid_pin(pin)) {
244
+ return -1;
245
+ }
246
+
247
+ ADMUX = (uint8_t)((1 << REFS0) | channel);
248
+ ADCSRA = (uint8_t)((1 << ADEN) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0));
249
+ ADCSRA |= (uint8_t)(1 << ADSC);
250
+
251
+ while (ADCSRA & (uint8_t)(1 << ADSC)) {
252
+ }
253
+
254
+ return ADC;
255
+ }
256
+
144
257
  static void sp_arduino_delay_ms(unsigned long ms) {
145
258
  while (ms > 0) {
146
259
  _delay_loop_2((uint16_t)(F_CPU / 4000UL));
@@ -148,12 +261,10 @@ static void sp_arduino_delay_ms(unsigned long ms) {
148
261
  }
149
262
  }
150
263
 
151
- static void sp_arduino_sleep_seconds(double seconds) {
152
- sp_arduino_delay_ms((unsigned long)(seconds * 1000.0 + 0.5));
264
+ void delay_ms(uint32_t ms) {
265
+ sp_arduino_delay_ms(ms);
153
266
  }
154
267
 
155
- #define system(cmd) sp_arduino_system(cmd)
156
- #define sleep(seconds) sp_arduino_delay_ms((unsigned long)((seconds) * 1000.0 + 0.5))
157
268
  #define fflush(stream) ((void)0)
158
269
 
159
270
  #endif
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Rubyduino
4
4
  module Spinel
5
- COMMIT = "a70208094343b9d4cd306218c909d99781661c2f"
5
+ COMMIT = "e7f714f213ca572912f7214f358a927f8e2152e5"
6
6
  ROOT = File.expand_path("../../vendor/spinel", __dir__)
7
7
  end
8
8
  end
@@ -26,9 +26,6 @@ load_spinel_compiler
26
26
  module SpinelArduinoCodegen
27
27
  def compile_no_recv_call_expr(nid, mname)
28
28
  case mname
29
- when "sleep"
30
- compile_arduino_sleep(nid)
31
- "0"
32
29
  when "rand"
33
30
  arduino_rand = compile_arduino_rand(nid)
34
31
  return arduino_rand if arduino_rand
@@ -41,21 +38,6 @@ module SpinelArduinoCodegen
41
38
 
42
39
  private
43
40
 
44
- def compile_arduino_sleep(nid)
45
- args_id = @nd_arguments[nid]
46
- return if args_id < 0
47
-
48
- arg_ids = get_args(args_id)
49
- return if arg_ids.empty?
50
-
51
- arg = arg_ids.first
52
- if numeric_literal_node?(arg)
53
- emit_arduino_delay_literal(numeric_literal_value(arg))
54
- else
55
- emit(" sp_arduino_sleep_seconds(" + compile_expr(arg) + ");")
56
- end
57
- end
58
-
59
41
  def compile_arduino_rand(nid)
60
42
  args_id = @nd_arguments[nid]
61
43
  return nil if args_id < 0
@@ -79,32 +61,9 @@ module SpinelArduinoCodegen
79
61
  "((mrb_int)(#{first} + (rand() % #{span})))"
80
62
  end
81
63
 
82
- def emit_arduino_delay_literal(seconds)
83
- ms = (seconds * 1000.0).round
84
- parts = []
85
- while ms >= 250
86
- parts << "_delay_ms(250.0)"
87
- ms -= 250
88
- end
89
- parts << "_delay_ms(#{ms}.0)" if ms > 0
90
- emit(" " + parts.join("; ") + ";") unless parts.empty?
91
- end
92
-
93
64
  def integer_literal_node?(nid)
94
65
  nid && nid >= 0 && @nd_type[nid] == "IntegerNode"
95
66
  end
96
-
97
- def numeric_literal_node?(nid)
98
- nid && nid >= 0 && %w[IntegerNode FloatNode].include?(@nd_type[nid])
99
- end
100
-
101
- def numeric_literal_value(nid)
102
- if @nd_type[nid] == "IntegerNode"
103
- @nd_value[nid].to_f
104
- else
105
- @nd_content[nid].to_f
106
- end
107
- end
108
67
  end
109
68
 
110
69
  Compiler.prepend(SpinelArduinoCodegen)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rubyduino
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
5
5
  end
@@ -122,7 +122,7 @@ PRISM_LIB = build/libprism.a
122
122
  CODEGEN_STAMP := build/stamps/spinel_codegen.rb.stamp
123
123
  PARSE_STAMP := build/stamps/spinel_parse.c.stamp
124
124
 
125
- .PHONY: all parse bootstrap codegen test retest clean-test-results regen-expected bench clean install uninstall deps
125
+ .PHONY: all parse bootstrap codegen test retest clean-test-results regen-expected bench optcarrot clean install uninstall deps
126
126
 
127
127
  all: parse regexp spinel_codegen$(EXE)
128
128
 
@@ -378,6 +378,34 @@ bench: spinel_parse$(EXE) $(SP_RT_LIB) spinel_codegen$(EXE)
378
378
  echo "Benchmarks: $$pass pass, $$fail fail, $$err error, $$skip skip"; \
379
379
  if [ $$fail -ne 0 ] || [ $$err -ne 0 ]; then exit 1; fi
380
380
 
381
+ # ---- Optcarrot integration test ----
382
+ #
383
+ # End-to-end pipeline: clone optcarrot's `experiment/spinel` branch,
384
+ # pack `lib/optcarrot/*.rb` into a single Ruby file via the upstream
385
+ # `tools/pack-for-spinel.rb`, compile through spinel, run the
386
+ # resulting binary against `examples/Lan_Master.nes`, and verify the
387
+ # output contains `fps: <num>` and `checksum: 59662` (the canonical
388
+ # 180-frame checksum for `--benchmark`).
389
+
390
+ OPTCARROT_DIR := build/optcarrot
391
+ OPTCARROT_REPO := https://github.com/mame/optcarrot.git
392
+ OPTCARROT_BRANCH := experiment/spinel
393
+
394
+ optcarrot: spinel_parse$(EXE) $(SP_RT_LIB) spinel_codegen$(EXE)
395
+ @if [ ! -d $(OPTCARROT_DIR) ]; then \
396
+ git clone --depth=1 --branch=$(OPTCARROT_BRANCH) $(OPTCARROT_REPO) $(OPTCARROT_DIR); \
397
+ fi
398
+ @ruby $(OPTCARROT_DIR)/tools/pack-for-spinel.rb > build/optcarrot-single.rb
399
+ @./spinel build/optcarrot-single.rb -o build/optcarrot-single
400
+ @out=$$($(TIMEOUT60) ./build/optcarrot-single 2>&1); \
401
+ echo "$$out"; \
402
+ if echo "$$out" | grep -qE "^fps: [0-9.]+$$" && echo "$$out" | grep -q "^checksum: 59662$$"; then \
403
+ echo "Optcarrot: OK"; \
404
+ else \
405
+ echo "Optcarrot: FAIL — expected 'fps: <num>' and 'checksum: 59662'"; \
406
+ exit 1; \
407
+ fi
408
+
381
409
  # ---- Install ----
382
410
 
383
411
  PREFIX ?= /usr/local
@@ -2934,6 +2934,18 @@ class Compiler
2934
2934
  end
2935
2935
  end
2936
2936
 
2937
+ # `<obj>.class` returns the class name as a string (Spinel
2938
+ # collapses Class/Module objects to their textual name; the
2939
+ # only consumers in practice are interpolation
2940
+ # `"#<#{self.class}>"` and `.to_s.split` chains, both of
2941
+ # which work fine on a plain string).
2942
+ if recv >= 0 && mname == "class"
2943
+ rt = infer_type(recv)
2944
+ if is_obj_type(rt) == 1
2945
+ return "string"
2946
+ end
2947
+ end
2948
+
2937
2949
  # `recv.__sp_ieval_<N>(...)`: the rewritten form of an
2938
2950
  # `recv.instance_eval { ... }` call. v1 only fired on top-level call
2939
2951
  # sites, where the call's value was always discarded — so its return
@@ -3994,6 +4006,16 @@ class Compiler
3994
4006
  end
3995
4007
  return "int_array"
3996
4008
  end
4009
+ # `replace(other)` returns the receiver, not a fresh array;
4010
+ # the inferred result must therefore preserve the receiver's
4011
+ # array type so that an expression-form `c = a.replace(b)`
4012
+ # still tags `c` as `int_array` (or whatever `a` is) rather
4013
+ # than falling through to `int`.
4014
+ if mname == "replace"
4015
+ if recv >= 0
4016
+ return infer_type(recv)
4017
+ end
4018
+ end
3997
4019
  if mname == "pop"
3998
4020
  if recv >= 0
3999
4021
  rt = infer_type(recv)
@@ -4643,7 +4665,7 @@ class Compiler
4643
4665
  if mname == "read" || mname == "binread"
4644
4666
  return "string"
4645
4667
  end
4646
- if mname == "exist?"
4668
+ if mname == "exist?" || mname == "readable?"
4647
4669
  return "bool"
4648
4670
  end
4649
4671
  if mname == "join"
@@ -17819,6 +17841,14 @@ class Compiler
17819
17841
 
17820
17842
  # ---- Forward declarations ----
17821
17843
  def emit_forward_decls
17844
+ # ARGV (sp_argv) is referenced from any function that uses
17845
+ # `ARGV.length` / `ARGV[i]`, so the declaration has to precede
17846
+ # all function bodies — not just main()'s. emit_main keeps the
17847
+ # runtime initialization (`sp_argv.len = argc - 1; ...`) where
17848
+ # main() can fill it in from `argc` / `argv`.
17849
+ emit_raw("typedef struct{const char**data;mrb_int len;}sp_Argv;")
17850
+ emit_raw("static sp_Argv sp_argv;")
17851
+ emit_raw("")
17822
17852
  # Emit block helper functions accumulated during collection
17823
17853
  if @block_funcs != ""
17824
17854
  emit_raw(@block_funcs)
@@ -20806,9 +20836,6 @@ class Compiler
20806
20836
 
20807
20837
  def emit_main
20808
20838
  stmts = get_body_stmts(@root_id)
20809
- emit_raw("typedef struct{const char**data;mrb_int len;}sp_Argv;")
20810
- emit_raw("static sp_Argv sp_argv;")
20811
- emit_raw("")
20812
20839
  emit_raw("int main(int argc,char**argv){")
20813
20840
  emit_raw(" sp_argv.len=argc-1;sp_argv.data=(const char**)malloc(sizeof(const char*)*(argc>1?argc-1:1));{int _i;for(_i=0;_i<sp_argv.len;_i++)sp_argv.data[_i]=sp_str_dup_external(argv[_i+1]);}")
20814
20841
  if @needs_rand == 1
@@ -21303,7 +21330,16 @@ class Compiler
21303
21330
  exit(1)
21304
21331
  end
21305
21332
  if t == "IntegerNode"
21306
- return @nd_value[nid].to_s
21333
+ # Suffix with `LL` so the literal is `long long` rather than
21334
+ # `int` in the emitted C. Without it, an expression of int
21335
+ # literals whose product exceeds int32 (e.g. APU mixer
21336
+ # constant `24329 * 256 * 500 = 3,114,112,000`) overflows
21337
+ # at constant-folding time and the C compiler emits a wrong
21338
+ # value. Plain literals at runtime sites get implicitly
21339
+ # promoted to mrb_int (= long long) on assignment, but
21340
+ # constant-init expressions are evaluated by the C compiler
21341
+ # using the literals' declared type.
21342
+ return @nd_value[nid].to_s + "LL"
21307
21343
  end
21308
21344
  if t == "FloatNode"
21309
21345
  return @nd_content[nid]
@@ -26285,6 +26321,37 @@ class Compiler
26285
26321
  end
26286
26322
  return "sp_IntArray_sort(" + rc + ")"
26287
26323
  end
26324
+ # `pack("C*")` — the only format Spinel implements: each
26325
+ # int element is written as a byte into a freshly allocated
26326
+ # NUL-terminated string. Sufficient for the optcarrot save-
26327
+ # RAM `@wrk.pack("C*")` shape and the symmetric inverse of
26328
+ # `String#bytes` we already support. Other format strings
26329
+ # fall through to the unresolved-call warning.
26330
+ if mname == "pack" && recv_type == "int_array"
26331
+ args_id = @nd_arguments[nid]
26332
+ if args_id >= 0
26333
+ a_pk = get_args(args_id)
26334
+ if a_pk.length == 1 && @nd_type[a_pk[0]] == "StringNode" && @nd_content[a_pk[0]] == "C*"
26335
+ tmp = new_temp
26336
+ ntmp = new_temp
26337
+ itmp = new_temp
26338
+ emit(" mrb_int " + ntmp + " = sp_IntArray_length(" + rc + ");")
26339
+ emit(" char *" + tmp + " = sp_str_alloc(" + ntmp + ");")
26340
+ emit(" for (mrb_int " + itmp + " = 0; " + itmp + " < " + ntmp + "; " + itmp + "++) " + tmp + "[" + itmp + "] = (char)sp_IntArray_get(" + rc + ", " + itmp + ");")
26341
+ emit(" " + tmp + "[" + ntmp + "] = 0;")
26342
+ return tmp
26343
+ end
26344
+ end
26345
+ end
26346
+ # `replace(other)` in expression position: the stmt-form
26347
+ # arm in compile_*_stmt only fires when the call's value is
26348
+ # discarded; in expression position (e.g. last stmt of a
26349
+ # method body, or rvalue of an assignment) we still need to
26350
+ # emit the side effect *and* yield the receiver as the
26351
+ # value. Use the comma operator so the result is `rc`.
26352
+ if mname == "replace" && recv_type == "int_array"
26353
+ return "(sp_IntArray_replace(" + rc + ", " + compile_arg0(nid) + "), " + rc + ")"
26354
+ end
26288
26355
  # take_while / drop_while: block-driven prefix scan. take_while
26289
26356
  # collects elements from the front while the block stays truthy;
26290
26357
  # drop_while skips them and returns the rest. Mirrors the
@@ -26539,6 +26606,14 @@ class Compiler
26539
26606
  if mname == "difference"
26540
26607
  return "sp_FloatArray_difference(" + rc + ", " + compile_arg0(nid) + ")"
26541
26608
  end
26609
+ if mname == "sort"
26610
+ # Non-bang: yield a fresh sorted copy. Mirror the
26611
+ # `sp_FloatArray_shuffle` pattern (new + replace) so the
26612
+ # source array is left untouched.
26613
+ tmp = new_temp
26614
+ emit(" sp_FloatArray *" + tmp + " = sp_FloatArray_new(); sp_FloatArray_replace(" + tmp + ", " + rc + "); sp_FloatArray_sort_bang(" + tmp + ");")
26615
+ return tmp
26616
+ end
26542
26617
  end
26543
26618
  if is_ptr_array_type(recv_type) == 1
26544
26619
  elem_type = ptr_array_elem_type(recv_type)
@@ -27529,9 +27604,32 @@ class Compiler
27529
27604
  if mname == "exist?"
27530
27605
  return "sp_file_exist(" + compile_arg0(nid) + ")"
27531
27606
  end
27607
+ if mname == "readable?"
27608
+ # Reuse the exist check: on every platform Spinel
27609
+ # targets, a fopen-able file is also readable from
27610
+ # the same process. Distinguishing the two would
27611
+ # need access(2), which isn't worth a runtime fn for
27612
+ # the dead-code optcarrot battery-save path that
27613
+ # surfaced this.
27614
+ return "sp_file_exist(" + compile_arg0(nid) + ")"
27615
+ end
27532
27616
  if mname == "delete"
27533
27617
  return "(sp_file_delete(" + compile_arg0(nid) + "), 0)"
27534
27618
  end
27619
+ if mname == "binwrite"
27620
+ # NOTE: `sp_file_write` uses fputs, so an embedded NUL
27621
+ # in the payload truncates the write — fine for the
27622
+ # optcarrot save-RAM dead-code path that surfaced
27623
+ # this, not safe for general binary use.
27624
+ args_id_bw = @nd_arguments[nid]
27625
+ if args_id_bw >= 0
27626
+ a_bw = get_args(args_id_bw)
27627
+ if a_bw.length >= 2
27628
+ return "(sp_file_write(" + compile_expr(a_bw[0]) + ", " + compile_expr(a_bw[1]) + "), 0)"
27629
+ end
27630
+ end
27631
+ return "0"
27632
+ end
27535
27633
  if mname == "join"
27536
27634
  args_id = @nd_arguments[nid]
27537
27635
  if args_id >= 0
@@ -27864,6 +27962,16 @@ class Compiler
27864
27962
  if @cls_is_value_type[ci] == 1
27865
27963
  arrow = "."
27866
27964
  end
27965
+ # `<obj>.class` — collapse to the class name as a string
27966
+ # literal. Spinel doesn't allocate runtime Class objects,
27967
+ # but the typical consumers (interpolation in inspect,
27968
+ # `.to_s.split("::").last` shape) all treat the result as
27969
+ # a string. Underscore-flattened nested module names
27970
+ # (`Optcarrot_NES`) render as-is rather than the original
27971
+ # `Optcarrot::NES` form; the loss is cosmetic.
27972
+ if mname == "class"
27973
+ return c_string_literal(cname)
27974
+ end
27867
27975
  # attr_reader
27868
27976
  readers = @cls_attr_readers[ci].split(";")
27869
27977
  j = 0
@@ -0,0 +1,20 @@
1
+ # `File.readable?` and `File.binwrite` — class-method stubs in
2
+ # the `rcname == "File"` dispatch block, plus `IntArray#pack("C*")`
3
+ # which is the symmetric inverse of `String#bytes`.
4
+ #
5
+ # Surfaced via optcarrot's battery-save dead code path:
6
+ # `return unless File.readable?(sav)` and
7
+ # `File.binwrite(sav, @wrk.pack("C*"))`.
8
+ #
9
+ # `readable?` reuses the exist check (close enough on POSIX).
10
+ # `binwrite` reuses `sp_file_write` — fputs-based, so embedded
11
+ # NULs truncate; acceptable for the NUL-free use sites that
12
+ # exist in practice.
13
+
14
+ path = "/tmp/spinel_file_class_test"
15
+
16
+ File.binwrite(path, [72, 105, 33].pack("C*")) # "Hi!"
17
+ puts File.readable?(path)
18
+ puts File.read(path)
19
+ File.delete(path)
20
+ puts File.exist?(path)
@@ -0,0 +1,3 @@
1
+ true
2
+ Hi!
3
+ false
@@ -0,0 +1,18 @@
1
+ # `Array#sort` (non-bang) on a float array. Surfaced via
2
+ # optcarrot's `@fps_history.sort[(@fps_history.length * 0.05).floor]`
3
+ # p95 calculation. Mirrors the existing int_array sort path —
4
+ # yields a fresh sorted array; the source stays untouched.
5
+
6
+ a = [3.5, 1.25, 2.0, 0.75, 4.125]
7
+ b = a.sort
8
+ puts b.length
9
+
10
+ i = 0
11
+ while i < b.length
12
+ puts b[i]
13
+ i = i + 1
14
+ end
15
+
16
+ # Source unchanged.
17
+ puts a[0]
18
+ puts a[1]
@@ -0,0 +1,8 @@
1
+ 5
2
+ 0.75
3
+ 1.25
4
+ 2.0
5
+ 3.5
6
+ 4.125
7
+ 3.5
8
+ 1.25
@@ -0,0 +1,23 @@
1
+ # `Array#replace` in *expression* position on an int_array.
2
+ # The stmt-form arm has long supported this; the expr-form was
3
+ # missing — unresolved-call warning + literal `0` emitted.
4
+ # Surfaced via optcarrot's `def load_battery; ...; @wrk.replace(sav.bytes); end`,
5
+ # where the call sits at the tail of the method (its value is
6
+ # the implicit return).
7
+
8
+ a = [1, 2, 3]
9
+ b = [10, 20, 30, 40]
10
+
11
+ # Expression position: assigned. Should yield the (mutated) `a`.
12
+ c = a.replace(b)
13
+
14
+ # `a` mutated in place
15
+ puts a[0]
16
+ puts a[1]
17
+ puts a[2]
18
+ puts a[3]
19
+ puts a.length
20
+
21
+ # `c` is the same array (replace returns the receiver)
22
+ puts c[0]
23
+ puts c.length
@@ -0,0 +1,7 @@
1
+ 10
2
+ 20
3
+ 30
4
+ 40
5
+ 4
6
+ 10
7
+ 4
@@ -0,0 +1,29 @@
1
+ # Spinel's integers are mrb_int (long long); Ruby integers don't
2
+ # overflow in the same range. But emitting a plain `24329 * 256 *
3
+ # 500` in C lets the C compiler fold the constant using `int`
4
+ # arithmetic, overflowing at 3,114,112,000 to a wrong negative
5
+ # result on 32-bit-int platforms.
6
+ #
7
+ # Surfaced via optcarrot's `APU::Mixer::TND_1 = 100 * 24329 * 256
8
+ # / 500`-shaped constant expressions whose intermediate products
9
+ # clear int32 max. Same hazard for any constant-init expression
10
+ # of integer literals whose folded result exceeds INT_MAX.
11
+ #
12
+ # Fix: emit the `LL` suffix on every IntegerNode literal so
13
+ # operands carry `long long` type at the C level. Runtime
14
+ # expressions assigned to `mrb_int` slots already promoted
15
+ # implicitly; only constant-init contexts (where the C compiler
16
+ # folds before any assignment) saw the overflow.
17
+
18
+ # Each chain straddles int32: max 2^31 - 1 = 2,147,483,647.
19
+ A = 24329 * 256 * 500 # 3,114,112,000
20
+ B = 100000 * 100000 # 10,000,000,000
21
+ C = 65535 * 65535 # 4,294,836,225
22
+ D = 2 ** 40 # 1,099,511,627,776
23
+ E = 1_000_000_000 + 1_000_000_000 # 2,000,000,000 — sum overflow
24
+
25
+ puts A
26
+ puts B
27
+ puts C
28
+ puts D
29
+ puts E
@@ -0,0 +1,5 @@
1
+ 3114112000
2
+ 10000000000
3
+ 4294836225
4
+ 1099511627776
5
+ 2000000000
@@ -0,0 +1,24 @@
1
+ # `<user_obj>.class` collapses to the class name as a string.
2
+ # Spinel doesn't allocate runtime Class objects, so the only
3
+ # coherent representation for `obj.class` is the textual name.
4
+ # The two real-world consumers in optcarrot are:
5
+ # 1. `"#<#{ self.class }>"`-style interpolation in `inspect`
6
+ # 2. `self.class.to_s.split("::").last`-style introspection
7
+ # Both work fine if `.class` simply yields the class name as a
8
+ # string (the second loses the `::` namespace because Spinel
9
+ # flattens nested modules with `_`, but that's a cosmetic loss).
10
+
11
+ class Foo
12
+ def to_s
13
+ "<" + self.class + ">"
14
+ end
15
+ end
16
+
17
+ class Bar
18
+ def label
19
+ self.class.to_s
20
+ end
21
+ end
22
+
23
+ puts Foo.new.to_s # => <Foo>
24
+ puts Bar.new.label # => Bar
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubyduino
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joseph Schito
@@ -27,6 +27,7 @@ files:
27
27
  - examples/hello.rb
28
28
  - lib/rubyduino.rb
29
29
  - lib/rubyduino/arduino_entry.c
30
+ - lib/rubyduino/arduino_uno.rb
30
31
  - lib/rubyduino/sp_runtime.h
31
32
  - lib/rubyduino/spinel.rb
32
33
  - lib/rubyduino/spinel_arduino_codegen.rb
@@ -345,6 +346,10 @@ files:
345
346
  - vendor/spinel/test/fiber_yield_across_method_call.rb.expected
346
347
  - vendor/spinel/test/file_basename_gc.rb
347
348
  - vendor/spinel/test/file_basename_gc.rb.expected
349
+ - vendor/spinel/test/file_class_methods_int_recv.rb
350
+ - vendor/spinel/test/file_class_methods_int_recv.rb.expected
351
+ - vendor/spinel/test/float_array_sort.rb
352
+ - vendor/spinel/test/float_array_sort.rb.expected
348
353
  - vendor/spinel/test/forward_call_class_method_inherited_init.rb
349
354
  - vendor/spinel/test/forward_call_class_method_inherited_init.rb.expected
350
355
  - vendor/spinel/test/forward_call_class_method_inherited_init_int_array.rb
@@ -407,6 +412,8 @@ files:
407
412
  - vendor/spinel/test/inspect.rb.expected
408
413
  - vendor/spinel/test/instance_eval_trampoline.rb
409
414
  - vendor/spinel/test/instance_eval_trampoline.rb.expected
415
+ - vendor/spinel/test/int_array_replace_expr.rb
416
+ - vendor/spinel/test/int_array_replace_expr.rb.expected
410
417
  - vendor/spinel/test/int_bracket_skip_sym_idx.rb
411
418
  - vendor/spinel/test/int_bracket_skip_sym_idx.rb.expected
412
419
  - vendor/spinel/test/int_keyed_hash_lookup_as_array.rb
@@ -417,6 +424,8 @@ files:
417
424
  - vendor/spinel/test/integer_div.rb.expected
418
425
  - vendor/spinel/test/integer_div_by_zero.rb
419
426
  - vendor/spinel/test/integer_div_by_zero.rb.expected
427
+ - vendor/spinel/test/integer_literal_no_int32_overflow.rb
428
+ - vendor/spinel/test/integer_literal_no_int32_overflow.rb.expected
420
429
  - vendor/spinel/test/interp_method_widening.rb
421
430
  - vendor/spinel/test/interp_method_widening.rb.expected
422
431
  - vendor/spinel/test/interp_symbol.rb
@@ -560,6 +569,8 @@ files:
560
569
  - vendor/spinel/test/nullable.rb.expected
561
570
  - vendor/spinel/test/obj_array.rb
562
571
  - vendor/spinel/test/obj_array.rb.expected
572
+ - vendor/spinel/test/obj_class_returns_string.rb
573
+ - vendor/spinel/test/obj_class_returns_string.rb.expected
563
574
  - vendor/spinel/test/object_new_sentinel.rb
564
575
  - vendor/spinel/test/object_new_sentinel.rb.expected
565
576
  - vendor/spinel/test/object_truthy.rb