@@ -30,34 +30,61 @@ Linker flags (passed after the source file):
3030 -lNAME link with libNAME (e.g. -lm for libmath)
3131 -LDIR add DIR to the library search path
3232 file.o/.a link with extra object/archive file
33+
34+ In-source directives (at the top of the .tin file):
35+ //!-lNAME link with libNAME
36+ //!+file.c compile C source file alongside the tin module
37+ //!+file.c -- FLAGS compile C source with extra clang flags
3338`
3439
35- // parseFileLinkerFlags scans the leading lines of src for //! directives and
36- // returns the flags they specify. Each directive line has the form:
40+ // cSource represents a C source file to compile alongside the tin module,
41+ // optionally with extra clang flags (from //!+file.c -- -DFOO directives).
42+ type cSource struct {
43+ path string
44+ flags []string
45+ }
46+
47+ // parseFileDirectives scans the leading lines of src for //! directives and
48+ // returns linker flags and C source files to compile in.
3749//
38- // //!-lm
39- // //!-lraylib
40- // //!-L/usr/local/lib
50+ // //!-lm → linker flag -lm
51+ // //!-lraylib → linker flag -lraylib
52+ // //!-L/usr/local/lib → linker flag -L/usr/local/lib
53+ // //!+helper.c → compile helper.c alongside the module
54+ // //!+src/foo.c -- -DDEBUG → compile src/foo.c with extra flag -DDEBUG
4155//
42- // Scanning stops at the first line that is not a comment or blank
43- func parseFileLinkerFlags ( src string ) [] string {
44- var flags []string
56+ // srcDir is the directory of the .tin file; relative C source paths are
57+ // resolved against it. Scanning stops at the first non-comment, non-blank line.
58+ func parseFileDirectives ( src , srcDir string ) ( linkerFlags []string , cSources [] cSource ) {
4559 for _ , line := range strings .SplitAfter (src , "\n " ) {
4660 trimmed := strings .TrimSpace (line )
4761 if trimmed == "" || strings .HasPrefix (trimmed , "//" ) && ! strings .HasPrefix (trimmed , "//!" ) {
48- // blank or ordinary comment - keep scanning
4962 continue
5063 }
5164 if strings .HasPrefix (trimmed , "//!" ) {
52- flag := strings .TrimSpace (trimmed [3 :])
53- if flag != "" {
54- flags = append (flags , flag )
65+ rest := strings .TrimSpace (trimmed [3 :])
66+ if rest == "" {
67+ continue
68+ }
69+ if strings .HasPrefix (rest , "+" ) {
70+ spec := strings .TrimSpace (rest [1 :])
71+ parts := strings .SplitN (spec , " -- " , 2 )
72+ cpath := filepath .Join (srcDir , strings .TrimSpace (parts [0 ]))
73+ var extraFlags []string
74+ if len (parts ) == 2 {
75+ for _ , f := range strings .Fields (parts [1 ]) {
76+ extraFlags = append (extraFlags , f )
77+ }
78+ }
79+ cSources = append (cSources , cSource {path : cpath , flags : extraFlags })
80+ } else {
81+ linkerFlags = append (linkerFlags , rest )
5582 }
5683 continue
5784 }
58- break // first non-comment, non-blank line - stop
85+ break
5986 }
60- return flags
87+ return
6188}
6289
6390func main () {
@@ -102,8 +129,8 @@ func main() {
102129 die ("error reading file: %v" , err )
103130 }
104131
105- // Collect linker flags declared in the source file via //! directives
106- fileLinkerFlags := parseFileLinkerFlags (string (src ))
132+ // Collect directives declared in the source file via //! lines
133+ fileLinkerFlags , fileCSources := parseFileDirectives (string (src ), filepath . Dir ( file ))
107134
108135 // Lex
109136 l := lexer .New (string (src ))
@@ -178,7 +205,7 @@ func main() {
178205 }
179206 }
180207 extraObjs = append (srcLinkFlags , extraObjs ... )
181- if err := compileIR (irText , out , libMode , extraObjs ); err != nil {
208+ if err := compileIR (irText , out , libMode , extraObjs , fileCSources ); err != nil {
182209 die ("compile error: %v" , err )
183210 }
184211
@@ -199,7 +226,7 @@ func main() {
199226 }
200227 }
201228 extraObjs = append (srcLinkFlags , extraObjs ... )
202- if err := compileIR (irText , out , false , extraObjs ); err != nil {
229+ if err := compileIR (irText , out , false , extraObjs , fileCSources ); err != nil {
203230 die ("compile error: %v" , err )
204231 }
205232
@@ -217,7 +244,7 @@ func main() {
217244 }
218245 }
219246 extraObjs = append (srcLinkFlags , extraObjs ... )
220- if err := compileIR (irText , tmp , false , extraObjs ); err != nil {
247+ if err := compileIR (irText , tmp , false , extraObjs , fileCSources ); err != nil {
221248 die ("compile error: %v" , err )
222249 }
223250 defer func (name string ) {
@@ -240,10 +267,11 @@ func main() {
240267 }
241268}
242269
243- // compileIR writes the LLVM IR to a temp .ll file and invokes clang
244- // If libMode is true, compile to an object file with -c (no linking)
245- // extraObjs are additional .o/.a files to link with
246- func compileIR (ir , outBin string , libMode bool , extraObjs []string ) error {
270+ // compileIR writes the LLVM IR to a temp .ll file and invokes clang.
271+ // If libMode is true, compile to an object file with -c (no linking).
272+ // extraObjs are additional .o/.a files and -l/-L flags to pass to the linker.
273+ // cSources are C source files to compile in alongside the IR.
274+ func compileIR (ir , outBin string , libMode bool , extraObjs []string , cSources []cSource ) error {
247275 // Write IR to temp file
248276 //goland:noinspection GoResourceLeak
249277 llFile , err := os .CreateTemp ("" , "tin-*.ll" )
@@ -263,18 +291,71 @@ func compileIR(ir, outBin string, libMode bool, extraObjs []string) error {
263291 rtC := filepath .Join (filepath .Dir (ex ), "runtime" , "runtime.c" )
264292
265293 if libMode {
266- // Library mode: compile to object file only (-c), no runtime, no linking
267- args := []string {"-O2" , "-c" , llFile .Name (), "-o" , outBin }
268- clang := exec .Command ("clang" , args ... )
269- clang .Stdout = os .Stdout
270- clang .Stderr = os .Stderr
271- return clang .Run ()
294+ // Library mode: compile to object file(s) with -c, then merge with ld -r.
295+ // clang -c cannot write multiple inputs to a single -o, so each source is
296+ // compiled separately and the results are partially linked together.
297+ irObj , err := os .CreateTemp ("" , "tin-ir-*.o" )
298+ if err != nil {
299+ return fmt .Errorf ("cannot create temp object file: %w" , err )
300+ }
301+ irObjName := irObj .Name ()
302+ _ = irObj .Close ()
303+ defer func () { _ = os .Remove (irObjName ) }()
304+
305+ clangIR := exec .Command ("clang" , "-O2" , "-c" , llFile .Name (), "-o" , irObjName )
306+ clangIR .Stdout = os .Stdout
307+ clangIR .Stderr = os .Stderr
308+ if err := clangIR .Run (); err != nil {
309+ return err
310+ }
311+
312+ objs := []string {irObjName }
313+ var tmpObjs []string
314+ for _ , cs := range cSources {
315+ cObj , err := os .CreateTemp ("" , "tin-c-*.o" )
316+ if err != nil {
317+ return fmt .Errorf ("cannot create temp object file: %w" , err )
318+ }
319+ cObjName := cObj .Name ()
320+ _ = cObj .Close ()
321+ tmpObjs = append (tmpObjs , cObjName )
322+ cArgs := []string {"-O2" , "-c" }
323+ cArgs = append (cArgs , cs .flags ... )
324+ cArgs = append (cArgs , cs .path , "-o" , cObjName )
325+ clangC := exec .Command ("clang" , cArgs ... )
326+ clangC .Stdout = os .Stdout
327+ clangC .Stderr = os .Stderr
328+ if err := clangC .Run (); err != nil {
329+ for _ , f := range tmpObjs {
330+ _ = os .Remove (f )
331+ }
332+ return err
333+ }
334+ objs = append (objs , cObjName )
335+ }
336+ defer func () {
337+ for _ , f := range tmpObjs {
338+ _ = os .Remove (f )
339+ }
340+ }()
341+
342+ // Merge all object files into the final output with ld -r (partial link)
343+ ldArgs := append ([]string {"-r" }, objs ... )
344+ ldArgs = append (ldArgs , "-o" , outBin )
345+ ld := exec .Command ("ld" , ldArgs ... )
346+ ld .Stdout = os .Stdout
347+ ld .Stderr = os .Stderr
348+ return ld .Run ()
272349 }
273350
274351 args := []string {"-O2" , llFile .Name ()}
275352 if _ , err := os .Stat (rtC ); err == nil {
276353 args = append (args , rtC )
277354 }
355+ for _ , cs := range cSources {
356+ args = append (args , cs .flags ... )
357+ args = append (args , cs .path )
358+ }
278359 args = append (args , extraObjs ... )
279360 args = append (args , "-o" , outBin )
280361
@@ -333,7 +414,8 @@ func runDirTests(dir string, extraFlags []string) {
333414 continue // no test blocks in this file
334415 }
335416
336- srcLinks := append ([]string {}, parseFileLinkerFlags (string (src ))... )
417+ fileLinks , fCSources := parseFileDirectives (string (src ), filepath .Dir (fpath ))
418+ srcLinks := append ([]string {}, fileLinks ... )
337419 for _ , lib := range cg .LinkLibs () {
338420 srcLinks = append (srcLinks , "-l" + lib )
339421 }
@@ -354,7 +436,7 @@ func runDirTests(dir string, extraFlags []string) {
354436 }(tmp .Name ())
355437
356438 irText := mod .String ()
357- if compErr := compileIR (irText , tmp .Name (), false , linkFlags ); compErr != nil {
439+ if compErr := compileIR (irText , tmp .Name (), false , linkFlags , fCSources ); compErr != nil {
358440 _ , _ = fmt .Fprintf (os .Stderr , " compile error: %v\n " , compErr )
359441 results = append (results , result {e .Name (), false })
360442 continue
0 commit comments