Sscalascript.dev

Architecture

This document describes the ScalaScript compiler pipeline and internal architecture.

Pipeline Overview

┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Source    │────▶│   Lexer/    │────▶│   Parser    │────▶│   Typer     │────▶│  Backend    │
│   (.ssc)    │     │   Scanner   │     │             │     │             │     │             │
└─────────────┘     └─────────────┘     └─────────────┘     └─────────────┘     └─────────────┘
                          │                   │                   │                   │
                          ▼                   ▼                   ▼                   ▼
                       Tokens              Raw AST            Typed IR          Target Code

Phase Details

1. Lexical Analysis (Lexer/Scanner)

Input: UTF-8 source text Output: Token stream

The lexer operates in two modes:

  1. Markdown mode (default): Recognizes headings, links, code fences, etc.
  2. Code mode: Inside fenced blocks, uses Scala-like tokenization

Key responsibilities:

Token types:

enum Token:
  // Markdown
  case Heading(level: Int, text: String)
  case CodeFenceStart(lang: Option[String])
  case CodeFenceEnd
  case LinkStart, LinkEnd, LinkTargetStart, LinkTargetEnd
  case Text(content: String)
  case Newline, Indent, Dedent

  // Code (inside fenced blocks)
  case Identifier(name: String)
  case IntLiteral(value: Int)
  case StringLiteral(value: String)
  case Keyword(kw: Keyword)
  case Operator(op: String)
  // ... etc

2. Parsing

Input: Token stream Output: Raw AST (untyped)

The parser builds an AST that preserves both Markdown structure and code semantics.

AST node types:

// Document structure
case class Module(
  manifest: Option[Manifest],
  sections: List[Section]
)

case class Section(
  heading: Heading,
  content: List[Content],
  subsections: List[Section]
)

enum Content:
  case Prose(text: String, interpolations: List[Interpolation])
  case CodeBlock(lang: String, ast: List[Statement])
  case Import(path: ModulePath, bindings: List[Binding])
  case DataList(items: List[ListItem])

// Code AST
enum Expr:
  case Literal(value: Any, tpe: Option[Type])
  case Ident(name: String)
  case Apply(fn: Expr, args: List[Expr])
  case Lambda(params: List[Param], body: Expr)
  case If(cond: Expr, thenp: Expr, elsep: Option[Expr])
  case Match(scrutinee: Expr, cases: List[CaseClause])
  case Block(stats: List[Statement], expr: Expr)
  // ... etc

enum Statement:
  case ValDef(name: String, tpe: Option[Type], rhs: Expr)
  case DefDef(name: String, params: List[Param], retTpe: Option[Type], body: Expr)
  case TypeDef(name: String, params: List[TypeParam], rhs: Type)
  case ClassDef(...)
  case ExprStatement(expr: Expr)

Parser structure:

UniML format modules

v1/lang/uniml is a standalone JVM/Scala.js lossless parsing layer, separate from the .ssc compiler frontend. Every source token carries one tree-VM instruction (Open, Emit, Close, Reframe, or Report), and ordered processors can be chained before the tree builder. Reframe is the generic indentation-language transition: one source-backed token can atomically close implicit frames, open replacements, emit once, and close final frames.

Concrete leaf modules currently provide strict JSON, secure XML, and a safe YAML 1.2.2 profile. Their canonical result is a presentation CST preserving punctuation, whitespace, comments, ordering, duplicates, and exact spans; semantic values are explicit projections. The YAML projection keeps tags inert, preserves aliases by default, and bounds opt-in alias resolution. These modules do not replace the existing runtime/front-matter readers yet.

3. Type Checking (Typer)

Input: Raw AST Output: Typed IR

The typer performs:

Current implementation note: some exported summaries and route/schema metadata still fall back to Any when the local typer cannot prove a shape. The planned tightening pass is specified in specs/typer-real-types-roadmap.md; it keeps Any as an explicit dynamic boundary while carrying structured type evidence through exported symbols, routes/remotes, schemas, typed data mapping, Spark, and plugins.

Typed IR:

case class TypedModule(
  name: String,
  version: SemVer,
  imports: List[ResolvedImport],
  scopes: List[TypedScope],
  exports: List[Symbol]
)

case class TypedScope(
  name: String,
  symbols: SymbolTable,
  definitions: List[TypedDef],
  nested: List[TypedScope]
)

enum TypedExpr:
  case TLiteral(value: Any, tpe: Type)
  case TIdent(sym: Symbol, tpe: Type)
  case TApply(fn: TypedExpr, args: List[TypedExpr], tpe: Type)
  // ... all expressions with Type attached

Type system features:

4. Backend Translation

Input: ir.NormalizedModule (post-Normalize). Output: A CompileResult variant: TextOutput, Segmented, BinaryOutput, Executed, or Failed(diagnostics).

Every backend implements the SPI trait in backend-spi:

package scalascript.backend.spi

trait Backend:
  def id:              String                              // "jvm", "js", "wasm", …
  def displayName:     String
  def spiVersion:      String                              // SpiVersion.Current
  def capabilities:    Capabilities                        // §11 — features/outputs/options
  def intrinsics:      Map[ir.QualifiedName, IntrinsicImpl] // §8 — platform-native calls
  def acceptedSources: Set[String]                         // §9 — embedded source languages
  def compile(ir: NormalizedModule, opts: BackendOptions): CompileResult

trait InteractiveBackend extends Backend:
  def openSession(opts: BackendOptions): Session

trait Session extends AutoCloseable:
  def feed(block: NormalizedBlock): CompileResult
  def invokeHandler(handlerRef: SymbolRef, args: List[Value]): Value
  def close(): Unit

