Bosatsu packages

Yichus/Simulation

Builtin package (resource simulation.bosatsu). This is the complete package source used by this build. The export list names its public API; definitions below give the types and behavior.

New to the API? Start with Make a calculator or Build an API, then use this page to look up a definition.

package Yichus/Simulation

from Bosatsu/Predef import Int, String, Bool, List, Option, Some, None
from Yichus/Num/Float64 import Float64, int_to_Float64
from Yichus/Geometry import Line, Point

export (
  Discreteness(),
  ChartRange(),
  XAxisMode(),
  OutputFormat(),
  WidgetRepr(),
  InputSpec(),
  BoundInput(),
  BoundOutput(),
  InputEntry(),
  OutputEntry(),
  AssumptionConfig(),
  SimBuilder(),
  SimConfig(),
  InputHandle,
  OutputHandle,
  value,
  int_slider,
  int_number_input,
  int_dropdown,
  bool_checkbox,
  bool_dropdown,
  string_dropdown,
  bind_discrete,
  bind_continuous,
  outputs,
  currency_output,
  currency_output_chartable,
  number_output,
  number_output_chartable,
  percent_output,
  percent_output_chartable,
  sim,
  LineCurve(),
  SampledCurve(),
  HLineCurve(),
  IntersectionMark(),
  AreaBetweenMark(),
  PointMark(),
  RegionMark(),
  GraphXBound(),
  AreaSide(),
  Dataset(),
  SeriesLayer(),
  Graph(),
  curve,
  sampled_curve,
  hline_at,
  intersection,
  x_at,
  x_of,
  on_line,
  on_hline,
  area_between_marks,
  point_mark,
  region,
  dataset,
  smoothing,
  regression,
  projection,
  graph,
  graph_output
)

exposes (Yichus/Num/Float64, Yichus/Geometry)

# ============================================================================
# Discreteness — Discrete drives bar charts, Continuous drives line charts.
# Required at every bind site; no default.
# ============================================================================
enum Discreteness:
  Discrete
  Continuous

# ============================================================================
# ChartRange — explicit sample count and optional override of the x-axis
# input's slider min/max. None means "use the input's slider bounds."
# ============================================================================
struct ChartRange(
  samples: Int,
  min_override: Option[Int],
  max_override: Option[Int]
)

# ============================================================================
# Opaque InputHandle — constructor not re-exported, so user code can hold
# handles returned from bind_* helpers but cannot manufacture them.
# Stores the name (runtime key) and the typed default value.
# ============================================================================
struct InputHandle[a](name: String, default: a)

# Accessor that extracts the typed default from a handle.
# Exists so user-side `calculate(value(income), value(deductions))` works
# without exposing the handle's internal layout.
def value[a](h: InputHandle[a]) -> a:
  InputHandle(_, d) = h
  d

# ============================================================================
# XAxisMode — typed cross-references via InputHandle[Int].
# The Bosatsu typechecker enforces:
#   - referenced handle must be in lexical scope (no typos)
#   - referenced handle must be Int-valued (no Bool/String x-axes)
# These were the responsibilities of the removed Stage 1c validation pass.
# ============================================================================
enum XAxisMode:
  XAxisDisallowed
  XAxisPinned(input: InputHandle[Int], range: ChartRange)
  XAxisDefault(
    default_input: InputHandle[Int],
    eligible: List[InputHandle[Int]],
    range: ChartRange
  )
  XAxisOptional(
    default_input: Option[InputHandle[Int]],
    eligible: List[InputHandle[Int]],
    range: ChartRange
  )

# ============================================================================
# OutputFormat — the three concrete display formats. Chart/Table are gone;
# charting is handled per-output via XAxisMode.
# ============================================================================
enum OutputFormat:
  Currency
  Percent
  Number
  GraphKind

# ============================================================================
# OutputHandle — opaque, defined for symmetry with InputHandle.
# Currently constructed internally but not surfaced through output helpers
# because outputs aren't cross-referenced today.
# ============================================================================
struct OutputHandle[a](name: String)

# ============================================================================
# WidgetRepr — monomorphic visual representation. Carries only what the
# JS renderer needs; defaults and value-type info live in InputSpec/BoundInput.
# ============================================================================
enum WidgetRepr:
  WSlider(min: Int, max: Int, step: Int)
  WNumberInput(min: Int, max: Int, step: Int)
  WCheckbox
  WDropdownInt(options: List[(String, Int)])
  WDropdownBool(options: List[(String, Bool)])
  WDropdownString(options: List[(String, String)])

