bama
BAMA: Build Any Mac App. Agent skills to research, clone, design, build, optimize, and test native macOS apps with Swift and SwiftUI.
翻译准备中
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
Synalog
Logic programming for AI agents: Datalog-family language compiling to optimized SQL
Synalog is a logic programming language from the Datalog family, a fork of Logica with the entire engine (parser, compiler and verifier) rewritten in Rust. It compiles to optimized SQL and ships as a Python package built on PyO3: parsing is ~86.7x faster and compilation ~13.7x faster than the original Python implementation, so validating and compiling a program is effectively instant.查看完整 README ↗Synalog is a logic programming language from the Datalog family, a fork of Logica with the entire engine (parser, compiler and verifier) rewritten in Rust. It compiles to optimized SQL and ships as a Python package built on PyO3: parsing is ~52x faster and compilation ~51x faster than the original Python implementation, so validating and compiling a program is effectively instant.
Synalog was built for the AI agents era. The main idea is to give an agent a dynamic semantic layer over its data: a layer of named concepts and rules that the agent both reads from and writes to at inference time. Unlike a traditional BI semantic layer, which is modeled once by humans and frozen, Synalog's layer is authored on the fly: the agent extracts entities and relationships into knowledge graphs, derives meaning with composable logical rules, and reasons over time with temporal reasoning: accumulating all of it as structured, reusable memory.
New here, or deciding whether this belongs in your stack? Why Synalog makes the case in business terms, including the trade-offs and the cases where another tool wins.
A raw table is just rows; an agent has to re-interpret what they mean on every query. Synalog turns that meaning into a layer the agent owns:
Because SQL engines are better optimized than logic programming engines, Synalog compiles into optimized SQL that runs on SQLite, DuckDB, BigQuery, PostgreSQL, Presto, Trino and Databricks: efficiently scaling to petabytes of data. See Supported engines.
Full documentation: https://synalinks.github.io/synalog/
import duckdb
import synalog
source = """
@Engine("duckdb");
Employee(name: "Alice", department: "Engineering", salary: 75000);
Employee(name: "Bob", department: "Marketing", salary: 65000);
Employee(name: "Charlie", department: "Engineering", salary: 80000);
@OrderBy(EngineeringTeam, "name");
EngineeringTeam(name:, salary:) :- Employee(name:, department: "Engineering", salary:);
"""
errors, warnings = synalog.check(source)
assert errors == []
sql = synalog.compile(source, "EngineeringTeam")
rows = duckdb.sql(sql).fetchall()
# [('Alice', 75000), ('Charlie', 80000)]
pip install synalog
Or with uv: uv add synalog (or uv pip install synalog). The CLI also runs without installing via uvx synalog.
Requires Python 3.10+. Wheels are published for Linux (x86_64, aarch64, armv7, s390x, ppc64le; glibc and musl), Windows (x64, x86, aarch64) and macOS (x86_64, aarch64).
Synalog ships an Agent Skill, a SKILL.md that teaches a coding agent the language, the CLI and the conventions, so it writes and runs programs correctly. It follows the open Agent Skills standard, so it works with Claude Code, Cursor, Codex, OpenCode, Cline, Windsurf and 70+ other agents. Install it with the skills CLI (GitHub is the registry, so there is nothing to publish or install first):
npx skills add SynaLinks/synalog # this project (./.claude/skills, ./.agents/skills, …)
npx skills add SynaLinks/synalog -g # user-wide (~/.claude/skills, …)
npx skills add SynaLinks/synalog -a cursor codex # only specific agents
The skill is maintained in this repo at skills/synalog/SKILL.md; npx skills add copies it into the right place for each agent.
Installing the package also installs a synalog command (also available as python -m synalog):
synalog program.l print Predicate # print compiled SQL
synalog program.l run Predicate # execute and print a table
synalog program.l run Predicate --csv # execute and print CSV
Both print and run validate the whole program first and exit 1 with the verifier's errors if it is invalid, so verification always happens before any SQL is produced or executed.
$ synalog program.l run EngineeringTeam
+---------+--------+
| name | salary |
+---------+--------+
| Alice | 75000 |
| Charlie | 80000 |
+---------+--------+
2 rows
- as the file reads the program from stdin; -c PROGRAM passes the program text inline, like python -c.--engine overrides the program's @Engine annotation (default duckdb).--limit / --offset paginate the result.--csv (with run) prints results as CSV instead of the rendered table.--search REGEX (with print/run) keeps only rows where some column matches the regular expression REGEX, e.g. synalog program.l run Customers --search "(?i)acme". In the interactive session the same is .search Customers (?i)acme.run executes on duckdb (the default), sqlite (stdlib) or psql (in a project connected to it) out of the box: synalog depends on duckdb and on psycopg with its bundled libpq, on macOS and Linux alike. For other engines, install their driver (pip install 'synalog[trino]', ...) or use print and run the SQL with your own client.import path.to.file.Pred; statements resolve path/to/file.l against the program file's directory, then the current directory; pass --import-root DIR (repeatable) to search elsewhere.--load TABLE=PATH (repeatable) loads a csv/tsv/json/jsonl/parquet file as a table before running, e.g. synalog senior.l run Senior --load employees=employees.csv.layer.tomlA folder with a layer.toml is a project. Its [connection] says which database its programs run on, as plain fields — commit it:
[connection]
engine = "psql"
host = "db.example.com"
port = 5432
database = "sales"
user = "analyst"
schema = "public"
Secrets never go in the file (synalog refuses it): each comes from the environment as SYNALOG_<ENGINE>_<FIELD> — SYNALOG_PSQL_PASSWORD, SYNALOG_DATABRICKS_ACCESS_TOKEN, or GOOGLE_APPLICATION_CREDENTIALS for BigQuery — usually from the project's .env, kept out of git. Inside the project (from the program's folder or the current directory, and their parents), run, print and verify use that engine and connection, imports resolve from the project's folder, and synalog introspect writes the project's tables/, one file per table of the database. The project's file is the only place a connection comes from; --engine and an @Engine annotation choose the dialect, and a remote engine runs only through the project's connection to it. synalog connect <engine> key=value ... writes it — the non-secret fields to layer.toml, the secret ones to .env — synalog connect shows it (secrets hidden), synalog connect clear removes it. The fields of every engine are in synalog.project.ENGINES.
Running synalog with no arguments starts an interactive session, in the spirit of python (the options above, e.g. --engine or --load, apply to it too):
$ synalog
Synalog 0.1.0 on duckdb, type .help for help
>>> Employee(name: "Alice", salary: 75000);
>>> Employee(name: "Bob", salary: 65000);
>>> Total(t? += salary) distinct :- Employee(salary:);
>>> Total
+--------+
| t |
+--------+
| 140000 |
+--------+
1 row
Type a rule ending in ; to add it to the session program (it is validated first, and rejected with an error if invalid), or a predicate name to compile and run it. .help lists the session commands (.show, .sql <Pred>, .engine <name>, .load <table> <path>, .clear, .exit).
import synalog
source = """
@Engine("duckdb");
Employee(name: "Alice", department: "Engineering", salary: 75000);
Employee(name: "Bob", department: "Marketing", salary: 65000);
Employee(name: "Charlie", department: "Engineering", salary: 80000);
@OrderBy(EngineeringTeam, "name");
EngineeringTeam(name:, salary:) :- Employee(name:, department: "Engineering", salary:);
"""
errors, warnings = synalog.check(source)
assert errors == []
sql = synalog.compile(source, "EngineeringTeam")
print(sql)
You can then execute the SQL with any database driver (sqlite3, duckdb, psycopg, google-cloud-bigquery, etc.).
parse(source, file_name=None, engine=None, import_root=None) -> strParse source and return the AST as a JSON string.
ast = synalog.parse(source)
compile(source, predicate, limit=None, offset=None, engine=None, import_root=None) -> strCompile a single predicate to SQL. limit is combined with the @Limit directive: actual = min(limit, @Limit).
sql = synalog.compile(source, "TopCustomers", limit=20, offset=40)
search(source, predicate, pattern, limit=None, offset=None, engine=None, import_root=None) -> strCompile a predicate to SQL that keeps only rows where some column matches the regular expression pattern (the per-column conditions are OR-ed, each column cast to text). The regex is evaluated by the target engine's native operator (~ on PostgreSQL, REGEXP on SQLite, regexp_matches on DuckDB, REGEXP_LIKE elsewhere). It is not a SQL LIKE pattern. limit/offset apply to the filtered rows.
sql = synalog.search(source, "Customers", "(?i)acme", limit=20)
compile_all(source, engine=None, import_root=None) -> dict[str, str]Compile every defined predicate in the program. Returns a mapping predicate_name -> sql.
sqls = synalog.compile_all(source)
for name, sql in sqls.items():
print(name, sql)
execute(source, predicate, engine=None, project=None, import_root=None, limit=None, offset=None, pattern=None, assertion=None, loads=()) -> tuple[list[str], list[tuple]]Run a predicate on its database (the program's or project's engine and connection) and return (columns, rows). Each recursion stops as soon as a step changes nothing, so it costs the steps its data needs, and @Recursive(P, -1) (until nothing changes) runs on every engine. plan() returns the same steps for a host that runs them itself.
columns, rows = synalog.execute(source, "TopCustomers", limit=20)
check(source, engine=None, import_root=None, assertions=True, project=None) -> tuple[list[str], list[str]]Run structural validation. Returns the error messages and the warning messages, as two lists. The program is valid when there is no error; warnings do not make it invalid. Inside a project whose layer.toml has a [connection] — the current directory's, or the folder project names — it also runs the program's @Assert statements against the database and reports each violated one as an error; assertions=False keeps it offline.
errors, warnings = synalog.check(source)
if errors:
for e in errors:
print(e)
All of these functions accept an optional engine keyword that overrides the program's @Engine annotation (one of sqlite, duckdb, bigquery, psql, presto, trino, databricks; default duckdb) and an optional import_root keyword listing directories where import statements look up .l files (default: the current directory). They raise ValueError on syntax or compilation errors.
A Synalog program is a project: a folder with a layer.toml, holding one predicate per file in three folders, the layout of semantic layers:
shop/
├── tables/orders.l # the data: one file per database table
├── concepts/customer.l # entities and relationships extracted from tables
├── rules/customer_revenue.l # insights derived from concepts
├── rules/top_customers.l
└── layer.toml # the project's name, description and database
Each file is named after its predicate and opens with front matter (name, the predicate; description, what its rows are), then imports what it builds on by folder, file and predicate, then orders its predicate with @OrderBy. A database table is referenced by its lowercase database name in tables/, and everything else builds on the PascalCase predicate that maps it:
tables/orders.l
---
name: Orders
description: One row per order, with its customer, amount and status.
---
@OrderBy(Orders, "order_id");
Orders(order_id:, customer_id:, amount:, status:) :-
orders(order_id:, customer_id:, amount:, status:);
concepts/customer.l
---
name: Customer
description: Every customer who placed at least one order.
---
import tables.orders.Orders;
@OrderBy(Customer, "customer_id");
Customer(customer_id:) distinct :- Orders(customer_id:);
rules/customer_revenue.l
---
name: CustomerRevenue
description: Total amount ordered by each customer, all statuses included.
---
import concepts.customer.Customer;
import tables.orders.Orders;
@OrderBy(CustomerRevenue, "customer_id");
CustomerRevenue(customer_id:, total? += amount) distinct :-
Customer(customer_id:), Orders(customer_id:, amount:);
Imports resolve from the project's folder, so every file runs on its own: synalog rules/customer_revenue.l run CustomerRevenue. The snippets below show the language itself, one rule at a time.
Synalog uses named arguments only (no positional arguments). The left side of : is the column name, the right side is the variable:
# column "amount" bound to variable "total"
Orders(amount: total)
# shorthand: column and variable share the same name
Orders(amount:)
Variables are defined with ==. Arithmetic, string and comparison operators are supported:
OrderWithTax(order_id:, total:) :-
Orders(order_id:, amount:),
total == amount * 1.10;
Operators: +, -, *, /, ^ (power), % (modulo), ++ (string concat), ==, !=, <, >, <=, >=, &&, ||, !, in, is null, is not null.
Aggregation uses the distinct keyword and special operators in the rule head:
# Sum
Revenue(total? += amount) distinct :- Orders(amount:);
# Count
OrderCount(n? += 1) distinct :- Orders(order_id:);
# Min / Max
Cheapest(min_price? Min= price) distinct :- Products(price:);
Priciest(max_price? Max= price) distinct :- Products(price:);
# Average
AvgOrder(avg? Avg= amount) distinct :- Orders(amount:);
# Collect into list / set
AllNames(names? List= name) distinct :- Users(name:);
UniqueNames(names? Set= name) distinct :- Users(name:);
# Value with max/min key
TopSeller(name? ArgMax= name -> revenue) distinct :- Sales(name:, revenue:);
# Conjunction (AND), comma
Result(x:, y:) :- TableA(x:), TableB(x:, y:);
# Disjunction (OR), pipe
Combined(x:) distinct :- SourceA(x:) | SourceB(x:);
# Negation (NOT), tilde
Inactive(user_id:) :- Users(user_id:), ~Logins(user_id:);
Multiple rule definitions for the same predicate combine results (union):
HighValue(user_id:) :- Orders(user_id:, amount:), amount > 10000;
HighValue(user_id:) :- Referrals(user_id:, tier: "vip");
OrderSize(order_id:, size:) :-
Orders(order_id:, amount:),
size == (if amount > 1000 then "large"
else if amount > 100 then "medium"
else "small");
Directives control predicate behavior and must be placed before the rule definition:
@OrderBy(TopCustomers, "total", "DESC");
@Limit(TopCustomers, 10);
TopCustomers(customer_id:, total? += amount) distinct :- Orders(customer_id:, amount:);
| Directive | Purpose |
|---|---|
@OrderBy(Pred, col1, ...) | Sort order. Append "DESC" for descending |
@Limit(Pred, n) | Maximum number of rows |
@Recursive(Pred, n) | Allow recursion with iteration limit |
@Ground(Pred) | Force materialization before dependents |
@Engine(name) | Target SQL engine |
Functors let you reuse predicate logic by parameterizing input predicates:
# Define a reusable pattern
@OrderBy(SegmentRevenue, "segment_id");
SegmentRevenue(segment_id:, total? += amount) distinct :-
Segment(segment_id:, user_id:),
Orders(user_id:, amount:);
# Apply to different segments
EnterpriseRevenue := SegmentRevenue(Segment: EnterpriseCustomers);
SMBRevenue := SegmentRevenue(Segment: SMBCustomers);
Recursive predicates compute transitive closures, for example finding all managers above an employee:
@Recursive(AllManagers, 20);
# Base case: direct manager
AllManagers(employee_id:, manager_id:) :- Employees(employee_id:, manager_id:);
# Recursive case: manager's managers
AllManagers(employee_id:, manager_id:) :-
AllManagers(employee_id:, manager_id: intermediate),
Employees(employee_id: intermediate, manager_id:);
Useful for: referral chains, org charts, product taxonomies, bill of materials.
Find shortest paths in weighted graphs by enumerating route costs recursively, then keeping the minimum per destination with a Min= aggregation:
# Enumerate route costs from the origin, hop by hop.
@Recursive(RouteCost, 10);
RouteCost(destination:, cost:) :-
ShippingRoutes(origin: "warehouse_main", destination:, cost:);
RouteCost(destination:, cost: total) :-
RouteCost(destination: hub, cost: hub_cost),
ShippingRoutes(origin: hub, destination:, cost:),
total == hub_cost + cost;
# Keep the cheapest cost per destination.
@OrderBy(ShippingCost, "destination");
ShippingCost(destination:, total? Min= cost) distinct :- RouteCost(destination:, cost:);
When working with timestamps or dates, always convert to string first:
@OrderBy(MonthlyOrders, "month");
MonthlyOrders(month:, count? += 1) distinct :-
Orders(created_at:),
month == Substr(ToString(created_at), 1, 7);
# Filter by date range
RecentOrders(order_id:) :-
Orders(order_id:, created_at:),
ToString(created_at) >= "2024-01-01";
Today(date:) (today's date as "YYYY-MM-DD") and Now(timestamp:) (the current instant as the engine's native timestamp) are built-in concepts. They are inlined per dialect by the compiler, with no runtime table needed, so they work on every engine. Now is the most precise value; derive coarser parts (date, time, hour) from it through the ToString → Substr pipeline. Join against Today whenever a rule needs "today":
@OrderBy(ThisMonthOrders, "order_id");
ThisMonthOrders(order_id:, created_at:) :-
Orders(order_id:, created_at:),
Today(date:),
Substr(ToString(created_at), 1, 7) == Substr(date, 1, 7);
They are reserved names: you cannot redefine, extend, or update them.
Entities and relationships become node and edge concepts, and rules traverse them. Edges join through the node concepts rather than the raw table, so a filter on a node applies to every edge built on it:
@OrderBy(Person, "person_id");
Person(person_id:, name:, profile_url:) distinct :- Employees(person_id:, name:, profile_url:);
@OrderBy(WorksIn, "person_id");
WorksIn(person_id:, department_id:) distinct :-
Person(person_id:),
Department(department_id:),
Employees(person_id:, department_id:);
Relationships with a lifetime carry it as a half-open interval [valid_from, valid_to), using "9999-12-31" as the open end so ISO strings compare correctly, and Today supplies the clock for point-in-time questions:
@OrderBy(CurrentEmployment, "person_id");
CurrentEmployment(person_id:, company_id:, role:) distinct :-
EmployedAt(person_id:, company_id:, role:, valid_from:, valid_to:),
Today(date:),
valid_from <= date, date < valid_to;
Node and edge patterns, temporal modeling, point-in-time queries and time-respecting traversals are covered in Knowledge graphs.
String: Substr, Length, Upper, Lower, Split, Join, Like, Format
Array: Size, Element, ArrayConcat, Range
Math: Abs, Floor, Ceil, Round, Sqrt, Log, Exp, Sin, Cos
Type casting: ToInt64, ToFloat64, ToString
Other: Coalesce, IsNull
Built-in concepts: Today(date:) is today's date as "YYYY-MM-DD"; Now(timestamp:) is the current instant as a native timestamp (see Temporal data above).
| Engine | @Engine value | Notes |
|---|---|---|
| DuckDB | duckdb | Default engine |
| SQLite | sqlite | |
| PostgreSQL | psql | |
| BigQuery | bigquery | |
| Trino | trino | |
| Presto | presto | |
| Databricks | databricks | Double-quoted string literals |
Each engine has its own SQL dialect for string literals, array syntax, GROUP BY style, record construction, regex matching, and standard library functions.
The Rust core is benchmarked against the original Python Logica implementation on every program of the compiler test suite (28,614 programs across 6 engines). Both run in-process: Synalog through the same PyO3 extension that pip install synalog ships, so the numbers measure exactly what a Python caller gets:
| Python Logica | Synalog (Rust) | Speedup | |
|---|---|---|---|
| Parse | 230.5 s | 4.1 s | 52x |
| Compile | 1117.7 s | 25.7 s | 51x |
| Verify | n/a | 12.1 s | Rust-only |
Speedup is the geometric mean of per-program speedups (every program weighted equally). Parsing is uniformly ~52x faster; compilation ranges from ~38x (trino, presto) to ~102x (duckdb). Verification (safety, stratification, recursion and reserved-name checks) is a Synalog-specific pass with no standalone Python equivalent. Per-engine tables and methodology are on the Benchmark page; reproduce with python3 benchmark.py.
Unlike Logica, which lets the database raise errors at execution time, Synalog embeds a formal verifier that catches issues at compile time.
| Check | What it detects |
|---|---|
| Safety | Head variables not bound in the body |
| Safe negation | Negated variables without a positive occurrence |
| Safe aggregation | Aggregated variables not bound outside the aggregate |
| Stratification | Negative recursion cycles |
| Arity | Predicates used with inconsistent argument counts |
| Recursion | Missing base cases, trivial loops, unbounded recursion without @Recursive |
| Reserved names | Rules that redefine a built-in library predicate (Num, Str, ArgMin, Today, Now, ...) |
Unsafe SqlExpr | User rules that reach for the raw-SQL escape hatch |
The algorithms behind the stratification and recursion checks, the compiler's unfolding of mutual recursion, the order a compiled program's tables run in, the contradiction warning, pagination, the quoting of text into SQL and the Cast type-name check are proved correct with Verus. See Verification.
errors, warnings = synalog.check(bad_source)
for e in errors:
print(e)
# Unbound variable 'y' in head of rule: Test(x:, y:) :- Numbers(x:)
Synalog doesn't support positional attributes like Logica or Datalog: it only uses named attributes, which reduce agent mistakes. This feature is optional in Logica; we made it mandatory.
In Synalog, the compiled SQL uses actual column names, not col{i} format, making it compatible with existing database schemas.
Pagination is critical for AI agents with limited context windows. It also avoids loading large amounts of data into memory, enabling use on memory-constrained cloud infrastructure.
Synalog applies pagination at compile time via the limit and offset arguments of compile(). The limit is combined with the @Limit directive: actual_limit = min(limit, @Limit).
Synalog embeds a formal verifier that catches structural errors before any SQL is generated. This prevents agents from producing programs that parse correctly but fail at execution time, a common failure mode when working with SQL directly.
The project uses maturin to build the Python wheel from the Rust crate.
pip install maturin
maturin develop --release # install into the active venv
maturin build --release # produce a wheel in target/wheels/
Run the Rust test suite:
cargo test
Run specific test groups:
cargo test --lib # unit tests
cargo test --test compiler_tests # compiler golden tests (all engines)
cargo test --test parser_tests # parser golden tests (all engines)
cargo test --test verifier_tests # verifier tests (all engines)
cargo test --test search_tests # search feature tests (all engines)
cargo test --test verus_proofs # the Verus proofs of verified/ (shell/install-verus.sh installs Verus)
cd synalog
./shell/test.sh
Synalog is a fork of Logica, the logic programming language created by Evgeny Skvortsov and the Logica team at Google LLC. The parser, compiler, and type-inference engine were rewritten in Rust, but the language design, semantics, and a large part of the algorithms originate from their work. Synalog would not exist without it, heartfelt thanks to the Logica authors for their amazing work.
Files derived from Logica carry an // Original work: Copyright 2020 Google LLC header and a note describing the modifications, as required by the Apache License 2.0.
Synalog is licensed under the Apache License, Version 2.0 see LICENSE and NOTICE.