Skip to content

Custom Syntax

Custom syntax is the foundation for Jiang’s All-in-one direction. Users can extend Jiang with their ideal domain syntax while keeping Jiang’s built-in type checking and systems-level runtime efficiency.

It lets a package parse a block of source at a specific syntax position, then return a Jiang syntax tree for normal name resolution, type checking, and code generation. It is intended for domain syntaxes such as queries, user interfaces, shaders, protocols, and low-level assembly blocks.

The current implementation supports block invocation:

User user = #sql {
select * from User where id == \(id)
};

The sql name in #sql is not a normal value name and is not found through import. It usually comes from the current package manifest’s dependency aliases. The compiler can also provide builtin syntax blocks, such as #asm.

The following snippets are sketches that show the kinds of problems custom syntax is meant to solve. The exact syntax is defined by the corresponding lang package.

Object-relational queries can check tables, fields, and interpolated parameters together:

User[] users = #query {
from User
where age >= \(min_age)
select id, name, email
};

User interface templates can keep an HTML-like shape while returning components, attributes, and bindings to Jiang’s type system:

View profile = #html {
<section class="profile">
<h1>{user.name}</h1>
<button on_click={save(user.id)}>Save</button>
</section>
};

Declarative user interfaces can use a tree-shaped syntax similar to SwiftUI, while the lang package checks component parameters, state bindings, and event signatures:

View settings = #ui {
VStack(spacing: 12) {
Text("Settings").font(.title)
Toggle("Enable cache", is_on: bind(settings.cache_enabled))
Button("Save") {
save(settings)
}
}
};

Shaders can use graphics-domain syntax, while the lang package generates checked host-side entry points and resource bindings:

Shader blur = #shader {
texture source: Texture2D<Float4>;
uniform radius: Float;
fragment Float4 main(Float2 uv) {
return sample_blur(source, uv, radius);
}
};

Vector computation can express batch math clearly, letting the lang package choose vector instructions or a normal backend path:

Vec4 result = #simd {
let a = load4(lhs);
let b = load4(rhs);
fma(a, b, bias)
};

Binary protocols can describe field numbers, byte order, length relationships, and optional fields in one place, then generate efficient encoders and decoders:

Codec<User> user_codec = #binary {
message User {
1: UInt64 id;
2: UInt8[]& name;
3: UInt8[]&? email;
}
};

State machines and protocol flows can spell out transitions so the lang package can check missing branches and illegal moves:

StateMachine connection = #state {
disconnected -> connecting on connect;
connecting -> connected on ready;
connected -> disconnected on close;
};

Command-line interfaces can be declared once, then generate parsing, help text, and a typed options object:

CliOptions options = #cli {
command build {
flag release: Bool = false;
option target: Target = native;
arg input: Path;
}
};

Service routes can keep path parameters, request bodies, and return values together while preserving static types for handlers:

Router api = #route {
GET /users/:id -> get_user(id: UserId) -> User;
POST /users -> create_user(body: NewUser) -> User;
};

Data pipelines can spell out steps and dependencies so the lang package can check whether each stage’s output matches the next stage’s input:

Pipeline image_pipeline = #pipeline {
load(path)
|> decode_png
|> resize(width: 256, height: 256)
|> encode_webp(quality: 90)
};

Registers and bit fields can mirror a hardware manual, then generate low-level access code:

RegisterBlock uart = #mmio {
base 0x4000_1000;
reg control: UInt32 offset 0x00 {
enable: bit 0 read_write;
mode: bits 1..2 read_write;
}
reg status: UInt32 offset 0x04 read_only;
};

#asm is a builtin syntax block available on the current development branch. It is meant for small pieces of target-specific assembly inside Jiang functions, such as system calls, startup code, special instructions, or rare backend boundary cases.

Int linux_syscall1(Int number, Int arg0) {
return #asm {
result Int;
code "syscall";
in "rax" number;
in "rdi" arg0;
out "rax";
clobber "rcx";
clobber "r11";
memory;
volatile;
};
}