# ============================================================================
# InputSpec[a] — the typed bundle produced by smart constructors. Pairs the
# visual WidgetRepr with the typed default and the bosatsu type repr string.
# `bosatsu_type_repr` is a typed payload concatenated into pin formula
# synthesis later (Stage 3); never inspected via match/switch.
# ============================================================================
struct InputSpec[a](
  bosatsu_type_repr: String,
  default: a,
  widget_repr: WidgetRepr
)

# ============================================================================
# Smart constructors — each returns an InputSpec[a] for a fixed `a`.
# The phantom-parameter loophole that allowed `Slider(...): Widget[Bool]`
# is closed: int_slider literally cannot return InputSpec[Bool].
# ============================================================================
def int_slider(min_v: Int, max_v: Int, step: Int, default: Int) -> InputSpec[Int]:
  InputSpec("Int", default, WSlider(min_v, max_v, step))

def int_number_input(min_v: Int, max_v: Int, step: Int, default: Int) -> InputSpec[Int]:
  InputSpec("Int", default, WNumberInput(min_v, max_v, step))

def int_dropdown(options: List[(String, Int)], default: Int) -> InputSpec[Int]:
  InputSpec("Int", default, WDropdownInt(options))

def bool_checkbox(default: Bool) -> InputSpec[Bool]:
  InputSpec("Bool", default, WCheckbox)

def bool_dropdown(options: List[(String, Bool)], default: Bool) -> InputSpec[Bool]:
  InputSpec("Bool", default, WDropdownBool(options))

def string_dropdown(options: List[(String, String)], default: String) -> InputSpec[String]:
  InputSpec("String", default, WDropdownString(options))

# ============================================================================
# BoundInput[a] — the typed bundle stored per input in SimBuilder.
# The `default: a` field ties the value type to the rest of the record.
# ============================================================================
struct BoundInput[a](
  bosatsu_type_repr: String,
  default: a,
  widget_repr: WidgetRepr,
  discreteness: Discreteness,
  name: String,
  label: String,
  handle: InputHandle[a]
)

# ============================================================================
# BoundOutput[a] — similar typed bundle for outputs. Carries the typed
# default value so the existential pack matches across the output list.
# ============================================================================
struct BoundOutput[a](
  label: String,
  format: OutputFormat,
  primary: Bool,
  x_axis: XAxisMode,
  default_value: a,
  handle: OutputHandle[a]
)

# ============================================================================
# Existential wrappers — `exists a.` lives inside these single-field structs
# so SimBuilder's lists are heterogeneous in `a`. Same shape as
# EntityHandler.ops in dynamo-transform.bosatsu.
# ============================================================================
struct InputEntry(payload: exists a. BoundInput[a])
struct OutputEntry(payload: exists a. BoundOutput[a])

# ============================================================================
# Configuration for "What if?" assumption toggles.
# ============================================================================
struct AssumptionConfig(
  name: String,
  description: String,
  variants: List[(String, String)]
)

# ============================================================================
# SimBuilder — accumulates typed input/output entries via continuation
# passing in bind_discrete / bind_continuous / outputs.
# ============================================================================
struct SimBuilder(
  inputs: List[(String, InputEntry)],
  outputs: List[(String, OutputEntry)]
)

# ============================================================================
# SimConfig — top-level metadata. `sweeps` is gone; chartable outputs
# subsume it per-output via XAxisMode.
# ============================================================================
struct SimConfig(
  name: String,
  description: String,
  package_name: String,
  function_name: String,
  inputs: List[(String, InputEntry)],
  outputs: List[(String, OutputEntry)],
  assumptions: List[AssumptionConfig],
  snippet_max_lines: Int
)

# ============================================================================
# bind_discrete — polymorphic in `a`. Builds the InputHandle, hands it to
# the continuation, then prepends the BoundInput to SimBuilder.inputs.
# ============================================================================
def bind_discrete[a](
  name: String,
  label: String,
  spec: InputSpec[a],
  next: InputHandle[a] -> SimBuilder
) -> SimBuilder:
  InputSpec(type_repr, default_v, widget_repr) = spec
  handle = InputHandle(name, default_v)
  built = next(handle)
  match built:
    case SimBuilder(rest_inputs, rest_outputs):
      bound = BoundInput(type_repr, default_v, widget_repr, Discrete, name, label, handle)
      SimBuilder(
        [(name, InputEntry(bound)), *rest_inputs],
        rest_outputs
      )

