Brief.kt
src/jvmMain/kotlin/afield/lang/brief/Brief.kt · 969 lines
@file:afield.runtime.completion.CompletionCategory("Solvers")
package afield.lang.brief
import kotlin.math.abs
import kotlin.math.min
Brief is a text notation for weighted geometric constraints: points, lines, planes and panels, and the relations they should satisfy, each with a strength.
A brief is what an architect writes before there is a building: a statement of what has to be
true, in no particular order, about things that do not exist yet. The vocabulary is the weighted
constraint set of afield.math.optimization.ceres.ConstraintScope, which otherwise is reachable
only from a Kotlin receiver lambda; Brief makes it available as text.
A program is a #brief-tagged region (a Kotlin triple-quoted string whose first line is #brief,
the same Route-A shape RectSL uses) or any string handed to compileBrief:
#brief
point west at (0, 0)
point east at (9, 0)
fix west east
prefer spine at 1/3 along (west, east)
insist distance(west, spine) = 6
Weight is a number
Every constraint statement starts with a strength, and the slot takes either a word from a decade
ladder (BriefStrength) or a bare number. prefer and insist differ by a factor of a thousand,
and the solved answer moves accordingly. The ladder words are names for numbers, so a bare number
is accepted in the same position.
Forward reference is the normal case
Lines, planes and panels resolve in a second pass, so a relation may cite geometry declared below
it. Points are declared on use: naming a point that was never declared creates one, seeded on a
deterministic golden-angle circle so a brief of pure relations starts from a non-degenerate spread
rather than a pile at the origin. The invented points are listed in BriefProgram.inventedPoints.
Two kinds of relation, one statement shape
spine.length = 4 is a relation over a property; spine parallel ridge is a relation over
elements. Both are strength relation, and a relation is an equation when a top-level =, >=
or <= appears and a predicate otherwise. Properties and functions of elements are terms, so
west.length = east.length and angle(spine, ridge) = 30deg sit in one grammar. planar,
mirrors and fix have no scalar left-hand side and stay predicates. The grammar is written up in
dev_docs/brief_notation.md.
Diagnostics are located and named
Every failure is a BriefDiagnostic with a line, a column, a span and, where a name was misspelled, a did-you-mean drawn from the names actually declared. compileBrief does not throw a bare Kotlin exception at a program that merely reads wrong.
compileBrief is a pure function of text, with no Box, output or server. toGeometryModel turns
the result into a GeometryModel, and solve solves it.
object Brief
The tag and language id that name Brief to the editor and to embedded-region detection.
object Brief {
const val TAG: String = "#brief"
The first-line tag that marks a #brief embedded region.
const val ID: String = "brief"
The language id, shared by the editor extension and the Monaco language.
Diagnostics
enum class BriefSeverity { ERROR, WARNING }
Whether a diagnostic stops the compile.
One error anywhere makes compileBrief throw BriefException with all of them; a warning rides along in BriefProgram.diagnostics and the program is still usable.
data class BriefDiagnostic
One located complaint about a program.
line and column are 1-based and count the source as handed to compileBrief, including the
#brief tag line if there is one, so a column number can be trusted against the editor. span is
the length of the offending text, for underlining. hint is the actionable half and is separated
from message so a UI can show one and not the other.
data class BriefDiagnostic(
/** Whether this stops the compile. */
val severity: BriefSeverity,
/** 1-based line in the source as handed to [compileBrief], `#brief` tag line included. */
val line: Int,
/** 1-based column of the offending text on that line. */
val column: Int,
/** How many characters the offending text runs to, for underlining it. */
val span: Int,
/** What is wrong, with no position in it; [format] prepends that. */
val message: String,
/** What to do about it, where there is something to say. */
val hint: String? = null,
) {
fun format(): String
3:8 error: unknown line 'spien' — did you mean 'spine'?, the form a box prints.
fun format(): String = buildString {
append(line).append(':').append(column).append(' ')
append(if (severity == BriefSeverity.ERROR) "error" else "warning").append(": ")
append(message)
hint?.let { append(" — ").append(it) }
}
override fun toString(): String = format()
format, so printing a diagnostic gives the located form rather than the data class one.
class BriefException(val diagnostics: List<BriefDiagnostic>) :
Thrown by compileBrief when a program has errors, carrying all of them rather than the first.
The message is every diagnostic, one per line, because a brief with four misspellings should tell you about four misspellings.
class BriefException(val diagnostics: List<BriefDiagnostic>) :
IllegalArgumentException(
"brief has ${diagnostics.count { it.severity == BriefSeverity.ERROR }} error(s):\n" +
diagnostics.joinToString("\n") { " " + it.format() }
)
Strength
enum class BriefStrength(val word: String, val weight: Double)
The strength ladder: four words that are names for four numbers, three decades apart.
The spacing is chosen, not arbitrary. DEMAND is 1e6 because that is
afield.math.optimization.ceres.ConstraintScope.lock's own default weight, so demand means exactly what the
Kotlin DSL already means by "hold this". PREFER is 1.0, the DSL's default weight and the scale
a length residual is measured on. Three decades between rungs is far enough that a lower rung
loses a direct fight by a factor of a thousand — visible in a figure, not merely in a residual —
and close enough that a thousand whispers can still outvote one prefer, which is the behaviour
a least-squares compromise should have.
The warning that comes with it, inherited from the underlying vocabulary: weights only compare
between residuals that share units. An area residual is off every length residual by a factor of
length², so insist roof.area = 12 against prefer spine.length = 4 on a building measured in
millimetres is not a fight between 1e3 and 1e0. Where a brief mixes units, say the numbers.
enum class BriefStrength(val word: String, val weight: Double) {
1e-3 — a tie-breaker. Loses to anything, shapes the answer where nothing else speaks.
WHISPER("whisper", 1e-3),
1e0 — the default, and the scale a length residual is measured on.
PREFER("prefer", 1.0),
1e3 — wins a direct fight against prefer by a factor of a thousand.
INSIST("insist", 1e3),
1e6 — ConstraintScope.lock's own weight. Still soft; nothing here is a hard constraint.
DEMAND("demand", 1e6),
;
companion object
val byWord: Map<String, BriefStrength> = entries.associateBy { it.word }
val DEFAULT: BriefStrength = PREFER
What an unqualified statement means.
Source positions and the AST
data class BriefSpan(val line: Int, val column: Int, val length: Int)
Where something is, for a diagnostic. 1-based, counting the source as handed to the compiler.
data class BriefName(val text: String, val at: BriefSpan)
A name as written, with where it was written — so an unresolved name can point at itself.
data class BriefName(val text: String, val at: BriefSpan) {
override fun toString(): String = text
The name as written, so a diagnostic can interpolate it without reaching for text.
enum class BriefElementKind(val noun: String)
The kinds of thing a brief can declare. The resolver keys its namespaces on this.
enum class BriefElementKind(val noun: String) {
POINT("point"),
LINE("line"),
PLANE("plane"),
PANEL("panel"),
}
sealed interface BriefDeclaration
A declaration statement.
sealed interface BriefDeclaration {
val name: BriefName
The name this statement declares, located so a redeclaration can point at the second one.
val kind: BriefElementKind
Which of the four namespaces the name goes into; each subtype fixes its own.
data class Point(override val name: BriefName, val x: Double, val y: Double, val z: Double) : BriefDeclaration
point west at (0, 0) — the one declaration that carries coordinates. z defaults to 0.
override val kind get() = BriefElementKind.POINT
data class Line(override val name: BriefName, val a: BriefName, val b: BriefName) : BriefDeclaration
line spine from west to east — two endpoints, by name, resolved in the second pass.
override val kind get() = BriefElementKind.LINE
data class Plane
plane roof through a b c — three points, by name.
data class Plane(
override val name: BriefName,
val a: BriefName,
val b: BriefName,
val c: BriefName,
) : BriefDeclaration {
override val kind get() = BriefElementKind.PLANE
data class Panel
panel bay corners a b c d — four corners, by name, which resolution requires to be distinct.
data class Panel(
override val name: BriefName,
val a: BriefName,
val b: BriefName,
val c: BriefName,
val d: BriefName,
) : BriefDeclaration {
override val kind get() = BriefElementKind.PANEL
enum class BriefComparison(val symbol: String) { EQ("="), AT_LEAST(">="), AT_MOST("<=") }
The comparison in an equation.
sealed interface BriefTerm
The left or right of an equation.
Literal carries radians so 30deg and 0.5236 are distinguishable after parsing: a cosine
target and an angle target are different constraints, and a program that writes deg where a
cosine was wanted deserves to be told.
sealed interface BriefTerm {
val at: BriefSpan
Where the term was written, so an equation can underline the half that is wrong.
data class Literal(val value: Double, val radians: Boolean, override val at: BriefSpan) : BriefTerm
A number. value is in radians when a deg suffix was written, and radians records that.
data class Literal(val value: Double, val radians: Boolean, override val at: BriefSpan) : BriefTerm
data class Property(val element: BriefName, val property: String, override val at: BriefSpan) : BriefTerm
spine.length, roof.area, ridge.z.
data class Property(val element: BriefName, val property: String, override val at: BriefSpan) : BriefTerm
data class Call(val function: String, val arguments: List<BriefName>, override val at: BriefSpan) : BriefTerm
angle(spine, ridge), distance(a, b), dihedral(edge, a, b).
data class Call(val function: String, val arguments: List<BriefName>, override val at: BriefSpan) : BriefTerm
}
sealed interface BriefRelation
The relation half of a constraint statement.
sealed interface BriefRelation {
val at: BriefSpan
Where the relation starts: the first token after the strength, if one was written.
data class Equation
left (comparison) right — a relation over properties.
data class Equation(
val left: BriefTerm,
val comparison: BriefComparison,
val right: BriefTerm,
override val at: BriefSpan,
) : BriefRelation
data class Predicate
subjects verb objects… — a relation over elements.
verb is one of BriefVerbs.all; modifier is the trailing offset 0.3 / clearance 0.2 /
in xy qualifier, already parsed. vector is the (x, y, z) a direction verb takes.
data class Predicate(
val verb: String,
val subjects: List<BriefName>,
val objects: List<BriefName>,
val number: Double? = null,
val vector: Triple<Double, Double, Double>? = null,
val axes: String? = null,
override val at: BriefSpan,
) : BriefRelation
}
data class BriefConstraint
One strength relation line. strengthWritten is null when the statement took the default.
data class BriefConstraint(
/** The weight the statement's residuals carry, already resolved from the word or the number. */
val strength: Double,
/** The strength exactly as typed, or null where the statement took the verb's default. */
val strengthWritten: String?,
/** The relation half: an equation over properties, or a predicate over elements. */
val relation: BriefRelation,
/** Where the whole statement starts, strength included. [BriefRelation.at] skips it. */
val at: BriefSpan,
) {
val label: String
The source line, reconstructed enough for a residual report to name the statement.
val label: String
get() = (strengthWritten ?: BriefStrength.byWord.values.firstOrNull { it.weight == strength }?.word
?: trimNumber(strength)) + " " + describeRelation(relation)
}
internal fun defaultStrength(relation: BriefRelation): Double
What a statement means when it does not say.
prefer for everything, with two exceptions that exist because the Kotlin DSL already has them.
fix is ConstraintScope.lock, whose own default weight is 1e6, and hold / all loose are
weak / weakAll, whose default is 1e-6. Making a bare fix p mean prefer would have been
consistent and wrong: lock at weight 1 does not hold anything, and a notation whose commonest
fixture silently does nothing is worse than one with two documented exceptions. An explicit strength
always wins.
internal fun defaultStrength(relation: BriefRelation): Double = when {
relation !is BriefRelation.Predicate -> BriefStrength.DEFAULT.weight
relation.verb == "fix" -> BriefStrength.DEMAND.weight
relation.verb == "hold" || relation.verb == "all-loose" -> 1e-6
else -> BriefStrength.DEFAULT.weight
}
internal fun describeRelation(relation: BriefRelation): String
internal fun describeRelation(relation: BriefRelation): String = when (relation) {
is BriefRelation.Equation ->
"${describeTerm(relation.left)} ${relation.comparison.symbol} ${describeTerm(relation.right)}"
is BriefRelation.Predicate -> buildString {
// A prefix verb reads before its subjects and an infix one after, so a label reproduces the
// statement rather than a normalised form nobody typed.
val prefix = relation.verb in BriefVerbs.prefix || relation.verb.startsWith("all-")
if (prefix) append(relation.verb.replace("all-", "all "))
if (relation.subjects.isNotEmpty()) {
if (prefix) append(' ')
append(relation.subjects.joinToString(" "))
if (!prefix) append(' ')
}
if (!prefix) append(relation.verb)
if (relation.objects.isNotEmpty()) append(' ').append(relation.objects.joinToString(" "))
relation.axes?.let { append(" in ").append(it) }
relation.number?.let { append(' ').append(trimNumber(it)) }
relation.vector?.let { append(" (").append(trimNumber(it.first)).append(", ")
.append(trimNumber(it.second)).append(", ").append(trimNumber(it.third)).append(')') }
}
}
internal fun describeTerm(term: BriefTerm): String
internal fun describeTerm(term: BriefTerm): String = when (term) {
is BriefTerm.Literal -> if (term.radians) "${trimNumber(Math.toDegrees(term.value))}deg" else trimNumber(term.value)
is BriefTerm.Property -> "${term.element.text}.${term.property}"
is BriefTerm.Call -> "${term.function}(${term.arguments.joinToString(", ") { it.text }})"
}
internal fun trimNumber(value: Double): String
internal fun trimNumber(value: Double): String =
if (value == value.toLong().toDouble() && abs(value) < 1e15) value.toLong().toString() else value.toString()
data class BriefSettle(val weight: Double?, val at: BriefSpan)
A settle directive: the whole-cloud prior weight, or null for "none".
data class BriefRobust(val kind: String, val scale: Double, val at: BriefSpan)
A robust directive: the loss every residual is wrapped in.
A brief's default is robust none — plain least squares — and that default is a decision, not an
inheritance. The underlying CeresProgram defaults to Huber(1e-3), under which the cost of a
residual larger than 1e-3 is linear, so two statements fighting over one unknown settle at the
weighted median of their targets rather than the mean, and the heavier one wins outright at any
ratio. A notation whose whole premise is that prefer and insist name different numbers cannot
ship on a loss that makes them name the same outcome. robust huber 1e-3 gets the old behaviour
back, and the two are visibly different figures.
The vocabulary tables — one place, so the editor extension and the parser cannot disagree
object BriefVerbs
Every reserved word in the notation, grouped the way a diagnostic wants to talk about them.
Two properties this table is responsible for. It is what makes NAME+ VERB unambiguous — a
declaration may not name an element with a reserved word, so a leading run of names ends exactly
where a verb begins. And it is the vocabulary the editor extension is checked against, in both
directions, by BriefEditorExtensionTest, which is the shape-4 guard the anatomy page recommends
for anything mirrored across the JVM/browser split.
object BriefVerbs {
val infix: Set<String>
Verbs that come after their subjects: spine parallel ridge.
val infix: Set<String> = setOf(
"parallel", "perpendicular", "same-vector", "on", "inside", "mirrors", "toward",
"vanishes-at", "along", "faces", "coincident", "colinear", "at",
"planar", "level", "same-x", "same-y", "same-z", "same-xy", "same-xz", "same-yz",
)
val prefix: Set<String> = setOf("fix", "hold", "all")
Verbs that come first: fix west east.
val declarations: Set<String> = setOf("point", "line", "plane", "panel")
Words that introduce a declaration.
val directives: Set<String> = setOf("settle", "robust")
Words that introduce a directive.
val losses: Set<String> = setOf("none", "huber", "soft-l1", "cauchy")
The robust-loss kinds robust accepts, mapped to RobustLoss.Kind.
val functions: Set<String>
Function names usable as an equation term.
val functions: Set<String> = setOf("distance", "angle", "cos-angle", "dihedral", "dot", "raw-dot")
val properties: Set<String> = setOf("length", "area", "x", "y", "z")
Properties reachable with element.property.
val glue: Set<String>
Glue words that are reserved but carry no relation of their own.
val glue: Set<String> = setOf(
"at", "from", "to", "through", "corners", "across", "offset", "clearance", "in",
"loose", "above-ground", "none", "deg",
)
val strengths: Set<String> = BriefStrength.byWord.keys
The ladder words, from BriefStrength, so the parser and the editor read one list.
val all: Set<String>
Everything a program may not use as a name.
val all: Set<String> =
infix + prefix + declarations + directives + functions + properties + glue + strengths + losses
}
Lexer
internal enum class TokKind { WORD, NUMBER, PUNCT, EOL, EOF }
internal data class Tok
internal data class Tok(
val kind: TokKind,
val text: String,
val line: Int,
val column: Int,
/** Set for NUMBER: the parsed value, and whether a `deg` suffix was written. */
val value: Double = 0.0,
val degrees: Boolean = false,
) {
val at: BriefSpan get() = BriefSpan(line, column, text.length.coerceAtLeast(1))
internal class BriefLexer(private val source: String)
Text to tokens. Line-oriented: an EOL token ends every statement, so a missing word is reported at the end of the line it is missing from rather than at the start of the next one.
# begins a comment. A word may contain - (so same-vector and above-ground are single
tokens), digits and _, but may not begin with a digit. A number is -?digits[.digits][e±digits]
with an optional deg suffix; 1/3 lexes as three tokens and the parser folds the division, which
is what makes fractions readable without a rational literal.
internal class BriefLexer(private val source: String) {
private val diagnostics = mutableListOf<BriefDiagnostic>()
fun diagnostics(): List<BriefDiagnostic> = diagnostics
fun lex(): List<Tok>
body — 76 lines, from line 453
fun lex(): List<Tok> {
val out = mutableListOf<Tok>()
source.lines().forEachIndexed { index, rawLine ->
val lineNumber = index + 1
var i = 0
// The `#brief` tag line, and any comment line, contribute nothing but their EOL.
while (i < rawLine.length) {
val ch = rawLine[i]
when {
ch == '#' -> i = rawLine.length
ch.isWhitespace() -> i++
// A '-' immediately before a digit is always part of the number. Brief has no
// subtraction operator at statement level — an identifier's internal hyphens are
// consumed by the word branch below, because a word always starts with a letter —
// so there is nothing for it to be ambiguous with, and the alternative (only after
// '(' or ',') silently refused `dihedral(e, a, b) = -1`.
ch.isDigit() || (ch == '-' && i + 1 < rawLine.length && rawLine[i + 1].isDigit()) -> {
val start = i
if (rawLine[i] == '-') i++
while (i < rawLine.length && (rawLine[i].isDigit() || rawLine[i] == '.')) i++
if (i < rawLine.length && (rawLine[i] == 'e' || rawLine[i] == 'E') &&
i + 1 < rawLine.length && (rawLine[i + 1].isDigit() || rawLine[i + 1] == '-' || rawLine[i + 1] == '+')
) {
i += 2
while (i < rawLine.length && rawLine[i].isDigit()) i++
}
var degrees = false
if (rawLine.startsWith("deg", i)) {
degrees = true
i += 3
}
val text = rawLine.substring(start, i)
val numeric = if (degrees) text.dropLast(3) else text
val parsed = numeric.toDoubleOrNull()
if (parsed == null || !parsed.isFinite()) {
diagnostics += BriefDiagnostic(
BriefSeverity.ERROR, lineNumber, start + 1, text.length,
"'$text' is not a finite number",
)
} else {
out += Tok(TokKind.NUMBER, text, lineNumber, start + 1, parsed, degrees)
}
}
ch.isLetter() || ch == '_' -> {
val start = i
while (i < rawLine.length && (rawLine[i].isLetterOrDigit() || rawLine[i] == '_' || rawLine[i] == '-')) i++
// A trailing '-' is punctuation, not part of the word.
while (i > start + 1 && rawLine[i - 1] == '-') i--
out += Tok(TokKind.WORD, rawLine.substring(start, i), lineNumber, start + 1)
}
else -> {
val two = if (i + 1 < rawLine.length) rawLine.substring(i, i + 2) else ""
val text = if (two == ">=" || two == "<=") two else ch.toString()
if (text !in PUNCTUATION) {
diagnostics += BriefDiagnostic(
BriefSeverity.ERROR, lineNumber, i + 1, text.length,
"'$text' means nothing here",
"brief punctuation is ${PUNCTUATION.sorted().joinToString(" ")}",
)
} else {
out += Tok(TokKind.PUNCT, text, lineNumber, i + 1)
}
i += text.length
}
}
}
out += Tok(TokKind.EOL, "end of line", lineNumber, rawLine.length + 1)
}
out += Tok(TokKind.EOF, "end of brief", source.lines().size, 1)
return out
}
private companion object {
val PUNCTUATION = setOf("(", ")", ",", ".", "=", ">=", "<=", "/", "-")
}
}
Parser
data class BriefSyntax
The parsed, unresolved program: declarations, constraints and directives in source order.
Unresolved on purpose. Nothing here has been checked against anything, which is what makes forward reference a property of the resolver rather than a special case in the grammar.
data class BriefSyntax(
/** The declaration statements, in source order, with nothing resolved against anything. */
val declarations: List<BriefDeclaration>,
/** The `strength relation` statements, in source order. */
val constraints: List<BriefConstraint>,
/** The `settle` directive, or null for the vocabulary's own default. The last one written wins. */
val settle: BriefSettle?,
/** The `robust` directive, or null, which leaves the brief on plain least squares. */
val robust: BriefRobust?,
/** What the parser complained about. The lexer's complaints are joined in by [compileBrief]. */
val diagnostics: List<BriefDiagnostic>,
)
internal class BriefParser(private val toks: List<Tok>)
internal class BriefParser(private val toks: List<Tok>) {
private var i = 0
private val declarations = mutableListOf<BriefDeclaration>()
private val constraints = mutableListOf<BriefConstraint>()
private var settle: BriefSettle? = null
private var robust: BriefRobust? = null
private val diagnostics = mutableListOf<BriefDiagnostic>()
fun parse(): BriefSyntax
body — 42 lines, from line 561
fun parse(): BriefSyntax {
while (peek().kind != TokKind.EOF) {
if (peek().kind == TokKind.EOL) {
i++
continue
}
val lineNumber = peek().line
try {
statement()
} catch (stop: Bail) {
// Reported already; drop the rest of the line and carry on, so one bad line does not
// hide the four good ones after it.
}
while (peek().kind != TokKind.EOL && peek().kind != TokKind.EOF && peek().line == lineNumber) i++
}
return BriefSyntax(declarations.toList(), constraints.toList(), settle, robust, diagnostics.toList())
}
private class Bail : RuntimeException(null, null, false, false)
private fun peek(offset: Int = 0): Tok = toks[min(i + offset, toks.size - 1)]
private fun error(at: BriefSpan, message: String, hint: String? = null): Nothing {
diagnostics += BriefDiagnostic(BriefSeverity.ERROR, at.line, at.column, at.length, message, hint)
throw Bail()
}
private fun expectWord(word: String): Tok {
val t = peek()
if (t.kind != TokKind.WORD || t.text != word) {
error(t.at, "expected '$word', found '${t.text}'")
}
i++
return t
}
private fun expectPunct(text: String): Tok {
val t = peek()
if (t.kind != TokKind.PUNCT || t.text != text) error(t.at, "expected '$text', found '${t.text}'")
i++
return t
}
A user name: any WORD that is not reserved.
private fun name(role: String): BriefName {
val t = peek()
if (t.kind != TokKind.WORD) error(t.at, "expected a $role name, found '${t.text}'")
if (t.text in BriefVerbs.all) {
error(
t.at, "'${t.text}' is a brief keyword and cannot be a $role name",
"rename it, or reorder the statement",
)
}
i++
return BriefName(t.text, t.at)
}
A number, folding a / b into a single value so 1/3 reads as a third.
source — 85 lines, from line 619
private fun number(role: String): Pair<Double, Boolean> {
val t = peek()
if (t.kind != TokKind.NUMBER) error(t.at, "expected $role, found '${t.text}'")
i++
var value = if (t.degrees) Math.toRadians(t.value) else t.value
var radians = t.degrees
while (peek().kind == TokKind.PUNCT && peek().text == "/") {
i++
val d = peek()
if (d.kind != TokKind.NUMBER) error(d.at, "expected a divisor after '/', found '${d.text}'")
i++
if (d.value == 0.0) error(d.at, "division by zero")
value /= if (d.degrees) Math.toRadians(d.value) else d.value
radians = radians || d.degrees
}
return value to radians
}
private fun vector(): Triple<Double, Double, Double> {
expectPunct("(")
val x = number("a coordinate").first
expectPunct(",")
val y = number("a coordinate").first
var z = 0.0
if (peek().kind == TokKind.PUNCT && peek().text == ",") {
i++
z = number("a coordinate").first
}
expectPunct(")")
return Triple(x, y, z)
}
private fun statement() {
val head = peek()
if (head.kind == TokKind.WORD) {
when (head.text) {
"point", "line", "plane", "panel" -> return declaration(head.text)
"settle" -> return settleDirective()
"robust" -> return robustDirective()
}
}
constraint()
}
private fun declaration(keyword: String) {
i++
val declared = name(keyword)
when (keyword) {
"point" -> {
expectWord("at")
val (x, y, z) = vector()
declarations += BriefDeclaration.Point(declared, x, y, z)
}
"line" -> {
expectWord("from")
val a = name("point")
expectWord("to")
val b = name("point")
declarations += BriefDeclaration.Line(declared, a, b)
}
"plane" -> {
expectWord("through")
declarations += BriefDeclaration.Plane(declared, name("point"), name("point"), name("point"))
}
"panel" -> {
expectWord("corners")
declarations += BriefDeclaration.Panel(
declared, name("point"), name("point"), name("point"), name("point"),
)
}
}
}
private fun settleDirective() {
val at = peek().at
i++
if (peek().kind == TokKind.WORD && peek().text == "none") {
i++
settle = BriefSettle(null, at)
return
}
val (weight, _) = number("a prior weight, or 'none'")
if (weight < 0.0) error(at, "a settle weight must not be negative")
settle = BriefSettle(weight, at)
}
robust none | robust huber 1e-3 | robust soft-l1 2 | robust cauchy 0.5.
Reachable, and off by default, because the loss changes what a weight means — see BriefRobust.
private fun robustDirective() {
val at = peek().at
i++
val kindTok = peek()
if (kindTok.kind != TokKind.WORD || kindTok.text !in BriefVerbs.losses) {
error(
kindTok.at, "'${kindTok.text}' is not a robust loss",
"the losses are ${BriefVerbs.losses.sorted().joinToString(", ")}",
)
}
i++
var scale = 1e-3
if (peek().kind == TokKind.NUMBER) {
scale = number("a loss scale").first
if (scale <= 0.0) error(at, "a loss scale must be positive")
}
robust = BriefRobust(kindTok.text, scale, at)
}
strength? relation.
The one ambiguity worth naming: a leading NUMBER is the strength unless the token after it
begins a comparison, a fraction or a property access — 250 spine.length = 4 weights a
statement, 4 = spine.length would be an equation with a literal on the left. The rule is
lexical and stated in the docs rather than resolved by backtracking.
source — 189 lines, from line 737
private fun constraint() {
val start = peek().at
var strength = BriefStrength.DEFAULT.weight
var written: String? = null
val head = peek()
if (head.kind == TokKind.WORD && head.text in BriefStrength.byWord) {
strength = BriefStrength.byWord.getValue(head.text).weight
written = head.text
i++
} else if (head.kind == TokKind.NUMBER && peek(1).let {
!(it.kind == TokKind.PUNCT && it.text in setOf("=", ">=", "<=", "/", "."))
}
) {
if (head.value < 0.0 || !head.value.isFinite()) {
error(head.at, "a strength must be finite and non-negative, not ${trimNumber(head.value)}")
}
strength = head.value
written = trimNumber(head.value)
i++
}
val relation = relation()
if (written == null) strength = defaultStrength(relation)
constraints += BriefConstraint(strength, written, relation, start)
}
private fun relation(): BriefRelation {
// An equation is recognised by a top-level comparison on this line. Scanning first is what
// keeps the two relation kinds in one grammar without a backtracking parser.
val line = peek().line
var scan = i
var comparisonAt = -1
while (toks[scan].line == line && toks[scan].kind != TokKind.EOL && toks[scan].kind != TokKind.EOF) {
if (toks[scan].kind == TokKind.PUNCT && toks[scan].text in setOf("=", ">=", "<=")) {
comparisonAt = scan
break
}
scan++
}
return if (comparisonAt >= 0) equation() else predicate()
}
private fun equation(): BriefRelation.Equation {
val start = peek().at
val left = term()
val op = peek()
val comparison = when {
op.kind == TokKind.PUNCT && op.text == "=" -> BriefComparison.EQ
op.kind == TokKind.PUNCT && op.text == ">=" -> BriefComparison.AT_LEAST
op.kind == TokKind.PUNCT && op.text == "<=" -> BriefComparison.AT_MOST
else -> error(op.at, "expected '=', '>=' or '<=', found '${op.text}'")
}
i++
val right = term()
return BriefRelation.Equation(left, comparison, right, start)
}
private fun term(): BriefTerm {
val t = peek()
if (t.kind == TokKind.NUMBER) {
val at = t.at
val (value, radians) = number("a value")
return BriefTerm.Literal(value, radians, at)
}
if (t.kind != TokKind.WORD) error(t.at, "expected a value, a property or a function, found '${t.text}'")
if (t.text in BriefVerbs.functions) {
i++
expectPunct("(")
val args = mutableListOf<BriefName>()
args += name("element")
while (peek().kind == TokKind.PUNCT && peek().text == ",") {
i++
args += name("element")
}
expectPunct(")")
return BriefTerm.Call(t.text, args, t.at)
}
val element = name("element")
expectPunct(".")
val prop = peek()
if (prop.kind != TokKind.WORD || prop.text !in BriefVerbs.properties) {
error(
prop.at, "'${prop.text}' is not a brief property",
"the properties are ${BriefVerbs.properties.sorted().joinToString(", ")}",
)
}
i++
return BriefTerm.Property(element, prop.text, BriefSpan(element.at.line, element.at.column,
element.at.length + 1 + prop.text.length))
}
private fun predicate(): BriefRelation.Predicate {
val start = peek().at
val head = peek()
if (head.kind == TokKind.WORD && head.text in BriefVerbs.prefix) return prefixPredicate()
val subjects = mutableListOf<BriefName>()
while (peek().kind == TokKind.WORD && peek().text !in BriefVerbs.all) subjects += name("element")
if (subjects.isEmpty()) {
val t = peek()
error(
t.at, "expected a statement, found '${t.text}'",
"a brief line is a declaration, a 'settle', or '[strength] relation'",
)
}
val verbTok = peek()
if (verbTok.kind != TokKind.WORD || verbTok.text !in BriefVerbs.infix) {
error(
verbTok.at, "'${verbTok.text}' is not a brief relation",
didYouMean(verbTok.text, BriefVerbs.infix + BriefVerbs.prefix)
?: "the relations are ${BriefVerbs.infix.sorted().joinToString(", ")}",
)
}
i++
val verb = verbTok.text
var objects = emptyList<BriefName>()
var number: Double? = null
var vec: Triple<Double, Double, Double>? = null
when (verb) {
"planar", "colinear", "level", "same-x", "same-y", "same-z", "same-xy", "same-xz", "same-yz" -> Unit
"parallel", "perpendicular", "same-vector", "coincident", "toward", "vanishes-at" ->
objects = listOf(name("element"))
"on", "inside" -> {
objects = listOf(name("plane"))
if (peek().kind == TokKind.WORD && (peek().text == "offset" || peek().text == "clearance")) {
i++
number = number("a distance").first
}
}
"mirrors" -> {
objects = listOf(name("point"))
expectWord("across")
objects = objects + name("plane")
}
"along" -> vec = vector()
"faces" -> {
vec = vector()
if (peek().kind == TokKind.NUMBER) number = number("a target cosine").first
}
"at" -> {
number = number("a fraction").first
expectWord("along")
expectPunct("(")
val u = name("point")
expectPunct(",")
val v = name("point")
expectPunct(")")
objects = listOf(u, v)
}
}
return BriefRelation.Predicate(verb, subjects, objects, number, vec, null, start)
}
private fun prefixPredicate(): BriefRelation.Predicate {
val start = peek().at
val verb = peek().text
i++
if (verb == "all") {
val what = peek()
if (what.kind != TokKind.WORD || (what.text != "loose" && what.text != "above-ground")) {
error(what.at, "expected 'loose' or 'above-ground' after 'all', found '${what.text}'")
}
i++
return BriefRelation.Predicate("all-${what.text}", emptyList(), emptyList(), at = start)
}
val subjects = mutableListOf<BriefName>()
while (peek().kind == TokKind.WORD && peek().text !in BriefVerbs.all) subjects += name("point")
if (subjects.isEmpty()) error(peek().at, "'$verb' needs at least one point")
var axes: String? = null
if (peek().kind == TokKind.WORD && peek().text == "in") {
i++
val a = peek()
if (a.kind != TokKind.WORD || a.text.lowercase() !in AXES) {
error(
a.at, "'${a.text}' is not an axis set",
"the axis sets are ${AXES.sorted().joinToString(", ")}",
)
}
i++
axes = a.text.lowercase()
}
return BriefRelation.Predicate(verb, subjects, emptyList(), axes = axes, at = start)
}
private companion object {
val AXES = setOf("x", "y", "z", "xy", "xz", "yz", "xyz")
}
}
did-you-mean
internal fun didYouMean(written: String, candidates: Collection<String>): String?
The closest candidate within an edit distance that scales with the length of what was written.
The threshold matters more than the metric: a fixed distance of 2 suggests x for z on a
one-letter name, which is worse than silence. So it is 1 + length/4, capped, and a candidate must
also share a first letter or a length — enough to make spien → spine land and foo → bar not.
internal fun didYouMean(written: String, candidates: Collection<String>): String? {
if (written.isEmpty() || candidates.isEmpty()) return null
val limit = 1 + written.length / 4
val best = candidates
.filter { it != written }
.map { it to editDistance(written.lowercase(), it.lowercase()) }
.filter { (candidate, distance) ->
distance <= limit && (candidate.firstOrNull() == written.firstOrNull() || candidate.length == written.length)
}
.minByOrNull { it.second }
?: return null
return "did you mean '${best.first}'?"
}
internal fun editDistance(a: String, b: String): Int
internal fun editDistance(a: String, b: String): Int {
if (a.isEmpty()) return b.length
if (b.isEmpty()) return a.length
var previous = IntArray(b.length + 1) { it }
var current = IntArray(b.length + 1)
for (x in 1..a.length) {
current[0] = x
for (y in 1..b.length) {
val substitution = previous[y - 1] + if (a[x - 1] == b[y - 1]) 0 else 1
current[y] = minOf(current[y - 1] + 1, previous[y] + 1, substitution)
}
val swap = previous
previous = current
current = swap
}
return previous[b.length]
}