An assembly block can also be used as a statement. If no value is needed, omit result:

() fence() {
#asm {
code "mfence";
memory;
volatile;
}
}

#asm is the short name. A user lang package may override that short name later. To explicitly use the compiler builtin, write the full builtin path:

Int linux_syscall1_builtin(Int number, Int arg0) {
return #jiang.asm {
result Int;
code "syscall";
in "rax" number;
in "rdi" arg0;
out "rax";
clobber "rcx";
clobber "r11";
memory;
volatile;
};
}

The current #asm block supports:

  • result T;: the result type. If omitted, the block returns unit.
  • code "...";: the assembly text. This item is required.
  • in "register-or-constraint" value;: an input operand.
  • out "register-or-constraint";: an output operand.
  • clobber "register-or-resource";: a register or resource modified by the assembly.
  • memory;: the assembly may read or write ordinary memory.
  • volatile;: the assembly has side effects and should not be freely removed or reordered.
  • noreturn;: the assembly does not return.

Assembly syntax is target-specific. Names such as rax, rdi, and syscall are only meaningful on specific processors and operating systems. Most application code should not use #asm; it is mainly for runtime code, system boundaries, and narrow platform-specific operations.

In a future design, custom syntax can also grow to whole source files. A package could import a non-.jiang file, let the matching lang package parse that file type, and add the result to the build as a Jiang module:

import schema = "user.proto";
import layout = "dashboard.ui";
import kernel = "blur.shader";
User user = schema.User(id: 1, name: "Jiang");
View page = layout.render(user);

In that model, external language files are not a separate code-generation step; they are part of the package dependency graph. Fields, functions, resources, and generated types can still participate in Jiang name resolution, type checking, and backend optimization.

Jiang itself is designed around ownership and lifetimes; it does not require garbage collection by default. If Jiang later introduces garbage collection, it should be an optional runtime capability chosen by users and packages for the cases that need it. On that optional runtime, custom syntax can host scripting languages. Script blocks or script files could become part of a Jiang package, useful for game logic, plugins, hot-update rules, and user automation:

Script startup = #script {
let player = world.find("player")
player.inventory.add("key")
on event.dialog_finished {
world.open_door("north_gate")
}
};

The script side can use a more dynamic object model, while the host side still uses Jiang to define resources, boundary types, and call interfaces. That gives scripts a flexible surface language while keeping performance-critical paths in Jiang’s ownership and lifetime model.

The using package declares a dependency:

[package]
name = app
root = src/main.jiang
[dependencies]
sql = ../sql_lang

The dependency must be a lang package:

[package]
name = sql_lang
root = lang.jiang
type = lang

The lang package root module must publicly export Lang, and Lang must satisfy std.jiang.syntax.Provider. The compiler builds that package into a host dynamic library and calls it during syntax parsing.

A provider has two entry points:

public std.jiang.syntax.ScanResult scan(
std.jiang.syntax.Input input,
std.jiang.syntax.Builder.Any& builder
);
public std.jiang.syntax.NodeId parse(
std.jiang.syntax.Input input,
std.jiang.syntax.Root.Kind entry_kind,
std.jiang.syntax.Builder.Any& builder
);

scan identifies the block boundary. parse converts the block contents into nodes in std.jiang.syntax.Tree. The returned node must match the entry kind passed by the compiler.

In ordinary source, custom syntax currently plugs into expression and statement positions. The public syntax tree also reserves file, declaration, type, and pattern entry kinds for future expansion.

Custom syntax changes parsing, but it does not bypass the Jiang compiler pipeline:

  • Generated nodes still go through normal name resolution, type checking, intermediate representation, and backend code generation.
  • A provider cannot directly generate intermediate representation or backend code.
  • Provider dynamic libraries are host-local cache artifacts and are not reusable across compiler versions.
  • Lang packages are still distributed as source packages; cache damage or interface mismatches should produce diagnostics.

This allows a domain-specific surface syntax while keeping type rules, visibility, monomorphization, and backend behavior under Jiang compiler control.