# ============================================================================
# bind_continuous — Int-restricted. Continuous only makes sense for numeric
# inputs; trying to mark a Bool/String input as Continuous is a type error.
# ============================================================================
def bind_continuous(
  name: String,
  label: String,
  spec: InputSpec[Int],
  next: InputHandle[Int] -> SimBuilder
) -> SimBuilder:
  InputSpec(type_repr, default_v, widget_repr) = spec
  handle = InputHandle(name, default_v)
  built = next(handle)
  match built:
    case SimBuilder(rest_inputs, rest_outputs):
      bound = BoundInput(type_repr, default_v, widget_repr, Continuous, name, label, handle)
      SimBuilder(
        [(name, InputEntry(bound)), *rest_inputs],
        rest_outputs
      )

# ============================================================================
# outputs — receives the calculator's result via continuation, expects a
# list of output entries built from it.
# ============================================================================
def outputs[r](
  result: r,
  build: r -> List[(String, OutputEntry)]
) -> SimBuilder:
  SimBuilder([], build(result))

# ============================================================================
# Output helpers — each produces a (name, OutputEntry) pair. The non-
# chartable variants emit XAxisDisallowed; the *_chartable variants take an
# explicit XAxisMode. The `*_handle` variant could later expose the handle
# but today's outputs aren't cross-referenced anywhere.
# ============================================================================
def currency_output[a](
  name: String,
  label: String,
  val: a,
  primary: Bool
) -> (String, OutputEntry):
  handle = OutputHandle(name)
  bound = BoundOutput(label, Currency, primary, XAxisDisallowed, val, handle)
  (name, OutputEntry(bound))

def currency_output_chartable[a](
  name: String,
  label: String,
  val: a,
  primary: Bool,
  x_axis: XAxisMode
) -> (String, OutputEntry):
  handle = OutputHandle(name)
  bound = BoundOutput(label, Currency, primary, x_axis, val, handle)
  (name, OutputEntry(bound))

def number_output[a](
  name: String,
  label: String,
  val: a,
  primary: Bool
) -> (String, OutputEntry):
  handle = OutputHandle(name)
  bound = BoundOutput(label, Number, primary, XAxisDisallowed, val, handle)
  (name, OutputEntry(bound))

def number_output_chartable[a](
  name: String,
  label: String,
  val: a,
  primary: Bool,
  x_axis: XAxisMode
) -> (String, OutputEntry):
  handle = OutputHandle(name)
  bound = BoundOutput(label, Number, primary, x_axis, val, handle)
  (name, OutputEntry(bound))

def percent_output[a](
  name: String,
  label: String,
  val: a,
  primary: Bool
) -> (String, OutputEntry):
  handle = OutputHandle(name)
  bound = BoundOutput(label, Percent, primary, XAxisDisallowed, val, handle)
  (name, OutputEntry(bound))

def percent_output_chartable[a](
  name: String,
  label: String,
  val: a,
  primary: Bool,
  x_axis: XAxisMode
) -> (String, OutputEntry):
  handle = OutputHandle(name)
  bound = BoundOutput(label, Percent, primary, x_axis, val, handle)
  (name, OutputEntry(bound))

# ============================================================================
# sim — assembles the top-level SimConfig from a SimBuilder and metadata.
# `sweeps` parameter is gone; chartable outputs subsume the use case.
# ============================================================================
def sim(
  name: String,
  description: String,
  package_name: String,
  function_name: String,
  builder: SimBuilder,
  assumptions: List[AssumptionConfig],
  snippet_max_lines: Int
) -> SimConfig:
  match builder:
    case SimBuilder(input_list, output_list):
      SimConfig(
        name,
        description,
        package_name,
        function_name,
        input_list,
        output_list,
        assumptions,
        snippet_max_lines
      )

# ============================================================================
# Graph output — multi-curve charts with auto and explicit marks.
# Auto helpers (`intersection`, `area_between_marks`) accept only Line-backed
# sides (`LineCurve` / `HLineCurve`), so sampled curves cannot be passed to a
# solvable mark (unrepresentable). Explicit helpers (`point_mark`, `region`)
# carry an author derivation; the simapp checker verifies that derivation's
# Matchless dataflow reaches the referenced curves.
# ============================================================================

struct LineCurve(
  id: String,
  label: String,
  line: Line
)

struct SampledCurve(
  id: String,
  label: String,
  samples: List[Point]
)

struct IntersectionMark(
  id: String,
  label: String,
  a: LineCurve,
  b: LineCurve
)

struct HLineCurve(
  id: String,
  label: String,
  mark: IntersectionMark
)

enum GraphXBound:
  XAt(x: Float64)
  XOf(mark: IntersectionMark)

# Area sides are Line-backed only: a compute Line, or a horizontal through
# an auto intersection's y. SampledCurve cannot inhabit this type.
enum AreaSide:
  OfLine(c: LineCurve)
  OfHLine(c: HLineCurve)