Full design + open questions: specs/backend-spi.md; out-of-process wire protocol: specs/backend-spi-protocol.md.

Bundled adapters (each in its own sbt subproject):

SubprojectBackend implidOutput kind
backend-jvm/codegen.JvmBackendjvmScalaSource
backend-js/codegen.JsBackendjsJavaScriptSource
backend-scalajs/codegen.ScalaJsPluginBackendscalajs-spaJavaScriptSource + HtmlSource
backend-interpreter/interpreter.InterpreterBackendintExecutionResult

InterpreterBackend is the only one extending InteractiveBackend (its Session underpins ssc serve).

Discovery. core/plugin/BackendRegistry combines two paths:

  1. ServiceLoader picks up every JAR with a

META-INF/services/scalascript.backend.spi.Backend entry — how the four bundled backends register, and how --plugin <jar> attaches a third-party JAR via URLClassLoader.

  1. plugin.yaml under $SCALASCRIPT_PLUGIN_PATH /

~/.scalascript/compiler/plugins/ (or --plugin-dir <dir>) declares subprocess plugins. SubprocessBackend wraps each as a stdio speaker.

BackendRegistry.lookup(id) consults both — in-process first, subprocess on miss (lazy spawn).

Symbol Table & Scopes

Scopes form a tree structure mirroring the heading hierarchy:

ModuleScope (root)
├── imports: [std/math → math]
├── symbols: [Circle, Rectangle, area]
└── children:
    ├── Scope("Shapes")
    │   ├── symbols: [Circle, Rectangle]
    │   └── children:
    │       ├── Scope("Circle") → [Circle class]
    │       └── Scope("Rectangle") → [Rectangle class]
    └── Scope("Calculations")
        └── symbols: [area]

Resolution rules:

  1. Look in current scope
  2. Look in parent scopes (up to root)
  3. Look in imports
  4. Look in prelude (built-in types)

Error Handling

Errors are accumulated, not fatal:

case class CompileError(
  phase: Phase,
  position: Position,
  message: String,
  severity: Severity
)

enum Severity:
  case Error, Warning, Info

Error recovery strategies:

IR Serialization

Typed IR can be serialized for:

Format options:

// IR dump example (JSON)
{
  "module": "geometry",
  "version": "1.0.0",
  "scopes": [
    {
      "name": "Shapes",
      "definitions": [
        {
          "kind": "class",
          "name": "Circle",
          "params": [{"name": "radius", "type": "Double"}]
        }
      ]
    }
  ]
}

Extension Points

Custom Backends

See docs/writing-a-backend.md for a step-by-step walk-through of a no-op backend in under 100 lines. Two distribution shapes:

drop a META-INF/services/scalascript.backend.spi.Backend entry listing your class, attach the JAR via --plugin <jar> (or place it on the bundled classpath). Worked example: examples/plugins/hello-backend/.

JSON. Drop a plugin.yaml under ~/.scalascript/compiler/plugins/; SubprocessBackend wraps the process and routes Backend.compile over stdio. Worked example: examples/plugins/canned-backend/.

Both shapes go through core/plugin/BackendRegistry and are interchangeable from the CLI's view.

Language Extensions

Future: macro system for compile-time metaprogramming.

IDE Integration

The compiler exposes:

Via Language Server Protocol (LSP) in future milestones.

Directory Structure (Implementation)

backend-spi/                # public, semver-stable
└── src/main/scala/scalascript/backend/spi/
    ├── Backend.scala          Backend / InteractiveBackend / Session
    ├── BackendOptions.scala
    ├── Capabilities.scala
    ├── CompileResult.scala
    ├── Diagnostic.scala
    ├── Feature.scala
    ├── OutputKind.scala
    ├── IntrinsicImpl.scala
    ├── SourceLanguage.scala
    └── SpiVersion.scala

ir/                         # NormalizedModule + JSON / MsgPack codecs
└── src/main/scala/scalascript/ir/Ir.scala

core/                       # parser, typer, normalize, registry, validation
└── src/main/scala/scalascript/
    ├── parser/Parser.scala
    ├── typer/Typer.scala, Types.scala
    ├── ast/                       # AST types + scalameta wrapper
    ├── imports/ImportResolver.scala
    ├── interpreter/Value.scala    # Computation Free monad (shared)
    ├── transform/Normalize.scala  # AST → IR
    ├── transform/Denormalize.scala# IR → AST (Stage 5 transitional)
    ├── transform/EffectAnalysis.scala
    ├── validate/CapabilityCheck.scala
    └── plugin/
        ├── BackendRegistry.scala
        ├── PluginManifest.scala
        ├── SubprocessBackend.scala
        └── WireProtocol.scala

backend-jvm/                # Backend adapter — JvmGen Scala source
backend-js/                 # Backend adapter — JsGen JavaScript
backend-scalajs/            # Backend adapter — Scala.js SPA
backend-interpreter/        # InteractiveBackend — tree-walking interpreter

cli/                        # ssc command + sbt-assembly fat jar

Maintenance Roadmap

The codebase is now broad enough that the next wins are small, conservative structure improvements rather than new abstractions.

After the leaf providers are split, introduce an internal CommandResult / ExitCode path so command tests do not need to intercept System.exit.

for signals, fetches, refs, and typed model scopes. Adopt it one backend at a time to prevent future View cases from being missed in collectors.

then consider structural field-path validation and semantic table lowering.

only when emitted output can be proved byte-identical.

families where it removes duplicate wiring without hiding sbt behavior.