@@ -46,3 +46,128 @@ fn golden_tests_asm() -> goldentests::TestResult<()> {
4646fn golden_tests_stack ( ) -> goldentests:: TestResult < ( ) > {
4747 run_golden_tests ( "stack" )
4848}
49+
50+ #[ test]
51+ #[ cfg( feature = "llvm" ) ]
52+ fn aot_emits_arm64_ios_object_and_header ( ) {
53+ use std:: io:: Write ;
54+
55+ let tmp = std:: env:: temp_dir ( ) . join ( "lyte_aot_smoke" ) ;
56+ let _ = std:: fs:: remove_dir_all ( & tmp) ;
57+ std:: fs:: create_dir_all ( & tmp) . unwrap ( ) ;
58+
59+ let src = tmp. join ( "smoke.lyte" ) ;
60+ let mut f = std:: fs:: File :: create ( & src) . unwrap ( ) ;
61+ writeln ! (
62+ f,
63+ "var freq: f32\n \
64+ var sample_rate: f32\n \
65+ \n \
66+ init(rate: f32, f: f32) {{\n \
67+ sample_rate = rate\n \
68+ freq = f\n \
69+ }}\n \
70+ \n \
71+ phase() -> f32 {{\n \
72+ return freq / sample_rate\n \
73+ }}"
74+ )
75+ . unwrap ( ) ;
76+ drop ( f) ;
77+
78+ let out_o = tmp. join ( "smoke.o" ) ;
79+ let status = std:: process:: Command :: new ( LYTE_BIN )
80+ . arg ( & src)
81+ . arg ( "--no-recursion" )
82+ . arg ( "--entry" )
83+ . arg ( "init,phase" )
84+ . arg ( "--aot" )
85+ . arg ( & out_o)
86+ . status ( )
87+ . expect ( "failed to invoke lyte" ) ;
88+ assert ! ( status. success( ) , "--aot exited with {}" , status) ;
89+ assert ! ( out_o. exists( ) , "object file not written" ) ;
90+
91+ let out_h = tmp. join ( "smoke.h" ) ;
92+ assert ! ( out_h. exists( ) , "header file not written" ) ;
93+
94+ // Check Mach-O 64-bit arm64 magic + cputype rather than shelling out to
95+ // `file`, so the test runs anywhere.
96+ let bytes = std:: fs:: read ( & out_o) . expect ( "read .o" ) ;
97+ assert ! ( bytes. len( ) >= 8 , ".o too short" ) ;
98+ let magic = u32:: from_le_bytes ( [ bytes[ 0 ] , bytes[ 1 ] , bytes[ 2 ] , bytes[ 3 ] ] ) ;
99+ assert_eq ! ( magic, 0xfeed_facf , "expected Mach-O 64-bit magic" ) ;
100+ let cputype = u32:: from_le_bytes ( [ bytes[ 4 ] , bytes[ 5 ] , bytes[ 6 ] , bytes[ 7 ] ] ) ;
101+ assert_eq ! ( cputype, 0x0100_000c , "expected arm64 cputype (got {:#x})" , cputype) ;
102+
103+ let header = std:: fs:: read_to_string ( & out_h) . expect ( "read .h" ) ;
104+ assert ! ( header. contains( "#define SMOKE_H" ) , "missing include guard" ) ;
105+ assert ! ( header. contains( "SMOKE_STATE_SIZE" ) , "missing state-size macro" ) ;
106+ assert ! ( header. contains( "smoke_init" ) , "missing init wrapper decl" ) ;
107+ assert ! ( header. contains( "smoke_phase" ) , "missing phase wrapper decl" ) ;
108+ assert ! ( header. contains( "SMOKE_OFFSET_FREQ" ) , "missing freq global offset" ) ;
109+ assert ! ( header. contains( "smoke_globals" ) , "missing metadata table" ) ;
110+ assert ! ( header. contains( "smoke_assert" ) , "missing assert hook decl" ) ;
111+ }
112+
113+ #[ test]
114+ #[ cfg( feature = "llvm" ) ]
115+ fn aot_two_objects_have_disjoint_public_symbols ( ) {
116+ // Two .lyte programs that both define internal helpers and entry points
117+ // with the same names must produce link-disjoint .o files when given
118+ // different prefixes.
119+ use std:: io:: Write ;
120+
121+ let tmp = std:: env:: temp_dir ( ) . join ( "lyte_aot_collision" ) ;
122+ let _ = std:: fs:: remove_dir_all ( & tmp) ;
123+ std:: fs:: create_dir_all ( & tmp) . unwrap ( ) ;
124+
125+ for prefix in & [ "nodea" , "nodeb" ] {
126+ let src = tmp. join ( format ! ( "{}.lyte" , prefix) ) ;
127+ let mut f = std:: fs:: File :: create ( & src) . unwrap ( ) ;
128+ writeln ! (
129+ f,
130+ "helper(x: f32) -> f32 {{\n \
131+ return x * x\n \
132+ }}\n \
133+ \n \
134+ run(input: f32) -> f32 {{\n \
135+ return helper(input)\n \
136+ }}"
137+ )
138+ . unwrap ( ) ;
139+ drop ( f) ;
140+ let out_o = tmp. join ( format ! ( "{}.o" , prefix) ) ;
141+ let status = std:: process:: Command :: new ( LYTE_BIN )
142+ . arg ( & src)
143+ . arg ( "--no-recursion" )
144+ . arg ( "--entry" )
145+ . arg ( "run" )
146+ . arg ( "--aot" )
147+ . arg ( & out_o)
148+ . status ( )
149+ . expect ( "failed to invoke lyte" ) ;
150+ assert ! ( status. success( ) , "--aot exited with {}" , status) ;
151+ }
152+
153+ // Read both .o files and grep their byte content for the public symbols.
154+ // We don't parse Mach-O; we just confirm that the *other* program's
155+ // wrapper name does NOT appear in this .o.
156+ let a = std:: fs:: read ( tmp. join ( "nodea.o" ) ) . unwrap ( ) ;
157+ let b = std:: fs:: read ( tmp. join ( "nodeb.o" ) ) . unwrap ( ) ;
158+ assert ! ( contains_substr( & a, b"nodea_run" ) , "nodea.o missing its wrapper" ) ;
159+ assert ! ( contains_substr( & b, b"nodeb_run" ) , "nodeb.o missing its wrapper" ) ;
160+ assert ! ( !contains_substr( & a, b"nodeb_run" ) , "nodea.o leaks nodeb_run" ) ;
161+ assert ! ( !contains_substr( & b, b"nodea_run" ) , "nodeb.o leaks nodea_run" ) ;
162+ // Internal helper from either program should NOT appear as an external
163+ // symbol with the bare name; either it's inlined away or set to private
164+ // linkage. We can't tell without a Mach-O parser, but the public-name
165+ // disjointness check above is the load-bearing one for "can they link".
166+ }
167+
168+ #[ cfg( feature = "llvm" ) ]
169+ fn contains_substr ( haystack : & [ u8 ] , needle : & [ u8 ] ) -> bool {
170+ haystack
171+ . windows ( needle. len ( ) )
172+ . any ( |w| w == needle)
173+ }
0 commit comments