Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
declare function support and linker flags
  • Loading branch information
cs01 committed Feb 24, 2026
commit 055f2c195751d6839a9f2c6a134fbfc979d1e116
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ jobs:
exit 1
fi

- name: Build tree-sitter TypeScript objects
- name: Build tree-sitter TSX objects
run: |
mkdir -p build
TS_SRC=node_modules/tree-sitter-typescript/typescript/src
TS_SRC=node_modules/tree-sitter-typescript/tsx/src
TS_INCLUDE=node_modules/tree-sitter-typescript
clang -c -O2 -fPIC -I $TS_SRC -I $TS_INCLUDE $TS_SRC/parser.c -o build/tree-sitter-typescript-parser.o
clang -c -O2 -fPIC -I $TS_SRC -I $TS_INCLUDE $TS_SRC/scanner.c -o build/tree-sitter-typescript-scanner.o
Expand Down Expand Up @@ -158,10 +158,10 @@ jobs:
exit 1
fi

- name: Build tree-sitter TypeScript objects
- name: Build tree-sitter TSX objects
run: |
mkdir -p build
TS_SRC=node_modules/tree-sitter-typescript/typescript/src
TS_SRC=node_modules/tree-sitter-typescript/tsx/src
TS_INCLUDE=node_modules/tree-sitter-typescript
clang -c -O2 -fPIC -I $TS_SRC -I $TS_INCLUDE $TS_SRC/parser.c -o build/tree-sitter-typescript-parser.o
clang -c -O2 -fPIC -I $TS_SRC -I $TS_INCLUDE $TS_SRC/scanner.c -o build/tree-sitter-typescript-scanner.o
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/cross-compile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
- name: Build tree-sitter objects
run: |
mkdir -p build
TS_SRC=node_modules/tree-sitter-typescript/typescript/src
TS_SRC=node_modules/tree-sitter-typescript/tsx/src
TS_INCLUDE=node_modules/tree-sitter-typescript
clang -c -O2 -fPIC -I $TS_SRC -I $TS_INCLUDE $TS_SRC/parser.c -o build/tree-sitter-typescript-parser.o
clang -c -O2 -fPIC -I $TS_SRC -I $TS_INCLUDE $TS_SRC/scanner.c -o build/tree-sitter-typescript-scanner.o
Expand Down
3 changes: 3 additions & 0 deletions src/analysis/semantic-analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,9 @@ export class SemanticAnalyzer {
}

