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:
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.
Representative Scenarios
Section titled “Representative Scenarios”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 interface templates can keep an HTML-like shape while returning components, attributes, and bindings to Jiang’s type system:
Declarative user interfaces can use a tree-shaped syntax similar to SwiftUI, while the lang package checks component parameters, state bindings, and event signatures:
Shaders can use graphics-domain syntax, while the lang package generates checked host-side entry points and resource bindings:
Vector computation can express batch math clearly, letting the lang package choose vector instructions or a normal backend path:
Binary protocols can describe field numbers, byte order, length relationships, and optional fields in one place, then generate efficient encoders and decoders:
State machines and protocol flows can spell out transitions so the lang package can check missing branches and illegal moves:
Command-line interfaces can be declared once, then generate parsing, help text, and a typed options object:
Service routes can keep path parameters, request bodies, and return values together while preserving static types for handlers:
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:
Registers and bit fields can mirror a hardware manual, then generate low-level access code:
Builtin Assembly Blocks
Section titled “Builtin Assembly Blocks”#asm is a builtin syntax block. 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.
An assembly block can also be used as a statement. If no value is needed, omit result:
#asm is the short name. A user lang package can override that short name. To explicitly use
the compiler builtin, write the full builtin path:
The current #asm block supports:
result T;: the result type. If omitted, the block returnsVoid.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.
Documentation Blocks
Section titled “Documentation Blocks”#doc attaches Markdown source to the declaration that follows it. Text on the header line creates a
single-line document:
If the header line has no text, an otherwise standalone #end ends the document. Use #doc(module)
before the first declaration other than an import declaration (including before an alias) to document the current module:
A lang dependency may override the short name #doc; use #jiang.doc to select the builtin provider
explicitly. Documentation is available to reflection and documentation generators.
Whole-File Integration
Section titled “Whole-File Integration”Custom syntax also supports whole source files. Import a non-.jiang file through a configured
extension mapping; the matching Lang provider parses it into a Jiang module:
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.
Embedded Scripting
Section titled “Embedded Scripting”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:
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.
Using a Lang Package
Section titled “Using a Lang Package”The using package declares a dependency:
The dependency must be a lang package:
Mark a concrete, zero-argument-constructible provider type in the package root with @entry(lang).
It must implement std.jiang.syntax.Provider; its name is arbitrary and it may be private.
Provider Entry Points
Section titled “Provider Entry Points”A provider implements std.jiang.syntax.Provider. Providers using Jiang’s default lexical rules only
need to implement parse:
The default scan recognizes nested delimiters, strings, comments, and end of file. A provider with
custom tokens can override scan, store Token<CustomKind> values in its own instance, and construct
Parser<CustomKind> in parse.
Parser methods create typed expression, statement, declaration, type, pattern, and attribute handles.
parser.ast(handle) returns the result. Choose a role that fits the position: expressions in expressions,
declarations at the top level, and members inside a type.
Language Aliases and File Extensions
Section titled “Language Aliases and File Extensions”Register the dependency once, then map a language alias to it:
Use #schema { ... } or import models.schema. The provider package may declare default extensions
in lang { extensions = ["schema", "model"]; }; the using package replaces that list with its own mapping. Without an explicit
extension list, the language alias is the default extension. .jiang retains native Jiang syntax.
Whole-file providers receive the .none delimiter and return declarations. The file can also be the
package root. Its contents and provider dependencies participate in compiler caching.
A package can provide both Lang and generation entries. An explicit same-named generate configuration takes precedence when selecting a generator by language alias.
Shared Keywords and Context
Section titled “Shared Keywords and Context”A stateless Provider can use the default EmptyLangContext and implement only parse. To share
keywords or other data, declare LangContext, initialize it with create_context(Session&), and read
it through context.lang. One language shares its context within a compilation; separate compilations
are isolated. Each block still has its own Provider instance. Keep block tokens and temporary parsing
state out of shared context.
This language accepts both #value { value 42 } and #value { 42 }:
session.intern() returns IDs compatible with default tokens, so keywords need not be registered for every block.
Nested Languages and Attributes
Section titled “Nested Languages and Attributes”At a RawBlock, call parser.parse_raw_block() to obtain Ast?. Inspect role() or use
as_attribute(), as_decl(), as_member() and the other typed accessors. Your language can therefore
accept #doc without recognizing its name or implementing Markdown parsing.
The receiving language chooses the target and attaches attributes to a Decl or Member with
with_attributes. Keep pending attributes in parser.list<Attribute>() until the next target arrives.
Report dangling attributes at the end of a block; do not attach an inner block’s documentation to
an outer declaration. Package configuration fields may instead ignore attributes.
parser.ast(member) returns one member. Use parser.members(span, values) for member sequences
and parser.declarations(span, values) for declaration sequences. Empty and single-element sequences
remain sequences; read them with parser.len(sequence) and parser.at(sequence, index).
If the current token is not a RawBlock, expansion reports a diagnostic and returns null without
consuming it. A failed RawBlock expansion consumes that token and keeps its diagnostics.
peek and advance only read tokens; they do not expand language blocks.
A custom tokenizer can call scan_raw_block() after begin_token(), then retain the returned block
ID and original span. Expand that saved default token with parser.parse_raw_block(raw); this overload
does not advance the custom parser’s cursor. Consume your own custom token explicitly, and do not
fabricate identities or reuse them across source files.
Lists and Backtracking
Section titled “Lists and Backtracking”Create syntax-handle lists with parser.list<T>(), then use append, len and at. AST construction
accepts managed lists alongside existing slice arguments. These lists hold supported syntax handles,
not arbitrary application values.
Inside parse, with parser and input already available:
Save parser.checkpoint() before a speculative parse. Use parser.rewind(saved) to try another
interpretation. Rewind restores tokens, diagnostics, AST changes and managed lists, including attributes
attached to existing nodes. Handles created after the checkpoint become invalid; earlier lists regain
their previous contents. On success, continue without a separate commit operation.
Provider fields, ordinary vectors and external side effects are not rolled back. Put pending attributes in managed lists when they must rewind. Default scanned tokens support repeated parsing; custom tokens must likewise remain readable if your parser retries them.
Default tokens use single_quoted/double_quoted to distinguish quote forms. SymbolId contains decoded
text; use the span to read original quotes and escapes. Constructing a Jiang character AST requires
exactly one Unicode scalar.