Getting Started Guide

Learn the fundamentals of Pulse programming in this guide.

Quickstart

Installation

npm install -g pulselang

Or create a new project:

npx create-pulselang-app my-app
cd my-app
npm install
npm run dev

Running a single .pulse file

Create a file called hello.pulse:

fn add(a, b) {
  return a + b
}

print(add(2, 3))

Run it:

pulse run hello.pulse

Expected output:

5

Compilation

Pulse compiles to JavaScript ES modules. Two modes:

1. Run mode (pulse run ) - Compiles and executes immediately using a temporary .mjs file

2. Build mode (pulse build ) - Generates permanent .mjs files you can deploy

The compilation pipeline: Source → AST → JavaScript (ESM)

Basic Syntax

Variables and Constants

const PI = 3.14159
let counter = 0
var oldStyle = 'avoid using var'

Functions

fn add(a, b) {
  return a + b
}

const multiply = (x, y) => x * y

async fn fetchData(url) {
  const response = await fetch(url)
  return response.json()
}

Classes

class Rectangle {
  constructor(width, height) {
    this.width = width
    this.height = height
  }

  area() {
    return this.width * this.height
  }

  static fromSquare(size) {
    return new Rectangle(size, size)
  }
}

const rect = new Rectangle(10, 20)
print(rect.area())

Expected output:

200

Working with Files

Pulse provides a file system API in the std/fs module:

import { readFile, writeFile, exists } from 'std/fs'

async fn main() {
  const content = await readFile('./data.txt', 'utf8')
  print(content)

  await writeFile('./output.txt', 'Hello World')

  if (await exists('./config.json')) {
    const data = JSON.parse(await readFile('./config.json', 'utf8'))
    print('Version:', data.version)
  }
}

spawn(main())

Reactivity

Pulse includes a reactive system inspired by Solid.js:

import { signal, effect, computed, batch } from 'pulselang/runtime'

const [count, setCount] = signal(0)
const [name, setName] = signal('Alice')

const message = computed(() => {
  return `${name()} has ${count()} items`
})

effect(() => {
  print(message())
})

setCount(5)
setName('Bob')

batch(() => {
  setCount(10)
  setName('Charlie')
})

Expected output:

Alice has 0 items
Alice has 5 items
Bob has 5 items
Charlie has 10 items

Signals provide fine-grained reactivity - when a signal changes, only its direct subscribers run. No virtual DOM, no reconciliation.

Concurrency

Channels are how tasks communicate. A channel is a pipe - one task writes, another reads.

await ch.send(value) blocks your task until someone does await ch.recv(). This is how determinism works - the scheduler knows exactly when each task is waiting and when to wake it up.

Basic Channel Example

import { spawn, sleep, channel } from 'std/async'

async fn producer(ch) {
  for (let i = 0; i < 3; i++) {
    await ch.send(i)
    print('Sent:', i)
  }
  ch.close()
}

async fn consumer(ch) {
  for await (const value of ch) {
    print('Received:', value)
  }
  print('Channel closed')
}

async fn main() {
  const ch = channel(0)  // Unbuffered channel

  spawn(producer(ch))
  spawn(consumer(ch))

  await sleep(100)
}

spawn(main())

Expected output:

Sent: 0
Received: 0
Sent: 1
Received: 1
Sent: 2
Received: 2
Channel closed

What happens:

  • Producer sends 0, blocks
  • Consumer receives 0, prints it, goes back to waiting
  • Producer wakes up, sends 1, blocks again
  • This ping-pongs until producer closes the channel
  • Consumer's for await sees the close and exits

Run this 100 times, you get the exact same output every time.

Buffered Channels

Unbuffered = handshake (both sides wait). Buffered = mailbox (you can drop stuff off and leave).

import { spawn, channel } from 'std/async'

async fn main() {
  const buffered = channel(10)  // Buffer size 10

  spawn(async () => {
    for (let i = 0; i < 12; i++) {
      await buffered.send(i)
      print('Sent:', i)
    }
  })

  await sleep(50)
}

spawn(main())

Expected output (first 10 sends don't block):

Sent: 0
Sent: 1
...
Sent: 9
(blocks on 11th send until someone reads)

Useful when producer and consumer run at different speeds.

Select Operations

Wait on multiple channels, first one ready wins:

import { spawn, sleep, channel, select, selectCase } from 'std/async'

async fn main() {
  const ch1 = channel(1)
  const ch2 = channel(1)

  spawn(async () => {
    await sleep(5)
    await ch1.send('from ch1')
  })

  spawn(async () => {
    await sleep(10)
    await ch2.send('from ch2')
  })

  const result = await select([
    selectCase({ channel: ch1, op: 'recv', handler: ([msg]) => msg }),
    selectCase({ channel: ch2, op: 'recv', handler: ([msg]) => msg })
  ])

  print('Got:', result.value)
}

spawn(main())

Expected output:

Got: from ch1

Still deterministic. If multiple channels are ready, scheduler picks based on logical time and source order. Same state = same choice.

Error Handling

Use try-catch blocks for error handling:

import { readFile } from 'std/fs'

async fn riskyOperation() {
  try {
    const data = JSON.parse(await readFile('./config.json', 'utf8'))
    return data
  } catch (error) {
    print('Error:', error.message)
    return null
  }
}

Modules and Imports

Named Exports

export fn add(a, b) {
  return a + b
}

export const PI = 3.14159

Default Exports

export default class Calculator {
  add(a, b) { return a + b }
  subtract(a, b) { return a - b }
}

Importing

import { add, PI } from './math.pulse'
import Calculator from './calculator.pulse'
import * as math from './math.pulse'

Standard Library Imports

import { spawn, sleep, channel } from 'std/async'
import { createServer } from 'std/http'
import { signal, effect } from 'pulselang/runtime'
import { readFile, writeFile } from 'std/fs'

Standard Library

Pulse includes a standard library:

  • std/async - spawn, sleep, channel, select, asyncAll, asyncRace
  • std/http - createServer, serve, json, text, redirect (Full scheduler support)
  • std/fs - File system operations (readFile, writeFile, exists, mkdir, etc.)
  • std/json - JSON parsing and stringification with error handling
  • std/math - Mathematical functions (abs, min, max, clamp, etc.)
  • std/path - Path manipulation (join, resolve, basename, dirname)
  • std/signal - Signal primitives export
  • std/console - Console logging (log, error, warn)
  • std/crypto - Cryptographic utilities
  • std/db - Database clients (SQLite, MySQL, PostgreSQL, Redis)

Explore the API Reference for complete documentation.

HTTP + Scheduler Integration

HTTP handlers have full scheduler support. You can use spawn(), sleep(), and channels() alongside async/await and signals.

Example with channels in HTTP handlers:

import { createServer } from 'std/http'
import { spawn, channel } from 'std/async'

const server = createServer(async (req, res) => {
  const ch = channel(1)
  spawn(async () => {
    await ch.send('data from spawned task')
  })
  const [data] = await ch.recv()
  res.end(data)
})

server.listen(3000)

The scheduler is fully integrated with HTTP request handling.

Next Steps

Now that you understand the basics, explore: