Skip to content

Commit 962d8e4

Browse files
committed
fix(zig,ruby): resolve all known limitations in Zig and Ruby compilers
Zig compiler (5 fixes): - Fix crypto builtin status metadata: mark all operations as implemented (they were correctly wired to ec/blake3/pq emitters but status reported as scaffolded) - Add FixedArray type resolution: new `fixed_array` variant in RunarType enum, proper mapping from FixedArrayType, accepted in property validation - Enhance while loop support in .runar.zig parser: support >, >=, != operators, reversed operands, and countdown patterns - Extract standalone dead code elimination pass (dce.zig): runs as pass 4.3 for all contracts, not just EC-heavy ones. EC optimizer delegates to the shared module. - Implement source map pipeline: parser captures source locations on statements and methods, ANF lowering stamps them on bindings, stack lowering tracks per-instruction source locs, emit records opcode-to- source mappings. Artifact JSON includes sourceMap array. Ruby compiler/SDK (5 fixes): - Fix ANF interpreter ByteString truthiness: hex "00", "0000", "80" (negative zero) now correctly evaluate as falsy, matching on-chain Bitcoin Script OP_IF semantics - Rewrite compile_check from placeholder to real frontend pipeline: invokes Parse → Validate → TypeCheck via lazy-loaded runar_compiler gem - Make interpreter loop limit configurable via max_loop_iterations kwarg on compute_new_state (IR loader already validates MAX_LOOP_COUNT=10000) - Implement RPCProvider#get_contract_utxo via scantxoutset RPC fallback with graceful error handling for unsupported nodes - Add compiler unit test suite: 16 tests across test_compiler.rb (7 integration tests with conformance golden files), test_parser_ts.rb (5), test_parser_ruby.rb (4), with Rakefile runner
1 parent b25e21a commit 962d8e4

File tree

21 files changed

+1117
-220
lines changed

21 files changed

+1117
-220
lines changed

compilers/ruby/Rakefile

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
require 'rake/testtask'
2+
3+
# Run each test file in its own subprocess to avoid constant collisions
4+
# between the TS and Ruby parsers (they share a namespace for token constants).
5+
task :test do
6+
test_files = FileList['test/test_*.rb']
7+
failures = 0
8+
9+
test_files.each do |f|
10+
puts "Running #{f}..."
11+
system("ruby -Ilib -Itest #{f}")
12+
failures += 1 unless $?.success?
13+
end
14+
15+
if failures > 0
16+
abort "#{failures} test file(s) failed"
17+
else
18+
puts "\nAll #{test_files.size} test files passed."
19+
end
20+
end
21+
22+
task default: :test
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'test_helper'
4+
5+
class TestCompiler < Minitest::Test
6+
# Conformance test directory relative to repo root
7+
CONFORMANCE_DIR = File.expand_path('../../../conformance/tests', __dir__)
8+
9+
def compile_ts_source(source, file_name = 'Test.runar.ts')
10+
parse_result = RunarCompiler.send(:_parse_source, source, file_name)
11+
raise "parse errors: #{parse_result.error_strings.join('; ')}" if parse_result.errors.any?
12+
raise "no contract found" if parse_result.contract.nil?
13+
14+
val_result = RunarCompiler.send(:_validate, parse_result.contract)
15+
assert_empty val_result.errors.map(&:format_message), "validation errors"
16+
17+
tc_result = RunarCompiler.send(:_type_check, parse_result.contract)
18+
assert_empty tc_result.errors.map(&:format_message), "type check errors"
19+
20+
program = RunarCompiler.send(:_lower_to_anf, parse_result.contract)
21+
RunarCompiler.compile_from_program(program, disable_constant_folding: true)
22+
end
23+
24+
# NOTE: The Ruby compiler's TS and Ruby parsers share a namespace for token
25+
# constants. Loading both in the same process causes constant collisions that
26+
# can corrupt tokenization. We compile Ruby-format sources via subprocess to
27+
# avoid this. This is a known pre-existing issue in the Ruby compiler.
28+
def compile_rb_source(source, file_name = 'Test.runar.rb')
29+
require 'tempfile'
30+
require 'json'
31+
32+
tmpfile = Tempfile.new([File.basename(file_name, '.*'), '.runar.rb'])
33+
tmpfile.write(source)
34+
tmpfile.close
35+
36+
lib_dir = File.expand_path('../lib', __dir__)
37+
out = `ruby -I#{lib_dir} -e "
38+
require 'runar_compiler'
39+
require 'json'
40+
a = RunarCompiler.compile_from_source('#{tmpfile.path}', disable_constant_folding: true)
41+
puts JSON.generate({ name: a.contract_name, script: a.script })
42+
" 2>&1`
43+
44+
tmpfile.unlink
45+
46+
unless $?.success?
47+
flunk "Ruby compilation failed: #{out}"
48+
end
49+
50+
data = JSON.parse(out.lines.last)
51+
OpenStruct.new(contract_name: data['name'], script: data['script'])
52+
end
53+
54+
# ------------------------------------------------------------------
55+
# Basic compilation tests
56+
# ------------------------------------------------------------------
57+
58+
def test_compile_p2pkh_ts
59+
source = <<~TS
60+
import { SmartContract, assert, PubKey, Sig, Addr, hash160, checkSig } from 'runar-lang';
61+
62+
class P2PKH extends SmartContract {
63+
readonly pubKeyHash: Addr;
64+
65+
constructor(pubKeyHash: Addr) {
66+
super(pubKeyHash);
67+
this.pubKeyHash = pubKeyHash;
68+
}
69+
70+
public unlock(sig: Sig, pubKey: PubKey): void {
71+
assert(hash160(pubKey) === this.pubKeyHash);
72+
assert(checkSig(sig, pubKey));
73+
}
74+
}
75+
TS
76+
77+
artifact = compile_ts_source(source, 'P2PKH.runar.ts')
78+
assert_equal 'P2PKH', artifact.contract_name
79+
assert artifact.script.length > 0, "script should be non-empty"
80+
# P2PKH script contains OP_DUP (76), OP_HASH160 (a9), OP_CHECKSIG (ac)
81+
assert_includes artifact.script.downcase, '76'
82+
assert_includes artifact.script.downcase, 'a9'
83+
assert_includes artifact.script.downcase, 'ac'
84+
end
85+
86+
def test_compile_p2pkh_rb
87+
source = <<~RB
88+
class P2PKH < Runar::SmartContract
89+
prop :pub_key_hash, Addr
90+
91+
def initialize(pub_key_hash)
92+
super(pub_key_hash)
93+
@pub_key_hash = pub_key_hash
94+
end
95+
96+
runar_public sig: Sig, pub_key: PubKey
97+
def unlock(sig, pub_key)
98+
assert hash160(pub_key) == @pub_key_hash
99+
assert check_sig(sig, pub_key)
100+
end
101+
end
102+
RB
103+
104+
artifact = compile_rb_source(source, 'P2PKH.runar.rb')
105+
assert_equal 'P2PKH', artifact.contract_name
106+
assert artifact.script.length > 0, "script should be non-empty"
107+
end
108+
109+
def test_ts_and_rb_produce_same_script
110+
ts_source = <<~TS
111+
import { SmartContract, assert, PubKey, Sig, Addr, hash160, checkSig } from 'runar-lang';
112+
113+
class P2PKH extends SmartContract {
114+
readonly pubKeyHash: Addr;
115+
116+
constructor(pubKeyHash: Addr) {
117+
super(pubKeyHash);
118+
this.pubKeyHash = pubKeyHash;
119+
}
120+
121+
public unlock(sig: Sig, pubKey: PubKey): void {
122+
assert(hash160(pubKey) === this.pubKeyHash);
123+
assert(checkSig(sig, pubKey));
124+
}
125+
}
126+
TS
127+
128+
rb_source = <<~RB
129+
class P2PKH < Runar::SmartContract
130+
prop :pub_key_hash, Addr
131+
132+
def initialize(pub_key_hash)
133+
super(pub_key_hash)
134+
@pub_key_hash = pub_key_hash
135+
end
136+
137+
runar_public sig: Sig, pub_key: PubKey
138+
def unlock(sig, pub_key)
139+
assert hash160(pub_key) == @pub_key_hash
140+
assert check_sig(sig, pub_key)
141+
end
142+
end
143+
RB
144+
145+
ts_artifact = compile_ts_source(ts_source, 'P2PKH.runar.ts')
146+
rb_artifact = compile_rb_source(rb_source, 'P2PKH.runar.rb')
147+
assert_equal ts_artifact.script.downcase, rb_artifact.script.downcase,
148+
"TS and Ruby parsers should produce identical script"
149+
end
150+
151+
# ------------------------------------------------------------------
152+
# Conformance golden-file tests
153+
# ------------------------------------------------------------------
154+
155+
def test_conformance_basic_p2pkh_ts
156+
return skip("conformance dir not found") unless File.directory?(CONFORMANCE_DIR)
157+
158+
ts_path = File.join(CONFORMANCE_DIR, 'basic-p2pkh', 'basic-p2pkh.runar.ts')
159+
expected_hex = File.read(File.join(CONFORMANCE_DIR, 'basic-p2pkh', 'expected-script.hex')).strip
160+
return skip("conformance files not found") unless File.exist?(ts_path)
161+
162+
artifact = RunarCompiler.compile_from_source(ts_path, disable_constant_folding: true)
163+
assert_equal expected_hex.downcase, artifact.script.downcase,
164+
"compiled script should match conformance golden file"
165+
end
166+
167+
def test_conformance_basic_p2pkh_rb
168+
return skip("conformance dir not found") unless File.directory?(CONFORMANCE_DIR)
169+
170+
rb_path = File.join(CONFORMANCE_DIR, 'basic-p2pkh', 'basic-p2pkh.runar.rb')
171+
expected_hex = File.read(File.join(CONFORMANCE_DIR, 'basic-p2pkh', 'expected-script.hex')).strip
172+
return skip("conformance files not found") unless File.exist?(rb_path)
173+
174+
artifact = RunarCompiler.compile_from_source(rb_path, disable_constant_folding: true)
175+
assert_equal expected_hex.downcase, artifact.script.downcase,
176+
"compiled script should match conformance golden file"
177+
end
178+
179+
# ------------------------------------------------------------------
180+
# Error handling tests
181+
# ------------------------------------------------------------------
182+
183+
def test_parse_error_for_invalid_source
184+
assert_raises(RuntimeError) do
185+
compile_ts_source("this is not valid typescript", 'bad.runar.ts')
186+
end
187+
end
188+
189+
def test_artifact_has_expected_structure
190+
source = <<~TS
191+
import { SmartContract, assert, PubKey, Sig, Addr, hash160, checkSig } from 'runar-lang';
192+
193+
class P2PKH extends SmartContract {
194+
readonly pubKeyHash: Addr;
195+
196+
constructor(pubKeyHash: Addr) {
197+
super(pubKeyHash);
198+
this.pubKeyHash = pubKeyHash;
199+
}
200+
201+
public unlock(sig: Sig, pubKey: PubKey): void {
202+
assert(hash160(pubKey) === this.pubKeyHash);
203+
assert(checkSig(sig, pubKey));
204+
}
205+
}
206+
TS
207+
208+
artifact = compile_ts_source(source, 'P2PKH.runar.ts')
209+
assert_respond_to artifact, :contract_name
210+
assert_respond_to artifact, :script
211+
assert_respond_to artifact, :abi
212+
assert_respond_to artifact, :asm
213+
assert_equal 'P2PKH', artifact.contract_name
214+
assert_kind_of String, artifact.script
215+
216+
# ABI should have constructor and methods
217+
assert artifact.abi.constructor
218+
assert_kind_of Array, artifact.abi.methods
219+
assert_equal 1, artifact.abi.methods.length
220+
assert_equal 'unlock', artifact.abi.methods.first.name
221+
assert artifact.abi.methods.first.is_public
222+
end
223+
end

compilers/ruby/test/test_helper.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# frozen_string_literal: true
2+
3+
require 'minitest/autorun'
4+
5+
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
6+
require 'runar_compiler'
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'test_helper'
4+
5+
class TestParserRuby < Minitest::Test
6+
def parse(source, file_name = 'Test.runar.rb')
7+
RunarCompiler.send(:_parse_source, source, file_name)
8+
end
9+
10+
def test_parses_simple_contract
11+
source = <<~RB
12+
require 'runar'
13+
14+
class Counter < Runar::SmartContract
15+
prop :count, Bigint
16+
17+
def initialize(count)
18+
super(count)
19+
@count = count
20+
end
21+
22+
runar_public
23+
def increment
24+
assert true
25+
end
26+
end
27+
RB
28+
29+
result = parse(source)
30+
assert_empty result.errors.map(&:format_message), "should parse without errors"
31+
refute_nil result.contract
32+
assert_equal 'Counter', result.contract.name
33+
assert_equal 1, result.contract.properties.length
34+
assert_equal 'count', result.contract.properties.first.name
35+
end
36+
37+
def test_parses_typed_params
38+
source = <<~RB
39+
require 'runar'
40+
41+
class P2PKH < Runar::SmartContract
42+
prop :pub_key_hash, Addr
43+
44+
def initialize(pub_key_hash)
45+
super(pub_key_hash)
46+
@pub_key_hash = pub_key_hash
47+
end
48+
49+
runar_public sig: Sig, pub_key: PubKey
50+
def unlock(sig, pub_key)
51+
assert hash160(pub_key) == @pub_key_hash
52+
assert check_sig(sig, pub_key)
53+
end
54+
end
55+
RB
56+
57+
result = parse(source)
58+
assert_empty result.errors.map(&:format_message)
59+
unlock = result.contract.methods.find { |m| m.name == 'unlock' }
60+
refute_nil unlock
61+
assert unlock.visibility == 'public'
62+
assert_equal 2, unlock.params.length
63+
end
64+
65+
def test_parses_stateful_contract
66+
source = <<~RB
67+
require 'runar'
68+
69+
class Counter < Runar::StatefulSmartContract
70+
prop :count, Bigint
71+
72+
def initialize(count)
73+
super(count)
74+
@count = count
75+
end
76+
77+
runar_public
78+
def increment
79+
@count += 1
80+
end
81+
end
82+
RB
83+
84+
result = parse(source)
85+
assert_empty result.errors.map(&:format_message)
86+
assert_equal 'StatefulSmartContract', result.contract.parent_class
87+
end
88+
89+
def test_snake_case_to_camel_case
90+
source = <<~RB
91+
require 'runar'
92+
93+
class P2PKH < Runar::SmartContract
94+
prop :pub_key_hash, Addr
95+
96+
def initialize(pub_key_hash)
97+
super(pub_key_hash)
98+
@pub_key_hash = pub_key_hash
99+
end
100+
101+
runar_public sig: Sig, pub_key: PubKey
102+
def unlock(sig, pub_key)
103+
assert hash160(pub_key) == @pub_key_hash
104+
assert check_sig(sig, pub_key)
105+
end
106+
end
107+
RB
108+
109+
result = parse(source)
110+
assert_empty result.errors.map(&:format_message)
111+
# Ruby snake_case property names should be converted to camelCase in AST
112+
prop = result.contract.properties.first
113+
assert_equal 'pubKeyHash', prop.name
114+
end
115+
end

0 commit comments

Comments
 (0)