Skip to content

Commit d0c6b44

Browse files
committed
feat: add C source file support and string replacement functionality
Signed-off-by: Ariel Simulevski <ariel@simulevski.at>
1 parent 8a1966e commit d0c6b44

11 files changed

Lines changed: 576 additions & 113 deletions

File tree

examples/csource_test/helper.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#include <stdio.h>
2+
3+
int add(int a, int b) { return a + b; }
4+
5+
#ifdef DEBUG
6+
void greet() { printf("debug mode\n"); }
7+
#else
8+
void greet() { printf("release mode\n"); }
9+
#endif

examples/csource_test/main.tin

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//!+helper.c
2+
3+
fn add(a i64, b i64) i64 = extern("add")
4+
fn greet() = extern("greet")
5+
6+
fn main() =
7+
let x i64 = add(3, 4)
8+
echo x
9+
greet()
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
//!+helper.c -- -DDEBUG
2+
3+
fn greet() = extern("greet")
4+
5+
fn main() =
6+
greet()

main.go

Lines changed: 113 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -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

6390
func 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

runtime/runtime.c

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -657,50 +657,6 @@ int32_t _tin_learn_atom(const char *str) {
657657
return code;
658658
}
659659

660-
661-
// -- String replace helper
662-
//
663-
// tin_str_replace(s, old, newstr): returns a new heap-allocated char* with all
664-
// occurrences of `old` replaced by `newstr`. Called from stdlib/strings/strings.tin
665-
// via extern("tin_str_replace"). Note: takes char* (fat-ptr data field, not TinString).
666-
char *tin_str_replace(const char *s, const char *old, const char *newstr) {
667-
if (!s || !old || !newstr) return (char *)s;
668-
size_t oldlen = strlen(old);
669-
size_t newlen = strlen(newstr);
670-
if (oldlen == 0) {
671-
char *dup = strdup(s);
672-
return dup ? dup : (char *)s;
673-
}
674-
// Count occurrences
675-
size_t count = 0;
676-
const char *p = s;
677-
while ((p = strstr(p, old)) != NULL) {
678-
count++;
679-
p += oldlen;
680-
}
681-
if (count == 0) return strdup(s);
682-
// Allocate result
683-
size_t slen = strlen(s);
684-
size_t rlen = slen + count * (newlen - oldlen) + 1;
685-
// But if newlen < oldlen, rlen might underflow - cap at slen+1 min
686-
if (newlen < oldlen && count * (oldlen - newlen) > slen) rlen = slen + 1;
687-
char *result = (char *)malloc(rlen);
688-
if (!result) return strdup(s);
689-
char *out = result;
690-
p = s;
691-
while (*p) {
692-
if (strncmp(p, old, oldlen) == 0) {
693-
memcpy(out, newstr, newlen);
694-
out += newlen;
695-
p += oldlen;
696-
} else {
697-
*out++ = *p++;
698-
}
699-
}
700-
*out = '\0';
701-
return result;
702-
}
703-
704660
// -- any equality
705661
// Runtime comparison for `any` values - dispatches on the type tag:
706662
// 0=i64, 1=f64, 2=string/atom, 3=bool, else pointer equality

stdlib/math/math.tin

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1+
//!-lm
2+
13
// stdlib/math - Mathematical functions
24
//
35
// Wraps standard C math.h functions
4-
// Requires linking with libm: pass -lm when building
5-
// tin build myfile.tin -lm
6-
// tin run myfile.tin -lm
76

87
// Basic arithmetic
98

0 commit comments

Comments
 (0)