Skip to content

coasys/ad4m-expression-language-template

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AD4M Expression Language Template

A starting point for building new AD4M expression languages using the modern ALDK (@coasys/ad4m-ldk) pattern.

An expression language owns a URI scheme and resolves URIs in that scheme to Expression records. Examples in the AD4M ecosystem:

  • literalliteral://json:<inline>, literal://string:<text>, literal://number:<n> for primitive values inlined in the URI.
  • language-languageQm… content-addresses of published language bundles.
  • git-expression-languagegit+https://<host>/<o>/<r>.git#<ref>:<path> for files at specific commits in any Git host.

This template gives you a working expression-language skeleton with:

  • defineLanguage({ expression: { get, isImmutable, … } }) wiring with the modern ALDK
  • Example URI parser (src/uri.ts) — adapt the regex to your scheme
  • Transport + Storage + Runtime + Signing adapter pattern (pure / impure separation) for testability
  • Template variable support for per-instance configuration
  • esbuild bundling for the Deno executor runtime
  • Unit tests that run outside the executor (Node.js + tsx)

Prerequisites

  • Deno (v1.32+) — used by the executor runtime and the build script
  • Node.js (v20+) + npm/pnpm — for dev dependencies and running tests
  • @coasys/ad4m-ldk — either:
    • Cloned at a sibling path: ../ad4m/ad4m-ldk/js/ (the default)
    • Or set AD4M_LDK_ENTRY env var to the compiled lib/index.js

Quick Start

# Install dev dependencies
pnpm install   # or npm install

# Build the bundle
deno run --allow-all esbuild.ts

# Run tests
node --experimental-vm-modules --import tsx --test tests/*.test.ts

# Type-check
npx tsc --noEmit

Project Structure

├── index.ts              # Main entry — uses defineLanguage() from @coasys/ad4m-ldk
├── esbuild.ts            # Build script (Deno + esbuild)
├── package.json          # Dependencies
├── tsconfig.json         # TypeScript config
├── src/
│   ├── types.ts          # DID, Expression, ExpressionProof
│   ├── uri.ts            # Example URI parser — replace with your scheme
│   ├── adapters.ts       # Transport / Storage / Runtime / Signing interfaces
│   └── adapters-deno.ts  # Deno-side adapter impls wrapping ad4m:host
├── tests/
│   └── uri.test.ts       # Sample tests for the parser
├── build/                # Output directory (bundle.js)
├── README.md
└── LICENSE               # CAL-1.0

Architecture

Expression vs. Link languages

Link language Expression language
Capability commit + sync + query (perspective-bound) expression (URI-bound)
State Owns a Perspective; carries link expressions Stateless resolver over a URI scheme
Lifecycle init, then per-perspective context init, then per-call resolution
Output PerspectiveDiff, link query results Expression<T>
Examples git-link-language, nostr-link-language, p-diff-sync (Holochain) literal, language-language, git-expression-language

An AD4M agent typically uses one Link Language per Neighbourhood and many Expression Languages — every URI scheme an app dereferences needs an Expression Language registered for it.

Pure / Impure Separation

Core logic stays free of runtime-specific imports:

  • Pure modules (types.ts, uri.ts, adapters.ts) — no ad4m:host imports. Depend only on TypeScript types + injected adapters. Testable in plain Node.js.
  • Impure modules (adapters-deno.ts) — wrap ad4m:host functions via @coasys/ad4m-ldk. Only imported in index.ts during init().

This separation means your resolver logic can run in node --test without any AD4M runtime.

Transport Adapter

The transport layer abstracts HTTP calls. In the executor, httpFetch from ad4m:host is the only outbound network primitive.

Important: httpFetch returns raw body text on 2xx and throws on non-2xx. The DenoTransport in src/adapters-deno.ts handles this by parsing the error message format to extract status codes.

In tests, inject a MockTransport that implements the Transport interface.

Storage Adapter

For resolvers that cache content, the storage layer wraps the executor's KV store (storageGet, storagePut, storageDelete, storageListKeys). In tests, use a simple Map-based mock.

Note: at the time of writing, the executor's File I/O extension is not always installed, so the KV is in-memory only in some builds. Design caches to tolerate "lost on restart".

defineLanguage()

The modern ALDK entry point. Instead of the old create(context: LanguageContext) pattern, you call defineLanguage() with a config object declaring your capabilities:

const language = defineLanguage({
    name: "my-expression-language",
    version: "0.1.0",
    isPublic: true,
    async init() { /* ... */ },
    expression: {
        async get(address: string) { /* return Expression or null */ },
        async isImmutable(address: string) { /* return boolean */ },
        // Optional — most expression languages don't implement these:
        // async create(content): Promise<string> { ... }
        // async addressOf(content): Promise<string> { ... }
    },
    expressionUI: {
        icon: () => "<svg>...</svg>",
        constructorIcon: () => "<svg>...</svg>",
    },
});

The returned object has flat exports (expressionGet, expressionIcon, etc.) that the executor binds to.

Trust model

Expression languages typically do not AD4M-sign the content they resolve. The Expression's proof is empty, and the underlying substrate's own integrity model (content addressing, host TLS, server-side ACLs) is what the consumer trusts. Document this clearly in your language's README — apps that need stronger guarantees can wrap your URIs in signed Link Expressions.

Template Variables

Template variables allow per-instance configuration at publish time. Mark them with the //!@ad4m-template-variable comment:

//!@ad4m-template-variable
const MY_CONFIG = "<to-be-filled>";

When the language is published via language.publish, the executor replaces "<to-be-filled>" with actual values from the templateParams map. The possibleTemplateParams export tells the executor which variables exist.

Add your own template variables for backend URLs, API keys, cache sizes, or any per-instance configuration.

Publishing

To publish your language to an AD4M executor:

  1. Build the bundle: deno run --allow-all esbuild.ts
  2. Use the executor's WebSocket API to call language.publish:
{
    "languagePath": "./build/bundle.js",
    "languageMeta": {
        "name": "my-ad4m-expression-language",
        "description": "My custom expression language",
        "possibleTemplateParams": ["UNIQUE_SEED"],
        "sourceCodeLink": "https://github.com/your-org/your-repo"
    }
}

Implementing your scheme

  1. Define the URI grammar in src/uri.ts. Aim for an existing canonical form (RFC 3986, npm convention, web URLs) rather than inventing a new one when possible.
  2. Replace index.ts's expression.get body with your resolution logic.
  3. Set isImmutable correctly — important for caching downstream. SHA-pinned URIs, content hashes, and other deterministic identifiers are immutable.
  4. Decide whether expression.create makes sense for your scheme. For resolvers over existing content (HTTP fetchers, Git readers), it usually doesn't.
  5. Add tests for the parser and the resolver. Mock the transport so the suite has no network dependency.

License

Cryptographic Autonomy License v1.0 (CAL-1.0)

About

A template for building AD4M expression languages using the modern ALDK pattern

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors