본문으로 건너뛰기
cccskills
무료GitHub에 공개

write-salam

Convert, rewrite, port, or redesign software from another language (PHP, TypeScript/JavaScript, Python, C, Go, Rust, Java, C#, …) into idiomatic, full-feature Salam: write new Salam code, and port C to Salam (including self-hosting the Salam compiler itself). Use whenever the request mentions "Salam" (the programming language / زبان سلام), a `.salam` file, "convert/port/ rewrite X to Salam", "rewrite the compiler in Salam", the Salam layout DSL, or building with the `salam` compiler. Covers both English Salam (Part I) and Persian Salam, سلام فارسی (Part II: every keyword, type, method and std package name). This is the authoritative, up-to-date reference for Salam's current syntax, its standard library, its strict compiler rules, the layout DSL, and porting low-level C. The tutorial books under `books/` are OUT OF DATE. Trust this file, `std/`, `tests/`, and the Persian course at https://www.salamlang.ir/learn/ instead.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

실행 점수 미검증

설치 방법 보기

포함된 파일(200)

  • SKILL.md191.1 KB
  • .chktexrc1.8 KB
  • .clang-format529 B
  • .dockerignore135 B
  • .editorconfig1.1 KB
  • .gitattributes724 B
  • .github/actionlint.yaml76 B
  • .github/actions/deploy-ssh/action.yml5.8 KB
  • .github/actions/publish-gh-pages/action.yml3.6 KB
  • .github/actions/setup-salam/action.yml7.8 KB
  • .github/actions/setup-salam/examples/ci.yml1003 B
  • .github/actions/setup-salam/examples/hello.salam441 B
  • .github/actions/setup-tcc/action.yml1.9 KB
  • .github/CODEOWNERS27 B
  • .github/codeql/javascript.yml95 B
  • .github/dependabot.yml914 B
  • .github/docker/armhf-builder/Dockerfile581 B
  • .github/ISSUE_TEMPLATE/bug_report.md1.3 KB
  • .github/ISSUE_TEMPLATE/custom.md541 B
  • .github/ISSUE_TEMPLATE/feature_request.md1.2 KB
  • .github/labeler.yml739 B
  • .github/linters/.codespellrc210 B
  • .github/linters/.gitleaks.toml6.6 KB
  • .github/linters/.hadolint.yaml81 B
  • .github/linters/.htmlhintrc26 B
  • .github/linters/.jscpd.json376 B
  • .github/linters/.luacheckrc216 B
  • .github/linters/.markdown-lint.yml115 B
  • .github/linters/.phpcs.xml306 B
  • .github/linters/.powershell-psscriptanalyzer.psd11.0 KB
  • .github/linters/.proselintrc.json91 B
  • .github/linters/.pylintrc601 B
  • .github/linters/.ruff.toml17 B
  • .github/linters/.sqlfluff891 B
  • .github/linters/.textlintrc1.5 KB
  • .github/linters/.yaml-lint.yml237 B
  • .github/linters/codespell.txt106 B
  • .github/linters/google_checks.xml21.3 KB
  • .github/linters/trivy.yaml146 B
  • .github/pull_request_template.md1.3 KB
  • .github/workflows/_changes.yml2.7 KB
  • .github/workflows/_reusable-android-build.yml6.6 KB
  • .github/workflows/agent-release.yml12.0 KB
  • .github/workflows/ai-deploy.yml11.8 KB
  • .github/workflows/app-android-pr-build.yml4.9 KB
  • .github/workflows/benchmark-pr.yml4.1 KB
  • .github/workflows/benchmark-publish.yml3.0 KB
  • .github/workflows/books-build-pdf.yml5.0 KB
  • .github/workflows/codeql.yml1.6 KB
  • .github/workflows/community-first-interaction.yml939 B
  • .github/workflows/compiler-clone-build.yml646 B
  • .github/workflows/compiler-nightly-release.yml3.3 KB
  • .github/workflows/compiler-release.yml122.0 KB
  • .github/workflows/compiler-selfhost.yml14.6 KB
  • .github/workflows/docker-release.yml3.7 KB
  • .github/workflows/editor-playground-build.yml3.2 KB
  • .github/workflows/editor-playground-deploy.yml5.3 KB
  • .github/workflows/extension-vscode.yml5.7 KB
  • .github/workflows/extensions.yml5.7 KB
  • .github/workflows/lint-memory-leaks.yml2.6 KB
  • .github/workflows/lint-prek-audit.yml873 B
  • .github/workflows/lint-prek-infra.yml982 B
  • .github/workflows/lint-prek-manual.yml878 B
  • .github/workflows/lint-prek.yml853 B
  • .github/workflows/lint-super-linter.yml1.6 KB
  • .github/workflows/lint-version-sync.yml695 B
  • .github/workflows/llvm-selfhost-gate.yml2.7 KB
  • .github/workflows/mcp-server.yml4.9 KB
  • .github/workflows/native-backend-gate.yml5.9 KB
  • .github/workflows/packages-os.yml1.5 KB
  • .github/workflows/packaging-release.yml10.1 KB
  • .github/workflows/pr-labeler.yml479 B
  • .github/workflows/runner-container-build.yml2.3 KB
  • .github/workflows/runner-deploy-cloudflare.yml2.1 KB
  • .github/workflows/website-deploy.yml5.9 KB
  • .gitignore2.6 KB
  • .mcp.json105 B
  • .prettierignore28 B
  • .prettierrc26 B
  • .readthedocs.yaml265 B
  • .salamfmtignore593 B
  • .shellcheckrc1.1 KB
  • .stylelintrc.json599 B
  • .trivyignore.yaml1.1 KB
  • agent/.gitignore70 B
  • agent/dist/favicon.ico14.2 KB
  • agent/dist/fonts/LICENSE-Shabnam4.4 KB
  • agent/dist/fonts/Shabnam-Bold.woff236.8 KB
  • agent/dist/fonts/Shabnam.woff234.9 KB
  • agent/dist/index.html2.5 KB
  • agent/dist/logo.svg2.8 KB
  • agent/README.md4.2 KB
  • agent/scripts/build-linux-i686.sh1.7 KB
  • agent/scripts/make-macos-icon.sh1.6 KB
  • agent/scripts/package.sh2.1 KB
  • agent/src-tauri/build.rs379 B
  • agent/src-tauri/capabilities/remote.json553 B
  • agent/src-tauri/Cargo.lock138.6 KB
  • agent/src-tauri/Cargo.toml1.5 KB
  • agent/src-tauri/icons/128x128.png3.8 KB
  • agent/src-tauri/icons/128x128@2x.png8.5 KB
  • agent/src-tauri/icons/32x32.png843 B
  • agent/src-tauri/icons/64x64.png1.8 KB
  • agent/src-tauri/icons/icon-macos.png58.2 KB
  • agent/src-tauri/icons/icon.icns210.3 KB
  • agent/src-tauri/icons/icon.ico14.2 KB
  • agent/src-tauri/icons/icon.png18.8 KB
  • agent/src-tauri/rustfmt.toml56 B
  • agent/src-tauri/src/approve.rs6.0 KB
  • agent/src-tauri/src/bridge.rs5.8 KB
  • agent/src-tauri/src/commands.rs3.4 KB
  • agent/src-tauri/src/config.rs2.7 KB
  • agent/src-tauri/src/device.rs2.8 KB
  • agent/src-tauri/src/lib.rs5.6 KB
  • agent/src-tauri/src/main.rs109 B
  • agent/src-tauri/src/tools/detect.rs2.1 KB
  • agent/src-tauri/src/tools/exec.rs6.8 KB
  • agent/src-tauri/src/tools/fs.rs6.1 KB
  • agent/src-tauri/src/tools/mod.rs18.0 KB
  • agent/src-tauri/src/tools/search.rs5.0 KB
  • agent/src-tauri/src/workspace.rs4.4 KB
  • agent/src-tauri/tauri.conf.json1.1 KB
  • ai/backend/.dockerignore11 B
  • ai/backend/activity.salam17.4 KB
  • ai/backend/admin.salam31.4 KB
  • ai/backend/billing.salam33.8 KB
  • ai/backend/bridge.salam25.1 KB
  • ai/backend/check.salam4.7 KB
  • ai/backend/Dockerfile1.2 KB
  • ai/backend/engine.salam22.6 KB
  • ai/backend/files.salam23.6 KB
  • ai/backend/guest.salam10.1 KB
  • ai/backend/live.salam13.9 KB
  • ai/backend/main.salam45.9 KB
  • ai/backend/oauth.salam44.5 KB
  • ai/backend/openai.salam38.4 KB
  • ai/backend/store.salam16.4 KB
  • ai/backend/tests/api_flow.mjs39.8 KB
  • ai/backend/tests/billing_flow.mjs11.6 KB
  • ai/backend/tests/bridge_flow.mjs8.9 KB
  • ai/backend/tests/engine_mock.mjs10.6 KB
  • ai/backend/tests/gateway_mock.mjs3.1 KB
  • ai/backend/tests/oauth_flow.mjs15.3 KB
  • ai/backend/turn.salam7.8 KB
  • ai/backend/web.salam13.2 KB
  • ai/frontend/bun.lock45.3 KB
  • ai/frontend/index.html1.4 KB
  • ai/frontend/package.json622 B
  • ai/frontend/public/apple-touch-icon.png7.6 KB
  • ai/frontend/public/icon-192.png8.3 KB
  • ai/frontend/public/icon-512.png15.4 KB
  • ai/frontend/public/logo.svg1.1 KB
  • ai/frontend/rsbuild.config.mjs664 B
  • ai/frontend/src/Activity.tsx6.3 KB
  • ai/frontend/src/Admin.tsx80.0 KB
  • ai/frontend/src/AdminChat.tsx5.6 KB
  • ai/frontend/src/Agent.tsx10.1 KB
  • ai/frontend/src/api.ts20.3 KB
  • ai/frontend/src/App.tsx24.8 KB
  • ai/frontend/src/Apps.tsx9.8 KB
  • ai/frontend/src/Attach.tsx7.3 KB
  • ai/frontend/src/Auth.tsx8.0 KB
  • ai/frontend/src/Bidi.tsx1.5 KB
  • ai/frontend/src/Composer.tsx7.5 KB
  • ai/frontend/src/Confirm.tsx6.5 KB
  • ai/frontend/src/Connect.tsx14.9 KB
  • ai/frontend/src/desktop.ts3.0 KB
  • ai/frontend/src/env.d.ts226 B
  • ai/frontend/src/Events.tsx3.3 KB
  • ai/frontend/src/fonts/LICENSE-Shabnam4.4 KB
  • ai/frontend/src/fonts/Shabnam-Bold.woff236.8 KB
  • ai/frontend/src/fonts/Shabnam-Light.woff241.5 KB
  • ai/frontend/src/fonts/Shabnam-Medium.woff236.3 KB
  • ai/frontend/src/fonts/Shabnam.woff234.9 KB
  • ai/frontend/src/format.ts3.2 KB
  • ai/frontend/src/highlight.ts4.3 KB
  • ai/frontend/src/i18n.ts1.3 KB
  • ai/frontend/src/icons.tsx6.0 KB
  • ai/frontend/src/Keys.tsx20.6 KB
  • ai/frontend/src/logo.svg1.1 KB
  • ai/frontend/src/main.tsx377 B
  • ai/frontend/src/Markdown.tsx5.0 KB
  • ai/frontend/src/Messages.tsx7.1 KB
  • ai/frontend/src/Plans.tsx10.3 KB
  • ai/frontend/src/Requests.tsx9.9 KB
  • ai/frontend/src/shiki.ts8.6 KB
  • ai/frontend/src/Sidebar.tsx8.9 KB
  • ai/frontend/src/styles.css63.3 KB
  • ai/frontend/src/theme.ts1004 B
  • ai/frontend/src/Trace.tsx7.1 KB
  • ai/frontend/tsconfig.json438 B
  • ai/README.md30.7 KB
  • android/.gitignore225 B
  • android/.keep0 B
  • android/app/.gitignore7 B
  • android/app/build.gradle.kts3.2 KB
  • android/app/proguard-rules.pro751 B
  • android/app/src/androidTest/java/ir/salamlang/app/ExampleInstrumentedTest.kt485 B
  • android/app/src/main/AndroidManifest.xml1.7 KB
  • android/app/src/main/ic_launcher-playstore.png41.5 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

Writing Salam

Salam is a statically typed, compiled, general-purpose systems language. The general language transpiles to C and builds to a native executable; embedded layout: blocks compile to HTML/CSS/JS. It can also be run with an interpreter (salam exec, pure compute only) that compiles each function to register bytecode on first call (SALAM_VM=0 falls back to the plain tree-walker), and cross-compiled via LLVM. Source can be written in English or Persian, with the same grammar and the same compiler rules in both.

This skill has two parts:

  • Part I (§1 to §12): English Salam. The full language, stdlib, rules and porting guide, with English keywords.
  • Part II (§13 to §19): Persian Salam (سلام فارسی). Every Persian keyword, type, method and std package name, the Persian-only rules, and complete Persian programs. Read Part I for semantics and Part II for spelling; the semantics never differ.

There is also a 25-lesson human tutorial of the language, in Persian at https://www.salamlang.ir/learn/ and in English at https://salamlang.github.io/Salam/learn/ (one page per topic, every example compiled and run), and a reference page for every std package under /std/<slug>/ on both sites (see §5).

Source of truth. When a detail is missing here, read real code: std/<pkg>/*.salam for exact stdlib signatures, and tests/en/{apps,basics,data,features,games,interop,stdlib,types,webframework}/** for idioms. Do not invent APIs.

Contents

  1. Conversion workflow
  2. Core syntax crib
  3. Types & data
  4. Generics, interfaces, polymorphism
  5. Standard library catalog
  6. Compiler rules & top pitfalls ← read this before writing
  7. Translating from PHP / TS-JS / Python / C-Go-Rust
  8. FFI, concurrency, conditional compilation
  9. Layout DSL (HTML/CSS/JS)
  10. Tooling & verification
  11. When a detail is missing
  12. Porting C → Salam & self-hosting the compiler (bit ops, unions, the module map)
  13. Persian: source basics (digits, commas, detection, the entry ریشه)
  14. Persian: keywords
  15. Persian: types, built-in methods and std package names
  16. Persian: syntax crib (every construct side by side)
  17. Persian: rules and traps
  18. Persian: complete programs
  19. Persian: the tutorial on salamlang.ir (lesson map)

1. Conversion workflow

When asked to convert program X (in some other language) into Salam:

  1. Understand X: its entry point, data types, control flow, external I/O, and dependencies.
  2. Map constructs to Salam using §7 (class→struct, interface→interface, generics→<T>, exceptions→bool/Option/sentinel, dict→HashMap, list→Vector, etc.).
  3. Pick stdlib packages from §5 rather than reimplementing (str, json, http, regex, math, sort, collections, db, …).
  4. Write idiomatic Salam using §2 to §4, obeying the strict rules in §6 (unused = error, until = while, manual .free(), integer / truncates, no exceptions, mut to reassign, pub to export, top-level ordering).
  5. Verify with salam exec file.salam (interpreter) or salam build file.salam --output=app (§10). Fix every warning; most are hard errors.

Keep the program's structure and names recognizable, but produce idiomatic Salam, not a transliteration.


2. Core syntax crib

func main:
    println "Hello, Salam!"          // print + newline; comma args are space-joined
end

Blocks open with : and close with end. { … } braces are also accepted. A single-statement body may follow the : on the same line: if n < 2: ret n end. Comments: // to end of line, /* … */ across lines.

Declarations & variables

name := "Sara"           // immutable, type inferred (str)
mut count := 0           // mutable
count += 3 * 4           // compound assignment; count is now 12
total := 250 as i64      // pick a type with `as`
x := 3.14                // f64
const MAX := 100         // compile-time constant

:= declares (immutable by default). Reassigning a non-mut variable is a compile error. mut makes it reassignable; const is a compile-time value. Typed declarations were removed: total: int = 250 and x: auto = 3.14 are parse errors ("declarations with a type annotation were removed"). Write name := value and use as when the inferred type is not the one you want. name: Type = value survives only for struct fields and parameters. A const name must be a single word (const MAX VALUE := 3 is a parse error); mut globals, locals and functions may still have multi-word names.

Printing

print/println (stdout) and printerr/printerrln (stderr) take comma-separated arguments and space-join them. They are statements, not calls: never wrap the whole argument list in parentheses. They always need a value: a bare println is an error, so write println "" for an empty line.

input reads one line from stdin (without the newline) and follows the same rules: it always takes a prompt and never parentheses. Write input "" or input "prompt" (Persian ورودی "" / ورودی "پیام"); a bare input and input() are errors. The prompt is printed first, with no newline. A variable works as the prompt (input label), but only that name is the prompt: input label + "x" appends "x" to the line read. For a computed prompt, start with a string: input "> " + label.

println ""
name := input "Name: "
line := input ""

Any struct, array, slice, Vector or HashMap can be printed directly: the compiler derives a stringify function for the type and prints what it returns, recursing into fields and elements.

struct Point:  x: int  y: int  end

p := Point {x = 10, y = 15}
println p                                   // Point {x = 10, y = 15}
println [p, Point {x = 20, y = 25}]         // [Point {x = 10, y = 15}, Point {x = 20, y = 25}]
println ["ali", "reza"]                     // ["ali", "reza"]

Rules worth knowing:

  • Nested str values are quoted ("ali") so an empty one stays visible; a top-level println s on a str is unquoted as always.
  • A HashMap prints as {"a": 1, "b": 2}; iteration order is the map's.
  • Every field is shown, pub or not - the derived function is compiler-written and is not held to the privacy rule.
  • Pointer fields print as null or <ptr>; they are never followed, so a cyclic structure still terminates. Enum fields print as their integer value.
  • Give a struct pub func to_str(): str to control its own rendering; the derived function calls that instead.
  • Anything with no derivable form (a File handle, a function value) is still the old error, and inside a struct renders as <its type>.

Operators

+ - * / %, ** (power, float result), == != < > <= >=, the comparison words eq neq lt gt lte gte for == != < > <= >= (Persian برابر نابرابر کوچکتر بزرگتر کوچکتربرابر بزرگتربرابر; کوچکتر برابر and بزرگتر برابر with a space work too), the logical words and or not (Persian و یا وارونه), ternary cond ? a : b, compound += -= *= /= %= **=, ++/--. Integer / truncates. Power is **, right-associative and tighter than unary minus (2 ** 3 ** 2 == 512, -2 ** 2 == -4). T** in a type is a pointer to a pointer; after as, x as i64 ** 2 is a cast then a power. There is no &&, ||, ! or ^^: the compiler rejects them and names the word to write instead. ^ on its own remains bitwise XOR. Bitwise operators (integer operands only): & | ^ ~ and shifts << >>, with compound forms &= |= ^= <<= >>=. Precedence follows C: shifts bind tighter than comparisons; & tighter than ^ tighter than |, all looser than == (so 1 | 2 & 3 == 3 and 1 << 4 + 1 == 32). + on str concatenates and coerces numbers to text ("n=" + 3 → "n=3").

Control flow

if x > 0:  println "positive"
else x == 0:  println "zero"          // "else <cond>" is else-if
else:  println "negative"
end

until i < n:  i = i + 1  end          // loops WHILE the condition holds - "while" doesn't exist, see box below
repeat 3:  println "hi"  end          // do 3 times
repeat n in i:  println i  end        // i = 0 .. n-1 ("in" binds the index)
repeat 1 to 5:  ...  end              // 1..5 inclusive
repeat 1 to 5 in i:  ...  end         // ...binding the loop variable
repeat 10 to 1 in i:  ...  end        // descending: the *bounds* pick the direction
repeat 0 to 20 by 2:  ...  end        // step with "by"; must be POSITIVE even descending
each x in xs:  println x  end         // iterate a collection/array/your own type
each i, x in xs:  println i, x  end   // index + value (or key, value for a map)
each ch in s:  println ch  end         // a str: one UTF-8 character (code point) per step, ch is a str
each i, b in s.bytes():  println b  end // the bytes of a str: b is a char, i the byte index
// break: exit the innermost loop (or switch, see below); break N: exit N levels;  continue: next iteration

⚠ until is Salam's only loop-while keyword. There is no while.

while does not exist in Salam; until is the sole spelling, and it runs its body while C is true, stopping when C becomes false. Read until C: as "loop while C" - not as "loop until C happens" and not as a do-until - which is the single most common porting mistake. (Persian's تا already reads correctly as "while".) When porting a loop from C/Python/JS/Go, copy the condition verbatim:

source loopSalamNOT
while (v != 0)until v != 0:until v == 0:
while (i < n)until i < n:until i >= n:
while (v) (truthy int)until v != 0:until v: (no truthiness; needs a real bool)
while (p) (pointer)until p != null:until p == null:
for (;;)until true: + break
do { B } while (c);until true: B if not c: break end end

An inverted until fails silently. until v == 0: with a nonzero v runs zero times and produces no error, so the function just returns its zero-value/empty result (this is exactly how a bit-length helper silently returns 0 and truncates a buffer). The compiler only rejects a literally constant-false condition (until false: → E068). So when a loop "did nothing", check its condition polarity first.

repeat a to b direction is decided at runtime by the bounds, so repeat n to 1 in i counts up 0, 1 when n is 0 instead of not running. Guard the count (if n >= 1: repeat n to 1 in i: … end end) when the start bound can fall below the end bound.

Switch

switch/ترابرد is a statement (not an expression like match), with real C-style fallthrough: without break, execution falls into the next label's body. There are no case/default keywords - each label is a bare value (or comma-list, range, or relational test) closed by its own end, the same shape as a match arm:

switch score:
    100:       println "perfect"  end         // falls through into the next label unless it breaks
    90 to 99:  println "A"  break  end        // break exits the switch (only), not an enclosing loop
    70, 80:    println "round"  break  end    // comma list
    > 60:      println "passing"  break  end  // leading relational operator: > >= < <= == !=
    else:      println "low"  end             // wildcard; must be the LAST case
end
  • break exits the switch only; continue is not caught by a switch and passes straight through to an enclosing loop (continue with no enclosing loop is still an error, same as everywhere else).
  • break N exits N levels of enclosing loops and switches, so break 2 inside a switch inside a loop leaves the loop too (break 1 is break). Deferred statements of every level left still run. N must be between 1 and the number of enclosing loops and switches, or it is a compile error.
  • switch true: with boolean labels replaces an if/else chain that tests different conditions (x < 0:, x == 0 or name == "zero":, ...): the first true label wins.
  • Enum subjects must be covered - a switch on an enum (or on an alias of one, type Paint = Color) with no else must list every member, or it is compile error E103 naming the missing members. Members may be written through the enum or the alias (Paint.Red, Color.Green). For other subjects, a switch with no matching label and no else does nothing.
  • Not usable on a Variant subject - that is a semantic error that tells you to use match's type patterns instead (see §3).
  • Prefer switch for C-style fallthrough or open-ended relational/range labels; prefer match when the subject is an enum or Variant and you want the compiler to check every case is handled (see §12.2 for the C switch (x) { case … } mapping).

Functions

func add(a: int, b: int): int:  ret a + b  end   // ": type" before the block colon = return type
func greet(name: str):  println "Hi,", name  end // no return type = void
func greet(name: str, prefix: str = "Hello"):     // default arguments allowed
    println prefix, name
end
  • Pass by value. Overloading by parameter types is allowed. Functions may call each other in any order, but every struct/enum/type/interface/ impl must come before the first function of the file (E087), and constants and globals before both (E084/E085).

  • Default values are constants or expressions of globals; a default may not refer to another parameter (func f(a: int, b: int = a) is E001 unknown identifier a). Use an overload instead.

  • Multi-word names: identifiers may contain spaces, e.g. func make counter(), func is weekend(d: Day), pet name: str. A call is is weekend(d); a field access dog.pet name.

  • Modifiers: pub inline noinline pure noret deprecated (combine freely), e.g. pub inline pure func Area(w: f64, h: f64): f64: ret w * h end. They work on struct methods too, in the same order: pub inline func len(): int: ret this.count end.

  • Reference parameters (name &: Type) pass by reference so the callee can mutate the caller's value (in/out params, and to avoid copying big structs):

    func bump(c &: Counter):  c.n = c.n + 1  end   // caller's Counter is modified
    func push_all(dst &: Vector<int>, src: Vector<int>): ... end
    

    This is how much of the stdlib mutates its argument (str.BufAppend(b &: StringBuilder, …)).

  • defer stmt runs at scope exit, LIFO, which is great for cleanup: defer v.free().

  • Closures/lambdas are first-class typed values, always written (params): ... end with an explicit ret for the value: (x: int): ret x * 2 end, or several statements (): n = n + 1 ret n end. Salam has no => at all; it was removed in 0.5.0. Function-typed parameters: func () int, func (int, int) bool.

    • Lambdas capture by value. Each lambda gets its own copy of the outer variables at creation time: after mut n := 1 g := (): ret n end n = 5, g() is still 1, and a block lambda that does n = n + 1 changes its own copy (it counts across calls), never the caller's n. Share state through a pointer, a heap collection or a mut global.
    • Never write a return type on a lambda. (x: int): int: ... end fails to parse; the return type is inferred from the ret ((x: int): ret x * 2 end).
    • A bare named function decays to its address, typed i64 - the slot C-style callback registries take (e.g. the net.router router): router.Get(r, "/", home). For a void* slot, or to cast to a typed C function pointer, use &fn instead. For a typed Salam callback (func (int) int), pass a lambda: apply((x: int): ret inc(x) end, 3).
    • A variable may not reuse a function's name. With a bare name being a value, test := 5 next to func test is rejected (E090), in both directions, so an identifier always means exactly one thing.

3. Types & data

Primitives: i8 i16 i32 i64, u8 u16 u32 u64, usize size, f32 f64, bool, char, str, void. Aliases: int=i32, uint=u32, float=f32. Literals: 42 (int), 3.14 (f64), true/false, null. Pointer-width integers: usize is unsigned (C size_t; Persian اندازه مثبت), size is signed (C intptr_t; Persian اندازه). Both are 64 bits on 64-bit targets and 32 bits on 32-bit ones (wasm32, arm32). sizeof(T) returns usize. Integer bases: decimal 255, hex 0xFF, binary 0b1010, octal 0o17 (all verified). str is UTF-8 bytes: str.Len(s) is the byte count, str.CharCount(s) the codepoint count; classify with str.IsDigit(code) etc. Iterate a string with each: each ch in s gives each code point as a str ("س", "👋"), each i, ch in s adds the character number i (0, 1, 2, ..., the same index s.char_at(i) takes), and each b in s.bytes() / each i, b in s.bytes() walk the raw bytes as char with the byte index. It is one linear pass, so prefer it over repeat s.char_count() in i: s.char_at(i), which rescans from the start on every step. Invalid UTF-8 bytes come out one byte at a time, so joining every ch gives back the original string. A character is a code point, not a grapheme cluster: "👋🏽" is two steps; use str.Graphemes(s) for user-perceived characters. s.bytes() is only valid as the target of each (E065 otherwise); read single bytes with s[i].

String/char literal forms (values go into text with a ``` text block and ${{ expr }}, see below, or with + and fmt.Sprintf):

SyntaxTypeEscapes (\n \t \" \\ \xHH \uHHHH \UHHHHHHHH …)MultilineNotes
"text"stryesnonormal string; raw newline in source is an error
"""text"""stryesyesmultiline, still processes escapes; for code or long text prefer a ``` text block
'c'charyes-one raw byte unless escaped, not UTF-8 safe for non-ASCII
`text`strnone, fully rawyesbacktick string; every byte up to the next ` is taken literally, including ", ', \, and real newlines. Only a literal backtick can't appear inside it (no escape exists for `)
u'c'char (UTF-8 codepoint)no-must decode to exactly one Unicode codepoint; use for non-ASCII single chars, e.g. u'م', u'中', u'€'
u"c"char (UTF-8 codepoint)yes-same as u'c' but escapes are processed first, e.g. u"\U0001F600"

Prefer backtick strings for any text containing literal ": JSON blobs, regex patterns, shell commands, HTML/CSS fragments, instead of escaping:

data := `{"name": "salam", "version": 2, "active": true, "pi": 3.5, "tags": ["a", "b"]}`

not

data := "{\"name\": \"salam\", \"version\": 2, \"active\": true, \"pi\": 3.5, \"tags\": [\"a\", \"b\"]}"

Only fall back to "..." with escaped quotes when the string must also contain a literal backtick, or when it needs an escape sequence (\n, \uXXXX, …) that backtick strings don't process.

Text blocks (```) hold multi-line text, such as generated C, LLVM IR, SQL, HTML or help text, without "\n" + "..." chains. The opening ``` ends its line; the closing ``` starts its own line, and its indentation is removed from every line, so the block sits indented in the code. The first and last line breaks are not part of the value. The text is raw like a backtick string: \n, " and ' are kept as typed.

const OB_SIZE := 65536

func header(name: str): str:
    ret ```
        #define SALAM_OB_SZ ${{ OB_SIZE }}
        #define HALF ${{OB_SIZE / 2}}
        /* built for ${{ name }} */
        static int nl(void) { return '\n' == 10; }
        ```
end
  • ${{ expr }} puts the value of any expression into the text: str, numbers, bool and char, the same as + on a string. Spaces inside are free (${{x}}, ${{ x }}), but $, {{ and }} must be written without spaces in them, so $ {{x}} and } } are plain text.
  • When every injected value is a constant, the whole block folds into one literal at compile time; otherwise it becomes a + chain.
  • $${{ writes the characters ${{. A lone $, {{, }} or $$ is plain text, so C, JS and shell code need no escaping.
  • An injection must close on the line it opens. Errors: text after the opening ```, a missing closing ```, a line indented less than the closing ```, an unclosed ${{ and an empty ${{ }}.
  • It works the same in Persian files: ${{ نام }}, and salam translate translates the code inside ${{ }} while keeping the text as it is.
  • std/ and compiler/ can only use text blocks once a released seed compiler understands them (see the seed lag rule).

Type aliases: type NodeId = int, type Bytes = u8* gives a new name for an existing type (declared at top level, before functions). An alias is the same type as its target, so values mix freely; type errors that involve a written alias name it, e.g. cannot pass 'i32' to 'Age' ('Years' is an alias of 'Age'). An alias of an enum reaches its members too: type Paint = Color then Paint.Red.

Casts & typed literals with as:

b := 250 as int as u8
n := fib(i) as i64
arr := [1, 2, 3]                 // already int[3]; `as int[3]` is E093 "useless cast"
big := [0 as i64, 5, 7]          // the first element picks the element type
v := Vector {} as Vector<int>
m := HashMap {} as HashMap<str, int>

Arrays (fixed size) & slices:

a := [1, 2, 3]                        // int[3], indexed 0..2
grid := [[1, 2, 3], [4, 5, 6]]        // int[2][3], 2-D
mid := a[1: 3]                        // slice (view) of a[1] and a[2]; writes through to `a`
whole := a[:]  head := a[: 2]  tail := a[1:]   // omitted bound = that end of `a`
func sum(view: int[]): int: ... end   // int[] = slice parameter, any length
sum(a)  sum([4, 5])  sum(a[1:])       // an array passes as a slice directly, no copy
len(a)                                // length builtin: arrays, slices, str (a Vector uses v.len())

Structs (fields + methods):

struct Account:
    pub balance: int = 0              // pub = visible/usable outside; default value
    pub func deposit(n: int):  this.balance = this.balance + n  end
end
mut a := Account { balance = 100 }    // struct literal; omitted fields use defaults
a.deposit(50)
println a.balance

Fields and methods are private by default; add pub to expose. this is the receiver.

Self is the enclosing type, usable anywhere inside a struct, an impl ... on T block or a distinct type's body: as a return type, a parameter type, a field type, in a cast and as a struct literal. On a generic struct it means the struct with its own type parameters, so Self stands for Pair<A, B> without respelling it. It works in a static func too, which is where constructors live. Persian spells it خود. There is no Self in an interface (that would be an associated type, which Salam does not have).

struct Node:
    pub v: int = 0
    pub func With(n: int): Self:          ret Self { v = n }      end
    pub static func Make(n: int): Self:   ret Self { v = n * 2 }  end
end

Static members: static func and const inside a struct belong to the type, not to a value. Call them through the type name, also across packages (pkg.Color.Hex(...)). A static func has no this, but it can read and set the struct's private fields, so it is the place for constructors. On a generic struct the type parameters come from the arguments or the expected type: Box.Of(42), ret Box.Empty(), Box.Empty() as Box<f64>.

struct Color:
    pub r: int  pub g: int  pub b: int
    pub const Max := 255
    pub static func Gray(v: int): Color:  ret Color { r = v, g = v, b = v }  end
end
w := Color.Gray(Color.Max)

mut func (read-only this): once any method of a struct is declared mut func, the compiler checks the whole struct. Its other methods get a read-only this (E105 when one assigns to a field or calls a mut func on this), and a mut func can only be called on a mut binding or a &: parameter (E106). Writes that go through a pointer, slice, Vector or HashMap field change the heap data, not the struct, so they stay allowed. Structs with no mut func keep the old rules. mut goes right after pub (pub mut inline func), is also allowed on interface methods, and cannot be combined with pure.

struct Counter:
    n: int = 0
    pub func Count(): int:  ret this.n  end     // this is read-only here
    pub mut func Tick():  this.n += 1  end
end
mut c := Counter {}
c.Tick()

Iterating your own type with each: each works on any struct that has one of two shapes, so you do not write an index loop by hand. No interface and no registration: the compiler checks the method shape.

  • Sequence - len() (or size()) returning an integer, plus get(i: int). each x in it walks 0 .. len()-1, and each i, x in it also binds the index.
  • Cursor - iter() returning a struct with has_next(): bool, value() and next(). Add key() to that cursor and each k, v in it binds both; without a key(), each over it takes a single binding.
struct RangeIter:
    pub cur: int = 0
    pub stop: int = 0
    pub func has_next(): bool:  ret this.cur < this.stop  end
    pub func value(): int:      ret this.cur              end
    pub func next():            this.cur = this.cur + 1   end
end
struct Range:
    pub lo: int = 0
    pub hi: int = 0
    pub func iter(): RangeIter:  ret RangeIter { cur = this.lo, stop = this.hi }  end
end
each v in Range { lo = 2, hi = 5 }:  println v  end      // 2 3 4

The method names are the usual built-ins, so a Persian file spells them طول, بگیر, پیمایش, داردبعدی, مقدار, کلید and بعدی. Every std container now has one of the two shapes, so each x in set, each x in stack, each x in queue, each x in deque, each x in list and each k, v in ordered_map all work. PriorityQueue is the exception: its heap order is not its sorted order, so it has no iter() - pop from it instead.

Embedding (use): composition instead of inheritance. use Animal inside a struct adds a field named Animal and promotes its fields and pub methods, so d.name means d.Animal.name and d.Describe() forwards to it. Promoted methods count for interfaces, <T: I> bounds and dyn I. The outer struct's own members win over promoted ones; two embeds that both provide a name are E107. Literals may set promoted fields directly (Dog { name = "Rex" }); an omitted embed defaults to Animal {} when all of its fields have defaults. pub use exposes the embed outside the struct; plain use keeps it private. Print and JSON show it as a nested object. The Persian spelling is شامل. A generic struct embeds with its type arguments (use Stack<str>); a pointer cannot be embedded.

struct Animal:  pub name: str = ""  pub func Describe(): str:  ret "I am " + this.name  end  end
struct Dog:
    pub use Animal
    pub breed: str = ""
end
d := Dog { name = "Rex", breed = "lab" }
println d.Describe()

Operator overloading: declare a method whose name is the operator itself, func +(o: V): V:. It is called when the left operand is the struct. != falls back to not (a == b) when there is no !=, and a += b uses your +. Unary - and binary - share the symbol, overloaded by arity: func -: V: (no parameters, empty parentheses optional) is negation, func -(o: V): V: is subtraction - define both if you need both:

struct Vec2:
    pub x: f64
    pub y: f64
    pub func +(o: Vec2): Vec2: ret Vec2 { x = this.x + o.x, y = this.y + o.y } end
    pub func -(o: Vec2): Vec2: ret Vec2 { x = this.x - o.x, y = this.y - o.y } end
    pub func -: Vec2: ret Vec2 { x = -this.x, y = -this.y } end   // unary -v
    pub func ==(o: Vec2): bool: ret this.x == o.x and this.y == o.y end
end

Only Salam's own operators can be overloaded: + - * / % ** == != < > <= >= not [] []=. A new symbol (***, +-, &) is an error. The parameter count decides unary or binary and is checked:

  • binary operators take exactly one parameter, the right operand
  • - takes none (negation, -v) or one (subtraction)
  • not takes none
  • [] takes one (the index), []= takes two (the index and the value)

Distinct types (type X: T): type Age = int is a plain alias, the same type as int. type Meters: int creates a new type built on int. It supports every int operator (the result is Meters), can declare its own operators and methods in a body closed by end, and never mixes silently with plain int:

type Meters: int
    func +(other: Meters): Meters:
        ret ((this as int) + (other as int)) as Meters
    end
    func *(k: int): Meters:             // overload by the operand's type
        ret ((this as int) * k) as Meters
    end
    func -: Meters:                     // unary minus, no parameters
        ret (0 - (this as int)) as Meters
    end
    func km: f64:                       // ordinary method: m.km()
        ret (this as f64) / 1000.0
    end
end

a := 5 as Meters                        // literals adopt the type: a + 3, 3 + a
b := a + 10                             // your operator +
println (a * 4).km(), a < b             // built-in '<' still works
n := a as int                           // conversions are explicit
  • The base type must be a number, bool, char or str; a struct already has its own operators.
  • Meters + int_variable is an error unless you declare an operator for it.
  • a != b uses your == when there is no !=.
  • a += b uses your +.
  • An alias of a distinct type (type Length = Meters) shares its operators.
  • From another file included as units, write units.Meters; its operators work there too.

Sign runs are errors: two +/- operators in a row are rejected, whether spaced or not. That covers - -x, a - -b, a + -b, a---b, n++ + 1 and +x (Salam has no unary +). Put the inner one in parentheses (a - (-b), (n++) + 1) or simplify (a + b, just x).

Enums & match:

enum Day: Mon, Tue, Wed, Thu, Fri, Sat, Sun end   // Mon=0 … Sun=6
enum Color: Red, Green = 5, Blue end               // explicit values (Blue=6)
d := Day.Sat
println d as int                                   // 5

grade := match score / 10:                         // match is an EXPRESSION
    10, 9: "A" end
    8: "B" end
    else: "F" end
end

A match arm is always pattern: … end. The older pattern => expr form was removed in 0.5.0, along with => itself: the token is not part of the language any more. An arm's block may hold statements, and its value (when match is used as an expression) is the bare expression it ends with.

A comma (, or Persian ،) is required between enum members - a bare newline is not enough, because member names may contain spaces (enum Status: not started, in progress, done end) and a newline alone can't tell where one multi-word name ends and the next begins. Leaving out the comma is a compile error ('end'/EOF right after a member with no comma before it).

Enums with data (sum types): a member may carry named values. Build one with Enum.Member(values...) (or Enum.Member when it has none) and take it apart in match with Member(a, b) (use _ to skip a value) or Member whole to bind the whole case. match must cover every member (or have else). Such enums print as Circle(r = 2) and compare with == when every value can. They work across packages (geo.Token.Num(4)). An enum with data needs at least two members, and members cannot also have = value. value.name() gives the member's name and Enum.Count() the number of members.

enum Shape:
    Circle(r: f64)
    Rect(w: f64, h: f64)
    Empty
end
func area(s: Shape): f64:
    ret match s:
        Circle(r): 3.14 * r * r end
        Rect(w, h): w * h end
        Empty: 0.0 end
    end
end
println area(Shape.Rect(3.0, 4.0))
println Shape.Circle(2.0)          // Circle(r = 2)

Generic enums, result.Result and ?: an enum with data may take type parameters (enum Maybe<T>: Some(v: T), Nothing end). A generic member is built where its type is known - returned from a function, or with as: Maybe.Nothing as Maybe<int>. import result gives result.Result<T, E> (Ok(value) / Err(error)) plus result.IsOk, IsErr, UnwrapOr(r, fallback) and Expect(r, msg). Inside a function that returns such an enum, a postfix ? unwraps Ok and returns any Err early (running defers): it must be a statement's whole value - x := f()?, x = f()?, f()? or ret f()?.

import result
func parse(s: str): result.Result<int, str>:
    if s == "7": ret result.Result.Ok(7) end
    ret result.Result.Err("bad " + s)
end
func sum(a: str, b: str): result.Result<int, str>:
    x := parse(a)?
    y := parse(b)?
    ret result.Result.Ok(x + y)
end

Struct patterns: Point{x = 0, y} matches a struct (or an enum member, Rect{w, h = 1.0}) by field name: name binds the field, name = expr requires it to equal expr. A struct pattern with tests acts like a guard, so keep a final pattern without tests or an else.

Match guards: any arm may add if cond after its patterns (Circle(r) if r > 10: "big" end, 7 if ready:). The guard sees the arm's bindings and runs only when the pattern matches; if it is false, matching continues with the next arm. A guarded arm does not count toward exhaustiveness, so keep an unguarded arm (or else) for that case.

Variant<A, B, …> is a tagged union (one slot sized to the largest member). Assign any member type; narrow it back with match on type-name patterns:

mut v := 21 as Variant<i32, f64, str>   // initial cast is OK from a *member* type (i32)
v = "offline"                            // then assign member-typed values directly
label := match v:
    i32 n: "int " + n end
    f64 f: "float " + f end
    str s: "text " + s end
end

Assign a value whose type is exactly one of the members; do not write "x" as Variant<…> (casting a str into a Variant fails). Struct fields typed as a Variant coerce a member-typed literal automatically (Reading { value = "offline" }). Variant match needs the compiled backend, not salam exec; see §10.

Option<T> (via import option) for maybe-absent values:

o := option.Some(99)
println option.UnwrapOr(o, 0)         // Some/None/IsSome/IsNone/Unwrap/UnwrapOr/Expect

Pointers & null: T* is a pointer, p[0] dereferences, null is the null pointer. Used with FFI and mem: p := mem.Allocate(8 as u64) as i64*; p[0] = 0; mem.Free(p as void*).


4. Generics, interfaces, polymorphism

func Max<T>(a: T, b: T): T:  if a > b: ret a end  ret b  end   // generic function
struct Stack<T>:  pub items: Vector<T> = Vector {}  pub count: int = 0  end  // generic struct

interface Shape:
    func area(): f64
    func name(): str
end

// A struct satisfies an interface structurally by having matching pub methods:
struct Circle:
    pub r: f64 = 0.0
    pub func area(): f64:  ret 3.14159 * this.r * this.r  end
    pub func name(): str:  ret "circle"  end
end

func describe<T: Shape>(s: T):  println s.name(), s.area()  end   // static bound (monomorphized)
func show<T: Shape + Named>(s: T):  println s.name()  end        // several bounds with '+'
func draw(s: dyn Shape):  println s.area()  end                  // dynamic dispatch
shapes := [ Circle { r = 1.0 }, Rect { w = 2.0, h = 3.0 } ] as dyn Shape[3]
reg := Vector {} as Vector<dyn Shape>                            // heterogeneous collection

dyn fields: a struct field can hold any implementation, e.g. pub shape: dyn Shape = Square {}. Storing a value copies it into a box, so changing the original afterwards does not reach the field. Copying the struct shares the box, the same way copying a Vector shares its storage, so a method that changes state through one copy shows in the other. Release the box with field.free() when you are done with it. copy() shares the box too, while deep_copy() boxes a deep copy of whatever value the field holds (a concrete type that holds a raw pointer is copied as a value, so the pointer itself is shared). That works for interfaces declared in your program; for an interface from another package deep_copy() refuses, so give the struct its own deep_copy().

One interface can use another - the same keyword structs embed with (Persian شامل). The base's methods, defaults included, become part of the using interface, so <T: Solid>, dyn Solid and impl Solid on T all see the full set. Two bases that provide the same method name is E107, and a cycle is E107 too.

interface Shape:  func Area(): f64  end
interface Named:  func Name(): str  end
interface Solid:
    use Shape
    use Named
    func Volume(): f64
end

Default methods: an interface method may carry a body. A struct that provides all of the interface's methods without bodies gets a copy of every default it does not define itself, and so does an impl I on T block, so defaults work with <T: I>, dyn I and direct calls.

interface Shape:
    func Area(): f64
    func Describe(): str:
        ret "area " + this.Area()
    end
end

impl adds interface methods to any type, including primitives:

interface Ranked:  func rank(): int  end
impl Ranked on int:  func rank(): int:  ret this  end  end
impl Ranked on str:  func rank(): int:  ret len(this)  end  end

An impl block is checked against its interface. Whatever the block supplies, the type must end up with every method the interface declares, with matching signatures, or the impl line itself is E002. So an empty block is the way to say "this struct implements this interface" and be told at the declaration site instead of at some distant dyn cast:

struct Circle:
    pub r: f64 = 1.0
    pub func Area(): f64:  ret 3.14159 * this.r * this.r  end
end
impl Shape on Circle:  end      // asserts conformance; E002 here if it drifts

5. Standard library catalog

Import a library by name, never quoted: import str, import math. Dotted subpackages: import db.sqlite, import encoding.json (called as json.X, the last part of the path; a bare import json or import yaml does not find it). Your own files use include with a quoted path (Persian فراخوانی): include "util.salam" (relative to this file), include "@/lib/util.salam" (from the project root, the entry file's folder), include mx "math.salam" (aliased; call mx.Square(…)). import "x.salam", include str and a path starting with ./ are all compile errors, and a missing or unreadable include stops the build. A file may declare package <name> (default main). Only pub symbols are importable. Function names are PascalCase; collection methods are snake_case.

Everything below is real; signatures come from std/. When unsure of an exact signature, grep the package file.

Reference pages. Every package has a page with guides, a "things that catch people out" list and a checked example for each function: https://www.salamlang.ir/std/<slug>/ (Persian) and https://salamlang.github.io/Salam/std/<slug>/ (English), where the slug is the import path with / as -: str, db-sqlite, net-http, encoding-json. The index is /std/ on both sites. The /learn/ course on both sites teaches the language only, not the libraries.

Rules that hold across the whole library:

  • A str ends at its first zero byte. Binary data (images, archives, keys, packets) passed through any str function or field is silently cut there: use the *Bytes/Raw/ByteBuf/pointer-and-length forms the package offers (archive.ReadBinaryFile, crypto.Sha256Bytes, tcp.ReadBytes, aws.S3GetBytes, Message.data).
  • Almost nothing panics. Failure is an empty string, 0, -1, false, an empty vector, a 0x0 matrix or an err &: out-parameter, so check the result: a missing file, an unknown JSON key and a server that refused a command all look like empty data.
  • int is 32 bits. Helpers that return int (json.GetInt, yaml.GetInt, conv.Atoi, "..".to_int()) clamp or wrap anything larger; use the i64 form (conv.ParseInt, json.Unmarshal into an i64 field) for IDs, sizes and timestamps.
  • Std functions that return a str hand you a fresh string you may free; arguments are never modified.
  • Packages that keep state in globals are not thread-safe: regex, the one-shot http.Get/Post client, log settings, testing counters, debug, grpc.ServeConn. Values with a New... constructor (sync, atomic, chan) crash when used as a zero {} literal.
  • b := a on a heap value (Vector, HashMap, bigint.Int, decimal.Dec, matrix.Matrix, a struct holding one) shares the storage; take a real copy with v.copy(), collections.clone(v) or the package's Clone.

Text & formatting

  • str: Upper Lower Title Trim TrimLeft TrimRight TrimPrefix TrimSuffix Reverse Repeat Concat Substr Split Join Fields Chars CharAt Contains Find IndexOf IndexFrom LastIndex Count StartsWith EndsWith Equals EqualFold CompareFold Compare Replace Len CharCount CharSubstr CharFind CodePoints FromCodePoints Graphemes GraphemeCount ReverseGraphemes NFC NFD NFKC NFKD Clone FromInt(i64) FromFloat(f64) ToInt ToFloat IsEmpty IsDigit IsAlpha IsAlnum IsSpace IsPunct ToEnglishDigits ToPersianDigits ToArabicDigits DigitValue IsAnyDigit IsPersianDigit IsArabicDigit HasEnglishDigits HasPersianDigits HasArabicDigits; plus a StringBuilder (NewBuilder, BufAppend, BufAppendChar, BufAppendCp, BufAppendInt, BufStr, BufReset, BufFree). Len, Substr, IndexOf count bytes; CharCount, CharSubstr, CharFind count code points; use those for Persian. Upper/Lower change ASCII only (Upper("café") is CAFé; use unicode.ToUpper per code point, or EqualFold to compare), and Trim strips only ASCII space, tab, CR and LF (a no-break space stays). ToInt returns a 32-bit int and clamps.
  • fmt: Sprintf(tmpl, Vector<str>) ({} placeholders), Fprintf, PadLeft PadRight Center (count characters), and Int Float FloatPrec Bool Hex Uint to build the string args you pass to Sprintf. There are no verbs (%d, {:.2}): format the value first with FloatPrec/PadLeft. A literal {} cannot be written in a template. Hex/Uint take a u64: fmt.Hex(n as u64).
  • conv: FormatInt FormatUint FormatHex FormatBool FormatFloat (shortest round-trip) FormatFloatShort (6 digits, like %g) FormatFloatPrec ParseInt(i64) Atoi(int) ParseFloat ParseBool ParseHex IsNaNF. Nothing reports bad input: ParseInt("abc") is 0, ParseInt wraps past 64 bits and Atoi keeps only 32 (Atoi("3000000000") is -1294967296), ParseBool is case-sensitive ("True" is false), and a comma is not a decimal point (ParseFloat("1,5") is 1). Persian digits are accepted.
  • text: ToUtf16(str): void* (yours to mem.Free), FromUtf16(void*): str, Utf16Len (what JavaScript and browsers count), Utf16UnitAt (no bounds check). For Windows/UTF-16 FFI.
  • template: Render(tmpl, ctx) (no escaping) RenderHTML (escapes & < > " ') NewContext Var EscapeHTML. Only {{name}}, {{#if name}}...{{else}}...{{/if}} and {{! comment }}: no loops, filters or dotted paths, and the context holds strings only. Escaping is not enough inside <script> or for a href (javascript: passes).
  • unicode: one code point at a time. Category CategoryName IsLetter IsAlphabetic IsUpper IsLower IsDigit IsNumber IsAlnum IsPunct IsSymbol IsSpace IsControl IsMark ToUpper ToLower ToTitle ToFold Script ScriptName BidiClass PairedBracket CombiningClass Compose GraphemeBreak. Case maps are the simple 1:1 ones (ß stays ß).
  • arabic: Persian/Arabic normalisation for search. Normalize(s, flags) with the flags FoldLetters StripHarakat StripTatweel FoldDigits FoldAlef FoldTehMarbuta StripZWNJ FoldPresentation, the presets NormalizePersian SearchKey EqualLoose, and IsArabicScript IsHaraka IsTatweel. SearchKey removes the ZWNJ (هوش‌مند matches هوشمند but not هوش مند), lowers ASCII only, and runs NFKC, which also rewrites ² and fi. Show the original text, never the key.
  • bidi: the Unicode bidirectional algorithm. ParagraphLevel(s, DirAuto|DirLTR|DirRTL) Levels VisualOrder Reorder. It does not mirror brackets, shape letters or break lines; reorder one line at a time.
  • i18n: New(fallback) Set Has T Tf Interpolate Plural LoadLocale DetectLocale Free. Plural knows only .one (exactly 1) and .other; an empty translation counts as missing; DetectLocale reads SALAM_LOCALE, then LANG, then LC_ALL.
  • types: classify text: IsInt IsFloat IsNumeric IsBool KindOf KindName (TypeKind*). IsInt checks characters only (a 20-digit number passes, then overflows); only true/false are bools.

Numbers

  • math: consts PI E TAU; Sqrt Cbrt Pow(**) Hypot Exp Log Log2 Log10 Sin Cos Tan Asin Acos Atan Atan2 Sinh Cosh Tanh Floor Ceil Round Trunc Abs Sign Min Max ClampF Lerp Radians Degrees Mod IsNaN IsInf NaN Inf; integer: MinI MaxI AbsI ClampI Gcd Lcm Factorial Pow10 and MinI64 MaxI64 AbsI64 GcdI64. Round rounds halves toward +infinity (Round(-2.5) is -2), Mod keeps the sign of the first argument like fmod, Factorial (i64) is exact only up to 20.
  • bigint: arbitrary-precision signed integers, one Int type (sign + base-2^32 magnitude). Build: Zero One FromInt FromUint Parse ParseBase ParseOr FromBytes Clone; inspect: Sign IsZero BitLen Bit IsOdd IsEven Cmp CmpAbs Equals Less Greater Min Max; arithmetic: Add Sub Mul Sqr Neg Abs AddInt SubInt MulInt Shl Shr; division in both conventions - DivMod Div Rem truncate toward zero (matching Salam's / and %), FloorDivMod FloorDiv FloorMod round toward minus infinity, and Mod always lands in [0, |m|); number theory Pow Pow10 ModExp Gcd Lcm ExtGcd ModInverse Sqrt Factorial; two's-complement bitwise And Or Xor AndNot Not PopCount TrailingZeros; out: ToStr ToStrBase ToHex ToInt ToIntClamped ToFloat ToBytes ByteLen. In-place forms (Set SetInt AddTo SubFrom MulBy AddIntTo MulIntBy NegateIn ShlBy ShrBy) mutate x &: Int and free the old value, for loops that should not allocate per iteration. Every returning function hands over a fresh value the caller frees (defer x.free()); operands are taken by value and are never mutated, so Mul(x, x) is fine. Not constant-time - for a secret exponent use the blinded routine in packages/ssh/bigint.salam. b := a shares the limbs (an in-place call on b then frees what a points at): Clone for a second number. Div Rem Mod ModExp by zero return 0 (use DivMod, which returns false); a negative Pow exponent, Sqrt/Factorial of a negative and a ToStrBase base outside 2..36 give 0 / "".
  • decimal: exact fixed-point decimal - this is the money type. A Dec is a bigint.Int plus a scale, so 0.1 + 0.2 is exactly 0.3 and nothing overflows. Build: Zero FromInt FromScaled(1999, 2) → 19.99 New Parse ParseOr FromFloat (lossy, documented); Add Sub Mul are exact and never round (Add/Sub take the larger scale, Mul the sum); Div cannot be, so it requires a scale and a Rounding mode - Div DivRound DivInt Pow MulInt AddInt SubInt Percent Sum. Rounding: Rounding.{Down,Up,Floor,Ceil, HalfUp,HalfDown,HalfEven} with Rescale Round(HalfEven) RoundHalfUp Truncate Trim. Compare Cmp Equals Less Greater Min Max Sign IsZero IsNegative Scale Unscaled (1.0 equals 1.00). Out: ToStr (always plain notation, never exponential) ToStrFixed FormatGrouped ToInt ToFloat. Money splitting that cannot lose a cent: Split(total, n) and Allocate(total, ratios) guarantee the parts sum back to the total exactly (100.00 by 3 → 33.34, 33.33, 33.33); free the result with FreeAll. Pitfalls: each Mul adds the scales, so round after every step a statement shows; ToStrFixed FormatGrouped Round Percent round half to even (2.345 shows as 2.34, call RoundHalfUp first); Div DivRound DivInt by zero return 0; b := a shares digits (Clone).
  • rand: Seed SeedAuto Int Int32 IntN IntRange Int32Range IntWithLength FloatRange Float Bool BoolP Choice{Int,Str,Char} Shuffle{Int,Str} Bytes TextFrom Alpha Alnum Digit Text UUID Hash. Not for secrets (clock seeded, predictable): tokens and keys come from crypto.RandomToken/ RandomBytes. IntN(0) and a Choice* on an empty vector stop the program (modulo by zero); IntRange(lo, hi) with lo > hi returns numbers outside the range; ChoiceChar/TextFrom pick bytes, so Persian letters break (use ChoiceStr over whole letters); Bytes never yields 0, so it is not key material.
  • stats: Sum Mean Median Min Max Variance PopVariance Stdev PopStdev Geomean Percentile(v, p) over Vector<f64>. An empty vector gives 0, which looks like data; Variance/Stdev are the sample (n-1) forms.
  • matrix: dense f64 linear algebra, one row-major Matrix for matrices and vectors. Build Zeros Ones Full Eye Diag FromArray RowVector ColVector Arange Linspace Clone; elementwise Add Sub MulElem DivElem Scale Neg AddScaled (+ ...InPlace), AddRowVector-style broadcasts and ufuncs Abs Sqrt Exp Log Round Clip Chop; products MatMul TransposeMul MulTranspose MatVec Dot Outer Kron MatPow Trace; shape Reshape Row Col SubMatrix HStack VStack Triu Tril; reduce Sum Mean Min Max Var Std Median ArgMin ArgMax; norms Norm1 Norm2 NormInf NormFrobenius Cond2; solve and factor Det Solve Inverse LeastSquares Decompose QRDecompose Cholesky SVDecompose EigSym EigVals MatrixRank PseudoInverse NullSpace RREF, Expm Sqrtm, iterative SolveCG SolveJacobi PowerIteration, statistics Covariance Correlation PCAFit LinearRegression, named Hilbert Vandermonde Toeplitz Random, output ToString ToStringPrec Print ToCSV. Shape errors and singular systems return a 0x0 matrix rather than panicking: check is_empty(). Compare with AllClose; free results in loops.

AI and machine learning

One family of packages, all pure Salam, seeded and deterministic (a fixed seed gives the same result on every machine at any thread count). The deep learning half is f32 on packages/tensor; the classical half is f64 on std/matrix. Shape errors return the empty tensor / 0x0 matrix.

  • tensor: n-d f32 arrays, contiguous row-major, no views. Tensor {data: Vector<f32>, shape: Vector<int>} with rank numel dim at set_at at2 set_at2 reshape_ free; build Zeros(shape) Zeros1..4 Ones Full Scalar FromArray Arange Clone CopyInto Shape1..4. Add Sub Mul Div broadcast (equal, scalar, trailing [N,D]+[D], [N,1]; BroadcastShape Expand for the general case), plus AddScalar Scale Neg Exp Log Tanh Sigmoid Relu Gelu Sqrt Square PowScalar Clip and *InPlace/*Into forms; shape Reshape Permute SliceAxis0 Narrow Concat2 Stack2 Pad2D; reduce SumAll MeanAll MaxAll ArgMaxAll SumAxis MeanAxis ArgMaxAxis Softmax LogSoftmax; products MatMul MatMulInto BatchMatMul MatVec Dot Transpose2D; conv Conv2DForward MaxPool2DForward Im2Col; random NewRng FillUniform FillNormal RandNormal; masks and gathers Greater EqualMask Where MaskedFill IndexSelect Gather OneHot TopK Tril Cumsum; SetThreads, ToMatrix FromMatrix WriteTensor ReadTensor AllClose ShapeString.
  • autograd: tape-based reverse mode. NewTape(seed), leaves Input (constant) Watch (tape-owned grad) Param(val, grad) (accumulates into the caller's buffer), recorded ops Add Sub Mul Div Neg Scale AddScalar Exp Log Tanh Sigmoid Relu Gelu Sqrt Square PowScalar MatMul BatchMatMul Sum Mean SumAxis MeanAxis Softmax LogSoftmax Reshape Transpose2D Permute Embedding Dropout LayerNorm RMSNorm BatchNorm GroupNorm Narrow SliceAxis0 Concat2 RoPE Silu Conv2D MaxPool2D AvgPool2D, Constant (an untracked tensor on the tape), losses MSELoss CrossEntropyLogits BCEWithLogits, then Backward(tp, loss), Value Grad, tp.reset() per step, tp.free(). GradCheck does central differences.
  • nn: ParamStore (owns values and grads; layers hold int handles) with add at zero_grad free, Bind; layers Linear Conv2D MaxPool2D AvgPool2D LayerNorm BatchNorm GroupNorm RMSNorm Embedding Dropout LSTM GRU MultiHeadAttention TransformerEncoderLayer (New* constructors, forward(tp, ps, x); NewTransformerDecoderLayer sets attn.causal, and CausalMask(seq) is the additive mask it adds), recurrent helper LastStep, the Layer interface with Sequential and the ReluLayer GeluLayer TanhLayer wrappers, Flatten SinusoidalPositions, init XavierUniform XavierNormal KaimingUniform KaimingNormal, checkpoints SaveCheckpoint LoadCheckpoint (SLMT, .gz aware) and the *Buf forms.
  • optim: SGD (momentum, nesterov, weight decay) Adam NewAdamW RMSProp with step(ps), ClipGradNorm ClipGradValue, schedules StepLR CosineLR WarmupCosine.
  • ml: struct-per-estimator over matrix; fit(x, y): bool, predict, predict_proba, transform, score, free, hyperparameters as fields (ml.KMeans { k = 3, seed = 7 }). Preprocessing StandardScaler MinMaxScaler LabelEncoder OneHotEncoder SimpleImputer PolynomialFeatures; models LinearReg Ridge Lasso NewElasticNet LogisticRegression KNNClassifier KNNRegressor GaussianNB MultinomialNB DecisionTree RandomForest AdaBoost GradientBoosting HistGradientBoosting LinearSVM SVC LDA QDA KMeans MiniBatchKMeans DBSCAN Agglomerative GaussianMixture PCA TruncatedSVD TSNE (+ KDTree, workers on KNN/KMeans); metrics Accuracy ConfusionMatrix Precision Recall F1 RocAuc RocCurve LogLoss MSE MAE RMSE R2Score SilhouetteScore ...; selection TrainTestSplit KFold StratifiedKFold CrossValScore GridSearch. Labels are a one-column matrix of 0, 1, 2...; fit returns false on bad shapes; fit scalers on the training rows only.
  • data: MakeBlobs MakeMoons MakeCircles MakeRegression MakeClassification (seeded), LoadCsv LoadCsvFile → Table (kinds to_matrix split_xy cat_levels), MatrixDataset.batches → BatchIter (next next_tensor reset), LoadIdx LoadMnist ParseIdx, LoadImageFolder, XTensor YLabels.
  • npy: ReadNpy ReadNpyFile WriteNpy WriteNpyFile ToMatrix FromMatrix NpzList NpzRead NpzWrite.
  • onnx: LoadOnnx ParseOnnx Run for MLP, CNN and transformer-style graphs (Conv Gemm MatMul activations, pooling, the norms, shape ops, reductions, Gather Slice Where Cast and more, about 65 ops); anything else fails with the op named in model.error.
  • gguf: LoadGguf ParseGguf (metadata, tensor directory), LoadTensorF32 dequantizing F32 F16 Q8_0 Q4_0 Q4_1.
  • llm: decoder-only models (Llama layout: RMSNorm, RoPE, GQA, SwiGLU). Config Model RandomModel NewKVCache(cfg) Forward(m, cache, tokens) (positions continue from cache.len), LogitsRow Greedy, Sampler (temperature top_k top_p repeat_penalty) with GenerateIds Generate, NewTokenizer (TOK_SPM, TOK_BPE; encode decode), and LoadGgufModel ModelFromGguf TokenizerFromGguf.
  • Pitfalls across the family: a shape mismatch silently yields an empty tensor (check is_empty()); the *InPlace/*Into forms do not broadcast; tp.reset() frees every value and gradient the tape made, so tensor.Clone what you keep; gradients accumulate (zero_grad every step; step does not clear them); set tape.training = false to evaluate; a checkpoint saves the ParamStore only (no BatchNorm running stats, no optimiser state); Adam defaults to lr 0.001, SGD 0.01. gguf/llm read files under 2 GB with F32 F16 Q8_0 Q4_0 Q4_1 weights only (no K-quants such as Q4_K_M, no BF16), expand every weight to f32 (Qwen2.5 0.5B needs about 3 GB), support Llama-layout models (Llama, Qwen2, Mistral; not Phi), and encode does not recognise special tokens like <|im_start|> (insert their IDs). onnx runs about 65 ops, all in f32, with no grouped/dilated Conv and no external-data models.

I/O, OS, filesystem

  • io: ReadFile WriteFile AppendFile Lines WriteLines Input Read Write ReadAll Readline Seek Close Copy EPrint EPrintln Flush. A missing file is not an error: ReadFile gives "" and Lines one empty line (check os.Exists). Lines split on \n only (a Windows \r stays), Readline returns "" for a empty line and at EOF alike, and everything is str, so text only. print/println to a pipe or file are buffered: io.Flush() pushes the buffer out, and a panic loses what is still in it (printerrln is unbuffered).
  • os: Args Env Cwd Chdir Exit Abort Pid Run RunCapture Output Exists IsDir FileSize Stat ReadFile WriteFile AppendFile Copy CopyTree Move Remove RemoveAll Mkdir MkdirAll Rmdir ListDir ListDirs Walk TempDir Open. Run/RunCapture/Output hand the whole string to /bin/sh (never splice user input in); RunCapture captures stdout only (add 2>&1). RemoveAll and Walk follow symlinks to directories, so RemoveAll deletes the link target's contents and Walk never ends on a loop. Move is a rename and fails (-1) across file systems. Exit skips defer (E109 inside main; use ret). Args()[0] is the program.
  • os.process: Start(path, args) StartCaptured Wait Kill Terminate ReadStdout ReadStderr Cleanup ExitCode Pid IsRunning. IsRunning is a flag, not a live check; Kill only signals, so Wait after it (zombie); Wait has no timeout; Cleanup a captured run (two temp files). os.shell Run Exec System (code, stdout and stderr together), os.temp File Directory Remove RemoveDir (Directory returns "" on failure; File always returns a path), os.user Username HomeDir ConfigDir CacheDir (env based; never for permissions), os.platform IsLinux IsWindows IsMac IsWasm OS Arch IsX64 IsArm64, os.error Last LastCode LastMessage HasError Clear (the last file/process error).
  • fs.file: Open Close ReadText ReadAll ReadBytes ReadLines WriteText WriteBytes Append AtomicWrite Size ModTime Stat Copy Move Rename Delete Exists Extension MimeType Md5Hex Sha256Hex. Not binary-safe: every read, write and digest goes through str and stops at the first NUL (ReadBytes too); use archive.ReadBinaryFile/WriteBinaryFile and crypto.Sha256Bytes. Exists is true for directories too; AtomicWrite resets permissions to the default.
  • fs.dir: Mkdir MkdirAll Remove RemoveAll List ListDirs ListFiles Walk Copy Move Exists IsDir Size. Exists is true only for a directory; Copy/Move merge into an existing target; RemoveAll Walk Size Copy follow symlinks. fs itself: ModTime AccessedTime CreatedTime Chmod Permissions (CreatedTime is 0 on most Linux file systems).
  • fs.path (Join Split Clean Normalize IsAbs Abs Rel Parent Dirname Filename Basename Ext Stem IsSafe Validate) and filepath (the same work, never touching the disk: Join Dir Base Ext Stem Clean Normalize IsAbs Abs Rel IsSafe Validate Separator). Join does not clean (Join("a/b", "../c") is a/b/../c) and an absolute second part is appended, not substituted; Ext is only the last extension; check user paths with IsSafe before using them.
  • env: Get GetOr Has GetInt GetFloat GetBool Set Unset Mask GetMasked Profile IsProduction IsDevelopment Load Overload LoadDotEnv LoadProfile Expand. GetInt turns abc into 0 and 12px into 12; GetBool accepts only lower-case true 1 yes on; Load never overrides a set variable (Overload/LoadDotEnv do).
  • flag: New(name, desc) AddStr AddInt AddBool AddFloat Parse(fs, args) GetStr GetInt GetBool GetFloat WasSet HelpWanted Positional Usage Error Free. Parse takes the arguments without the program name; a value flag takes the next argument whatever it is (--name --verbose); a negative positional needs -- first.
  • cli (subcommands): New Register Dispatch RemainingArgs Usage RequireFlags Confirm Prompt PromptOr, CmdHelp CmdVersion CmdUnknown. Dispatch wants RemainingArgs(os.Args()); it owns help -h --help version -v --version.
  • config: INI/env/YAML settings with dotted keys: LoadINI LoadEnv LoadYAML Parse* Get GetOr GetInt GetFloat GetBool Has Set Remove Keys Section Merge Format* Free. Nothing reports an error (a missing file is an empty config); in INI/env a # after a space starts a comment, so quote passwords.
  • log: Debug Info Warn Error Fatal + *f and *KV variants, KV Field FieldInt FieldBool, SetLevel SetPrefix SetFormat(FormatText| FormatJSON) ShowTime ToFile ToStdout ToStderr SetRotation CloseFile. A file log is buffered (call CloseFile on every exit path); settings are unlocked globals (set them at startup); text format does not escape line breaks (use JSON for user input).
  • debug Assert Enter Exit Stack Trace StartTimer StopTimer MemStats (Assert runs in release builds too), runtime OS Arch CpuCount TotalMemory AvailableMemory UsedMemory MarkStart UptimeMs BuildFlags (machine-wide figures, -1 when unknown, each call on Windows starts PowerShell), error structured errors: New Newf Wrap WrapKind WithMeta Kind Is Message RootMessage Format Ok IsNil Free, Kind* constants, IsNotFound IsTimeout IsRetryable ..., NewRetryPolicy DelayForAttempt ShouldRetry (you do the sleeping; Wrap of a nil error makes a real one, so check Ok first). core: Panic(msg) (abort, exit 134, defer does not run).

Terminal & TUI

  • term: everything ANSI. Never hand-roll \x1b[ sequences - this package is the one place they belong.
    • Terminal: IsTTY IsTerminal Size Cols Rows Write WriteErr Emit Flush Bell SetTitle EnableAnsi. Raw mode: MakeRaw / MakeCbreak (Ctrl-C still works) / Restore; always defer term.Restore(st) on the next line, or the user's shell stays raw.
    • Colour: Red Green Yellow Blue Magenta Cyan White Gray Bright* On* Bold Dim Italic Underline Inverse Strike Style Paint Color256 RGB OnRGB, raw Sgr Seq{Fg,Bg}256 Seq{Fg,Bg}RGB. All honour SetColorMode(ColorAuto|ColorAlways|ColorNever); ColorAuto obeys NO_COLOR/CLICOLOR_FORCE/TERM=dumb/isatty, so one code path serves a terminal and a pipe. Probes SupportsColor Supports256Color SupportsTrueColor.
    • Escape-aware Strip Width Truncate PadRight PadLeft Center: use these, not len/fmt.PadRight, on coloured text. Width counts one column per code point (CJK and the ZWNJ are miscounted).
    • Cursor/screen, each a pure Seq* builder plus a write-now twin: MoveTo Up Down Left Right Column Home Save/RestoreCursor Hide/ShowCursor ClearScreen ClearLine ClearBelow Scroll{Up,Down} SetScrollRegion Enter/ExitAltScreen Enable/DisableMouse; build a frame with Seq* and write once.
    • Input: Decode DecodeMouse (pure, testable without a tty), ReadKey GetKey ReadByte KeyReady ReadPassword, Key/MouseEvent, Key* codes.
    • Progress: NewBar BarSet BarAdd BarDraw BarFinish, NewSpinner SpinnerTick SpinnerStop SpinnerFree, FormatBytes FormatDuration; off a terminal they write one plain final line.
import term
func main:
    mut st := term.MakeCbreak(term.StdinFd)
    defer _ok := term.Restore(st)
    term.Emit(term.SeqHideCursor() + term.SeqClearScreen() + term.SeqMoveTo(1, 1))
    println term.Bold(term.Green("ready")), term.Dim("(q to quit)")
    until true:
        k := term.ReadKey()
        if k.code == term.KeyChar and k.ch == "q": break end
        if k.code == term.KeyUp: println "up" end
    end
    term.ShowCursor()
end

Collections (heap-allocated: call .free(), idiom defer x.free())

  • Vector<T> (built-in; also import collections): push pop get(i) ref(i) set(i,x) len is_empty first last insert remove_at reserve clear iter copy free; index via v[i] (read) / v[i] = x (write); free functions contains index_of last_index_of count_of slice clone reverse swap extend.
  • HashMap<K,V>: put(k,v) get(k) get_or(k, def) has(k) remove(k) size is_empty iter copy free; iterate with each k, v in m:. get of a missing key returns the zero value (a null str that crashes when used), so test has or use get_or. Iteration is in hash order, which changes as the map grows; sort the keys when order shows. get returns a copy of a struct value: change it and put it back.
  • Set<T> (add has remove size), Stack<T> (push pop peek size is_empty), Queue<T> (enqueue dequeue peek size), Deque<T> (push_front push_back pop_front pop_back front_val back_val), PriorityQueue<T> (push pop peek, a min-heap on <), LinkedList<T>, Counter<K> (add add_n count distinct), OrderedMap (str to str in insertion order: set get get_or has remove key_at val_at len), Pair, CircularList.
  • Pitfalls: b := a shares the elements (b.set(0, x) changes a, a push on one can move the other's memory); copy with v.copy(). pop peek dequeue first last and an index past the end stop the program on an empty structure (a --release build reads garbage instead).
  • sort: Sort SortDesc SortBy StableSortBy Sorted SortedBy IsSorted SortIndex BinarySearch LowerBound UpperBound Min Max Reverse Swap + named algorithms (QuickSort MergeSort HeapSort IntroSort …). Comparators: func (T, T) bool (less). Sort, SortDesc, SortBy and SortIndex are not stable (use StableSortBy), and SortBy goes quadratic on many equal keys. BinarySearch/LowerBound on an unsorted vector answer wrongly without error; Min/Max of an empty vector stop the program. Strings sort by code point, which is wrong for Persian (پ چ ژ ک گ after و).
  • option: Some None IsSome IsNone Unwrap UnwrapOr Expect, fields .present .value. Never read .value unchecked: on an empty Option<str> it is a null pointer and printing it segfaults. No match and no ? on an Option.
  • result: Result<T, E> (Ok/Err) with IsOk IsErr UnwrapOr Expect and postfix ? (error types must match). There is no Unwrap, map or and_then; Expect does not print the error.

Data formats

  • json (import encoding.json): Valid Get GetInt GetFloat Has Keys Minify Indent Escape Object(members) Array(items) Member/MemberInt/MemberBool/MemberFloat/MemberRaw Str. Typed codecs the compiler derives per type: Marshal(v) MarshalIndent Unmarshal(text, out, err) UnmarshalLenient, with @json "wire" to rename a field, @json "-" to drop it, and @json "" "omitempty"/"optional"/"string" for the rest. An enum with data is written with its member as the key, {"Circle":{"r":1.5}}, and a member without data as a bare string, "Empty". Schema(v) derives the type's JSON Schema (2020-12, $defs/$ref) from the same declaration and markers (the argument's value is unused); a @doc "..." marker on a struct, enum or field becomes its description (one @doc per definition); SchemaEnvelope/SchemaMerge give the parts. Also GetPath(text, "a.b.0.c"), Items Unquote, and the dynamic tree Decode(text, err): value.Value / Encode(v, opts) / EncodeDefault. Pitfalls: GetInt returns a 32-bit int and clamps (3000000000 reads as 2147483647): read IDs and timestamps by Unmarshal into an i64. Get GetInt Has see only the top level (GetPath for nested). Unmarshal fails on a missing field, an unknown key or a wrong type; UnmarshalLenient keeps defaults and skips unknown keys. Reset the encerr.EncodingError before each call.
  • xml (import encoding.xml): two models in one package.
    • Simple tree XMLNode (tag attrs children text): Decode DecodeStrict Valid Encode EncodeIndent Child Children Attr HasAttr SetAttr AppendChild NewXMLNode ToValue FromValue EscapeText EscapeAttr, plus the tokenizer NewXMLTokenizer/NextToken (checks nothing; run WellFormed first). Ignores namespaces and mixed-content order: fine for a config file.
    • Full Document (an arena of int node handles, strict parsing with line/column errors, namespaces, bounded entity expansion): Parse(text, err) ParseWith(text, opts, err) WellFormed, ParseOptions (preserve_whitespace keep_comments namespaces expand_entities max_depth; whitespace-only text is dropped by default). Read Root Kind Name LocalName NamespaceURI Parent ChildCount ChildAt Elements ElementsNamed FirstElementNamed Descendants NextSibling AttributeValue AttributeValueNS Text Line Column; edit NewDocument CreateElement CreateText Append InsertBefore RemoveChild SetAttribute SetText; write Write WriteIndent WriteNode Canonical. Free(d), FreeList(list).
    • XPath 1.0: Select(d, context, expr, err): NodeList (context -1 = document) SelectFirst SelectString SelectNumber SelectBoolean Evaluate; all axes and the core function library, no variables, no namespace axis. Name tests match the name as written, prefix included: use local-name()/namespace-uri() when the prefix may vary. Attributes come back as negative handles (IsAttributeRef ReferencedAttribute).
    • DTD validation: ParseDTD Validate ValidateInternalSubset CheckNamespaces ApplyDefaults returning a Report (ok, items of Violation, ReportText, FreeReport FreeDTD).
  • yaml (import encoding.yaml): Valid Get GetInt GetFloat GetBool Has Keys Len TypeOf Parse Decode ToJSON Encode Dump DumpWith SplitDocuments plus the Object Array Member* builders. No anchors or aliases (&a, *a, <<:; an anchored map silently drops the rest of the document); yes/no/on/off are strings; GetInt is 32-bit and wraps; every path call reparses, so Decode once for many reads.
  • csv (import encoding.csv): ReadLine(str): Vector<str>, WriteLine(Vector<str>): str, ReadAll WriteAll FreeTable ToValue FromValue. The separator is always a comma; a quoted field with a line break cannot be read back (ReadAll splits lines first); "Pen" ,blue (space after the quote) ends the row silently; spaces are kept.
  • toml (import encoding.toml): DecodeStrict Decode Valid EncodeStrict Encode over value.Value. Decode never reports errors (a typo drops the rest), so use DecodeStrict on edited files. Triple quoted strings are invalid and dates stay strings; Encode loses comments and key order.
  • value (import encoding.value): the dynamic Value every format shares: ValueNull ValueBool ValueInt ValueFloat ValueString NewValueArray NewValueObject Is* As* ArrayLen ArrayGet ArraySet ArrayPush ObjectGet ObjectGetOk ObjectSet ObjectHas ObjectDelete ObjectKeys ValueClone ValueFree Equal. As* never checks the kind (AsInt of a string is 0); ObjectGet of a missing key is null (chains are safe) but ArrayGet past the end stops the program; a copy shares its map/elements (ValueClone); keys come back in hash order; Equal(1, 1.0) is false.
  • serde Detect DetectFormat Transcode TranscodeStrict (detection is a guess and almost any text is YAML: pass the format when you know it), encerr the shared EncodingError (NoError resets it; a reader only writes it on failure; position counts bytes), msgpack Marshal Unmarshal UnmarshalBytes (no bin/ext types, string map keys only, trailing bytes ignored), binary WriteU32BE ReadI64LE ... (reading past the end yields 0 and sets r.bad; ReadU32* comes back in a signed int, so widen with (v as i64) & 4294967295; writers truncate silently), protobuf (.proto parser, wire helpers WriteVarint ReadVarint WriteTag ReadTag SkipField ZigZag* and Salam code generation; generated readers do not accept packed repeated fields).
  • url (import encoding.url): Parse(s): URL HostPort Hostname Query SetParam RemoveParam KeepParams Join Domain Subdomain PublicSuffix IsIP. Parse never fails or validates (check scheme and host yourself), nothing is lower-cased, and editing one parameter re-encodes the whole query. query (import encoding.query): Get GetAll Has on the text, or Decode to a QueryMap with Set Add Remove Retain Encode; missing and empty look the same to Get. envfile (import encoding.envfile): Parse ParseStrict ParseRaw Get GetOr Set Format Expand RefNames; Parse drops bad lines silently (use ParseStrict), and ${NAME} only sees lines above it.
  • encoding: Base64Encode Base64EncodeBytes Base64Decode Base64DecodeStrict Base64DecodeBytes HexEncode HexDecode URLEncode URLDecode. Base64Decode/HexDecode return a str, so binary is cut at the first zero byte (use Base64DecodeBytes); HexDecode stops at the first bad pair without error; URL-safe base64 (-_) is damaged by the lenient decoder.
  • compress: ByteBuf (NewByteBuf BufPush BufGet BufLen BufFree BufFromStr BufToBase64), Compress/Decompress(method) over Method*, DeflateCompress GzipCompress GzipDecompress CRC32 Adler32. Gzip does not verify the CRC; the method is not stored in the output; free every buffer.
  • archive: ZipCreate ZipCreateFile ZipList ZipExtractAll ZipExtractByName TarCreate TarList TarExtractAll RarList RarExtractAll GzipCompressFile ReadBinaryFile WriteBinaryFile. No zip64 (under 2 GB), ZIP methods 0 and 8 only, CRC not checked, tar names over 100 bytes are cut, RAR extracts only stored entries.
  • excel: xlsx, ods and csv workbooks: Open Save New NewWorkbook NewSheet with sheet methods set_string set_number get_cell get_string row_count ..., CellRef ParseCellRef SheetToJSON SheetToCSV. No .xls (IsXLS detects it); the format comes from the file extension; formulas are not evaluated; rows and columns count from 0.
  • regex: Compile Match Find Replace ReplaceAll (Regex handle) and one-shot MatchStr FindStr ReplaceStr ReplaceAllStr. Captures: FindMatch returns a MatchResult value (.ok .start .stop .count) to read with Group GroupStart GroupEnd GroupOk GroupByName GroupIndex GroupCount, plus FindSubmatch FindAll FindAllN FindAllMatches Split SplitN (all Vector-returning, so defer v.free()) and $1/${name} rewriting with Expand ReplaceExpand ReplaceAllExpand; IsValid checks a pattern. Syntax: . [] \d \w \s \D \W \S ^ $ | (…) (?:…) (?<name>…) (?P<name>…) * + ? {n,m} and the lazy *? +? ?? {n,m}?. No \b (it means b), no (?i), no lookaround, no back references; an unsupported pattern is invalid and never matches (test with IsValid). The engine works on bytes: \w never matches a Persian letter, [آ-ی] does not work and ب+ repeats only the last byte (write (ب)+). ^/$ anchor the whole text, never a line. At most 16 groups and {n} up to 64; a match over 400000 steps is reported as no match. Global state: one thread at a time.
  • crypto: Sha1Hex Sha256Hex Sha512Hex Md5Hex, Sha256Bytes/Sha384Bytes/ Sha512Bytes (+ streaming Sha256New/Update/Final), HMAC HmacSha256Hex/HmacSha384Hex/HmacSha512Hex and the byte-oriented HmacSha256Bytes/HmacSha384Bytes/HmacSha512Bytes (write into a caller buffer) / HmacSha256Raw/HmacSha384Raw/HmacSha512Raw (pointer+length, for binary keys), HashPassword VerifyPassword Pbkdf2HmacSha256Hex, RandomHex RandomToken RandomBytes SecretEquals. HashPassword uses only 1000 PBKDF2 rounds; for new systems build the pbkdf2-sha256$<rounds>$<salt>$<hex> string yourself with Pbkdf2HmacSha256Hex and 600000 rounds (VerifyPassword reads the rounds from the string). Compare secrets with SecretEquals, never ==. Sha256Hex of a str hashes only up to the first NUL. HashStr HashBytes HashInt are hash-table hashes, not cryptographic.

Time, memory, testing

  • time: Now NowMillis NowMicros NowNanos Sleep(ms) Format FormatISO FormatDate FormatTime Year Month Day Hour Minute Second Weekday Since Until ElapsedMs MonoNanos MonoMillis and YearUTC MonthUTC DayUTC HourUTC ...; DateTime type. An instant is an i64 of Unix seconds. Year Month Day Weekday Format FormatDate FormatISO use the local zone (servers differ from laptops: use the ...UTC forms and FormatISOUTC/ FormatRFC3339 for anything stored); Now has whole seconds and follows clock changes, so time code with MonoNanos; Days(30) is not a month; FromUTCParts rolls over (30 February is 1 March) while the parsers reject impossible days. Sleep takes milliseconds.
    • Duration (nanoseconds, so it cannot be read in the wrong unit): Nanoseconds Microseconds Milliseconds Seconds Minutes Hours Days SecondsF ZeroDuration, methods nanos millis secs seconds minutes hours days add sub mul div ratio neg abs truncate round cmp less equals is_zero; FormatDuration ("1h30m0s") and ParseDuration/ ParseDurationOr (DurationResult { ok, value }); SleepFor SinceDuration DurationBetween AddDuration.
    • Parsing: ParseISO ParseISOIn ParseISOOr ParseHTTPDate ParseFormat(pattern, s) (strftime %Y %m %d %H %M %S %f %b %a %z %Z), all returning TimeResult { ok, epoch, nanos }; an unzoned string is local time. FromLocalParts (DST-correct) / FromUTCParts, LocalOffsetMinutesAt.
    • Timers (monotonic): NewTimer (expired remaining wait reset stop), NewTicker (fired wait remaining reset stop advance, a slow body drops ticks and counts them in dropped), AfterFunc Every RepeatEvery WaitUntil; a lambda captures locals by value, so callbacks share state through mut globals.
  • calendar: Gregorian, Jalali (Persian solar) and Hijri (tabular Islamic) dates through one Julian Day Number: GregorianToJalali JalaliToGregorian GregorianToHijri HijriToGregorian JalaliToHijri HijriToJalali, *ToJDN/JDNTo*; per calendar IsLeap* DaysIn*Month IsValid*Date *MonthName{En,Fa,Ar} WeekdayName{En,Fa,Ar} Compare*, the system clock Today{Gregorian,Jalali,Hijri} NowClockTime, from an epoch EpochTo{Gregorian,Jalali,Hijri,ClockTime} *ToUnixUTC, text Format* FormatJalaliLongFa FormatHijriLongAr FormatGregorianLongEn; arithmetic AddDays* AddWeeks* AddMonths* AddYears* DaysBetween* StartOfMonth* EndOfMonth* NextWeekdayOnOrAfter (days are exact, months and years clamp: 2024-01-31 + 1 month = 2024-02-29). Types GregorianDate JalaliDate HijriDate ClockTime.
    • Zones: real IANA zones from the host tzdata, LoadZone(name) LocalZone FreeZone ZoneOffsetSeconds ZoneIsDST ZoneAbbrevAt TimeZoneAt ZoneAt(name, epoch) ZonedToEpoch (no tzdata on Windows: ok = false). The fixed TimeZone values TZUTC TZTehran TZKabul TZRiyadh TZDubai TZIstanbul TZLondon TZNewYork TZTokyo FixedOffsetZone TZLocal with WallClock{Gregorian,Jalali,Hijri,Time} ConvertZoned* are standard time: TZLondon is UTC+0 all year, so use ZoneAt("Europe/London", epoch) for DST zones.
    • Pitfalls: TodayJalali/EpochToJalali use the machine's zone (a UTC server is still on yesterday until 03:30 Tehran time): use WallClockJalali(time.Now(), TZTehran()). JalaliWeekday counts from Saturday = 0 and takes a JDN, the other Weekday* count from Sunday = 0. Conversions do not validate (month 13 gives a wrong date): check with IsValidJalaliDate first.
  • mem: Allocate AllocateZeroed AllocateArray Reallocate Free Copy Set MemMove, arenas NewArena ArenaAlloc ArenaReset ArenaFree, fixed-size pools NewPool PoolAlloc PoolFree PoolDestroy; leak tooling CheckLeaks LiveBytes AllocCount PeakBytes. Nothing is checked (overruns and double frees corrupt silently; salam memcheck finds them for Allocate blocks), every allocator can return null, and memory from a C library goes back to that library's free, never mem.Free.
  • testing: AssertTrue AssertFalse AssertEqInt AssertEqStr AssertEqFloat AssertEqBool AssertEqInt64 AssertNeInt AssertNeStr AssertContains AssertNil AssertNotNil AssertMsg Passed Failed Reset Summary() (ret testing.Summary() from main). A failure says what differed, not where (put the case name in AssertMsg); an exit code keeps 8 bits, so 256 failures read as 0; counters are unlocked globals (assert from the main thread).

Networking & web

  • net.http (import net.http, called http.), client: Get Post Put Patch Delete Head Options (+ *With(..., headers) taking a HashMap<str,str> from NewHeaders), Request(method, url, headers, body), then Status Body Error Ok GetHeader Headers SetCookieLines (or the fields .status .body .err); NewClient() keeps cookies (c.Get, c.Post, c.SetHeader). Redirects are off unless RequestOptions.max_redirects (or c.SetMaxRedirects) is set; every call has a 30 s idle timeout and a 256 MB body cap, and Do(method, url, headers, body, opts) or c.SetTimeout(ms) bounds the whole request (errors read the request timed out); Post/Put/Patch label the body as a form unless you pass Content-Type; Status 0 means unreachable; Ok is 2xx; the one-line calls keep no state, so threads may call them freely, and a NewClient() client is safe to share between threads (Free it at the end; a Client {} literal has no lock). Cookies go back only to the host that set them.
  • Server (net.http + net.router): mut r := router.NewRouter(), router.Get/Post/Put/Patch/Delete(r, "/books/:id", handler), Group Static; mut s := http.NewServer(port), http.Use(s, r), then Run(s), RunTLS(s, cert, key) or RunBackground(s) (returns once bound, serves on its own thread, so one process can run several servers). Limits SetMaxBodyBytes SetMaxHeaderBytes SetKeepAliveIdle EnableAccessLog. A handler is func h(ctx: i64) passed by bare name, using Text, Html, Json, SetStatus, SetHeader, Redirect, Param, Query, Form, Body, Header, Method, Path, Cookies, CookieValue, HasCookie, SetCookie, ClearCookie and Own. Each of these also takes a Context, so c := http.Ctx(ctx) lets you pass c to the same names instead of the raw id. Pitfalls: register every route before Use (the server copies the router; a later route is a 404); strings read from a request are freed when the handler returns, so str.Clone what you keep in a global or session (Own hands a string you built to the request); router params are not URL-decoded; no automatic 405; Body is a str (binary uploads are cut).
  • net.router alone: Dispatch (a Match) FreeMatch ParamGet EnableListing for routing without a server. net.http.middleware Log AllowAll Secure NoCache Cache (AllowAll omits PATCH; Secure sets no CSP or HSTS), net.http.validate Required Length IsEmail Matches Assert (Required accepts spaces, IsEmail is loose, Matches is unanchored, and you must ret after a failed Assert), net.http.session Configure Start Save Regenerate Destroy Value SetValue CSRFToken CSRFVerify (call Save after changes, Regenerate after login; sessions live in process memory).
  • net.http raw calls: requests with your own header text and any bytes: ParseEndpoint SendRaw SendRawTimeout SendRawTLS FreeRaw RawText HeaderValue (result type RawResponse, body in .data/.len, chunked bodies decoded). A call ends when a Content-Length or chunked answer is complete; an answer with neither is read until the server closes, so send Connection: close for those; Endpoint and the raw calls are in the same package as Get/Post.
  • net.http.swagger (OpenAPI + a documentation page): New(title, version) Describe Server BearerAuth ApiKeyAuth; Scan(doc, router) adds every registered route, Route creates one and At(doc, method, pattern) selects one (after an At that finds nothing the following calls do nothing), then Summary Details Tag OperationId Deprecated Secure Query QueryTyped QueryEnum Header PathParam. Accepts(doc, desc, NewTodo {}) and Returns(doc, 200, desc, Todo {}) take a value and read its type (the schema comes from the struct and its @json names), plus ReturnsText ReturnsHtml ReturnsFile ReturnsRaw ReturnsNothing ReturnsHeader AcceptsForm AcceptsRaw. Spec SpecIndent WriteSpec; check with Problems Undocumented Missing(doc, r); serve with Mount(doc, r, "/docs") or Serve(doc, port); Free(doc). Mount copies the spec when called: describe every route first, then Mount, then http.Use. The docs are public (CORS *).
  • net.websocket (RFC 6455 client and server, ws:// and wss://): Listen Accept AcceptWSS Dial DialWith DialWSS DialWSSWith DialWSSUnverified, SendText SendBinary SendBinaryBytes Ping Close Stop, Receive (blocks) / Poll(c, ms) (returns a TIMEOUT message), plus a Hub for rooms and broadcast. A binary Message carries .data/.len next to .text (that buffer is reused by the next message, so copy it). One thread reads one connection; a Hub has no lock (wrap it in a mutex); unregister with the copy you registered, since Receive clears conn.fd on close.
  • net.socketio (import net.socketio): a Socket.IO v4/v5 client (polling and WebSocket transports). Dial(url) DialWith(url, opts), or NewManager Open Of(m, "/nsp") for several namespaces; the URL path is the NAMESPACE, the HTTP path is Options.path (default /socket.io). Send Emit EmitStr EmitInt EmitJSON EmitBytes EmitArgs EmitVolatile; EmitAck blocks for the answer, Reply ReplyStr ReplyJSON answer ev.ack_id. Receive with On Once Off OnAny + Run/RunFor, or better Next(c, ms)/NextEvent(c, ms) in a loop (a lambda captures by value). Arguments: NewArgs AddStr AddInt AddJSON AddBytes, ArgCount ArgStr ArgInt ArgValue ArgBytes, Ack*; free with ArgsFree EventFree AckFree. Nothing runs in the background: the connection only advances inside Pump/Next/Run/EmitAck, so a program that stops pumping misses heartbeats and is dropped. Next also returns connection events (EV_CONNECT, ...). Socket.IO 2 servers are not supported. Wire helpers EncodePacket DecodePacket EncodeEngine are public for tests.
  • net.tcp (import net.tcp): Bind(port) Accept Dial(host, port) Read Write ReadBytes WriteBytes Close CloseServer Ok ConnOk SetDeadline SetReadDeadline SetNoDelay UpgradeTLS DialTLS, plus a connection Pool. An empty Read means closed, timed out or failed (and binary is cut at a NUL: use ReadBytes); Write may send part (WriteBytes loops); TCP has no message boundaries, so frame them; Bind listens on every IPv4 interface. net.udp Bind Open Send SendTo Receive ReceiveBytes SetTimeout SetBroadcast JoinMulticast (IPv4 only; a timeout looks like an empty packet; a short buffer drops the rest of the packet).
  • net.dns Lookup (system resolver, one IPv4) and LookupA LookupAAAA LookupMX LookupTXT LookupCNAME LookupPTR QueryServer (own UDP queries to /etc/resolv.conf servers, falling back to 8.8.8.8; no TCP retry for truncated answers, no DNSSEC). net.mime MimeType MimeExt ParseContentType ParamGet ParseMultipart NewMultipartWriter (about 25 extensions; everything else is application/octet-stream; case sensitive). net.proxy NewHTTPProxy NewSOCKS5Proxy Dial NewChain DialChain (no handshake timeout, no IPv6 proxy address, ignores HTTPS_PROXY). net.email: Send and BuildMessage, or drive DialSMTP/DialSMTPTLS Hello StartTLS AuthPlain MailFrom RcptTo Data Quit; DialIMAP Login Select Fetch, DialPOP3 Retr Dele (BuildMessage adds no Date/Message-ID and does not encode a non-ASCII subject; IMAP Login breaks on a password with " or \). net.http2 frames, HPACK and streams (NewConn ClientHandshake SendHeaders SendData Poll; one thread per Conn).
  • tls (TLS 1.2/1.3 in pure Salam, no OpenSSL): Dial WrapClient WrapServer NewConfig NewServerConfig ConnOk ConnError Read Write ReadBytes WriteBytes Close, plus AesGcmSeal, AesGcmOpen, HkdfSha256, the EcdhP256 functions, P256Sign and P256Verify. Check ConnOk after Dial; no client certificates; about 800 KB/s for bulk transfers. ssl is an OpenSSL-style API over it (NewClientContext SetHostname Wrap Connect Read Write Shutdown; SetHostname is required before Connect, and Close does not close the TCP socket).
  • ssh: Connect/Dial with NewConfig WithPassword WithKeyFile WithHostKeyMode, RunCommand Exec NewSession RequestPty Shell, SFTP SftpOpen Upload Download ListDir Mkdir Remove Rename Stat. RSA keys only (OpenSSH format, no passphrase; Ed25519 and ECDSA are not read) and the server needs an RSA host key; each Exec is a fresh shell (a cd does not carry over). ftp Dial DialTLS Login Binary List Upload Download Read Write Mkdir Delete Rename Quit (call Binary before transfers; Read/Write are str, so text only; no resume).
  • Clients: aws (S3/SQS/STS and S3-compatible: NewConfig ForMinio ForR2 S3Put S3Get S3GetBytes S3UploadFile S3DownloadFile S3ListAll S3Delete ...; S3Get is a str, so binary needs S3GetBytes; S3ListPrefix returns one page of 1000), google (service account ParseServiceAccount AccessToken Authorization), telegram.bot (NewBot SendMessage SendPhoto Poll SetWebhook InlineKeyboardMarkup ...; every call is a GET, so no local file upload, and Poll never returns), webpush (GenerateKeys KeysFrom Send; new keys break every subscription), graphql server (Parse Execute SetResolver HandleHttp; subscriptions run once; resolvers run field by field, so batch your loads), grpc (Dial UnaryCall NewServer RegisterMethod Serve; the server is h2c only, one call at a time), rpc (JSON-RPC 2.0 and MessagePack-RPC: Register NewServer HandleText Call DialClient).
  • Browser/JS targets: dom (ById Query QueryAll SetText SetHtml SetStyle OnEvent OffEvent SetTimeout EveryFrame Store*; a missing element is JS null, not 0, so test Exists(id); never SetHtml user text), web.canvas (Of Get FillRect BeginPath MoveTo LineTo Arc Fill FillText DrawImage; paths pile up until BeginPath), web.keys (Init Track Down Pressed), node (Node.js interop: Require GetProp CallS1 ...; handles are i64, every number is f64). web itself holds the Color Length Url value types the layout DSL uses (§9); it is not a server.
  • webview (desktop windows: Create SetTitle SetSize SetHTML Navigate Eval Init Run Destroy IsClosed; -DSALAM_WEBVIEW_CEF renders with a bundled Chromium instead of the OS webview, see packages/webview/native/BUILD.md). Put data into the HTML you pass to SetHTML; the page cannot call back into Salam, Eval right after SetHTML usually hits the old page, and a failed Create returns a window that IsClosed() already.
  • Native audio: audio holds a Clip of interleaved f32 samples with rate, channels, frames and bits. Core: New Sample SetSample Duration Peak RMS GainToDb DbToGain. Editing: Slice Trim Concat Mix MixAt Gain Scale Normalize Clamp FadeIn FadeOut Reverse Pad Repeat Split TrimSilence. Converting: ToMono ToStereo ToChannels ToRate ToBits Channel Deinterleave Interleave. Sniffing: DetectFormat FormatFromExt FormatName FormatIsLossless. MsbReader and MsbWriter are the MSB-first bitstream pair every codec needs; compress's BitReader is LSB-first and will not do. audio.wav: OpenWAV SaveWAV SaveWAVBits EncodeWAV EncodeWAVFloat EncodeMuLaw EncodeALaw DecodeWAV ProbeWAV ReadFormat Chunks FindChunk, covering PCM 8/16/24/32, IEEE float, A-law and mu-law. audio.dsp: FFT IFFT FFTVec Magnitude PowerSpectrum (radix-2, power-of-two lengths only), MakeWindow ApplyWindow BesselI0, LowPass HighPass BandPass Notch Peaking LowShelf HighShelf BiquadStep BiquadProcessAll BiquadResponse, ResampleSinc ResampleCubic ResampleTo Correlation DCRemove. Effects and dynamics: Delay Echo Reverb Chorus Flanger Tremolo CompressorProcess LimiterProcess GateProcess TimeStretch PitchShift. Analysis: STFT SpectrumAt SpectralCentroid SpectralRolloff SpectralFlatness SpectralFlux FreeSpectrum ZeroCrossingRate DetectPitch DetectOnsets DetectBPM LoudnessLUFS NormalizeLUFS TruePeakDb. Shared sample conversion lives in the core so every codec uses one copy: DecodePcm EncodePcm DecodeFloatPcm EncodeFloatPcm DecodeLawPcm EncodeLawPcm PcmDepthValid MuLawToPcm PcmToMuLaw ALawToPcm PcmToALaw. audio.wav: OpenWAV SaveWAV SaveWAVBits EncodeWAV EncodeWAVFloat EncodeMuLaw EncodeALaw DecodeWAV ProbeWAV ReadFormat Chunks FindChunk, covering PCM 8/16/24/32, IEEE float, A-law and mu-law. audio.aiff: OpenAIFF SaveAIFF SaveAIFFBits EncodeAIFF DecodeAIFF ProbeAIFF ReadCommon. Big-endian, the sowt little-endian variant, and the 80-bit extended float sample rate. 8-bit AIFF is signed, unlike WAVE. audio.au: OpenAU SaveAU EncodeAU EncodeAUEncoding DecodeAU ProbeAU ReadHeader. A 24-byte big-endian header; 8-bit is signed here too. audio.flac: OpenFLAC SaveFLAC SaveFLACLevel EncodeFLAC EncodeFLACLevel DecodeFLAC ProbeFLAC ReadStreamInfo ReadFrameHeader Blocks VerifyFLAC CRC8 CRC16. Real lossless compression both ways; the decoder handles constant, verbatim, fixed and LPC subframes and all four channel assignments. FreeStreamInfo releases the MD5. audio.mp3: ParseHeader FindSync CountFrames Headers FrameAt SeekToSecond ReadVbrInfo ProbeMP3 AudioStart IsMP3. Frame and VBR-header parsing only; mp3 audio decoding is not implemented, and codec.CanDecode(FormatMP3) says so. audio.tag: ReadTags ReadTagsFile FreeTags WriteID3v1 WriteID3v2 ReadID3v1 ReadID3v2 ReadVorbisComment WriteVorbisComment ReadFlacTags StripTags AudioStart AudioEnd. ID3v1, ID3v2.2-2.4 in all four encodings, Vorbis comments and cover art; text always comes back UTF-8. Every string is owned, so call FreeTags. audio.music: NoteToFreq FreqToNote NoteName ParseNote Scale ScaleNotes Chord ChordNotes Transpose Interval Cents JustRatio PythagoreanRatio BeatSeconds. Persian modes are in cents, not semitones: Dastgah DastgahFreqs NoteToFreqCents KoronCents SoriCents, because a dastgah does not fall on the twelve-tone grid. audio.synth: Tone ToneEnvelope Chord NewOsc OscNext WaveValue ADSRAt ADSRTotal NoiseWhite NoisePink NoiseBrown FMTone Additive KarplusStrong NoteAt RenderNotes. audio.midi: NewSong NewTrack AddTrack AddNote AddTempo AddProgram AddControl EncodeMIDI DecodeMIDI OpenMIDI SaveMIDI TicksToSeconds Bpm TotalTicks Notes Render FreeSong. Render plays a song through audio.synth. audio.codec: Open Save Decode Encode Probe ProbeFile CanDecode CanEncode, the format-agnostic entry points. They live in their own package because a parent cannot import its own children; it carries WAV, AIFF, au and FLAC. 8-, 16- and 24-bit PCM round-trip a Clip bit for bit; a true 32-bit integer source loses its low 8 bits to the f32 significand. Free every Clip with audio.Free and every ByteBuf with compress.BufFree; Blocks, Headers, Notes, Scale and friends return vectors the caller frees.
  • Native media: image (Open Save SaveQuality New Resize Crop DrawText ..., RGBA; GIF reads frame one; JPEG drops alpha), opencv (needs the salam_opencv_shim library and OpenCV 4; Color is RGB while a Mat is BGR; free every Mat), graph (Dijkstra ShortestPath PageRank Components ...; unreachable is GRAPH_INF, and Dijkstra returns false on a negative weight), llvm (build, verify, JIT and link LLVM IR; ParseIR/RunJIT take a .ll path, call Verify before emitting, free builder, module, context in that order) and llvm.triple.

JWT and API route protection

  • jwt: JSON Web Tokens (RFC 7519/7515) over HMAC-SHA2, and the HTTP layer that uses them - one package, because the HTTP half is only useful with the token half and dead-code elimination means a CLI that merely signs a token links none of it.
    • Tokens: NewClaims SetStr/SetInt/SetBool/SetRaw ExpiresIn NotBeforeIn ClaimsJSON; Sign SignHS256/384/512 SignWithKid SignJSON SignatureOf; Verify VerifyHS256 IsValid DefaultOptions OptionsFor; Decode (parses, verifies nothing); claim readers Str Int Bool Raw Has ListHas HasScope HasAudience SecondsRemaining; ErrorText IsSupportedAlg; error codes JWT_OK JWT_ERR_*. Options carries the policy (alg issuer audience subject leeway now require_exp/nbf/iat/sub/jti). alg comes from Options, never from the token; "none" and RS*/ES* are rejected (no asymmetric support). require_exp is off by default, so a token without exp is accepted forever unless you set it; the secret is not checked for strength; claims are signed, not encrypted.
    • HTTP (needs net.http + net.router): NewGuard bundles the secret with an Options; Require RequireScope RequireClaim RequireSubject verify at the top of a handler and, on failure, write the 401/403 themselves and return false. Also Check (verify, write nothing), ParseBearer BearerToken TokenFrom Unauthorized MissingCredential Forbidden, issuing (Issue IssueFor IssuePair) and cookies (SetSessionCookie SetSessionCookieInsecure ClearSessionCookie).

Passwords and secrets live in crypto, not here: HashPassword VerifyPassword (PBKDF2-HMAC-SHA256), SecretEquals (constant-time compare), RandomHex.

func guard(): jwt.Guard:
    mut g := jwt.NewGuard(os.Env("API_SECRET"))
    g.opts.issuer = "api.example.com"
    ret g
end

func me(ctx: i64):
    mut t := jwt.Token { }
    if not jwt.Require(ctx, guard(), t): ret end     // 401 already written
    http.Json(ctx, `{"sub":"` + t.claims.sub + `"}`)
end

Worked examples: tests/en/apps/auth/ (seven programs), the five REST APIs in tests/en/apps/*api/, and tests/en/webframework/authapi.salam. Package tests: tests/en/stdlib/jwt_demo.salam.

Databases (import db, import db.<engine>)

db itself is the engine-independent layer: three interfaces plus helpers that work on any driver. Take a dyn db.Connection and the same code runs on any engine (tests/en/db/shared_iface.salam runs one function against sqlite and postgres and expects identical output).

  • interface Connection: Kind Ok Ping Exec Query Begin LastInsertId Changes Error Quote Close
  • interface Rows: Ok Next ColumnCount ColumnName ColumnType IsNull Text Int Int64 Float Close - forward-only, so the loop is until r.Next():, and Close() is required
  • interface Tx: Ok Exec Query Commit Rollback
  • Helpers: QueryInt QueryInt64 QueryFloat QueryText CountRows Run RunAll InTx QuoteLiteral; constants SQLITE MYSQL POSTGRES ELASTICSEARCH OPENSEARCH and TYPE_NULL TYPE_INT TYPE_FLOAT TYPE_TEXT TYPE_BLOB
  • Pool (NewPool Add Acquire Leased Conn Release Size Idle InUse DropDead CloseAll Free): a fixed set of connections leased one at a time. It holds connections you opened; it is not a factory. No locking of its own.
  • Migrations (NewMigrator NewMigratorIn AddMigration MigrationCount EnsureTable SchemaVersion IsApplied PendingCount MigrateUp Rollback MigrateTo FreeMigrator SplitStatements): each step runs in a transaction with the row recording it; steps split on ; outside quotes, since MySQL rejects multi-statement queries.
  • Pitfalls: QuoteLiteral only doubles ' (on MySQL use c.Quote, which also escapes backslashes); MigrateUp compares version numbers, so a migration numbered below the highest applied one is skipped silently; a $$ function body is cut apart at its ; (run it with c.Exec).

Each driver keeps its own full surface, and Conn(d) boxes one as a dyn db.Connection:

  • db.sqlite: Available Version Open Ok Exec Query Next Text Int Int64 Float IsNull Finish Prepare BindText BindInt BindFloat BindNull Reset LastInsertId Changes QueryInt QueryText Close Conn. One Database is one connection: give each writing thread its own Open. There is no BLOB bind (store hex or base64), and QueryInt/QueryText take no parameters, so use them only on SQL you wrote.
  • db.postgres (native wire protocol, no libpq; TCP only, SCRAM/MD5/password auth, TLS via sslmode): Available ClientVersion ServerVersion Open OpenFull Ok Ping Reset Close Error Exec ExecCount Query Quote QueryInt QueryText DatabaseName UserName HostName Port Conn. Placeholders are $1, $2, not ?. There is no last insert id - use INSERT ... RETURNING id; LastInsertId() goes through lastval() and is 0 when the session has used no sequence. Results are materialised, so every Rows must be closed (page large tables with LIMIT). No COPY FROM STDIN. Commit returns -1 when the server rolled the transaction back. ServerVersion is major * 10000 + minor.
  • db.mysql (MariaDB/MySQL, native protocol, no libmysqlclient; TCP only, native_password and caching_sha2 auth, TLS when the server offers it, SALAM_MYSQL_SSL_MODE = DISABLED/REQUIRED/VERIFY_IDENTITY): Open Ok Close Ping Error Errno Exec Query QueryOk Next Finish Text Int Int64 Float IsNull ColumnCount RowCount ColumnName AffectedRows LastInsertId Begin Commit Rollback Autocommit Escape SetCharset SelectDB QueryInt QueryText Conn. This API hands every value over as text, so ColumnType answers TYPE_TEXT where the others report the column's own type; DDL commits implicitly, so Tx.Rollback() cannot undo it. Threading: a connection cannot be used by two threads at once (it segfaults, it does not error), so a threaded server gives each request its own connection. Call LibraryInit() once from main before any thread starts, and ThreadInit() from each thread before its first query - tests/en/apps/*api/_store.salam is the worked example.
  • db.sql (read and check SQL without a database, the sqlc idea with your own DDL as the schema): DialectOf DialectNamed give a Dialect for SQLITE MYSQL MARIADB POSTGRES; Lint(sql, d) parses one statement or a whole ; separated script and returns a Report of Diag values (code message help line col offset length, rendered by ReportText/DiagText); ParamCount/Placeholders count and list the placeholders. With a schema: NewCatalog(d) then Apply(cat, ddl) reads CREATE TABLE, ALTER TABLE, DROP and CREATE VIEW into a Catalog (so the migrations are the schema), TableCount TableName HasTable ColumnCount ColumnAt HasColumn look around it, and Check(sql, cat) resolves names: unknown table or column with the nearest real name offered, ambiguous column, INSERT arity, a NOT NULL column with no default left out, a write with no WHERE. Analyze(sql, cat) adds what the query takes and returns - Param { marker ordinal table column kind not_null } and ResultCol { name table kind nullable }, with outer joins making a whole side nullable - and SalamType(kind, not_null) names the Salam type to read it into (option.Option<T> when it can be null). Codes: SQL0xx lexical, SQL1xx syntax, SQL2xx a dialect rule (? in postgres, AUTO_INCREMENT in sqlite, ILIKE outside postgres, :: outside postgres, LIMIT a, b in postgres, ON DUPLICATE KEY outside mysql, TRUNCATE/PRAGMA in the wrong engine), SQL3xx the schema. Messages are English in both language modes; match on code. Free with FreeReport, FreeCatalog, FreeAnalysis. Putting values in: the three drivers do not all prepare and bind (only db.sqlite has Prepare/Bind*), so NewArgs + AddInt AddFloat AddText AddBool AddNull + Fill(query, args, d, err) writes the values into the SQL properly - the query is lexed first, so a ? inside a string literal is left alone, the count has to match (SQL400), quotes are doubled, backslashes are doubled on MySQL, and a boolean is TRUE/FALSE on postgres and 1/0 elsewhere. It is substitution, not a prepared statement; ArgCount and FreeArgs round it out. Code generation, the sqlc half: annotate a .sql file with -- name: GetUser :one (:one :many :exec :execrows), then ParseQueries(src, err) and Generate(pkg, queries, cat, err) write a row struct and a typed function per query - parameters named and typed from the column each placeholder binds against, nullable columns as option.Option<T> read through IsNull, every query analysed against the catalog first so a typo stops generation with the query's name. One dyn db.Connection signature serves all three engines. The command is salam sql generate queries --schema=db --output=gen/store.salam --package=store. The salam sql check command runs the same checks over .sql files and over every SQL string literal in .salam files (--schema= a file or a directory; without --dialect= each file is read with the engine it imports); a literal assembled from pieces (glued with + or .concat, or one that simply stops mid-statement) is a fragment, so it is counted in the summary and skipped instead of reported.
  • Official packages live in packages/, not std/, and do not ship with Salam: db.sqlite db.postgres db.mysql db.redis db.meilisearch db.elasticsearch db.typesense db.opensearch, audio and every audio.*, tensor autograd nn optim ml data gguf llm onnx npy, net.http.swagger net.http.middleware net.http.session net.http.validate net.http2 net.websocket net.socketio net.email net.proxy, grpc rpc graphql ftp ssh aws google webpush telegram.bot, webview opencv excel. A program that imports one must be in a project (salam init) that ran salam add <name> (for example salam add tensor); otherwise the import fails with that hint. Inside a source checkout they resolve from packages/ directly. Their API notes below still apply.
  • db.redis (RESP over TCP): Connect Ok Error Close Ping Auth Select Set SetEx Get Exists Del Expire TTL Incr IncrBy Decr Append Keys HSet HGet HDel HExists HGetAll LPush RPush LPop RPop LLen LRange SAdd SRem SIsMember SMembers Publish, anything else via Command and Reply*. Failures come back as empty values ("", 0, false); Command shows the server's REPLY_ERROR text. A plain Set removes a key's TTL; Auth sends only the password (an ACL user needs Command); it can publish but not subscribe.
  • db.meilisearch (Meilisearch over HTTP, the whole v1 API): New WithAgent WithTimeout WithMetadata Ok ErrorString Request RequestAs Health IsHealthy GetVersion GetStats CreateIndex GetIndex ListIndexes UpdateIndex RenameIndex DeleteIndex SwapIndexes GetIndexStats CompactIndex ListFields AddDocuments UpdateDocuments AddDocumentsWith AddDocumentsNdjson AddDocumentsCsv AddDocumentsInBatches AddDocumentsOf GetDocument GetDocumentWith GetDocumentAs GetDocuments DocumentsAs DeleteDocument DeleteDocuments DeleteDocumentsByFilter DeleteAllDocuments Search Query HitsAs MultiSearch FederatedSearch FacetSearch SimilarDocuments GetSettings UpdateSettings ResetSettings Get/Update/Reset<Setting> GetTask GetTasks WaitForTask Wait CancelTasks DeleteTasks GetTaskDocuments CompactTasks GetBatch GetBatches CreateKey GetKey GetKeys UpdateKey DeleteKey TenantToken ListWebhooks CreateWebhook GetWebhook UpdateWebhook DeleteWebhook ListSearchRules GetSearchRule UpdateSearchRule DeleteSearchRule DeleteAllSearchRules ListChatWorkspaces GetChatWorkspace DeleteChatWorkspace GetChatSettings UpdateChatSettings ResetChatSettings ChatCompletion SetStderrLogs NetworkControl RenderTemplate. Requests go over net.http raw calls: no timeout unless WithTimeout(c, ms) (then err.code == "timeout"), and a Client is a plain value that threads can share. CompactTasks leaves the server refusing writes until it is restarted. Struct fields have Persian names too (ک.میزبان, خ.کد), and a Persian file must use them. Runnable examples for every part of the API live in tests/en/stdlib/meilisearch/examples and, with Persian names throughout, tests/fa/stdlib/meilisearch/examples. Every call ends with err &: meilisearch.Error (empty code = success, check with Ok(err)). Writes return a TaskInfo and apply later; chain meilisearch.Wait(c, meilisearch.AddDocuments(c, "movies", json, "id", err), err) when the next step needs the change. Hits and documents are raw JSON strings; decode into structs with HitsAs/GetDocumentAs (lenient, extra fields like _formatted are ignored). SearchRequest sends only what you set (-1 numbers, empty strings/lists and false flags are omitted); filter takes an expression or a raw JSON array.
  • db.elasticsearch (Elasticsearch 7/8, Elastic Cloud and OpenSearch over net.http raw calls; no outside library; Distribution(c) tells them apart and SQL/point in time switch endpoints on their own). NewClient(Config) takes several nodes (rotated, a node that refuses connections is skipped for DEAD_NODE_MS, 502/503/504 retried), basic auth, api_key (encoded or id:key), bearer, cloud_id, ca_file/insecure, timeout_ms and keep_alive (one pooled connection per node, on by default); Connect ConnectAuth ConnectApiKey ConnectCloud are shortcuts. Documents and query clauses are raw JSON str. Documents: Index Create Get Exists Update Upsert UpdateScript Delete MGet Count (+...With(Options) for refresh, routing, if_seq_no/if_primary_term). Search: build a SearchRequest { query, size, sort, aggs, ... } from Match Term Terms Range Bool And Or Not Filtered MultiMatch Knn SortDesc and AddAgg(aggs, name, TermsAgg(...)), then Search; read res.hits, AggValue, AggBuckets. Paging: SearchScroll/Scroll/ClearScroll, OpenPit + search_after. Bulk: NewBulk, BulkIndex/BulkUpdate/BulkDelete(b, ...), RunBulk (check res.items when res.errors). Admin: CreateIndex PutMapping Refresh PutAlias SwapAlias Reindex DeleteByQuery UpdateByQuery CatIndices ClusterHealth, pipelines (PutPipeline SimulatePipeline), data streams (CreateDataStream Rollover), PutLifecyclePolicy (ILM, or ISM on OpenSearch), snapshots, cluster settings, background tasks (StartReindex StartUpdateByQuery GetTask), stored scripts and SearchTemplate, SubmitAsyncSearch, Explain ValidateQuery FieldCaps TermVectors IndexStats. SQL: Sql/SqlWith return SqlRows (a db.Rows that pages with the cursor), and Conn(c) is a read-only dyn db.Connection (Exec is -1, no transactions). Anything else: Perform(c, method, path, body). Check Error(c)/ErrorType(c)/Status(c) after a call; a Get of a missing doc is found = false with no error.
  • db.typesense (Typesense over HTTP, 29.x and 30.x): New NewCluster SetNearestNode AddNode AddHeader Free Ok IsNotFound IsConflict ErrorString Request RequestAs Health ServerVersion Status Stats Metrics CreateSnapshot ClearCache CompactDB SchemaChanges UpdateConfig CreateCollection GetCollection CollectionExists ListCollections UpdateCollection AddFields DropFields CloneCollection TruncateCollection DeleteCollection UpsertAlias GetAlias ListAliases DeleteAlias CreateDocument UpsertDocument EmplaceDocument UpdateDocument GetDocument GetDocumentAs DeleteDocument DeleteByFilter UpdateByFilter ImportDocuments ImportJSONL ImportOf ExportDocuments Search SearchRaw MultiSearch UnionSearch HitsAs Highlight CreateKey SearchOnlyKey GetKey ListKeys DeleteKey ScopedSearchKey GenerateScopedSearchKey UpsertSynonymSet UpsertSynonymItem UpsertSynonym UpsertCurationSet UpsertCurationItem UpsertOverride UpsertPreset UpsertSearchPreset UpsertStopwords ImportStemmingDictionary CreateAnalyticsRule SendEvent FlushAnalytics CreateConversationModel CreateNLSearchModel. Every call ends with err &: typesense.Error (empty kind = success, Ok(err); IsNotFound/IsConflict for 404/409). A client holds several nodes and skips one that fails for healthcheck_ms, retrying retries times, so a 5xx or a dead node is not an error until every try failed. Build a schema with Schema { name } plus NewField/FacetField/VectorField/EmbeddingField (Field.kind is the type, sort is 1/-1/0 for on/off/default). SearchParams sends only what you set; anything without a field goes through Set(p, key, value), and a query string over MAX_GET_QUERY bytes goes through /multi_search by itself. Imports answer 200 even when lines fail: check ImportResult.failed. Synonym sets and curation sets are Typesense 30; UpsertSynonym/UpsertOverride are the 29 per collection forms. ScopedSearchKey signs a browser key locally.
  • db.opensearch (OpenSearch over its REST API, native TCP/TLS, no outside library; db.elasticsearch covers the shared core API too, pick this one for PPL, k-NN/neural/hybrid, ISM, the security plugin, alerting, ML Commons or AWS SigV4): connect with Connect ConnectAuth ConnectInsecure ConnectAWS NewClient(Config) (several nodes with retries and dead node skipping, basic/bearer auth, AWS SigV4 for SERVICE_ES/SERVICE_AOSS, ca_file, insecure for a self signed test server). Documents Index IndexWith Create Get GetSource Exists Update UpdateWith Upsert UpdateScript Delete MGet Count DeleteByQuery UpdateByQuery Reindex; bulk NewBulk BulkIndex BulkCreate BulkUpdate BulkUpsert BulkDelete BulkSend BulkFree; search Search SearchWith(SearchRequest) SearchRaw SearchParamsWith MSearch SearchScroll Scroll ClearScroll CreatePit DeletePit SearchTemplate Explain Validate; results Buckets AggValue AggField Suggestions JsonPath; query DSL builders return JSON text (Match Term Terms Range BoolOf(BoolQuery{}) Nested Knn KnnFiltered Neural Hybrid ScriptScore FunctionScore SortDesc Highlight TermsAgg AvgAgg DateHistogramAgg AddAgg SubAggs); admin CreateIndex CreateIndexWith PutMapping PutSettings Refresh PutAlias SwapAlias PutIndexTemplate CreateDataStream Rollover CatIndices ClusterHealth WaitForStatus GetTask PutPipeline SimulatePipeline CreateSnapshot; plugins Sql SqlWith Ppl SqlExplain (JDBC rows that page with the cursor), k-NN KnnIndexBody KnnVectorField KnnWarmup, ISM PutPolicy AddPolicy ExplainPolicy DeleteAfterPolicy, security WhoAmI PutUser PutRole RoleBody PutRoleMapping ChangePassword, PutSearchPipeline NormalizationPipeline, alerting monitors and Ml*; anything else through Perform. Every call records Status Error ErrorType ErrorReason on the client; Conn(c) is a dyn db.Connection whose Query runs SQL (Exec only does SQL DELETE, there are no transactions). Get of a missing id is found = false with an empty error. A bulk delete of a missing id counts as ok.
import db
import db.sqlite

func count(c: dyn db.Connection): int:
    ret db.QueryInt(c, "SELECT COUNT(*) FROM users")
end

func main:
    d := sqlite.Open("app.db")
    defer sqlite.Close(d)
    c := sqlite.Conn(d)
    mut m := db.NewMigrator() defer db.FreeMigrator(m)
    db.AddMigration(m, 1, "users",
        "CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT NOT NULL)",
        "DROP TABLE users")
    db.MigrateUp(m, c)
    println count(c)
end

6. Compiler rules & top pitfalls

Salam's semantic checker is strict. These are the rules that most often turn a naive port into compile errors (each corresponds to a case in tests/en/errors/):

  1. until <cond> loops WHILE the condition is true; there is no while. until i < n: iterates for i from small to n; the C while (v != 0) is until v != 0:, not until v == 0:. This is the single most common porting mistake, and it is silent: an inverted condition runs the body zero times with no diagnostic (only a literal until false: is caught, as E068). See the box in §2.
  2. Unused = error. An unused variable, mut, parameter, import, or function is a hard error. Remove it; a _ prefix silences the check but leaves dead code behind, so keep it for a parameter whose signature is fixed (a callback). Only mark something mut if you actually reassign it.
  3. Reassignment requires mut. x := 1; x = 2 fails; use mut x := 1.
  4. Heap collections must be freed. Vector/HashMap/Set/... allocate; call .free() (idiom: defer v.free() right after creation). Also free elements when they own memory.
  5. Integer / truncates toward zero. Bitwise operators (& | ^ ~ << >>) are supported on integers with C precedence and compound forms (&= |= ^= <<= >>=), but they require integer operands (a bitwise op on a float is a compile error).
  6. No exceptions / no try-catch. Signal failure with a bool return and an err &: out-parameter (the std style), an option.Option<T> (import option), a result.Result<T, E> (import result), or a sentinel value, and check it at the call site.
  7. Privacy. Struct fields/methods and package symbols are private by default; expose with pub. Accessing a private field/method from outside is an error.
  8. Top-level ordering. Within a file: package first, then all imports, then all includes (an import after an include is E108), then top-level const/variable declarations (E084/E085), then types (struct enum type interface impl, E087), then functions. A body-less extern: block ranks with the imports; an export: block ranks with the private functions, so it must come before the first pub func. A top-level if must be a compile-time constant condition (see §8). No if branch may be empty - not at top level, not in a function, and not in an else if. Instead of if X: with an empty body followed by else:, negate the condition: if not X:.
  9. pure functions are checked: they may not write globals, call impure functions, mutate parameters, or print. Only mark a function pure if it is.
  10. match on an enum or a Variant must be exhaustive (or have an else) - both as a statement and as an expression - and use valid type/member patterns. Enum match patterns must be real members. switch is the fallthrough alternative - on an enum it must still cover every member or have an else (E103); it cannot be used on a Variant at all (§2, §3, §12.2).
  11. Enum members are separated by a comma (, or ،) or a newline - on a single line the comma is required, since member names may contain spaces.
  12. Types are checked strictly: no implicit narrowing; use as. Ternary branches must share a type; a condition must be bool. Array-literal length must match the declared size.
  13. Dead code is rejected: an always-false if/until/repeat/each, an unreachable ret, etc. are errors, not warnings.
  14. Useless casts are rejected (E093): x as T where x already has type T, including [1, 2, 3] as int[3].
  15. main returns the exit code. Write ret 1 in main to fail; calling os.Exit inside main is E109 because it skips main's defers. ret alone (or falling off the end) exits with 0. Other functions may still call os.Exit.
  16. No typed declarations. x: T = v is a parse error; write x := v as T (see §2).

When the compiler complains, fix the code; do not try to suppress the check.


7. Translating from other languages

General mapping that applies to all source languages:

Source conceptSalam
classstruct with pub fields + methods (this receiver)
static method / constructorstatic func inside the struct, called as Type.Name(...)
interface / protocol / traitinterface + structural pub methods; add to existing types with impl I on T
inheritance / base classembed with use Base (fields + methods promoted); override by redefining
subtype polymorphismdyn Interface (dynamic) or <T: Interface> (static)
generics / templates<T>, struct Box<T>, func F<T>(…)
dict / map / objectHashMap<K,V> (put/get/has)
list / array / vectorVector<T> (push/get(i)/set/len) or fixed T[n]
setSet<T>
tuple / recordsmall struct, or Pair, or Variant for sum types
string opsstr.* package + + concatenation + len()
exception / errorresult.Result<T, E> + postfix ?, or bool flag / Option<T>; no throw/catch
null / nil / Nonenull (pointers) or Option.None()
lambda / closure(x: int): ret expr end; type func (…) R
enum / unionenum (C-like, or members with data: Circle(r: f64)) or Variant<…>
module / package / importpackage name + import pkg (only pub exported)
free functiontop-level func; a bare name is its address (i64), &fn is a void*
whileuntil (no while keyword exists - same "loop while true" semantics)
switch / caseswitch: bare labels, no case/default, C-style fallthrough (§2, §12.2)
for i in range(n)repeat n in i:
for x in xseach x in xs:
destructor / cleanupdefer x.free()

From PHP

  • $var → plain name; PHP arrays split into Vector<T> (lists) and HashMap<K,V> (assoc arrays); choose per use.
  • class/interface/trait → struct/interface/impl. Visibility public→pub; everything else is private by default.
  • String interpolation "$a-$b" → a + "-" + b or fmt.Sprintf.
  • echo → print/println. Superglobals ($_GET, $_POST) → the net.http package's Query/Form/Body.
  • Exceptions → return bool/Option; dynamic typing → pick concrete types or Variant.

From TypeScript / JavaScript

  • class→struct, interface→interface, enum→enum, generics carry over (Array<T>→Vector<T>, Map→HashMap, Set→Set, object literal→struct or HashMap<str, …>).
  • Arrow functions (x) => x*2 become (x: int): ret x * 2 end (add types; Salam has no =>). Promise/async/await have no equivalent, so use synchronous code, or spawn/join + sync (§8) for real parallelism.
  • let/const→mut/:=+const. null/undefined→null/Option. JSON.parse/stringify→json.*. throw→bool/Option.
  • Truthiness is gone: conditions must be real bool.

From Python

  • class→struct (self→this); __init__ defaults → struct field defaults + literal T { … }. Duck typing → interface + dyn/<T: I>.
  • dict→HashMap, list→Vector, set→Set, tuple→struct/Pair, None→null/Option.None().
  • Comprehensions → an explicit repeat/each loop building a Vector.
  • f"{a}" → a + … or fmt.Sprintf. def→func (add types; Salam is static). try/except→bool/Option. Integer /: Python // == Salam /; Python / (true division) needs float operands.

From C / Go / Rust

  • C: struct maps directly; malloc/free→mem.Allocate/Free; pointers T* and p[0] carry over; call libc directly via extern: (§8). printf works through FFI, but prefer println/fmt.
  • Go: struct+methods→same; interface→interface/dyn; goroutines→ spawn; sync.Mutex/WaitGroup→sync.*; multiple returns → a struct or out-params via pointers; error return → bool/Option; slices → Vector or T[] slices; map→HashMap.
  • Rust: struct→struct, enum with data→enum with data (Circle(r: f64)); trait→interface+ impl … on …; Option/Result→Option/bool; generics + bounds <T: Trait>→<T: Interface>; ownership/Drop→manual defer x.free() (Salam does not borrow-check). Pattern match maps to Salam match.

8. FFI, concurrency, conditional compilation

FFI (call C directly):

link dynamic "sqlite3"                // link an external library (-lsqlite3)
extern:
    func printf(format: str, ...): int   // ... = variadic
    func sqrt(x: f64): f64
    func malloc(size: u64): void*
    func free(ptr: void*)
end
func main:  printf("%d\n", 42)  end

C pointer types (void*, u16*, T*) and null are available for interop.

Exporting Salam functions to C (export:): extern: only declares symbols defined elsewhere (no bodies). A Salam function that C code, a C library callback, or the compiler-generated runtime must reach by a fixed name goes in an export: block. It is emitted under its plain name (no mangling) with external linkage, and it is never dropped as dead code:

export:
    func on_ready(_ctx: void*): int:    // C sees `int on_ready(void* _ctx)`; `_` because C fixes the signature
        ret 0
    end
end
func main:
    cb := (&on_ready) as extern func (void*) int
    println cb(null)
end

Rules: every entry needs a body. No variables, no generics, no .... No pub, inline or noinline: an exported function is always a public, out-of-line C symbol. Only deprecated, pure (checked against the body) and noret may modify it. Persian: درون‌داد: = extern:, برون‌داد: = export: (with ZWNJ or a space). A body inside extern: is an error that points you to export:.

link REQUIRES an explicit kind before the library name - there is no bare link "X" and no @link(...) attribute form, only one way to write this: link dynamic "X" (-lX) vs link static "X" (-l:libX.a on the LLVM-native/JIT toolchain path; falls back to dynamic on the legacy tcc path, since tcc's own linker doesn't support that syntax) vs link framework "X" (macOS only, -framework X). Persian keywords: ایستا/پویا/چارچوب.

Concurrency:

import sync
mut lock := sync.NewMutex()
mut wg := sync.NewWaitGroup()
sync.Add(wg, 1 as i64)
t := spawn(worker)                    // worker takes no arguments
t2 := spawn(work_on, job)             // ... or exactly one POINTER argument
join(t)                               // wait for it
sync.Lock(lock)  /* critical section */  sync.Unlock(lock)
sync.Wait(wg)  sync.Destroy(lock)  sync.DestroyWaitGroup(wg)

spawn(f, arg) is how a thread gets its own data; arg's type must be a pointer, because it travels through the OS's single thread-argument slot. Without it a worker can only reach globals.

Concurrency pitfalls: a zero sync.Mutex {}, atomic.I64 {} or an un-inited chan.Chan {} crashes on first use (create with New.../init, for globals at the top of main); a mutex is not reentrant (locking it twice, or Do inside its own Once, hangs); join every thread exactly once (thread.JoinAll, thread.Collect; a second Join uses freed memory); Destroy... only after the last user is joined; a channel copies a Vector or pointer as a handle, so both threads share it.

Rest of sync: CondVar (NewCondVar/WaitCond/WaitCondTimeout/Signal/ Broadcast; WaitCondTimeout is true when woken, false on timeout - always re-test your predicate in an until loop, wakeups can be spurious), RWMutex (RLock/RUnlock/WLock/WUnlock, writer-preferring), Semaphore (Acquire/TryAcquire/Release), Once (Do(o, (): ... end)), SleepMs, NowMs (unspecified epoch - only differences mean anything). Everything blocks on a condition variable rather than polling.

import chan
import pool
import atomic
import ctx

mut jobs := chan.Chan {} as chan.Chan<i64>
jobs.init(64 as i64)                          // n >= 1 buffered, 0 = rendezvous
jobs.send(v)                                  // blocks while full; false if closed
mut ok := true
v := jobs.recv(ok)                            // ok = false once closed AND drained
v2 := jobs.recv_timeout(ok, 500 as i64)       // ok = false: timed out OR closed
jobs.send_timeout(v, 500 as i64)              // false if it could not be delivered
jobs.close()   jobs.free()                    // also: try_send/try_recv/len/is_closed

pool.ParallelFor(n, 8 as i64, (i: i64): body(i) end)   // self-scheduling workers
pool.ParallelForAuto(n, (i: i64): body(i) end)         // one worker per CPU
pool.CPUCount()

mut hits := atomic.NewI64(0 as i64)
_ := atomic.Add(hits, 1 as i64)               // Load/Store/Swap/CompareAndSwap
mut stop := atomic.NewFlag(false)
if atomic.TestAndSet(stop): /* exactly one caller wins */ end

root := ctx.Background()                      // free the root LAST
job := ctx.WithTimeout(root, 5000 as i64)     // or WithCancel / WithDeadline
until ctx.IsDone(job):  _ := ctx.Wait(job, 100 as i64)  end
ctx.Cancel(job)   ctx.Err(job)                // "" | "cancelled" | "deadline exceeded"
ctx.Free(job)   ctx.Free(root)

init(0) makes a rendezvous channel: send does not return until a receiver takes the value, so the two threads meet instead of queueing. try_send is always false on one - there is no way to know a receiver is parked without waiting.

A ctx tree shares one lock; cancelling any node finishes everything below it, and a child's deadline can only shorten what it inherits, never extend it.

atomic is lock-free everywhere except tcc. atomic_load/store/add/swap/cas are language intrinsics on an i64* cell (a __atomic_* built-in in the C backend, a real atomicrmw/cmpxchg in LLVM, both seqcst; plain reads and writes in JS, which has no threads). atomic_add returns the value _after the add, atomic_swap the value before it. tcc 0.9.27 has neither the __atomic nor the __sync family, so under SALAM_CC_TCC the atomic package falls back to a mutex per cell - same semantics, higher cost, invisible to callers. The bare intrinsics have no such fallback: they are always a __atomic_* built-in, so on a tcc older than 0.9.28 a program that calls them fails to build with a #error naming tools/bash/install-tcc.sh. The pinned tcc (tools/ci/tinycc-ref.txt, 0.9.28rc) has the built-ins, which is why CI is unaffected; note it does not define __ATOMIC_SEQ_CST, so that macro is not a usable feature test - __TINYC__ < 928 is. A declared function of the same name shadows an intrinsic.

There is no async/await.

Conditional compilation (a top-level or inline if on a compile-time constant). Predefined: SALAM_OS_WINDOWS/MAC/LINUX/UNIX/FREEBSD/ANDROID/WASM, SALAM_ARCH_X64/ARM64/X86/ARM/WASM, string forms SALAM_OS/SALAM_ARCH; plus your own -DNAME defines from the build command. The predefined SALAM_* names are also ordinary constant values anywhere else - switch SALAM_OS:, println SALAM_ARCH, is_win := SALAM_OS_WINDOWS, or inside a runtime condition such as if SALAM_OS_MAC and retries > 0:.

if SALAM_OS_WINDOWS:  const SEP := "\\"
else:                 const SEP := "/"
end

In Persian these constants have Persian names, and only those work. Persian has no upper case, so a leading سلام is what marks a name as the language's own rather than yours; the rest of the words follow it separated by spaces, the way multi-word names already are elsewhere:

EnglishPersian
SALAM_OS_WINDOWS / _LINUX / _MAC / _UNIXسلام سیستم ویندوز / لینوکس / مک / یونیکس
SALAM_ARCH_X64 / _ARM64سلام معماری ایکس۶۴ / آرم۶۴
SALAM_OS / SALAM_ARCHسلام سیستم / سلام معماری
SALAM_VERSION / SALAM_GIT_COMMITسلام نگارش / سلام کامیت گیت
defined(X)تعریف‌شده(X)

These names are matched the way Persian keywords are, so a ZWNJ, a space, an Arabic yeh for a Persian one and an Arabic kaf for a keheh are all the same name: سلام اشکال‌زدایی حافظه and سلام اشکال زدایی حافظه both name SALAM_MEM_DEBUG. Write whichever reads better.

اگر سلام سیستم لینوکس:
    سرچاپ سلام سیستم، سلام معماری، سلام نگارش
پایان

The two spellings do not mix, and the error names the one you wanted: SALAM_OS_LINUX in a Persian file reports this file's language writes that built-in constant as سلام سیستم لینوکس, and the reverse in an English file. Which set applies is decided per file, by the same language detection the keywords use, so a Persian program importing English std/ is fine. Your own -DNAME defines are never translated, in either language.

SALAM_JS / سلام جاوااسکریپت is the one flag that is only defined on the JS backend rather than defaulting to false, so test it with defined in either language - اگر تعریف‌شده(سلام جاوااسکریپت): - the same as you would in English. A bare if SALAM_JS: is an unknown identifier on a native build.

Cross-compile by passing an LLVM triple: salam build app.salam --target=x86_64-w64-windows-gnu --output=app.exe (routes through LLVM; link dynamic "user32" → -luser32).


9. Layout DSL (HTML/CSS/JS)

A layout: block (either its own .salam file built with salam page build, or embedded after a general program) declares a UI tree that compiles to HTML + CSS + JS. Elements open with : … end; attributes and style properties are name = value pairs (no commas needed) written inside the element.

//! mode: layout | title: Web page        // optional editor directive
page:
    title = "My Page"
    lang = "english"                       // or "en"
    dir = "left to right"                  // or "ltr"
    header:
        background = "blue"
        color = "white"
        padding = "24px"
        heading: size = 1 content = "Welcome to Salam" font_size = "32px" end
    end
    box:                                   // generic container (div)
        padding = "20px"
        paragraph: content = "Generated from Salam." end
    end
    form:
        id = "contactForm"
        input: id = "name" type = "text" required = true end
        button: type = "submit" content = "Submit" end
    end
end

Elements (std/layout/elements/, one LayoutElement const per element or context variant): every current HTML element, with the full list, Persian names and placement rules in docs/LAYOUT_ELEMENTS.md. Common ones: box (div), paragraph, heading (size = 1..6), span, link (a), image (img), list/ordered list/item, table/row/column, form, input, select/option, text area, button, label, video/audio/source, figure, details/summary, dialog. Every element also accepts its HTML tag name (div, a, img, li, ...), except the table parts that come from context: write header/main/footer and column inside table, never thead/tbody/tfoot/th/td.

The same name can mean different tags by context. Inside table, header, main and footer become <thead>, <tbody> and <tfoot>. column is <th> inside a table header row (or with heading = true) and <td> everywhere else. A plain table: row: column: emits <table><tr><td> with no implicit tbody.

Attributes are scoped per element. The same Salam name maps to the right HTML name for each element: url is href on link/head link and src on image/iframe/script (source, href, src and نشانی are aliases). Global attributes (id, class, title, lang, dir, hidden, tabindex, role, content, repeat), ARIA (aria label ... and any aria <name>), data attributes (data view, or any data <name> which becomes data-<name>), and every on<event> handler work on all elements. All standard CSS properties are registered (background color, z index, ...), with hover/focus/ active/before/after prefixes (hover color = "red"; in Persian اشاره, تمرکز, فشردن, پیش‌از, پس‌از).

CSS values are checked. Almost every property validates its value (keyword, length, number, time, angle, color, ...) and accepts Persian keywords and units: نمایش = "فلکس", فضا = "۱۰ پیکسل", مدت گذار = "0.3 ثانیه". Shorthands are checked too: border = "1 solid red", font = "bold 16/1.5 serif", transition = "opacity 0.3s ease", transform = "rotate(45deg)", box shadow = "0 2 4 black". Bare numbers in a shorthand get px, except zero. A wrong value is E018. Only grid, grid template and offset still pass through unchecked. The full list with status per property is docs/CSS_PROPERTIES.md.

Values are expressions. An attribute value can use constants, operators and parentheses, and a static layout folds it at compile time: box: content = 4 * 10 end prints 40; title = SITE + " | Home" reads a global const; width = WIDE + "px", margin = -5, size = N > 3 ? 2 : 3. Word operators (and, or, not, و, یا) need parentheses: content = (A and B). Unquoted CSS words keep their hyphens (justify content = space-between, background = linear-gradient(red, blue)), so write subtraction with spaces (a - b). A static layout cannot read a variable or call a Salam function (E129); a bad fold (division by zero, overflow, true + 1) is E130. A constant named like a value word (const red := ... then color = red) wins, with W131.

Runtime layouts. layout: is a static page built at compile time. A named layout with typed parameters is a runtime page: the compiler builds the whole document and its CSS at compile time and turns the layout into a pure func Name(params): str that only fills in escaped values. There is no template interpreter. Call it like any function, e.g. from a router handler:

import net.http
import rand

component Card(title: str, n: int):          // typed component: also Card(...) -> str fragment
    box: heading: size = 3 content = title end end
end

layout Home(user: str, lucky: int, items: Vector<str>, admin: bool):
    title = "Hi " + user
    paragraph: content = "Lucky number: " + lucky end
    if lucky > 50:
        paragraph: content = "Big!" end
    else:
        paragraph: content = "Small" end
    end
    list:
        each i, it in items:
            item: content = i + ": " + it end
        end
    end
    input: type = "checkbox" checked = admin end   // bool attribute toggled at runtime
    Card: title = user n = lucky end
end

func home_h(ctx: i64):
    http.Html(ctx, Home(http.Query(ctx, "name"), rand.IntN(100) as int, items(), false))
end

Rules, all checked at compile time:

  • The layout function is pure: no I/O, network, random numbers or global writes inside the UI (E012). Fetch data in the handler and pass it in. Reading a Vector/HashMap parameter is fine (len, is_empty, get, first, last, xs[i], has, get_or, size, each x in xs); a method that changes it (push, pop, set, put, remove, ref) is E012.
  • Text and attribute values are always HTML-escaped; there is no raw-HTML type.
  • A runtime value for a URL attribute must be a web.Url, and a runtime CSS value must be a web.Color, a web.Length or a number (E135). Build them with web.ParseUrl(s, ok), web.ParseColor, web.ParseLength, web.Px(n), web.Rgb(r, g, b). Their fields are private, so they cannot be forged (E017). A runtime CSS value is written as an inline style; class CSS stays static.
  • Enum-like attributes, heading size, hover ... styles and style/script bodies must be known at compile time (E132); pick between fixed variants with if. if/each need a runtime layout (E133). A fixed id inside each would repeat (E134). A runtime font url needs a static type.
  • Typed components can be called directly (Card("x", 1)) and return their own <style> plus HTML, with class names prefixed by the component name.

Fonts. font/قلم declares a web font and emits only a CSS @font-face rule, never an element. It sits directly under page; name and url (source, منبع, نشانی) are required. type (ttf, otf, woff, woff2, ttc) is optional: without it the compiler reads the extension from the URL, then the magic bytes of a local file next to the source. A remote URL with neither is E126, and a type that disagrees with the extension or the file is E127. The compiler never fetches over the network. Optional: weight (300, "100 900"), style (italic), display (swap), range ("U+0600-06FF"). Several space-separated URLs become a fallback chain.

page:
    font: name = "Estedad" url = "fonts/Estedad.woff2" display = swap end
    box: content = "hi" font family = "Estedad, sans-serif" end
end

Safety. URL-typed attributes reject javascript:, vbscript: and any data: that is not image/font/audio/video. CSS values cannot contain { } ; < > or line breaks. A style/script body cannot contain </style or </script. Quoted CSS strings are escaped.

Everything is checked at compile time:

codemeaning
E004 / E117wrong direct parent / missing required ancestor (item outside list)
E005missing required attribute (link and image need url)
E018value has the wrong type (URL, int, uint, float, color, date, MIME type, ...)
E111 / E112unknown element / unknown attribute, with "did you mean"
E113 / E114attribute not allowed on this element / given twice
E115 / E116child not allowed here / forbidden anywhere inside (link in link)
E118 / E119void element with children / element that cannot hold text
E120element allowed only once (main, table header)
E121 / E122for/aria controls point at a missing id / duplicate id
E123a std generator (std/layoutgen) failed to compile or run
E125element must come first (caption in table, legend, summary)
E126 / E127font type cannot be detected / declared type disagrees with the file
W128a local font file is missing next to the source
E129 / E130value not known at compile time / a constant expression that cannot fold
W131a constant hides a layout value word of the same name
E132 / E133value must be static here / if/each in a static layout
E134 / E135fixed id inside each / runtime value of an unsafe type for this attribute
E136one class given different styles on two elements
W124the schema in std/layout itself has a problem (a std bug, not yours)

Void elements print <img ...> (never />). page build exits non-zero and writes no files on any of these errors.

Generators. Most elements use the data-driven default generator. A generator field on an element or on an enum value picks a std generator: a plain Salam function in std/layoutgen that takes a layoutgen.Node and returns a layoutgen.Output { html, css, js, head, shared_css, shared_js }. Example: input: type = "jalali date" end emits <input type="text" class="elm_1"> plus the JS that validates and normalises Jalali dates. The schema format is documented in docs/LAYOUT_SCHEMA.md.

Build: salam page build page.salam → page.html + page.css + page.js; --inline → one self-contained HTML file; multiple files → per-page HTML with merged style.css/script.js.


10. Tooling & verification

salam check app.salam                   # lex, parse and type-check only: the fast loop after every edit
salam exec app.salam                    # run with the interpreter (pure compute, no C toolchain)
salam run app.salam                     # build + run, keep nothing
salam build app.salam --output=app      # native executable (add --keep-temp to inspect the C)
salam obj app.salam                     # object file only
salam build app.salam --target=<triple> # cross-compile via LLVM
salam page build page.salam [--inline]# layout DSL → HTML/CSS/JS
salam format app.salam                  # reformat in place (--check to verify; --lang=fa for Persian)
salam sql check src --schema=db/      # check every SQL query in .salam/.sql files against the schema
salam sql generate q --schema=db/ --output=gen/store.salam   # typed query functions from annotated .sql
salam new name                          # scaffold a project: name/main.salam + salam.toml
salam init                              # same, in the current directory
salam add github.com/owner/repo         # add a GitHub package (import "github.com/owner/repo"); remove/update/deps too
salam install github.com/owner/tool     # build a program into ~/.salam/bin; salam search <word> finds packages
salam update / salam update pkg@latest  # same-major updates by default; uninstall, deps clean too (docs/PACKAGES.md)
salam build --lib[=static]              # .so/.dylib/.dll (or .a) + NAME.h from the entry file's export: block
salam memcheck app.salam                # build with AddressSanitizer and run
salam version                           # print the compiler version (NOT --version)
salam upgrade [X.Y.Z] [--check]         # upgrade salam and std/ to the newest release (or X.Y.Z)
salam inspect app.salam --emit-tokens | --emit-ast | --emit-symbol   # dump a stage as XML

Version check is salam version, a subcommand. There is no --version or -v flag: salam --version fails with unknown command '--version' and suggests salam version.

Always verify a converted program. Run salam check file.salam after every edit (exit 1 on any error, --error-format=json for one JSON object per diagnostic), then salam exec file.salam for a quick check of pure logic; use salam build … --output=… (or salam run) when it uses FFI, threads, or the full stdlib. Resolve every diagnostic; in Salam most are hard errors (§6).

salam exec (the interpreter) is a subset of the compiled language. Some features (notably Variant type-pattern match, and parts of FFI/threads) only work through the C/LLVM backend. If exec reports a runtime error like "undefined variable 'i64'" on a match arm, that feature isn't interpreted: verify it with salam run / salam build instead. Treat the compiled backend as the source of truth, which is also what a self-hosting port targets.


11. When a detail is missing

Read the real code; it is the specification:

  • Idioms & complete programs: tests/en/ (basics/, features/, types/, stdlib/, interop/, apps/, webframework/, games/).
  • Exact stdlib signatures & behavior: std/<pkg>/*.salam (grep -rn "pub func" std/<pkg>).
  • The precise semantic rules (what is and isn't allowed): the .salam cases in tests/en/errors/.

12. Porting C → Salam (and how the compiler itself was self-hosted)

The Salam compiler itself is now self-hosted: its ~45,000 lines of C (formerly compiler/src/) were fully ported to Salam; compiler/ now holds one flat .salam file per former module (compiler/lexer.salam, compiler/semantic.salam, compiler/codegen.salam, …), and compiler/salam is the tracked bootstrap binary that builds new versions of itself from compiler/main.salam. There is no C source left to read, so this section is kept as general guidance for any other large, low-level C→Salam port you're asked to do (the same techniques apply), and §12.4's module map is kept as a historical record of how the compiler's own port was sequenced. Read §1 to §11 first; the notes below are the C-specific deltas and the hard limitations you must plan around.

12.1 Bitwise operators (needed everywhere in a compiler port)

Salam has first-class bitwise operators on integers, mapping 1:1 to C, so the compiler's bit-heavy code (UTF-8 encoding, hashing, flag sets, codegen/print_fmt.c, codegen/codegen_type.c) ports directly with no workarounds:

CSalamnotes
a & ba & bbitwise AND
a | ba | bbitwise OR
a ^ ba ^ bbitwise XOR
~a~abitwise NOT (unary)
a << n / a >> na << n / a >> nshifts (>> is arithmetic on signed, logical on unsigned)
a &= b, |=, ^=, <<=, >>=samecompound assignment

Operands must be integers (a bitwise op on a float is a compile error). Precedence follows C: * / % › + - › << >> › < <= > >= › == != › & › ^ › | › and › or. So flags & MASK == MASK parses as flags & (MASK == MASK), so add parentheses ((flags & MASK) == MASK) exactly as you would in C.

not (Persian وارونه) is a unary prefix that binds tighter than any binary operator, so not a == b means (not a) == b; write not (a == b) to negate a comparison. and/or are و/یا in Persian.

mut flags := 0
flags |= 1 << 3          // set bit 3
flags &= ~(1 << 3)       // clear bit 3
on := (flags >> 2) & 1   // test bit 2
hi := (cp >> 6) & 0x3F   // UTF-8 continuation byte, straight from the C

& is still also reference-parameter (x &: T) and function-address (&fn) in their own syntactic positions; the parser disambiguates by context (prefix &fn vs infix a & b), so there is no ambiguity in practice.

Nested generics work too: Vector<Vector<int>>'s trailing >> is understood as two closing angle brackets, not a shift.

12.2 C construct → Salam

CSalam
typedef struct { … } T;struct T: … end (fields default private → add pub)
union { … } / tagged unionVariant<A, B, …>, narrowed by match on type patterns
enum { A, B=5 }enum E: A, B = 5 end (members on one line need commas; one per line needs none)
#define NAME 3 (const)const NAME := 3
#define MAX(a,b) … (macro fn)inline func Max(a: int, b: int): int: … end
#ifdef / #if / platform #ifdef _WIN32@if-style top-level if SALAM_OS_WINDOWS: on compile-time constants (§8)
#include "x.h" / header+source splitpackage + import; export with pub (no headers)
function pointer int (*f)(int)typed value func (int) int (pass a lambda); or a bare fn/&fn for i64/void* callback tables
void* / char* / T*void* / str or u8* / T*; deref p[0]; null
malloc/calloc/realloc/freemem.Allocate / AllocateZeroed / Reallocate / Free
memcpy/memset/memmovemem.Copy / mem.Set / mem.MemMove
strlen/strcmp/strcpy/strcatstr.Len / str.Compare / str.Clone / str.Concat (+ StringBuilder)
growable array / realloc bufferVector<T> (push/get(i)/set/len), remember .free()
hash table (symbol table)HashMap<K, V>
switch (x) { case … } (fallthrough)switch x: … end - bare labels (no case/default), same fallthrough, break exits it (§2)
switch used for exhaustive/tagged dispatchmatch x: … end instead - exhaustive on enum/Variant, no fallthrough (§3)
gotonot available; restructure with functions/flags/loops
break / continuebreak / continue (innermost loop or switch for break, break N for N levels; continue always targets the loop)
while (c)until c: with the same condition, never negated (while (v != 0) → until v != 0:)
while (v) / while (p) (truthy)until v != 0: / until p != null: (Salam has no truthiness)
do { B } while (c);until true: B if not c: break end end
for (;;)until true: + break
for (i = 0; i < n; i++)repeat n in i: (i = 0 .. n-1)
for (i = n; i >= 1; i--)repeat n to 1 in i: (descending; guard n >= 1, see §2)
for (i = 0; i < n; i += 2)repeat 0 to n - 1 by 2 in i: (by is always positive)
variadic f(int, ...)only in extern/FFI; pure Salam passes a Vector
static file-localdefault (package-private); top level, not pub
static/global mutable statetop-level mut globals are allowed (mut g_count := 0)
const T xconst NAME := … (compile-time) or an immutable := binding
errno / return-code error handlingbool / Option<T> / sentinel + an error record/struct
assert()import testing (AssertTrue, …) or an explicit if … : print + os.Exit
int8_t … uint64_t, size_t, ssize_ti8…i64, u8…u64, usize for size_t, size for ssize_t/intptr_t/ptrdiff_t
bitfields / flag enumsbitwise ops on an integer (flags & MASK, §12.1), or a small struct of bools

12.3 Building blocks the compiler needs (all in Salam today)

  • Reading source: io.ReadFile(path) → str; scan bytes/codepoints with str.CharAt/str.Chars/str.Len; classify with str.IsDigit/IsAlpha/IsSpace.
  • Buffers/output: str.StringBuilder (NewBuilder, BufAppend, BufAppendInt, BufStr, BufFree) for emitting generated C / IR.
  • Tables & lists: HashMap (symbol tables, string interning), Vector (token streams, AST child lists, scope stacks), Set, Stack.
  • AST nodes: a struct per node kind, or one node struct with a kind enum field plus a Variant payload; walk with recursion and match.
  • Arenas / lifetime: mem.Allocate/Free; or lean on Vector/HashMap ownership + defer x.free(). There is no borrow checker, so free deliberately.
  • Diagnostics: return a bool/Option up the call chain and collect an error list (mirror src/diag); no exceptions to unwind.

12.4 The compiler's module map (recreate this shape)

The C pipeline is source → lexer → token → parser → AST → semantic → codegen (with llvm and jsgen as alternative backends), all wired by driver. Approximate sizes (from compiler/src/, ~45k LOC total); port roughly in dependency order:

ModuleLOCRole
core1.1kshared utilities (arena, strings, containers)
source / token0.1k / 0.4ksource buffer; token kinds & values
lexer1.8ktext → tokens (numbers, strings, idents, operators, layout, trivia)
ast0.6kAST node types
parser2.7ktokens → AST (decls, exprs, FFI, layout)
semantic8.2kname resolution, type checking, the strict rules in §6, const-fold
condcomp0.6kconditional compilation (SALAM_OS_*, -D)
codegen4.8kAST → C (the default backend)
llvm5.7kAST → LLVM IR (cross-compile backend)
jsgen / layout / minify / web2.6k / 2.5k / 0.3k / 0.4kJS + layout-DSL → HTML/CSS/JS
interp3.5ktree-walking interpreter (salam exec)
driver / cli4.6k / 0.7kcommand dispatch, build orchestration
diag / logger / xml0.8k / 0.3k / 0.2kerrors, logging, --emit-tokens/-ast/-symbol
i18n / langpack1.6k / 0.4kEnglish/Persian keyword & symbol tables
fmt1.0ksource formatter (salam format)

Suggested port order: core → token/source → lexer → ast → parser → semantic → one backend (codegen C) → driver/cli, then the remaining backends and tools. Port module-by-module, keeping the existing C build runnable, and validate each stage against the current compiler's --emit-tokens / --emit-ast / --emit-symbol output and the tests/ suite. The bit-heavy code (lexer, hasher, codegen) ports directly now that bitwise operators exist (§12.1).


Part II: Persian Salam (سلام فارسی)

Everything in Part I holds for Persian source: same grammar, same types, same strict rules, same stdlib. Only the spelling changes. This part is the full spelling reference plus the few things that behave differently because the source is Persian. The human-facing version of this material is the Persian course at https://www.salamlang.ir/learn/ (§19 maps its lessons).

13. Persian source basics

  • Language detection is automatic from the keywords in the file; pass --lang=fa to force it (salam build app.salam --lang=fa). Diagnostics are printed in Persian for Persian files.
  • Language directive: a Persian file may state its language with // زبان: فارسی in its first 8 lines; an English file uses // lang: en. Mixed forms such as // lang: fa or // زبان: en are a compile error.
  • The entry function is ریشه, not main: تابع ریشه: ... پایان. Its return value is the exit code, as in English (برگشت ۱ to fail).
  • Digits: Persian ۰۱۲۳۴۵۶۷۸۹ and Arabic-Indic ٠١٢٣٤٥٦٧٨٩ digits work in number literals, mixed freely with ASCII. The decimal point is always . (۳.۱۴); the Persian decimal separator ٫ is not accepted.
  • Printed numbers and booleans are ASCII: سرچاپ ۱۲ prints 12, and a منطقی prints true/false. Convert digits yourself if the output must be Persian.
  • Separators: the Persian comma ، works everywhere the ASCII , does (arguments, parameters, enum members, array literals, struct literals, match patterns). A new line also separates call arguments and parameters.
  • Names: identifiers may be Persian and may contain spaces or ZWNJ (تابع جمع دو عدد(...), تکه‌ها). A space and a ZWNJ (U+200C) are the same inside a name, so آرگومان ها and آرگومان‌ها are one name. Arabic ي/ك equal Persian ی/ک.
  • Keywords with ZWNJ (نادرست‌چاپ, درون‌داد, بی‌کاره, ...) may be written with a ZWNJ or a space, but not glued together: نادرستسرچاپ is an unknown identifier.
  • Strings are UTF-8 bytes, as in English: "سلام".طول() is 8 (bytes); use .شمارنویسه() for the letter count (4).
  • salam translate fa file.salam rewrites English source to Persian (translate en goes back): keywords, true/false/null/this, the entry function, primitive type names, and the built-in methods of §15. Comparisons come out as words in Persian (بزرگتر, برابر, ...) and as symbols in English (>, ==, ...); generic <T> keeps its brackets.

14. Persian keywords

EnglishPersianEnglishPersian
funcتابعretبرگشت
ifاگرelseوگرنه
until (while)تاrepeatتکرار
to (in repeat)تاby (step)هر
eachهرinدر
matchهمخوانswitchترابرد
breakبشکنcontinueگذر
mutناپایاconstپایا
typeگونهstructساختار
enumجداشمارinterfaceمیانجی
implکاربستon (impl X on T)بر
endپایانasبرگردان
importواردسازیincludeفراخوانی
packageبستهpubهمگانی
true / falseدرست / نادرستnullپوچ
thisاینdeferدیرکن
print / printlnچاپ / سرچاپprinterr / printerrlnنادرست‌چاپ / نادرست‌سرچاپ
inputورودیexternدرون‌داد
exportبرون‌دادpageصفحه
componentبخشinline / noinlineدرخط / نادرخط
pureنابnoretنابرگشت
deprecatedبی‌کارهand / or / notو / یا / وارونه
eq / neqبرابر / نابرابرmain (entry)ریشه
lt / gtکوچکتر / بزرگترlte / gteکوچکتربرابر / بزرگتربرابر

Context words (keywords only in their position, usable as names elsewhere): static ایستا (ایستا تابع, پیوند ایستا), dynamic پویا (پیوند پویا), dyn پویا (before a type: پویا شکل), link پیوند, framework چارچوب, use (struct embedding) شامل. page صفحه is a keyword only at the start of a line that ends with : (page:, صفحه خوشامد(نام: رشته):); elsewhere the word is a normal name. mut func is ناپایا تابع.

The built-in functions have Persian names, and a Persian file must use them - the English spelling is an error (E001):

EnglishPersian
lenطول
sizeofاندازه‌گونه
argsآرگومان‌ها
envمتغیر‌محیطی
langزبان
openبازکردن
listdirفهرست‌پوشه
hashدرهم
char_codeکدنویسه
char_from_codeنویسه‌ازکد
strcmpمقایسه‌رشته
spawnنخ‌ساز
joinنخ‌پیوند
callhandlerفراخوان‌دستگیره
atomic_loadاتمی‌بخوان
atomic_storeاتمی‌بنویس
atomic_addاتمی‌بیفزا
atomic_swapاتمی‌جابجا
atomic_casاتمی‌مقایسه

Stay English in Persian files: the compile-time constants (SALAM_OS, SALAM_OS_WINDOWS, ...) and C names declared in درون‌داد.

Two Persian words have two meanings, told apart by position:

  • تا is until at the start of a statement and to inside a تکرار header: تا ک < ۳: loops while ک < ۳; تکرار ۱ تا ۵ در ای: counts 1..5. Persian تا reads naturally as "while", so the §2 polarity trap is less likely, but the rule is the same: the loop runs while the condition holds.
  • هر is each at the start of a statement and by (the step) inside a تکرار header: هر ع در لیست: vs تکرار ۰ تا ۲۰ هر ۲ در ای:.

و is a reserved word (and), so it can never be a name; و۱ and وکتور are fine.

15. Persian types, built-in methods and std package names

Types:

EnglishPersianEnglishPersian
voidتهیboolمنطقی
charنویسهucharیونیکد
strرشتهint (i32)صحیح (صحیح۳۲)
i8 i16 i64صحیح۸ صحیح۱۶ صحیح۶۴uint (u32)طبیعی (طبیعی۳۲)
u8 u16 u64طبیعی۸ طبیعی۱۶ طبیعی۶۴usize / sizeاندازه مثبت / اندازه
float (f32)اعشار (اعشار۳۲)f64اعشار۶۴
Vector<T>وکتور<T>HashMap<K, V>نگاشت<K, V>
MapIterپیمایشگرنگاشتFileپرونده
Variant<...>گوناگون<...>

Type digits may be Persian or ASCII (صحیح۶۴ = صحیح64). Note that اعشار is f32; a float literal such as ۲.۵ is اعشار۶۴, so write اعشار۶۴ for ordinary floating point.

Built-in methods (on str, Vector, HashMap, File and iterators):

EnglishPersianEnglishPersian
pushبیفزاpopدربیاور
getبگیرrefارجاع
setبنشانlenطول
capظرفیتfreeآزادکن
putدرجhasدارد
removeحذفsizeاندازه
iterپیمایشhas_nextداردبعدی
keyکلیدvalueمقدار
nextبعدیreadخواندن
readlineخواندن خطwriteنوشتن
seekجابجاییcloseببند
concatپیوستsubstrزیررشته
findبیابsplitبشکاف
trimپیراستto_intبه صحیح
to_floatبه اعشارchar_countشمارنویسه
char_atنویسه شمارهchar_substrزیررشته نویسه
char_findبیاب نویسهstarts_withشروع با
ends_withختم باincludesدربردارد
copyرونوشتdeep_copyرونوشت عمیق
bytesبایت ها

The free built-in len(x) (arrays, slices and str only) is spelled طول(x) in a Persian file: len(x) there is error E001 ("identifier must be Persian in a Persian file"). The method form is x.طول() and works on a وکتور too, which the free form does not.

Std packages and their functions have Persian names declared with @fa "..." next to @en "..." in std/. Import by the Persian name and call through it; never guess a name, read the @fa line in std/<pkg>/:

واردسازی رشته
واردسازی ریاضی
واردسازی سیستم عامل

تابع ریشه:
    سرچاپ رشته.طول("سلام")، ریاضی.جذر(۱۶.۰)
    سرچاپ سیستم عامل.آرگومان‌ها().طول()
پایان

Common packages: str رشته, math ریاضی, os سیستم عامل, io ورودی خروجی, fmt قالب بندی, conv تبدیل, time زمان, rand تصادفی, sort مرتب سازی, json جیسون, regex عبارت باقاعده, collections مجموعه ها, mem حافظه, path مسیر, fs سیستم پرونده, file پرونده, dir پوشه, sync همگام سازی, thread نخ, chan کانال, testing آزمایش, template قالب, log گزارش, result نتیجه, option اختیاری, crypto رمزنگاری, bigint عدد بزرگ, net شبکه, net/http اچ تی تی پی (imported as شبکه.اچ تی تی پی), web وب, db دیتابیس, db/typesense تایپسنس (imported as دیتابیس.تایپسنس), db/meilisearch میلیسرچ (imported as دیتابیس.میلیسرچ), db/opensearch اپن سرچ (imported as دیتابیس.اپن سرچ), dom دام, term پایانه, cli خط فرمان, audio صدا, audio/wav ویو (imported as صدا.ویو), audio/dsp پردازش سیگنال (imported as صدا.پردازش سیگنال), audio/codec کدک (imported as صدا.کدک), audio/flac فلاک, audio/mp3 ام‌پی‌تری, audio/aiff ایف, audio/au ای‌یو, audio/tag برچسب صدا, audio/music موسیقی, audio/synth صداساز, audio/midi میدی (each imported under صدا.).

A few function names, to show the style: str.Len طول, str.Split تفکیک کردن, str.Join بپیوند, str.Trim پیرایش, str.Replace جایگزینی, str.Contains شامل است, str.ToInt تبدیل به عدد, str.FromInt از عدد, math.Sqrt جذر, math.Abs قدرمطلق, math.Pow توان, math.Min / Max کمینه / بیشینه, os.Args آرگومان ها. English std names are rejected in a Persian file (رشته.Len(...) is E001 "identifier must be Persian in a Persian file"), so look the Persian name up instead of guessing it.

Give your own pub API both spellings the same way:

@en "Twice"
@fa "دوبار"
همگانی تابع دوبار(ع: صحیح): صحیح:
    برگشت ع * ۲
پایان

16. Persian syntax crib

پایا بیشینه := ۱۰                        // const (one word)
ناپایا شمار := ۰                         // mut global

ساختار نقطه:
    همگانی ایکس: صحیح = ۰
    همگانی ایگرگ: صحیح = ۰
    همگانی تابع جمع(): صحیح:
        برگشت این.ایکس + این.ایگرگ
    پایان
پایان

جداشمار رنگ: قرمز، سبز، آبی پایان

تابع دوبرابر(ن: صحیح): صحیح:
    برگشت ن * ۲
پایان

تابع ریشه:
    نام := "سارا"                        // immutable
    ناپایا ک := ۰                        // mutable
    ع := ۲.۵ برگردان اعشار۶۴             // cast with برگردان
    اگر ک > ۱۰ و ک < ۲۰:
        سرچاپ "بین"
    وگرنه ک == ۰:                        // else-if
        سرچاپ "صفر"
    وگرنه:
        سرچاپ "دیگر"
    پایان
    تا ک < ۳:                            // while
        ک += ۱
    پایان
    تکرار ۳:                             // three times
        چاپ "*"
    پایان
    تکرار ۱ تا ۵ در ای:                  // 1..5 inclusive
        چاپ ای، ""
    پایان
    تکرار ۰ تا ۲۰ هر ۵ در ای:            // with a step
        چاپ ای، ""
    پایان
    آ := [۱۰، ۲۰]
    هر ش، م در آ:                      // index and value
        سرچاپ ش، م
    پایان
    ن := نقطه { ایکس = ۳، ایگرگ = ۴ }
    سرچاپ ن.جمع()، دوبرابر(ن.ایکس)
    متن := همخوان رنگ.سبز:               // match: bare member names
        قرمز، آبی: "گرم یا سرد" پایان
        سبز: "سبز" پایان
    پایان
    ترابرد ک:                            // switch: each label has its own پایان
        ۳:
            سرچاپ "سه"
            بشکن
        پایان
        وگرنه:
            سرچاپ "?"
        پایان
    پایان
    سرچاپ نام، ع، متن
پایان

More forms, each checked with the current compiler:

// collections
ناپایا و۱ := وکتور {} برگردان وکتور<صحیح>
دیرکن و۱.آزادکن()
و۱.بیفزا(۵)
سرچاپ و۱.بگیر(۰)، و۱.طول()
ناپایا نگ := نگاشت {} برگردان نگاشت<رشته، صحیح>
دیرکن نگ.آزادکن()
نگ.درج("الف"، ۱)
هر کلید، مقدار در نگ:
    سرچاپ کلید، مقدار
پایان

// lambdas: no return type, captured by value
دوبرابر := (ع: صحیح): برگشت ع * ۲ پایان
رده := (نمره: صحیح):
    برگشت نمره >= ۱۰ ? "قبول" : "مردود"
پایان
تابع به‌کاربردن(ر: تابع (صحیح) صحیح، مقدار: صحیح): صحیح:
    برگشت ر(مقدار)
پایان

// guards use اگر
جداشمار شکل:
    دایره(شعاع: اعشار۶۴)
    مستطیل(پهنا: اعشار۶۴، بلندی: اعشار۶۴)
پایان
تابع مساحت(ش: شکل): اعشار۶۴:
    برگشت همخوان ش:
        دایره(ر): ۳.۱۴ * ر * ر پایان
        مستطیل(پ، ب) اگر پ == ب: پ * پ پایان
        مستطیل(پ، ب): پ * ب پایان
    پایان
پایان

// switch on true replaces an if chain
ترابرد درست:
    ک < ۰:
        سرچاپ "منفی"
        بشکن
    پایان
    ک == ۳ یا ک == ۴:
        سرچاپ "سه یا چهار"
        بشکن
    پایان
پایان

// interfaces, impl on a built-in type, generics, پویا (dyn)
میانجی رتبه‌دار:
    تابع رتبه(): صحیح
پایان
کاربست رتبه‌دار بر رشته:
    تابع رتبه(): صحیح: برگشت طول(این) پایان
پایان
تابع بالاتر<ت: رتبه‌دار>(الف: ت، ب: ت): صحیح:
    اگر الف.رتبه() > ب.رتبه():
        برگشت الف.رتبه()
    پایان
    برگشت ب.رتبه()
پایان
تابع توصیف(ش: پویا شکل‌دار):             // پویا is dyn
    سرچاپ ش.نام()
پایان

// static members, mut methods, embedding
ساختار شمارنده:
    ن: صحیح = ۰
    همگانی پایا سقف := ۱۰۰
    همگانی ایستا تابع تازه(آغاز: صحیح): شمارنده:
        برگشت شمارنده { ن = آغاز }
    پایان
    همگانی ناپایا تابع بیفزای():
        این.ن += ۱
    پایان
پایان
ساختار سگ:
    همگانی شامل جانور                     // embeds جانور's fields and methods
    همگانی نژاد: رشته = ""
پایان

// Variant
تابع شرح(م: گوناگون<صحیح، رشته>): رشته:
    برگشت همخوان م:
        صحیح ع: "عدد " + ع پایان
        رشته ر: "متن " + ر پایان
    پایان
پایان

// reference parameter, defer, pointers
تابع واریز(ح &: حساب، مبلغ: صحیح):
    ح.موجودی += مبلغ
پایان
دیرکن سرچاپ "پاکسازی"
پ := پوچ برگردان صحیح*

// packages: بسته in the library file, فراخوانی in the user
بسته ابزار
همگانی تابع چهاربرابر(ع: صحیح): صحیح:
    برگشت ع * ۴
پایان
// ...and in the program:
فراخوانی ابزار "ابزار.salam"
سرچاپ ابزار.چهاربرابر(۵)

// C functions and compile-time branches
درون‌داد:
    تابع sqrt(x: اعشار۶۴): اعشار۶۴
پایان
پیوند پویا "sqlite3"
اگر SALAM_OS_WINDOWS:
    پایا جداکننده := "\\"
وگرنه:
    پایا جداکننده := "/"
پایان

// threads: spawn and join stay English
ر := spawn(کارگر)
join(ر)

17. Rules and traps specific to Persian

All of §6 applies. In addition:

  1. Entry is ریشه. A Persian file with تابع main has no entry point.
  2. «…» is a string literal, in Persian files only. It behaves exactly like "…", including \n escapes and +, so pick whichever reads better: سرچاپ «سلام دنیا». It does not nest, for the same reason " does not. In an English file « is not a delimiter at all. Leaving one open reports its own error rather than the double-quote one, since the fix is different: رشته‌ی '«' بسته نشده است؛ آن را با '»' ببندید، نه با دابل‌کوتیشن. A » with no « before it is its own error too, rather than being swallowed into the identifier that follows it. Text blocks (```, §6) work the same in a Persian file, and the code inside ${{ }} uses Persian names: ${{ نام }}, ${{ عدد + 1 }}, ${{ «}}» }}.
  3. The language's own constants are Persian here (سلام سیستم ویندوز, سلام نگارش, تعریف‌شده(…)); the SALAM_* spellings do not work in a Persian file. See §8.
  4. Enum patterns in همخوان are bare member names (سبز:), not رنگ.سبز:; the qualified form is a parse error in a pattern.
  5. ترابرد labels are blocks: each label ends with its own پایان, and fallthrough continues into the next label unless you بشکن.
  6. و is reserved and cannot be a name; pick و۱, واحد, ...
  7. No ٫ decimal separator; write ۱۲.۵.
  8. Output digits are ASCII and booleans print as true/false.
  9. اعشار is f32. Use اعشار۶۴ unless you want single precision (۰.۱ برگردان اعشار prints 0.10000000149011612). A float literal is already اعشار۶۴: casting a literal is allowed, but casting a variable to the type it already has is a useless cast (E093).
  10. No typed declarations, as in English: ک: صحیح = ۰ is a parse error; write ک := ۰ or ک := ۰ برگردان صحیح۶۴.
  11. Top-level order is the same (§6 rule 8): بسته, واردسازی, فراخوانی, پایا/ناپایا globals, then ساختار/جداشمار/گونه/ میانجی/کاربست, then تابعs, private before همگانی.
  12. پایا names are one word: پایا حد بالا := ۳ is a parse error; use حدبالا or حد_بالا.
  13. Unknown Persian std name? Read the @fa line in std/<pkg>/*.salam. The English name is not a fallback in a Persian file, and an invented translation will not resolve.
  14. Diagnostics are Persian. The error codes (E001, E087, ...) are the same as in English, so search tests/en/errors/ by code.

18. Complete Persian programs

A command-line program with a struct, a vector, a map and a match:

واردسازی رشته

جداشمار سطح: کم، متوسط، زیاد پایان

ساختار دانشجو:
    همگانی نام: رشته = ""
    همگانی نمره: صحیح = ۰
پایان

تابع سطح از(نمره: صحیح): سطح:
    اگر نمره >= ۱۷:
        برگشت سطح.زیاد
    وگرنه نمره >= ۱۲:
        برگشت سطح.متوسط
    پایان
    برگشت سطح.کم
پایان

تابع برچسب(س: سطح): رشته:
    برگشت همخوان س:
        کم: "ضعیف" پایان
        متوسط: "خوب" پایان
        زیاد: "عالی" پایان
    پایان
پایان

تابع ریشه:
    ناپایا کلاس := وکتور {} برگردان وکتور<دانشجو>
    دیرکن کلاس.آزادکن()
    کلاس.بیفزا(دانشجو { نام = "سارا"، نمره = ۱۹ })
    کلاس.بیفزا(دانشجو { نام = "علی"، نمره = ۱۳ })
    کلاس.بیفزا(دانشجو { نام = "رضا"، نمره = ۹ })

    ناپایا شمار := نگاشت {} برگردان نگاشت<رشته، صحیح>
    دیرکن شمار.آزادکن()
    ناپایا جمع := ۰
    هر د در کلاس:
        ب := برچسب(سطح از(د.نمره))
        سرچاپ د.نام، د.نمره، ب
        جمع += د.نمره
        قبلی := شمار.دارد(ب) ? شمار.بگیر(ب) : ۰
        شمار.درج(ب، قبلی + ۱)
    پایان
    سرچاپ "میانگین:"، جمع / کلاس.طول()
    سرچاپ "عالی‌ها:"، شمار.بگیر("عالی")
    سرچاپ رشته.طول("پایان")
پایان

The same program in English is a direct keyword-for-keyword translation; salam translate en produces it.

19. The Persian tutorial on salamlang.ir

The course at https://www.salamlang.ir/learn/ teaches the whole language in Persian, one lesson per page, and every example on it is compiled and run when the site is built. Point Persian-speaking users to the matching lesson:

TopicLesson
First program, چاپ/سرچاپ, comments, ورودیhttps://www.salamlang.ir/learn/start/
Variables, ناپایا, پایا, globalshttps://www.salamlang.ir/learn/variables/
Types, integer sizes, casts with برگردانhttps://www.salamlang.ir/learn/types/
Operators, bitwise, ternary, و/یا/وارونهhttps://www.salamlang.ir/learn/operators/
Strings and their methodshttps://www.salamlang.ir/learn/strings/
اگر/وگرنهhttps://www.salamlang.ir/learn/conditions/
تا, تکرار, هر, بشکن, گذرhttps://www.salamlang.ir/learn/loops/
همخوان and ترابردhttps://www.salamlang.ir/learn/match/
Functions, defaults, overloads, multi-word nameshttps://www.salamlang.ir/learn/functions/
Lambdas and function typeshttps://www.salamlang.ir/learn/lambdas/
دیرکن, ناب, درخط and other modifiershttps://www.salamlang.ir/learn/defer/
Arrays and sliceshttps://www.salamlang.ir/learn/arrays/
وکتور and نگاشتhttps://www.salamlang.ir/learn/collections/
Structs, methods, ایستا, شاملhttps://www.salamlang.ir/learn/structs/
Enums, enums with datahttps://www.salamlang.ir/learn/enums/
گونه, new types, operator overloadinghttps://www.salamlang.ir/learn/custom-types/
Generics, میانجی, کاربست, پویاhttps://www.salamlang.ir/learn/generics/
بسته, واردسازی, فراخوانیhttps://www.salamlang.ir/learn/packages/
Compiler rules and error codeshttps://www.salamlang.ir/learn/rules/
Values, references, memoryhttps://www.salamlang.ir/learn/memory/
Compile-time اگرhttps://www.salamlang.ir/learn/compile-time/
C interop, درون‌داد, پیوندhttps://www.salamlang.ir/learn/c-interop/
spawn/joinhttps://www.salamlang.ir/learn/threads/
Layout DSL صفحهhttps://www.salamlang.ir/learn/layout/
Full Persian/English glossaryhttps://www.salamlang.ir/learn/keywords/

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

salam

무료

Use when writing, reading, debugging or reviewing Salam source (.salam files) - the Salam programming language with English/Persian keywords. Covers its enforced declaration ordering, the `until` = `while` rule (there is no `while`), `switch` vs `match`, the enum comma requirement, stdlib lookup, and the toolchain traps that silently produce wrong behaviour. Triggers on .salam files, "Salam", "salamlang", or any request to compile, check or run Salam code.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

SalamLang/Salam642026년 10월 25일 업데이트

SalamLang의 스킬 모두 보기

이 스킬의 문제 신고