///|
fn object(value : Json) -> Map[String, Json] raise SolveError {
match value {
Object(items) => items
_ => raise Invalid("expected object")
}
}
///|
fn array(value : Json) -> Array[Json] raise SolveError {
match value {
Array(items) => items
_ => raise Invalid("expected array")
}
}
///|
fn number(value : Json) -> Double raise SolveError {
match value {
Number(n, ..) =>
if n == n && n >= -1.0e300 && n <= 1.0e300 {
n
} else {
raise Invalid("nonfinite number")
}
_ => raise Invalid("expected number")
}
}
///|
fn integer(value : Json) -> Int raise SolveError {
let n = number(value)
if n < -2147483648.0 || n > 2147483647.0 || n != n.to_int().to_double() {
raise Invalid("expected signed 32-bit integer")
}
n.to_int()
}
///|
fn string(value : Json) -> String raise SolveError {
match value {
String(s) => s
_ => raise Invalid("expected string")
}
}
///|
fn boolean(value : Json) -> Bool raise SolveError {
match value {
True => true
False => false
_ => raise Invalid("expected boolean")
}
}
///|
fn required(items : Map[String, Json], key : String) -> Json raise SolveError {
match items.get(key) {
Some(value) => value
None => raise Invalid("missing field: " + key)
}
}
///|
fn read_rules(value : Json) -> RuleModel raise SolveError {
let items = object(value)
let labels = array(required(items, "labels")).map(string)
let weights = if items.get("weights") is Some(values) {
array(values).map(number)
} else {
Array::make(labels.length(), 1.0)
}
let neighbors = array(required(items, "neighbors")).map(row => {
array(row).map(list => array(list).map(integer))
})
{ labels, neighbors, weights, }
}
///|
fn run_request(input : String) -> Json raise {
if input.length() > 16000000 {
raise Invalid("JSON request limit")
}
let job = object(@json.parse(input))
let mode = string(job.get("mode").unwrap_or("overlap"))
let width = integer(job.get("width").unwrap_or(32))
let height = integer(job.get("height").unwrap_or(32))
let seed_value = number(job.get("seed").unwrap_or(1))
if seed_value < 0.0 ||
seed_value > 4294967295.0 ||
seed_value != seed_value.to_int64().to_double() {
raise Invalid("seed must be UInt32")
}
let seed = seed_value.to_int64().to_int().reinterpret_as_uint()
let periodic = boolean(job.get("periodic").unwrap_or(false.to_json()))
let budget = integer(job.get("budget").unwrap_or(10000000))
let pins = array(job.get("pins").unwrap_or([])).map(pair => {
let values = array(pair)
if values.length() != 2 {
raise Invalid("pin must be [cell,tile]")
}
(integer(values[0]), integer(values[1]))
})
let restrictions = array(job.get("restrictions").unwrap_or([])).map(pair => {
let values = array(pair)
if values.length() != 2 {
raise Invalid("restriction must be [cell,[tiles]]")
}
(integer(values[0]), array(values[1]).map(integer))
})
if mode == "learn" || mode == "overlap" {
if !restrictions.is_empty() {
raise Invalid(
"cell restrictions require rules or tiled mode; overlap uses pixel pins",
)
}
let sample = array(required(job, "sample")).map(integer)
let sample_width = integer(required(job, "sampleWidth"))
let sample_height = integer(required(job, "sampleHeight"))
let size = integer(job.get("size").unwrap_or(3))
let symmetry = integer(job.get("symmetry").unwrap_or(1))
let periodic_input = boolean(
job.get("periodicInput").unwrap_or(true.to_json()),
)
let model = learn_patterns(
sample,
sample_width,
sample_height,
size,
symmetry~,
periodic_input~,
)
if mode == "learn" {
return { "ok": true.to_json(), "model": ToJson::to_json(model) }
}
let pixels = model.generate(
width,
height,
seed~,
periodic~,
pins~,
ground=boolean(job.get("ground").unwrap_or(false.to_json())),
budget~,
)
return match pixels {
None => { "ok": true.to_json(), "status": "unsat" }
Some(pixels) =>
{
"ok": true.to_json(),
"status": "solved",
"width": width.to_json(),
"height": height.to_json(),
"pixels": pixels.to_json(),
"patternCount": model.patterns.length().to_json(),
}
}
}
let tiled : TiledModel? = if mode == "expand" || mode == "tiled" {
let definitions = array(required(job, "tiles")).map(value => {
let items = object(value)
let pixels = array(required(items, "pixels")).map(values => {
array(values).map(integer)
})
TileDefinition::{
name: string(required(items, "name")),
symmetry: string(items.get("symmetry").unwrap_or("X")),
weight: number(items.get("weight").unwrap_or(1)),
pixels,
}
})
let neighbors = array(required(job, "neighbors")).map(pair => {
let pair = array(pair)
if pair.length() != 2 {
raise Invalid("neighbor must be [left,right]")
}
(string(pair[0]), string(pair[1]))
})
let subset = array(job.get("subset").unwrap_or([])).map(string)
Some(
expand_tiles(
definitions,
integer(required(job, "tileSize")),
neighbors,
subset~,
),
)
} else {
None
}
if mode == "expand" {
return { "ok": true.to_json(), "model": ToJson::to_json(tiled.unwrap()) }
}
let model = if tiled is Some(tiled) {
tiled.rules
} else if mode == "rules" || mode == "validate" {
read_rules(required(job, "model"))
} else {
raise Invalid("unknown request mode")
}
if mode == "validate" {
let solution : Solution = {
width,
height,
tiles: array(required(job, "tiles")).map(integer),
decisions: 0,
backtracks: 0,
}
return {
"ok": true.to_json(),
"valid": solution
.validate_rules(model, pins~, restrictions~, periodic~)
.to_json(),
}
}
let solution = solve_rules(
model,
width,
height,
seed~,
periodic~,
budget~,
pins~,
restrictions~,
)
match solution {
None => { "ok": true.to_json(), "status": "unsat" }
Some(solution) => {
let result : Map[String, Json] = {
"ok": true.to_json(),
"status": "solved",
"solution": ToJson::to_json(solution),
}
if tiled is Some(tiled) {
result["pixels"] = tiled.render(solution).to_json()
result["width"] = (width * tiled.tile_size).to_json()
result["height"] = (height * tiled.tile_size).to_json()
}
result.to_json()
}
}
}
///|
/// Numeric-pixel/file-independent request interface shared by the Node worker and
/// browser worker. File decoding belongs to the host; solving remains MoonBit.
pub fn evaluate_json(input : String) -> String {
(run_request(input) catch {
BudgetExhausted =>
{
"ok": false.to_json(),
"status": "budget-exhausted",
"error": "search budget exhausted",
}
e => { "ok": false.to_json(), "error": Repr(e).to_string().to_json() }
}).stringify()
}