Imported from nihalnihalani/jachacks-sf-2026 (
plugins/jac-codex/skills/jac-core-cheatsheet/SKILL.md). Install upstream withnpx skills add nihalnihalani/jachacks-sf-2026 --skill jac-core-cheatsheet. Copyright stays with the author.
Jac is strict-typed. Every def parameter and return, every has field needs an explicit type; the escape hatch is lowercase any plus the as cast - full rules, narrowing patterns, and error codes in jac-types. Syntax-wise: Python-flavored, every statement ends with ;, every block is { }-braced - except match/case bodies, which use Python indentation (see below). Top-level code runs inside with entry { ... }.
import os;
import from math { pi }
def double(x: int) -> int {
return x * 2;
}
with entry {
name: str = "alice";
age: int = 30;
tags: list[str] = ["a", "b"];
manager: str | None = None;
doubled: list[int] = [n * 2 for n in [1, 2, 3]];
label: str = "adult" if age >= 18 else "minor"; # ternary: A if cond else B
inc = lambda (x: int) { x + 1; }; # typed lambda, implicit return
square = lambda (x: int) -> int { return x * x; }; # optional return type, explicit return
print(inc(4), square(5));
greeting = f"hello {name}, {len(tags)} tags";
print(greeting, label, manager);
for n in doubled {
if n > 4 { print(f"big: {n}"); }
elif n > 2 { print(f"mid: {n}"); }
else { print(f"small: {n}"); }
}
try {
_ = int("not-a-number"); # discard with `_` - unread names warn W2003
} except ValueError as e {
print(f"parse error: {str(e)}");
}
}
Match statements - the ONE indentation-sensitive construct
case arms take a colon + indented body, NOT braces. case 0 { ... } is a parse error (E0001 Expected ':', got '{'). Guards (case x if cond:) and destructuring work as in Python:
obj Point { has x: int = 0; has y: int = 0; }
def describe(value: any) -> str {
match value {
case 0:
return "zero";
case int() as n if n < 0: # guard clause
return f"negative: {n}";
case [x, y]: # sequence destructure
return f"pair: {x}, {y}";
case {"kind": k}: # dict pattern
return f"kind={k}";
case Point(x=px, y=py): # class pattern
return f"point {px},{py}";
case _:
return "other";
}
}
There is also a C-style switch value { case 1: ... default: ... } - it falls through like C; end each case with break;.
Globals and entry points
glob counter: int = 0; # module-level variable - `glob`, not bare assignment
def increment {
counter += 1; # assigns the glob directly - no `global` statement
}
with entry { print("runs on EVERY import of this module"); }
with entry:__main__ { increment(); } # only when run directly (= Python __main__)
Scoping - there is no global or nonlocal statement. A bare assignment (including +=) inside a function assigns to the nearest enclosing binding - an enclosing function's local, or a glob-declared module variable; a new local is created only when no such binding exists. (Python's classic gotcha - x += 1 on a global raising UnboundLocalError - doesn't exist.) To shadow an outer binding instead, write a typed declaration (x: int = 5;) before any use or assignment of that name in the scope; a typed declaration after the name has already been rebound there is an error (E0064). Loop targets, except ... as, and with ... as targets always bind fresh locals. Only glob-declared module variables are implicitly rebindable - assigning the name of an import, function, or class creates a local.
Pitfall for importable libraries: plain with entry executes every time the module is imported. Put demo/CLI code in with entry:__main__ or importing your module will run it.
Import forms
Plain .jac file imports:
import os; # module - takes `;`
import numpy as np; # aliased - full PyPI access (see jac-python-interop)
import from jaclang.byllm.lib { Model } # selective - NO `;`
import type from billing { Invoice } # annotation-only - breaks circular imports (see jac-types)
import ".styles/global.css"; # file - takes `;`
Client imports (in code inferred client - it carries JSX or an npm import):
import from .button { Button } # relative (dots)
import from "@jac/runtime" { Router, Routes, Route } # npm (quoted)
Codespaces are inferred - markers are optional overrides. JSX and string-path npm imports mark a declaration client, and the helpers/globs/imports client code references join the client bundle (scope-aware propagation); unmarked code defaults to server; def:pub endpoints and walkers always stay server (client calls become auto-RPC); extern C-decl imports (import from lib { def f(x: f64) -> f64; }) mark a declaration native and its users follow (consuming a native module is not a signal; pure code stays server). Explicit cl/sv/na blocks, statement prefixes, and file-extension variants like .sv.jac always win over inference - the useful one is sv to pin a declaration server-side. See jac-codespaces.
main.jac mixes contexts. Server imports go at the top (server is the default context - no block needed). The client section - CSS import, top-level component, def:pub app (no-arg for manual routing; app(children) that renders children for file-based routing - see jac-cl-routing) - is inferred client from its JSX and string-path imports; a cl block around it is the optional explicit wrapper.
No-dot imports are project-root absolute. In server/native code (.jac, .sv.jac), import from engine.math.vec3 { Vec3 } resolves against the project root (the nearest jac.toml dir) from anywhere in the project - the importing file may sit at the root, under tests/, or any depth, and the import is identical. This is the idiomatic form; prefer it over dot-counting. A test in tests/ imports the modules it exercises with the same no-dot path it would use at the root.
Relative (dotted) imports walk up from the importing file's own directory - each leading . is one folder. They are mainly needed in client code (inferred client from JSX or npm imports), where the bundler resolves them. sv import carries the same dot semantics.
| Dots | Meaning | Use when |
|---|---|---|
shared.X |
project-root absolute | default - resolves from any depth in the project (server/native) |
.store |
same folder | store is a sibling module in this same folder |
..shared.X |
one folder up | importing file is one level deep (recipes/X.jac) |
...shared.X |
two folders up | importing file is two levels deep (recipes/parts/X.jac) |
A no-dot import is depth-independent: moving a file between directories never changes it. Dot-counted forms (.., ...) DO break when a file moves to a different depth - wrong dot count = silent resolution failure = imported names become <Unknown> → cascading type errors.
Server modules should prefer the no-dot form, and a .. that climbs out of a package is a bug waiting to happen. import from ..shared.github { fetch } resolves fine under jac start but fails jac test <file> with attempted relative import beyond top-level package, because the test runner roots the package at the target file's own directory. import from shared.github { fetch } works in both. Client modules keep the dotted form - that is what the bundler resolves.
Also available (Python semantics, brace bodies)
Generators (yield / yield from), decorators (@deco above def), walrus (n := len(items)), context managers (with open(f) as fh { ... }), C-style loops for i = 0 while i < 10 with i += 1 { }, null-safe access user?.profile?.name, cfg?["key"] (returns None instead of raising - even for missing keys/out-of-range indices), and the default idiom name = user?.name or "Anonymous";.
Pitfalls
- Reserved keywords cannot be used as variable or parameter names - declaration words (
node,edge,walker,obj,def,impl), OSP / control words (visit,disengage,report,spawn,flow,wait,skip,del), andwith,can,has. (entryandexitare not reserved - fine as identifiers.) Escape with a single leading backtick:`visit(no closing backtick;`visit`is a lexer error). - Python reserved words can't name
hasfields or parameters - even backtick-escaped.has `class: str;failsjac checkwith E0067: the generated Python uses the name as a real identifier, so escaping can't help. Pick a non-reserved name (kind,cls). Jac-only keywords that aren't Python keywords (visit,node, ...) escape fine everywhere. `anyvsany: bareanyis the gradual type; backticked`any(...)calls the builtin truthiness function.import from X { Y };fails with E0030. Brace imports take NO trailing semicolon. Plain module formimport X;does.- There is no
passstatement (E0010). For an intentionally empty block write empty braces:{}. - Unused names warn (
W2003). Prefix intentionally-unused names with_, or for unread exception bindings drop the clause:except ValueError { ... }, notexcept ValueError as e. A value bound only to validate still counts as unused - discard with_ = int(s);. This is the #1 reason otherwise-correct parsing/validation code failsjac check. - Booleans are
True/False, null isNone- capitalized. Lowercasefalseparses as an undefined name, soreturn false;fails with the misleadingE1002: Cannot return <Unknown>, expected bool. - Docstrings go immediately before a declaration, never inside its body (
W0060, often +E0002). - Lambdas have ONE form:
lambda (params) { body }. Params always parenthesized and annotated - zero-arglambda { onSign(); }, single paramlambda (v: str) { gbName = v; }(in client code alsolambda (e: ChangeEvent) { ... }), multi-paramlambda (exports: any, fps: int) { ... }- with an optional return type:lambda (x: int) -> int { return x * x; }. A body that is exactly one expression statement IS the implicit return (lambda (x: int) { x + 1; }returnsx + 1); multi-statement bodies need an explicitreturn ...;or the lambda returnsNone(fine for event handlers). A param annotation may be omitted only where the type is inferable from context; otherwise it's E1119. The Python colon forms (lambda x: x,lambda x: int : x + 1) and any paren-less form that carries a parameter (lambda x { ... },lambda v: str { ... }) are parse errors - only a zero-parameter lambda may drop the parens (lambda { ... }). - Ternary is Python-style:
A if cond else B. NOTcond ? A : B- parse error. - Boolean operators are
and/or/not- C-style&&/||do not exist (parse error). - Python stdlib needs explicit import - Jac auto-imports nothing.
datetime.now()withoutimport from datetime { datetime }= runtimeNameError. sv importcalls areasync- alwaysawaitthem.items = fetch_items()assigns aPromise, not the data.import:pydoes not exist - LLMs hallucinate it; useimport json;/import from datetime { datetime }.- Enums use Jac form, NOT Python
class X(Enum). Writeenum Color { RED, GREEN }. When members must BEint/strinstances (JSON, wire formats), use typed-baseenum HttpStatus: int { OK = 200 }(desugars toIntEnum) orenum Tag: str { OPEN = "open" }(StrEnum) - then do NOT add.value, members already are the base type. - Concatenating a string with an Exception fails - wrap with
str(e).
See also
jac-types (type system, as casts, any boundaries) · jac-has-fields (fields) · jac-impl-files (file layout) · jac-codespaces (inferred client/server/native placement) · jac-python-interop (PyPI, ::py::, calling Jac from Python) · jac-concurrency (flow/wait, async)