private analyzeFunction(func: FunctionNode): void {
// External declarations have no body to analyze
if (func.declare) return;

this.currentFunction = func.name;

this.checkFunctionUnionTypes(func.name, func.paramTypes, func.returnType);
Expand Down
3 changes: 3 additions & 0 deletions src/ast/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,9 @@ export interface FunctionNode {
async?: boolean;
parameters?: FunctionParameter[];
loc?: SourceLocation;
// External C function declaration (declare function foo(): void)
// When true, codegen emits LLVM `declare` instead of `define`, no _cs_ prefix
declare?: boolean;
}

export interface ClassMethod {
Expand Down
47 changes: 47 additions & 0 deletions src/chad-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import {
setTargetCpu,
setTargetTriple,
setVerbose,
addLinkObj,
addLinkLib,
addLinkPath,
} from "./native-compiler-lib.js";
import { getDtsContent } from "./codegen/stdlib/embedded-dts.js";
import { ArgumentParser } from "./argparse.js";
Expand Down Expand Up @@ -62,6 +65,21 @@ parser.addScopedOption(
"build,run,ir",
);
parser.addScopedOption("target-cpu", "", "Set LLVM target CPU", "native", "build,run,ir");
parser.addScopedOption("link-obj", "", "Extra .o files to link (comma-separated)", "", "build,run");
parser.addScopedOption(
"link-lib",
"",
"Extra libraries to link, -l flags (comma-separated)",
"",
"build,run",
);
parser.addScopedOption(
"link-path",
"",
"Extra library paths, -L flags (comma-separated)",
"",
"build,run",
);
parser.addPositional("input", "Input .ts or .js file");

parser.parse(process.argv);
Expand Down Expand Up @@ -180,6 +198,35 @@ if (cpuOpt.length > 0) {
setTargetCpu(cpuOpt);
}

// Parse extra linker flags (comma-separated lists)
const linkObjOpt = parser.getOption("link-obj");
if (linkObjOpt.length > 0) {
const parts = linkObjOpt.split(",");
let _loi = 0;
while (_loi < parts.length) {
addLinkObj(parts[_loi]);
_loi = _loi + 1;
}
}
const linkLibOpt = parser.getOption("link-lib");
if (linkLibOpt.length > 0) {
const parts = linkLibOpt.split(",");
let _lli = 0;
while (_lli < parts.length) {
addLinkLib(parts[_lli]);
_lli = _lli + 1;
}
}
const linkPathOpt = parser.getOption("link-path");
if (linkPathOpt.length > 0) {
const parts = linkPathOpt.split(",");
let _lpi = 0;
while (_lpi < parts.length) {
addLinkPath(parts[_lpi]);
_lpi = _lpi + 1;
}
}

const inputFile = parser.getPositional(0);
if (inputFile.length === 0) {
console.log("chad: error: no input files");
Expand Down
26 changes: 26 additions & 0 deletions src/chad-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import {
setTarget,
setTargetCpu,
setStaticLink,
addLinkObj,
addLinkLib,
addLinkPath,
} from "./compiler.js";
import { LogLevel, logger } from "./utils/logger.js";
import { runInit } from "./codegen/stdlib/init-templates.js";
Expand Down Expand Up @@ -52,6 +55,21 @@ parser.addScopedOption(
"build,run,ir",
);
parser.addScopedFlag("static", "", "Link statically", "build,run");
parser.addScopedOption("link-obj", "", "Extra .o files to link (comma-separated)", "", "build,run");
parser.addScopedOption(
"link-lib",
"",
"Extra libraries to link, -l flags (comma-separated)",
"",
"build,run",
);
parser.addScopedOption(
"link-path",
"",
"Extra library paths, -L flags (comma-separated)",
"",
"build,run",
);
parser.addPositional("input", "Input .ts or .js file");

// Node's process.argv includes [node, script, ...] — skip both.
Expand Down Expand Up @@ -218,6 +236,14 @@ if (targetOpt) {
const cpuOpt = parser.getOption("target-cpu");
if (cpuOpt) setTargetCpu(cpuOpt);

// Parse extra linker flags (comma-separated lists)
const linkObjOpt = parser.getOption("link-obj");
if (linkObjOpt) linkObjOpt.split(",").forEach((o) => addLinkObj(o));
const linkLibOpt = parser.getOption("link-lib");
if (linkLibOpt) linkLibOpt.split(",").forEach((l) => addLinkLib(l));
const linkPathOpt = parser.getOption("link-path");
if (linkPathOpt) linkPathOpt.split(",").forEach((p) => addLinkPath(p));

if (command === "ir") {
setEmitLLVMOnly(true);
setKeepTemps(true);
Expand Down
13 changes: 7 additions & 6 deletions src/codegen/expressions/calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -627,16 +627,17 @@ export class CallExpressionGenerator {
}
}

// Declared functions (TS `declare function`) are external C symbols —
// use their real name without the _cs_ prefix
const mangledName =
func && func.declare ? resolvedFuncName : this.ctx.mangleUserName(resolvedFuncName);

if (returnType === "void") {
this.ctx.emitCallVoid(`@${this.ctx.mangleUserName(resolvedFuncName)}`, argsList.join(", "));
this.ctx.emitCallVoid(`@${mangledName}`, argsList.join(", "));
return "0";
}

const temp = this.ctx.emitCall(
returnType,
`@${this.ctx.mangleUserName(resolvedFuncName)}`,
argsList.join(", "),
);
const temp = this.ctx.emitCall(returnType, `@${mangledName}`, argsList.join(", "));

return temp;
}
Expand Down
94 changes: 83 additions & 11 deletions src/codegen/llvm-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
AwaitExpressionNode,
BinaryNode,
SourceLocation,
FunctionParameter,
} from "../ast/types.js";
import { BaseGenerator, SymbolKind, SymbolTable } from "./infrastructure/base-generator.js";
import {
Expand Down Expand Up @@ -76,6 +77,8 @@ import {
stripNullable,
tsTypeToLlvm,
tsTypeToLlvmJson,
mapReturnTypeToLLVM,
mapParamTypeToLLVM,
} from "./infrastructure/type-system.js";
import type { ResolvedType } from "./infrastructure/type-system.js";
import { DiagnosticEngine } from "../diagnostics/engine.js";
Expand Down Expand Up @@ -1413,6 +1416,9 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext {
private typeAliasesCount: number = 0;

private usesTreeSitter: boolean = false;
// Tracks function names from user `declare function` to avoid duplicate
// LLVM declarations when a name overlaps with hardcoded runtime declarations
public declaredExternFunctions: Set<string>;
public sourceCode: string = "";
public filename: string = "";

Expand All @@ -1421,6 +1427,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext {

// Initialize complex fields in constructor (field initializers don't work in native code)
this.externalFunctions = new Set();
this.declaredExternFunctions = new Set();
this.topLevelObjectVariables = new Map();
this.globalVariables = new Map();
this.importAliasNames = [];
Expand Down Expand Up @@ -2517,7 +2524,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext {
if (this.usesTreeSitter) {
const tsDecls = this.treesitterGen.generateDeclarations();
if (tsDecls) {
irParts.push(tsDecls);
irParts.push(this.filterDuplicateDeclarations(tsDecls));
}
irParts.push("\n");
}
Expand Down Expand Up @@ -2576,13 +2583,15 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext {
}

finalParts.push(
getLLVMDeclarations({
curl: this.usesCurl !== 0,
crypto: this.usesCrypto !== 0,
sqlite: this.usesSqlite !== 0,
testRunner: this.usesTestRunner !== 0,
targetOS: this.getTargetOS(),
}),
this.filterDuplicateDeclarations(
getLLVMDeclarations({
curl: this.usesCurl !== 0,
crypto: this.usesCrypto !== 0,
sqlite: this.usesSqlite !== 0,
testRunner: this.usesTestRunner !== 0,
targetOS: this.getTargetOS(),
}),
),
);

if (this.usesCurl) {
Expand All @@ -2608,7 +2617,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext {
if (this.usesMongoose) {
const httpServerDecls = this.httpServerGen.generateDeclarations();
if (httpServerDecls) {
finalParts.push(httpServerDecls);
finalParts.push(this.filterDuplicateDeclarations(httpServerDecls));
}
finalParts.push("\n");
}
Expand All @@ -2621,15 +2630,15 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext {
if (needsLibuv) {
const libuvDecls = this.libuvGen.generateDeclarations(this.usesCurl !== 0);
if (libuvDecls) {
finalParts.push(libuvDecls);
finalParts.push(this.filterDuplicateDeclarations(libuvDecls));
}
finalParts.push("\n");
}

if (needsPromise) {
const promiseDecls = this.promiseGen.generateDeclarations();
if (promiseDecls) {
finalParts.push(promiseDecls);
finalParts.push(this.filterDuplicateDeclarations(promiseDecls));
}
finalParts.push("\n");
}
Expand Down Expand Up @@ -2689,6 +2698,36 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext {
return finalParts;
}

/**
* Remove `declare` lines whose function name is already in the
* user-declared extern set (from `declare function` in TS source).
* Prevents LLVM "invalid redefinition" errors when hardcoded runtime
* declarations overlap with user declarations.
*/
private filterDuplicateDeclarations(ir: string): string {
if (this.declaredExternFunctions.size === 0) return ir;
const lines = ir.split("\n");
const filtered: string[] = [];
for (let dli = 0; dli < lines.length; dli++) {
const line = lines[dli];
if (line.startsWith("declare ")) {
const atIdx = line.indexOf("@");
if (atIdx !== -1) {
const rest = line.substring(atIdx + 1);
const parenIdx = rest.indexOf("(");
if (parenIdx !== -1) {
const fnName = rest.substring(0, parenIdx);
if (this.declaredExternFunctions.has(fnName)) {
continue;
}
}
}
}
filtered.push(line);
}
return filtered.join("\n");
}

/**
* Generates LLVM IR for a function declaration and implementation.
* Handles parameter types, allocas, body code generation, and return.
Expand Down Expand Up @@ -2723,6 +2762,11 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext {
}

private generateFunction(func: FunctionNode): string {
// External C function declaration: emit LLVM `declare` with correct types,
// no _cs_ prefix (external symbols use their real C names)
if (func.declare) {
return this.generateDeclareFunction(func);
}
if (this.debugInfoEnabled && func.name) {
const line = this.getLocLine(func);
this.currentSubprogramId = this.dbgCreateSubprogram(func.name, line);
Expand All @@ -2733,6 +2777,34 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext {
return ir;
}

/** Emit an LLVM `declare` for an external C function (from TS `declare function`). */
private generateDeclareFunction(func: FunctionNode): string {
const retType = func.returnType
? mapReturnTypeToLLVM(func.returnType, this.isEnumType(func.returnType))
: "double";

const paramLlvmTypes: string[] = [];
if (func.paramTypes) {
for (let i = 0; i < func.paramTypes.length; i++) {
const pt = func.paramTypes[i];
paramLlvmTypes.push(
mapParamTypeToLLVM(pt, func.params[i] || "", this.isEnumType(stripNullable(pt)), false),
);
}
} else if (func.parameters) {
for (let i = 0; i < func.parameters.length; i++) {
const p = func.parameters[i] as FunctionParameter;
const pType = p.type || "number";
paramLlvmTypes.push(
mapParamTypeToLLVM(pType, p.name || "", this.isEnumType(stripNullable(pType)), false),
);
}
}

this.declaredExternFunctions.add(func.name);
return `declare ${retType} @${func.name}(${paramLlvmTypes.join(", ")})\n`;
}

/**
* Allocate stack space for a variable declaration.
* Handles all variable types: strings, arrays, objects, maps, sets, regex, classes, Response, etc.
Expand Down
Loading