struct AreaBetweenMark(
  id: String,
  label: String,
  upper: AreaSide,
  lower: AreaSide,
  from_x: GraphXBound,
  to_x: GraphXBound
)

struct PointMark(
  id: String,
  label: String,
  point: Point,
  along: SampledCurve
)

struct RegionMark(
  id: String,
  label: String,
  area: Float64,
  along: SampledCurve
)

# A named point series from the compute Result, drawn as scatter dots.
# Layers are functions of a Dataset plus optional slider parameters.
struct Dataset(
  id: String,
  label: String,
  points: List[Point]
)

enum SeriesLayer:
  Smoothing(id: String, label: String, data: Dataset, window: InputHandle[Int])
  Regression(id: String, label: String, data: Dataset)
  # A dashed overlay of `data`. The points are whatever the compute
  # program produced — a vintage forecast, an exponential path, OLS
  # samples from project_linear, etc. The layer does not pick a form.
  Projection(id: String, label: String, data: Dataset)

struct Graph(
  graph_id: String,
  title: String,
  x_label: String,
  y_label: String,
  x_min: Float64,
  x_max: Float64,
  line_curves: List[LineCurve],
  hline_curves: List[HLineCurve],
  sampled_curves: List[SampledCurve],
  intersections: List[IntersectionMark],
  areas: List[AreaBetweenMark],
  point_marks: List[PointMark],
  regions: List[RegionMark],
  datasets: List[Dataset],
  layers: List[SeriesLayer]
)

def curve(id: String, label: String, line: Line) -> LineCurve:
  LineCurve(id, label, line)

def sampled_curve(id: String, label: String, samples: List[Point]) -> SampledCurve:
  SampledCurve(id, label, samples)

def hline_at(id: String, label: String, mark: IntersectionMark) -> HLineCurve:
  HLineCurve(id, label, mark)

def intersection(id: String, label: String, a: LineCurve, b: LineCurve) -> IntersectionMark:
  IntersectionMark(id, label, a, b)

def x_at(x: Int) -> GraphXBound:
  XAt(int_to_Float64(x))

def x_of(mark: IntersectionMark) -> GraphXBound:
  XOf(mark)

def on_line(c: LineCurve) -> AreaSide:
  OfLine(c)

def on_hline(c: HLineCurve) -> AreaSide:
  OfHLine(c)

def area_between_marks(
  id: String,
  label: String,
  upper: AreaSide,
  lower: AreaSide,
  from_x: GraphXBound,
  to_x: GraphXBound
) -> AreaBetweenMark:
  AreaBetweenMark(id, label, upper, lower, from_x, to_x)

def point_mark(
  id: String,
  label: String,
  point: Point,
  along: SampledCurve
) -> PointMark:
  PointMark(id, label, point, along)

def region(
  id: String,
  label: String,
  area: Float64,
  along: SampledCurve
) -> RegionMark:
  RegionMark(id, label, area, along)

def dataset(id: String, label: String, points: List[Point]) -> Dataset:
  Dataset(id, label, points)

def smoothing(
  id: String,
  label: String,
  data: Dataset,
  window: InputHandle[Int]
) -> SeriesLayer:
  Smoothing(id, label, data, window)

def regression(id: String, label: String, data: Dataset) -> SeriesLayer:
  Regression(id, label, data)

def projection(id: String, label: String, data: Dataset) -> SeriesLayer:
  Projection(id, label, data)

def graph(
  graph_id: String,
  title: String,
  x_label: String,
  y_label: String,
  x_min: Int,
  x_max: Int,
  line_curves: List[LineCurve],
  hline_curves: List[HLineCurve],
  sampled_curves: List[SampledCurve],
  intersections: List[IntersectionMark],
  areas: List[AreaBetweenMark],
  point_marks: List[PointMark],
  regions: List[RegionMark],
  datasets: List[Dataset],
  layers: List[SeriesLayer]
) -> Graph:
  Graph(
    graph_id,
    title,
    x_label,
    y_label,
    int_to_Float64(x_min),
    int_to_Float64(x_max),
    line_curves,
    hline_curves,
    sampled_curves,
    intersections,
    areas,
    point_marks,
    regions,
    datasets,
    layers
  )

def graph_output(
  name: String,
  label: String,
  g: Graph,
  primary: Bool
) -> (String, OutputEntry):
  handle = OutputHandle(name)
  bound = BoundOutput(label, GraphKind, primary, XAxisDisallowed, g, handle)
  (name, OutputEntry(bound))