devabhasha
v1.0.0
Published
देवभाषा — a Sanskrit programming language for the web that transpiles to JavaScript
Maintainers
Readme
देवभाषा · Devabhāṣā
A small Sanskrit programming language that transpiles to JavaScript, with a web/DOM layer and a zero-build browser playground. Source is written in Devanagari; output is readable JavaScript.
कार्य अभिवादनम् (नाम) { function abhivaadanam(naama) {
फलम् "नमस्ते, " + नाम। → return ("namaste, " + naama);
} }Quick start
node src/cli.js run examples/namaste.deva # compile + execute (Node)
node src/cli.js build examples/ganaka.deva # → examples/ganaka.js
node build-playground.js # → playground/index.html
node test/test.js # run the test suiteOpen playground/index.html in any browser — no server needed.
Language reference
| Sanskrit | Meaning | Compiles to |
|----------|---------|-------------|
| चर / नियत | varies / fixed | let / const |
| कार्य | work to be done | function (statement or expression) |
| फलम् | fruit / result | return |
| यदि / अन्यथा | if / otherwise | if / else |
| यावत् | as long as | while |
| प्रत्येकम् (x : समूह) | for each | for (const x of …) |
| भङ्ग / अनुवृत्तम् | break / continue | break / continue |
| सत्यम् / असत्यम् / शून्यम् | true / false / void | true / false / null |
| दर्शय(…) | cause to show | console.log(…) |
| अङ्गम्(tag, …) | limb / part | document.createElement + children |
| योजय(node[, target]) | join / attach | append to DOM |
| श्रोता(node, ev, fn) | listener | addEventListener |
- Statements end with the danda
।or;(both optional before}/EOF). - Comments begin with
#. - Numbers may use Devanagari digits (
०१२…) or ASCII. - Identifiers may be Devanagari; they're transliterated to stable ASCII in the
output (
गणना→gannanaa), so the JS is portable and debuggable.
कारक — Pāṇinian case-role construction
The signature feature. Sanskrit marks a word's grammatical role with its
vibhakti (case ending), not its position. Devabhāṣā uses this for
order-free DOM construction via the verb रचय (racaya, "construct").
रचय पटः वाक्यम् "वर्धय" स्पर्शाय करणेन वर्धक।
│ │ │ │
│ │ │ └ करण/instrumental → handler
│ │ └ सम्प्रदान/dative → event ("click")
│ └ कर्म/accusative → content
└ कर्तृ/nominative → tag (<button>)Each argument's role comes from its ending, so the arguments may be written in any order and compile to identical JavaScript:
__DB.construct({ tag: "button", content: "वर्धय", event: "click", handler: वर्धक })The seven recognized cases and their kāraka roles:
| Case (vibhakti) | Kāraka | DOM slot |
|-----------------|--------|----------|
| प्रथमा nominative -ः | कर्तृ agent | element tag |
| द्वितीया accusative -म् | कर्म patient | content |
| तृतीया instrumental -ेन | करण instrument | handler |
| चतुर्थी dative -ाय | सम्प्रदान recipient | event |
| पञ्चमी ablative -ात् | अपादान source | data source |
| षष्ठी genitive -स्य | सम्बन्ध relation | attribute |
| सप्तमी locative -े | अधिकरण locus | mount parent |
Scope (honest): the case engine (src/vibhakti.js) implements the
a-stem (अकारान्त) singular paradigm — the most common declension, covering
most coined technical vocabulary. Other stem-classes and genders extend it
by adding rows to the paradigm table. ASCII words and uninflected stems are
correctly not treated as case-marked, which is what keeps free word order
parseable.
Tag and event vocabulary lives in src/karaka-web.js (पट→button,
शीर्ष→h1, स्पर्श→click, …).
समास — compound composition (nested DOM trees)
Where kāraka handles one element's roles, समास (samāsa, "compound") handles how elements nest — the structural counterpart. A block form
रचय <tag-nominative> { ...children... }is a तत्पुरुष (container) whose body is a द्वन्द्व (sibling list).
It composes recursively, so DOM trees of any depth are just nested रचय
blocks. Container tags use the nominative (मूलः=div, सूचीः=ul); content
uses the accusative marker वाक्यम्:
चर पृष्ठम् = रचय मूलः {
रचय शीर्षः वाक्यम् "देवभाषा"।
रचय वाक्यः वाक्यम् "संस्कृतेन रचितम्।"।
रचय सूचीः {
रचय पङ्क्तिः वाक्यम् "प्रथमम्"।
रचय पङ्क्तिः वाक्यम् "द्वितीयम्"।
}
}।
योजय(पृष्ठम्)।compiles to nested __DB.construct({ tag, children: [...] }) and renders:
<div><h1>देवभाषा</h1><p>संस्कृतेन रचितम्।</p>
<ul><li>प्रथमम्</li><li>द्वितीयम्</li></ul></div>kāraka and समās compose: a child element can carry its own case-marked
slots (an event handler, attributes) while sitting in the tree. Children
attach by DOM append, which moves nodes, so the model stays correct
whether or not a child was auto-mounted.
रूप — CSS-in-Sanskrit styling
रूप (rūpa, "form/appearance") attaches styles to an element, with both
property names and value words in Sanskrit:
रचय पटः वाक्यम् "स्पृश" रूप {
वर्णः: श्वेतः, # color: white
पृष्ठभूमिः: रक्तः, # backgroundColor: crimson
अन्तरालः: "10px 20px", # padding (quoted CSS passes through)
कोणवृत्तिः: "6px", # borderRadius
सीमा: अदृश्यम् # border: none
}।The vocabulary (src/style.js) covers color (वर्णः), background
(पृष्ठभूमिः), typography (अक्षरमानम्/font-size, अक्षरभारः/font-weight,
संरेखणम्/text-align), the box model (अन्तरालः/padding,
बाह्यान्तरः/margin, कोणवृत्तिः/border-radius), and fl/layout
(प्रदर्शनम्/display, दिक्/flex-direction, न्यायः/justify). Value words
include colors (रक्तः→crimson, नीलः→navy, केसरः→#F4C430) and keywords
(केन्द्रम्→center, प्रवाहः→flex, गुरुः→bold).
Resolution rules: a known Sanskrit value word becomes a CSS literal; a
bare variable stays a reference (वर्णः: मुख्यवर्णः reads the variable);
a quoted string ("18px") and an unknown CSS property (cursor)
pass through unchanged; an expression (अक्षरमानम्: n + "px") is
emitted as-is. रूप composes with समās — whole styled trees are nested रचय
blocks each carrying their own रूप.
सूची-दत्तांशः — data rendering & named styles
List rendering. A समās child can be any expression that yields DOM
nodes, and array children are flattened — so mapping a data array with
.प्रतिचित्रय (map) renders a list:
ग्रन्थाः.प्रतिचित्रय(कार्य(ग) {
फलम् रचय पङ्क्तिः वाक्यम् ग.नाम। # one <li> per data item
})placed inside a रचय सूचीः { … } produces a <ul> of <li>s, one per row.
Named styles (रूपनाम). Declare a reusable style once and apply it by name, with optional per-element overrides:
रूपनाम कार्डः = रूप { पृष्ठभूमिः: श्वेतः, अन्तरालः: "12px", कोणवृत्तिः: "8px" }।
रचय मूलः रूप कार्डः { … } # apply the named style
रचय मूलः रूप कार्डः { पृष्ठभूमिः: नीलः } { … } # named base + overrideरूपनाम X = रूप {…} compiles to const X = {…CSS…}; a reference compiles
to Object.assign({}, X), and a base-plus-override to
Object.assign({}, X, {…}) so overrides win. A subtle parser point: after a
named reference, { is disambiguated by lookahead — { word: … } is a
style override, anything else is a समās children block.
Together these make real data-driven UIs (examples/suchi.deva): a styled
container whose children are a heading plus a mapped list of styled cards.
भाव — reactive state
भाव (bhāva, "state/condition") declares a reactive state cell, and
दृश्य (dṛśya, "view") declares a region that re-renders automatically
when any state it reads changes. The model is subscribe-on-read /
re-run-on-write: a view subscribes to every भाव it reads while rendering,
and a later write re-runs just that view.
भाव गणकः = ०। # a reactive counter cell
दृश्य { # a self-updating view
रचय मूलः {
रचय शीर्षः वाक्यम् "गणना: " + गणकः। # reads गणकः → subscribes
रचय पटः वाक्यम् "वर्धय"
स्पर्शाय करणेन कार्य(){ गणकः++। }। # writes गणकः → re-renders
}
}The syntax is transparent: a भाव name reads as its value and assigns
normally — under the hood गणकः compiles to गणकः() (read), गणकः = v
to गणकः(v) (write), and गणकः++ / गणकः += n desugar through the cell.
दृश्य { … } mounts to the page root by default, or दृश्य (container) { … }
to a chosen element. A view's final expression is what gets rendered.
Because a view is just a समās tree, reactivity composes with everything:
styled, data-mapped, event-wired trees that rebuild on state change.
examples/todo.deva is a complete reactive todo app — भाव task list,
.प्रतिचित्रय rendering each task, buttons that add/remove and re-render.
प्रभाव — fine-grained reactivity
दृश्य is coarse: when any cell it read changes, it re-runs and rebuilds
its whole subtree. That's simple and fine for small views, but it's not how
modern frontend frameworks (Solid, Svelte) achieve speed — they update only
the exact DOM bound to the changed value. Devabhāṣā now has that model too, as
a layer alongside दृश्य:
प्रभाव(कार्य(){ … })(prabhāva, "influence/effect") runs the function once, tracks exactly whichभावcells it reads, and re-runs only that function when one of those cells changes — nothing else.बन्ध(कार्य(){ … })(bandha, "binding") returns a text node whose content is kept in sync by an effect; when its dependencies change, only that one node's text updates — no rebuild, no lost focus or scroll.
भाव गणकः = ०।
चर नोड = बन्ध(कार्य(){ फलम् पाठ"गणना: {गणकः}"। })। # a live text node
गणकः = गणकः + १। # ONLY नोड's text updates — the surrounding DOM is untouchedThe engine is a real signal/effect system: a subscriber stack tracks the
current effect, each cell records its subscribers, and an effect cleans up its
old dependencies before every re-run — so conditional reads (यदि (ध्वजः) {
… ब … }) correctly drop a dependency when the branch stops taking it (the
classic stale-subscription bug, handled). दृश्य and प्रभाव coexist: the
coarse view is still there for simple cases, fine-grained for performance.
Fine-grained is also automatic. A रचय whose content is dynamic (reads
a भाव cell) and is used outside a दृश्य compiles straight to a bound
text node — no बन्ध call needed:
भाव गणकः = ०।
चर शीर्षम् = रचय शीर्षः वाक्यम् पाठ"गणना: {गणकः}"। # dynamic content
गणकः = गणकः + १। # ONLY this node's text updates, in placeThe compiler detects the भाव read and emits a fine-grained binding; static
content (no भाव read) stays a plain value with zero overhead, and content
inside a दृश्य stays coarse so the two models never double-update. So you
get fine-grained performance by default, just by writing dynamic content.
घटकाः — components & props
A component is just a कार्य that takes props and returns a रचय tree —
no new syntax, because functions and रचय already compose. You call it like
any function and reuse it freely:
कार्य पत्रम् (शीर्षकम्, मूल्यम्) {
फलम् रचय मूलः { रचय शीर्षः वाक्यम् शीर्षकम्। रचय वाक्यः वाक्यम् मूल्यम्। }।
}
चर अ = पत्रम्("नाम", "रामः")। # reuse with different props
चर ब = पत्रम्("वयः", "तिंशत्")।The one thing that needs care is reactive props — a prop that should stay
live across the call boundary. Passing a भाव cell normally reads its value
at the call site (a dead snapshot). Instead, wrap it in सूत्र(…) (sūtra,
"thread"), which passes a reactive reference — the component renders it and
it updates fine-grained when the cell changes, while only that node updates:
भाव गणकः = ०।
चर कार्डः = आँकडापत्रम्("क्लिक्", सूत्र(गणकः), "navy")। # live reactive prop
गणकः = गणकः + १। # the card's value updates in place; siblings untouchedसूत्र is explicit on purpose — reactivity crossing a boundary is visible
at the call site rather than implicit, matching the language's preference for
predictable over clever. Static props (plain values) and callback props
(passing a कार्य for, e.g., a click handler) work as ordinary arguments.
examples/घटकाः.deva composes a reusable stat-card component (used three
times) with reactive, static, and callback props.
आवली — keyed list rendering
Rendering a list with .प्रतिचित्रय inside a दृश्य rebuilds every row when
the data changes — losing each row's DOM state (focus, scroll, input values).
आवली (āvalī, "row/series") is keyed reconciliation: it identifies rows by
a stable key and, on each change, reuses the DOM nodes of surviving keys
(moving them if reordered), builds only genuinely new keys, and removes
vanished ones — the model Solid and Svelte use.
भाव वस्तूनि = [कोष{ कुं: १, … }, कोष{ कुं: २, … }, …]।
चर पटः = आवली(
सूत्र(वस्तूनि), # the reactive data
कार्य(व){ फलम् व.कुं। }, # key — a stable identity per item
कार्य(व){ फलम् रचय वाक्यः वाक्यम् व.नाम। } # render one row
)।Because nodes are reused by key, a row's DOM state moves with it across a
reorder — the defining benefit of keyed rendering. The reconciler runs inside
an effect, so it re-runs only when the data signal changes, and it touches the
DOM minimally (no rebuild of unchanged rows). examples/आवली.deva is a
shuffling colored list where each row keeps its identity through reordering.
Reactive styles & cleanup
Fine-grained binding extends to style properties: a रूप { } value that
reads a भाव cell (outside a दृश्य) updates just that one CSS property in
place when the cell changes — no rebuild, the rest of the element untouched.
भाव वर्णः = "tomato"।
चर पटः = रचय मूलः रूप { पृष्ठभूमिः: वर्णः, अन्तरालः: "40px" }। # bg is reactive
वर्णः = "steelblue"। # ONLY background-color updatesEach dynamic property gets its own effect; static properties (and styles inside
a दृश्य) stay plain with zero overhead. The same applies to a सक्रियः ?
रक्तः : धूसरः ternary — the property re-evaluates fine-grained.
For teardown, सफाई(कार्य(){ … }) (saphāī, "cleanup") registers a hook inside
a प्रभाव that runs before the effect's next re-run — the place to clear a
timer, remove a listener, or cancel a subscription so reactive code doesn't
leak:
प्रभाव(कार्य(){
चर घटी = कालचक्र(कार्य(){ … }, १०००)।
सफाई(कार्य(){ कालनाशः(घटी)। })। # tear down the old timer before re-running
})।Refs need no special form — रचय already returns the DOM node (चर न = रचय
…), so you hold the handle directly and can read or mutate it.
आलस्यचित्रम् — lazy-loaded images
आलस्यचित्रम्(src, opts) (ālasya-citra, "lazy image") builds an img that
loads its real source only when it scrolls into view, via IntersectionObserver
— the standard technique for fast image-heavy pages. It shows opts.placeholder
(or nothing) until then, keeps the real URL in data-src, sets the native
loading="lazy" hint, and falls back to eager loading where the observer is
unavailable:
चर चित्रम् = आलस्यचित्रम्(लेख.चित्रम्, कोष{
alt: लेख.शीर्षकम्,
placeholder: "data:image/svg+xml,…" # a tiny inline placeholder
})।examples/ब्लॉग.deva is a complete blog website: a post list whose cards are
components with lazy-loaded cover images, reactive view-switching between the
list and a full-post view (a प्रभाव effect rebuilding the content area when
the selected post changes), and a back button — composing components, effects,
and lazy images into a real app.
examples/vyaya.deva goes further — a reactive expense tracker with a
live computed total, category filter buttons (whose active colour comes from
a सक्रियः ? रक्तः : धूसरः ternary, color words resolved in-expression),
.गालय filtering, and add-entry interaction: several भाव cells and derived
views composing into one app.
examples/flappy.deva (विहगः) is a real-time Flappy-Bird-style game — a
different stress test entirely: an animation loop, continuous state mutation,
collision detection, and keyboard input. It uses three general-purpose host
bindings added for interactive programs: कालचक्र(fn, ms) (kālacakra, "wheel
of time" → a repeating timer / game loop), कालनाशः(id) (stop a timer), and
कुञ्जिश्रोता(fn) (kuñjiśrotā, "key-listener" → a keyboard handler receiving
the pressed key). The whole game state lives in one भाव cell that the loop
mutates ~30×/sec; absolute positioning uses the शीर्षात्/वामतः/अधस्तात्/
दक्षिणतः (top/left/bottom/right) and स्तरः (z-index) style properties.
Building it surfaced a real fix to the reactive model: a भाव cell skipped
re-rendering when written the same reference — fine for immutable updates,
but a game mutates its state object in place, so the view never refreshed. The
guard now skips only unchanged primitives; object/array state always
re-renders, which is the behavior mutable game loops (and most reactive code)
expect.
गणित — the math module
A comprehensive mathematics layer. Elementary and transcendental functions
come from a native bridge to JavaScript's Math (exact, full precision);
the statistics and number-theory layer (examples/ganita.deva) is written
in Devabhāṣā on top of them — the self-hosting payoff applied to a
standard library.
Native bridge — गणित.<fn>(…):
वर्गमूलम् (sqrt), घनमूलम् (cbrt), घातः (pow), निरपेक्षम् (abs),
अधःपातः/ऊर्ध्वपातः (floor/ceil), सन्निकर्षः (round), धनर्णचिह्नम् (sign),
ज्या/कोटिज्या/स्पर्शज्या (sin/cos/tan), विलोमज्या etc. (asin…),
घातीयम् (exp), लघुगणकः (log), अधिकतमः/न्यूनतमः (max/min),
यादृच्छिकम् (random). Constants: गणित.पाई (π), गणित.यूलरांकः (e),
गणित.मूलद्वि (√2).
A nice piece of history: ज्या (jyā) and कोटिज्या (koṭijyā) are the actual classical Sanskrit names for sine and cosine — the words that, via Arabic jiba → jayb, became the Latin sinus and our "sine."
Statistics & number theory (written in Devabhāṣā, examples/ganita.deva):
माध्यम् (mean), मध्यमा (median), बहुलकः (mode), शतमक (percentile),
प्रसरणम्/मानविचलनम् (variance/stddev), न्यूनतमम्/अधिकतमम्/परिसरः
(min/max/range), योगफलम्/गुणनफलम् (sum/product), क्रमणम् (sort),
महत्तमसमापवर्तकः/लघुत्तमसमापवर्त्यः (gcd/lcm), क्रमगुणितम् (factorial),
अभाज्यः (is-prime), plus angle conversion (अंशेभ्यःरेडियनम्), सीमन
(clamp), रैखिकान्तर्वेशनम् (lerp), and परिवृत्तिः (round to N places).
Verified against Python's statistics/numpy to full precision.
Transcendentals from first principles. To show the language isn't merely
forwarding to Math, घातीयश्रेणी (exp) and ज्याश्रेणी (sin) are computed
in pure Devabhāṣā via their Taylor series — and agree with the native bridge
to ~15 digits.
चर दत्तांशः = [४, ८, १५, १६, २३, ४२]।
दर्शय("माध्यम्:", माध्यम्(दत्तांशः))। # 18
दर्शय("मानविचलनम्:", मानविचलनम्(दत्तांशः))। # 12.3153…
दर्शय("e via Taylor:", घातीयश्रेणी(१))। # 2.71828… (no Math used)
दर्शय("ज्या(π/६):", गणित.ज्या(गणित.पाई / ६))। # 0.5अन्तर्न्यासः — string interpolation
पाठ"…" (pāṭha, "text") marks an interpolated string: any {expr} inside is
evaluated and spliced in, compiling to a JS template literal.
दर्शय(पाठ"{नाम} वर्षाणि {वयः}")। # variables
रचय वाक्यः वाक्यम् पाठ"योगः: {योगम्(व्ययाः)} रूप्यकाणि"। # any expression
दर्शय(पाठ"\{ब्रेस\} रक्षितम्")। # \{ \} for literal bracesIt is opt-in via the पाठ marker for a deliberate reason: making every
"…" interpolate would mis-read the literal braces in existing strings —
including the JS that the self-hosted compiler emits as string literals — so
plain "…" stays completely literal, and only पाठ"…" interpolates. The
marker must touch the quote, so पाठ remains usable as an ordinary
identifier. Interpolation holds full expressions (calls, member access,
arithmetic, even nested strings), and is the idiomatic way to build labels
and views — examples/vyaya.deva uses it throughout.
मधुरचिह्नानि — operator sugar
Convenience operators that desugar to the core language:
- compound assignment —
x += y,-=,*=,/=,%=(works on variables, object fields, and array elements);x += ybecomesx = x + y - ternary —
परीक्षा ? तदा : अन्यथा, chainable forelse-ifladders - null-coalescing —
a ?? b(yieldsbonly whenais null/undefined) - increment / decrement —
i++,i--
चर कुल = ०।
प्रत्येकम् (x : [१, २, ३, ४]) {
कुल += x > २ ? x : ०। # add x only when x > 2
}
दर्शय(कुल)। # 7
चर नाम = दत्त ?? "अज्ञातः"। # default when दत्त is nullThese live in the JavaScript-hosted compiler; the self-hosted bootstrap sources deliberately don't use them, so the fixpoint is unaffected.
आयात / निर्यात — the module system
Modules use compile-time resolution and linking (the Rust/Python
lineage, not C-style textual inclusion): each .deva file is compiled
independently, exports are explicit, and a bundler resolves the import graph
and links everything into one self-contained program.
Exporting — prefix any declaration with निर्यात (niryāta, "sending
out"). Anything not marked is private to its module:
# गणितागारम्.deva
निर्यात नियत पाई = ३।
निर्यात कार्य द्वि (न) { फलम् न * २। }
कार्य गुप्तम् () { … } # private — not importableImporting — आयात (āyāta, "incoming") with आ (ā, "from") as the
source preposition, in three forms:
आयात { द्वि, पाई } आ "गणितागारम्"। # named — bind specific exports
आयात * रूपेण ग आ "गणितागारम्"। # namespace — ग.द्वि, ग.पाई
आयात "उपस्करः"। # side-effect — just run the moduledevabhasha build entry.deva (or run) resolves every आयात relative to
the importing file (appending .deva), compiles each module once, orders
them dependency-first (topological sort), and links them: each module
becomes an IIFE returning its export object, named imports destructure it,
namespace imports bind the whole object. Diamond dependencies compile the
shared module a single time; missing modules and exports of non-declarations
are compile-time errors. See examples/modules/ for a math library split
into a module and consumed by a main file.
The bundler lives in src/bundler.js; compileModule(src) returns
{ code, exports, imports } for tooling.
आदर्शकोशः — the standard library (written in Devabhāṣā)
The standard library is itself written in Devabhāṣā, as .deva modules
under examples/stdlib/ — the clearest proof the module system earns its
keep, and the "move features to libraries" principle in practice. Each is
plain Devabhāṣā built on the array/string/object primitives, with zero
compiler support:
- सूची (list):
योगः(sum),गुणनफलम्(product),न्यूनीकरणम्(fold),अन्वेषय(find),सन्ति/सर्वे(any/all),न्यूनतमम्/महत्तमम्(min/max),अद्वितीयम्(unique),आदिमानि/शेषाणि(take/drop),समतलीकृ(flatten),परिसरः(range). - कोष (object):
कुञ्जयः/मूल्यानि/प्रविष्टयः(keys/values/entries),अस्ति(has-key),सङ्ख्या(count),विलयः(merge),प्रतिचित्रयमूल्यानि(map-values),गालयकुञ्जीभिः(pick). Built on theसङ्ग्रह(Object) global. - पाठ (string):
आवर्तय(repeat),वामपूरणम्(pad-left),प्रथमाक्षरोच्च(capitalize),पदानि(words),व्युत्क्रमः(reverse),परिवर्तय_सर्वम्(replace-all),रिक्तः(is-blank). - परीक्षा (test framework):
परीक्षा(नाम, fn)registers and runs a test,अपेक्ष(actual)returns an asserter (.समम्/equal,.असमम्/not-equal,.सत्यम्ता/truthy,.असत्यम्ता/falsy),समम्(अ, ब)is a standalone deep structural equality, andसारः()prints theN पास, M फेलtally. Staying true to the no-exceptions design, assertions record their outcome into a collector rather than throwing.
Use them like any module — and they compose, importing from several at once:
आयात { परिसरः, योगः } आ "सूची"।
आयात { आवर्तय } आ "पाठ"।
दर्शय(योगः(परिसरः(१, ५)))। # 10
दर्शय(आवर्तय("=", योगः([१,२])))। # "==="With परीक्षा, the language tests itself: Devabhāṣā programs (including the
standard library above) can be tested in Devabhāṣā, completing the
self-hosting story that already covers the compiler. examples/परीक्षा-उदाहरणम्.deva
tests the सूची module using परीक्षा — both written in the language:
आयात { परीक्षा, अपेक्ष, सारः } आ "stdlib/परीक्षा"।
आयात { योगः, अद्वितीयम् } आ "stdlib/सूची"।
परीक्षा("अद्वितीयम्", कार्य(){ अपेक्ष(अद्वितीयम्([१,२,२,३])).समम्([१,२,३])। })।
सारः()। # → "1 पास, 0 फेल"Reflection helpers underpin it: स्वरूपम्(v) gives a value's type as a
Sanskrit name (अङ्क, वाक्, सूची, कोष, सत्यासत्य, रिक्त), and
सूचीवत्(v) tests for a list — enabling the framework's deep equality.
उपकरणानि — tooling (VS Code extension, dev server, source maps & LSP)
VS Code extension. devabhasha-1.0.0.vsix is an installable VS Code
extension (Extensions panel → "Install from VSIX…", or
code --install-extension devabhasha-1.0.0.vsix). It gives .deva files
syntax highlighting (a TextMate grammar), completion, hover, go-to-definition,
rename, and live diagnostics, plus a "Run current file" command. Rather than
spawning the stdio language server, the extension wires the same
dependency-free analyzer core (src/analyzer.js) directly to VS Code's
provider APIs in-process — so the server and the extension are two frontends
over one analyzer, with no bundled language-client and no child process. The
analyzer graph is bundled to CommonJS for the extension with node
build-extension.js (esbuild is a build-time tool only; nothing extra ships in
the .vsix).
Dev server with live reload. devabhasha serve file.deva [--port N] starts
a zero-dependency dev server (Node's built-in http + fs.watch, live reload
over Server-Sent Events — no packages) that:
- serves an HTML page running your compiled web program, mounted at
#मूलम्, - watches the source (and its
आयातimports) and recompiles + reloads the browser automatically when you save, and - shows compile and runtime errors on the page instead of a blank screen, staying up so you can fix and re-save.
So the loop is: edit .deva, save, see it update in the browser — the modern
front-end dev experience, for a Sanskrit language.
Source maps. devabhasha build file.deva --sourcemap emits the JS plus a
standard Source Map v3 (.js.map, with sourcesContent and a
//# sourceMappingURL comment), so browser dev-tools and Node stack traces
point back at the original Devanagari source. Mapping is at statement
granularity — each statement node carries its source line/col, and the
codegen tracks output position as it emits. The maps are VLQ-encoded and
validated against the standard source-map library. The API is
compileWithMap(source) → { code, map }.
Language server (LSP). src/server.js is an editor-agnostic Language
Server speaking the Language Server Protocol over stdio — any LSP client
(VS Code, Neovim, Helix…) can connect by running node src/server.js. It
provides:
- diagnostics — compile errors with precise ranges, live as you type (cleared automatically when the code becomes valid);
- completion — the full vocabulary (keywords, stdlib methods, गणित constants, रूप properties and color words, element tags, events), filtered by the prefix being typed;
- hover — what a Sanskrit word means and what it translates to
(
वर्णः→ color,रक्तः→ crimson,वर्गमूलम्→ Math.sqrt); - go-to-definition — jump from any use of a name to where it is bound;
- rename — rename a binding and every reference that resolves to it, atomically, as one workspace edit.
Go-to-definition and rename are backed by a scope-aware symbol table
(src/symbols.js) that walks the AST tracking lexical scopes — चर/नियत,
कार्य names and parameters, भाव/रूपनाम declarations, and प्रत्येकम्
loop variables. It respects shadowing: a renamed inner parameter never
touches an outer variable of the same name, and vice versa. (This is why
identifiers and declaration names carry source positions — JS-side metadata,
stripped before the self-hosted parser comparison, so the bootstrap is
unaffected.)
The analysis is pure functions in src/analyzer.js (diagnostics,
completions, hover, definition, renameOccurrences, wordAt);
server.js is a thin JSON-RPC wrapper, so the same engine can power other
front-ends.
सञ्चिका / जाल — file & network I/O
I/O is layered: a program calls the Sanskrit surface (सञ्चिका for
files, जाल for the network), which the codegen lowers to a runtime
interface __IO, which a host-injected backend implements. The program
is bound to the interface, never to Node — so the same code runs against
the Node backend (CLI), a browser backend, or an in-memory test backend
(this is exactly how the test suite proves it, running one program against
two backends).
Every operation is async and returns a परिणाम (Result) — composing the
two foundations above. You प्रतीक्षा (await) the call and inspect the
Result; a failure is a value, never a thrown exception, so a missing file
can't crash the program:
असमकालिक कार्य मुख्यम् () {
प्रतीक्षा सञ्चिका.लिख("संदेशः.txt", "नमस्ते")। # write
चर र = प्रतीक्षा सञ्चिका.पठ("संदेशः.txt")। # read
यदि (र.सफल) { दर्शय(र.मूल्यम्)। } अन्यथा { दर्शय(र.दोषः)। }
चर जालम् = प्रतीक्षा जाल.आनय("https://api.example.com")। # fetch
यदि (जालम्.सफल) { दर्शय(जालम्.मूल्यम्.स्थितिः)। } # → HTTP status
}
मुख्यम्()।File ops (सञ्चिका): पठ (read), लिख (write), विद्यते (exists),
निष्कासय (remove), सूचीकृ (list a directory). Network (जाल):
आनय (fetch), whose Result value carries स्थितिः (status), पाठः (body
text), and सफलम् (ok). The Node backend lives in src/io-node.js;
devabhasha run injects it, and devabhasha build inlines it so the
produced .js is self-contained under Node. See examples/io.deva.
सेवक — HTTP server (backend)
Because Devabhāṣā compiles to JavaScript and runs on Node, it can be a
backend language too. सेवक(handler, port) (sevaka, "server") starts an HTTP
server — the same host-bridge pattern as सञ्चिका/जाल, lowered to a __SRV
runtime the Node backend implements (src/server-node.js; run injects it,
build inlines it). The handler receives a request and response, both
keyed in Devanagari:
सेवक(असमकालिक कार्य(अनुरोधः, प्रत्युत्तरम्){
यदि (अनुरोधः.मार्गः == "/जनाः") { # route by path
यदि (अनुरोधः.रीतिः == "GET") { # branch by method
प्रत्युत्तरम्.प्रेषय_जेसन(सूची)। # JSON response
} अन्यथा {
चर परि = प्रतीक्षा अनुरोधः.देहम्_जेसन()। # parse JSON body → परिणाम
यदि (परि.सफल) {
सूची.योजय(परि.मूल्यम्)।
प्रत्युत्तरम्.स्थिति(२०१).प्रेषय_जेसन(परि.मूल्यम्)।
} अन्यथा {
प्रत्युत्तरम्.स्थिति(४००).प्रेषय_जेसन(कोष{ दोषः: "bad" })।
}
}
} अन्यथा {
प्रत्युत्तरम्.स्थिति(४०४).लेखय("न प्राप्तम्")।
}
}, ८०८०)।The request (अनुरोधः) exposes मार्गः (decoded path), रीतिः (method),
शीर्षाणि (headers), प्रश्नाः (query params), and async body readers देहम्()
(text) and देहम्_जेसन() (→ परिणाम, so a malformed body is a value, never a
crash). The response (प्रत्युत्तरम्) is chainable: स्थिति(code),
शीर्षम्(k, v), लेखय(text), प्रेषय_जेसन(value). The handler may be
असमकालिक (async); if it throws, the server returns 500 and stays up.
This is a deliberately minimal HTTP primitive — enough to write a real JSON API
or static server (examples/सेवकः.deva is a small books API), not a
production framework like Express. Its value is the same as the rest of the
language: an honest, teachable surface — and, paired with the reactive frontend,
the rare ability to write both ends of a web app in one Sanskrit source. A
routing/middleware library written in Devabhāṣā on top of सेवक (the
"framework" proper, like परीक्षा is for testing) is the natural next layer.
examples/पूर्णस्तर/ is a full-stack app: a reactive Sanskrit frontend
(अग्रिम.deva) fetching from a Sanskrit HTTP/JSON backend (पश्चिम.deva) over
real HTTP, sharing the language, the Result error model, and the JSON shapes.
Run it with node चालय.mjs and open http://localhost:8100.
मार्गकः — a routing library (written in Devabhāṣā)
सेवक is the bare primitive; मार्गकः (mārgaka, "router") is the framework
layer on top — and, like परीक्षा the test framework, it's written in
Devabhāṣā itself (examples/stdlib/मार्गकः.deva), not baked into the runtime.
Import it and register routes by method and path:
आयात { मार्गकः } आ "मार्गकः"।
चर ऐप = मार्गकः()।
ऐप.उपयुज्(असमकालिक कार्य(अ, प्र, अग्रे){ दर्शय(अ.मार्गः)। प्रतीक्षा अग्रे()। })। # middleware
ऐप.प्राप्("/ग्रन्थाः", कार्य(अ, प्र){ प्र.प्रेषय_जेसन(सूची)। })। # GET
ऐप.प्राप्("/ग्रन्थाः/:अंकः", कार्य(अ, प्र){ ... अ.प्राचलाः["अंकः"] ... })। # path param
ऐप.स्थापय("/ग्रन्थाः", असमकालिक कार्य(अ, प्र){ ... })। # POST
ऐप.चालय(८०८०)।The API: प्राप् (GET), स्थापय (POST), परिवर्तय (PUT), निष्कासय (DELETE),
उपयुज् (middleware), चालय (listen). Path parameters (:अंकः) are captured
into अनुरोधः.प्राचलाः. Middleware follows the Koa contract — असमकालिक, and
प्रतीक्षा अग्रे() to pass control downstream — which is what lets the server
await the whole chain before ending the response.
On "optimum": each route's path is compiled once at registration into a
segment list (static segments + :param markers), and routes are grouped by
method, so a request does only a segment-by-segment compare against the routes
of its own method — no regex, no re-parsing per request. A radix tree would win
at very large route counts; at app scale this segment-compare is effectively
optimal and far simpler, an honest tradeoff stated in the source.
examples/stdlib/मार्गक-उदाहरणम्.deva is a books API built on it.
JSON & structured data. प्रदत्त (pradatta, "data") parses and serializes
JSON, both returning a परिणाम (since JSON.parse throws and the language
has none): प्रदत्त.विश्लेषय(पाठः) (parse) and प्रदत्त.सूत्रय(मूल्यम्)
(serialize). अङ्कय(पाठः) parses a string to a number, also as a Result. The
I/O layer adds JSON convenience ops so a config or API response becomes usable
data in one step: सञ्चिका.पठप्रदत्त/लिखप्रदत्त (read/write JSON files) and
जाल.आनयप्रदत्त (fetch + parse, value carries प्रदत्तम्). These compose with
प्रतीक्षा and अथवा:
चर विन्यासः = (प्रतीक्षा सञ्चिका.पठप्रदत्त("config.json")) अथवा कोष{ }। # parsed config, or {}
चर सङ्ख्या = अङ्कय(आदानम्) अथवा ०। # parsed number, or 0असमकालिक — async & await
For operations that complete later (timers, and the upcoming I/O layer),
Devabhāṣā has real async/await — the model JS, Python, and Rust all
converged on, and a near-direct map to JS since the target is JS.
असमकालिक कार्य …(asamakālika, "asynchronous") marks an async function; it returns a promise (आश्वासन).प्रतीक्षा <expr>(pratīkṣā, "waiting") awaits a promise, valid only inside anअसमकालिकfunction — the compiler enforces this (a top-level or nested-syncप्रतीक्षाis a compile error, matching JS's coloring rule).- promises chain with
.ततः(then),.दोषे(catch),.अन्ततः(finally).
असमकालिक कार्य मुख्यम् () {
चर परि = प्रतीक्षा आनय(कोषः)। # await a promise
यदि (परि.सफल) { दर्शय(परि.मूल्यम्)। } # …of a परिणाम (Result)
अन्यथा { दर्शय(परि.दोषः)। }
}
मुख्यम्()।async composes with the Result model: a fallible async operation returns a
promise of a परिणाम, so प्रतीक्षा yields a Result the caller inspects —
honest async and honest errors, with no exceptions. Event handlers can be
async too (स्पर्शाय करणेन असमकालिक कार्य(){ … प्रतीक्षा … }), the usual
shape for I/O-on-click.
परिणाम — the error model (Result values)
Devabhāṣā handles fallible operations with explicit Result values, not
exceptions — keeping with the language's preference for visible over hidden
control flow (and avoiding a new non-local jump in the self-hosted compiler).
A fallible function returns a परिणाम (pariṇāma, "result/outcome"), built
with one of two constructors:
साधितम्(मूल्यम्)(sādhitam, "achieved") — success, carrying a valueविफलम्(दोषः)(viphalam, "failed") — failure, carrying an error
A परिणाम has three fields: सफल (ok?), मूल्यम् (the value), दोषः
(the error). The caller inspects it with ordinary यदि:
कार्य भाग (अंशः, हरः) {
यदि (हरः == ०) { फलम् विफलम्("शून्येन भागः")। } # Err
फलम् साधितम्(अंशः / हरः)। # Ok
}
चर फल = भाग(१०, २)।
यदि (फल.सफल) { दर्शय("ok:", फल.मूल्यम्)। }
अन्यथा { दर्शय("दोष:", फल.दोषः)। }Results compose: a function can map an Ok value and pass an Err through
unchanged, which is how fallible steps chain without exceptions. The
constructors live in a host-independent prelude (__RT), so Result works in
any environment — including the I/O layer, where operations return परिणाम
rather than throwing.
अथवा — value-or-fallback. The common "use the value, or a default if it
failed" pattern would otherwise be a four-line यदि/अन्यथा block. अथवा
(athavā, "or else") collapses it to an expression: it yields the Result's
मूल्यम् when सफल, else a fallback that is evaluated lazily (only on
failure). It's pure expression sugar — no hidden control flow — and composes
with await, which is exactly the I/O case:
चर सामग्री = (प्रतीक्षा सञ्चिका.पठ("config.txt")) अथवा "रिक्तम्"। # default on read failure
चर सङ्ख्या = संख्यांकय(आदानम्) अथवा ०। # default on parse failureअथवा chains right-associatively (अ अथवा ब अथवा ग tries each in turn,
first Ok wins) and also guards plain null/non-Result values, so it
doubles as a nullish fallback.
दोषनिरूपणम् — error reporting
Compiler errors are structured (DevabhashaError with line, col, and a
kind of lex/parse/codegen) and render with source context and a caret:
अक्षरदोषः (lex error): अज्ञातं चिह्नम् (unknown character) '@'
line 2, column 9
२ | दर्शय(a @ b)।
| ^Line numbers display in Devanagari numerals. The same formatting appears in
the CLI (devabhasha run) and live in the playground as you type. Use
formatError(err, source) to render any caught DevabhashaError.
मानकपुस्तकालय — standard library (strings, objects, arrays)
The machinery a compiler is made of. These are what make self-hosting possible: a lexer needs to index strings, accumulate substrings, push records onto arrays, and read object fields.
Strings — .दीर्घता (length), .अक्षरः(i) (charAt), .सङ्केतः(i)
(charCodeAt), .खण्ड(a,b) (slice), .अस्ति(x) (includes), .विभज(s)
(split), .उच्च/.नीच (upper/lower), .आरभते/.समाप्यते (starts/ends).
Arrays — .योजय(x) (push), .अपनय() (pop), .दीर्घता (length),
.प्रतिचित्रय(fn) (map), .गालय(fn) (filter), .सम्मील(s) (join),
.अनुक्रमणिका(x) (indexOf), indexing via सूची[i].
Builtins — संकेताक्षर(code) (saṅketākṣara, "code-letter") →
String.fromCharCode, e.g. संकेताक्षर(९२) yields a backslash. This is
what lets the self-hosted codegen build escape characters without writing
escape literals.
Objects — the कोष (kośa, "dictionary") literal:
चर व्यक्तिः = कोष { नाम: "राम", आयुः: ३० }।
दर्शय(व्यक्तिः.नाम)। # field access
व्यक्तिः.आयुः = ३१। # field assignmentProperty names live in their own namespace, so a word that's a keyword
elsewhere (योजय = DOM mount) is still usable as a method name
(सूची.योजय = array push). The stdlib name→JS mapping lives in
src/stdlib.js.
Self-hosting milestone
With strings, objects, and arrays in place, compiler machinery can now be written in Devabhāṣā itself.
All three compiler stages are written in Devabhāṣā, and the bootstrap is a closed fixpoint — the compiler reproduces itself byte-for-byte.
examples/lexer.deva— port ofsrc/lexer.js. Character classes are built from codepoint arithmetic (no regex); keyword/operator tables are Devabhāṣā data.examples/parser.deva— port ofsrc/parser.js: recursive descent + Pratt expression parsing. Since Devabhāṣā functions don't close over outer mutable state, the parser threads an explicit state objectअ = कोष { शब्दाः, स्थानम् }through every function.examples/codegen.deva— port ofsrc/codegen.js: the AST→JS emitter, the Devanagari→ASCII transliteratorआईडी, and string quoting. The quoter builds every special character (backslash, quote, newline, tab) from its code point via theसंकेताक्षर(String.fromCharCode) builtin, so the codegen source contains no escape literals — which is what lets it reproduce its own source exactly.
Verified by the standard compiler-bootstrap fixpoint check
(test/fixpoint.test.js):
- the JS-hosted compiler compiles the three
.devasources → a Devabhāṣā-hosted compiler (stage 1); - that compiler recompiles all three sources (stage 2);
- a compiler built from stage-2 output recompiles them again (stage 3);
- stage 2 === stage 3, byte-for-byte, for all three components — the same criterion GCC/Rust/Go use to certify a self-hosting bootstrap.
The stage-3 compiler — built entirely from Devabhāṣā-generated JavaScript,
with no original toolchain involved — runs real programs (recursion,
objects, arrays) correctly. node selfhost-demo.mjs shows the full
self-hosted compiler compiling and running Fibonacci.
Architecture
A textbook transpiler pipeline — each stage is one file, no dependencies:
source .deva
│ src/lexer.js tokenize Devanagari → token stream
▼
tokens
│ src/parser.js recursive descent (statements) + Pratt (expressions) → AST
▼
AST
│ src/codegen.js tree-walk → JavaScript text (+ optional DOM runtime)
▼
JavaScriptsrc/keywords.js— the only Sanskrit-aware file. Edit vocabulary here without touching the engine. The lexer maps each Devanagari keyword to a language-neutral token type, so the parser and codegen never see Sanskrit.src/index.js—compile(source, { includeRuntime }) → string.src/cli.js—build/runcommands.build-playground.js— inlines the ES modules into one HTML file.
Extending it
- Add a keyword: one line in
keywords.js. If it needs new syntax, add a parse branch inparser.jsand an emit case incodegen.js. - Add an operator: add to
OPERATORS(longest-first) and toBINARY_PREC. - Richer web layer: the runtime in
codegen.js(__DB) is intentionally tiny — extendelto support a JSX-like compound (समास) syntax, or add a reactivity primitive.
Suggested roadmap from here
- Source maps — you have line/col on every token already; thread them into codegen for real debugging.
- A JSX-like element syntax using compounds, e.g.
अङ्गम् दिव् { … }block form instead of nested calls. - Type-ish annotations drawn from Sanskrit grammatical categories.
- Editor support — a TextMate grammar / VS Code extension highlighting the
keyword set in
keywords.js. - Sanskrit error messages — partly done; expand
parser.jsdiagnostics.
License
MIT.
