Skip to content

Commit

Permalink
Add record type, #each record key/list index iteration, list & object…
Browse files Browse the repository at this point in the history
… spread
  • Loading branch information
ChiriVulpes committed Oct 10, 2024
1 parent dfa623e commit 9191d08
Show file tree
Hide file tree
Showing 11 changed files with 230 additions and 34 deletions.
3 changes: 1 addition & 2 deletions lib/default/variables.chiri
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ $font-scale-factor: 1
2.0
3.0

#for var i = 0, i < font-sizes::length, set i++:
#dec font-size = font-sizes::at(i)
#each in font-sizes as var i, var font-size:
$font-#{i + 1}: calc(#{font-size}rem / $font-scale-factor)
$font-scaling-#{i + 1}: #{font-size}

Expand Down
4 changes: 2 additions & 2 deletions lib/function/list/join.chiri
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
#string separator ??= "

#string result = "
#for var i = 0, i < values::length, set i++:
#each values as var i, var value:
#if i > 0:
#set result .= separator

#set result .= values::at(i)
#set result .= "#{value}

#return result
2 changes: 2 additions & 0 deletions src/chc/read/consume/consumeTypeConstructorOptional.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ChiriType } from "../../type/ChiriType"
import type TypeDefinition from "../../type/TypeDefinition"
import type { ChiriLiteralList } from "../../type/typeList"
import type { ChiriLiteralRecord } from "../../type/typeRecord"
import type ChiriReader from "../ChiriReader"
import type { ChiriPosition } from "../ChiriReader"
import type { ChiriLiteralString } from "./consumeStringOptional"
Expand All @@ -27,6 +28,7 @@ export type ChiriLiteralValue =
| ChiriLiteralBool
| ChiriLiteralUndefined
| ChiriLiteralList
| ChiriLiteralRecord

type VerifyChiriStatement = ChiriLiteralValue["position"]

Expand Down
51 changes: 42 additions & 9 deletions src/chc/read/consume/macro/macroEach.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { ChiriType } from "../../../type/ChiriType"
import typeString from "../../../type/typeString"
import typeUint from "../../../type/typeUint"
import type { ChiriPosition, ChiriStatement } from "../../ChiriReader"
import consumeBody from "../consumeBody"
import type { ChiriCompilerVariable } from "../consumeCompilerVariableOptional"
import consumeCompilerVariableOptional from "../consumeCompilerVariableOptional"
import consumeWhiteSpace from "../consumeWhiteSpace"
import consumeWhiteSpaceOptional from "../consumeWhiteSpaceOptional"
import type { ChiriWord } from "../consumeWord"
import consumeWord from "../consumeWord"
import MacroConstruct from "./MacroConstruct"

export interface ChiriEach {
type: "each"
iterable: ChiriWord
keyVariable?: ChiriCompilerVariable
variable: ChiriCompilerVariable
content: ChiriStatement[]
position: ChiriPosition
Expand All @@ -20,35 +24,64 @@ export default MacroConstruct("each")
.consumeParameters(async reader => {
consumeWhiteSpace(reader)

let e = reader.i
reader.consumeOptional("in ")

const e = reader.i
const iterable = consumeWord(reader)
const iterableVariable = reader.getVariable(iterable.value)
if (!reader.types.isAssignable(iterableVariable.valueType, ChiriType.of("list", "*")))
if (!reader.types.isAssignable(iterableVariable.valueType, ChiriType.of("list", "*"), ChiriType.of("record", "*")))
throw reader.error(e, `Expected list or record, was ${ChiriType.stringify(iterableVariable?.valueType)}`)

consumeWhiteSpace(reader)
reader.consume("as")
consumeWhiteSpace(reader)

e = reader.i
const variable = await consumeCompilerVariableOptional(reader, false)
if (!variable)
const variable1 = await consumeCompilerVariableOptional(reader, false)
if (!variable1)
throw reader.error("Expected variable declaration")

if (!reader.types.isAssignable(iterableVariable.valueType.generics[0], variable.valueType))
throw reader.error(e, `Iterable of type "${ChiriType.stringify(iterableVariable.valueType.generics[0])}" is not assignable to "${ChiriType.stringify(variable.valueType)}"`)
let variable2: ChiriCompilerVariable | undefined
if (reader.consumeOptional(",")) {
consumeWhiteSpaceOptional(reader)

variable2 = await consumeCompilerVariableOptional(reader, false)
if (!variable2)
throw reader.error("Expected variable declaration")
}

if (!variable2 && iterableVariable.valueType.name.value === "record")
throw reader.error("Expected variable declarations for both a key and its associated value")

if (iterableVariable.valueType.name.value === "record" && !reader.types.isAssignable(variable1.valueType, typeString.type))
throw reader.error(e, `Iterable value of type "${ChiriType.stringify(variable1.valueType)}" is not assignable to "${ChiriType.stringify(typeString.type)}"`)

if (!reader.types.isAssignable(iterableVariable.valueType.generics[0], (variable2 ?? variable1).valueType))
throw reader.error(e, `Iterable value of type "${ChiriType.stringify(iterableVariable.valueType.generics[0])}" is not assignable to "${ChiriType.stringify((variable2 ?? variable1).valueType)}"`)

const keyVariable = variable2 ? variable1 : undefined
if (keyVariable)
keyVariable.valueType = iterableVariable.valueType.name.value === "list" ? typeUint.type : typeString.type

const variable = variable2 ?? variable1
variable.valueType = iterableVariable.valueType.generics[0]

return {
iterable,
keyVariable,
variable,
}
})
.consume(async ({ reader, extra: { iterable, variable }, position }): Promise<ChiriEach> => {
.consume(async ({ reader, extra: { iterable, variable, keyVariable }, position }): Promise<ChiriEach> => {
reader.consume(":")
const body = await consumeBody(reader, "inherit", sub => sub.addOuterStatement(variable))
const body = await consumeBody(reader, "inherit", sub => {
if (keyVariable)
sub.addOuterStatement(keyVariable)
sub.addOuterStatement(variable)
})
return {
type: "each",
iterable,
keyVariable,
variable,
...body,
position,
Expand Down
2 changes: 2 additions & 0 deletions src/chc/type/ChiriTypeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type TypeDefinition from "./TypeDefinition"
import typeInt from "./typeInt"
import typeList from "./typeList"
import typeRaw from "./typeRaw"
import typeRecord from "./typeRecord"
import typeString from "./typeString"
import typeUint from "./typeUint"

Expand All @@ -22,6 +23,7 @@ const typesList = [
typeInt,
typeUint,
typeList,
typeRecord,
typeBody,
typeBool,
typeRaw,
Expand Down
35 changes: 28 additions & 7 deletions src/chc/type/typeList.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type ChiriReader from "../read/ChiriReader"
import type { ChiriPosition } from "../read/ChiriReader"
import consumeBlockEnd from "../read/consume/consumeBlockEnd"
import consumeBlockStartOptional from "../read/consume/consumeBlockStartOptional"
Expand All @@ -8,38 +9,46 @@ import consumeExpression from "../read/consume/expression/consumeExpression"
import { ChiriType } from "./ChiriType"
import TypeDefinition from "./TypeDefinition"

export interface ChiriLiteralListSpread {
type: "list-spread"
value: ChiriExpressionOperand
position: ChiriPosition
}

export interface ChiriLiteralList {
type: "literal"
subType: "list"
valueType: ChiriType
value: ChiriExpressionOperand[]
value: (ChiriExpressionOperand | ChiriLiteralListSpread)[]
position: ChiriPosition
}

const TYPE_LIST = ChiriType.of("list")
export default TypeDefinition({
type: ChiriType.of("list"),
type: TYPE_LIST,
stringable: true,
generics: 1,
consumeOptionalConstructor: (reader): ChiriLiteralList | undefined => {
const position = reader.getPosition()
if (!reader.consumeOptional("["))
return undefined

const expressions: ChiriExpressionOperand[] = []
const expressions: (ChiriExpressionOperand | ChiriLiteralListSpread)[] = []
const multiline = consumeBlockStartOptional(reader)
if (!multiline) {
consumeWhiteSpaceOptional(reader)
do expressions.push(consumeExpression.inline(reader))
do expressions.push(consumeOptionalSpread(reader) ?? consumeExpression.inline(reader))
while (reader.consumeOptional(", "))

} else {
do expressions.push(consumeExpression.inline(reader))
do expressions.push(consumeOptionalSpread(reader) ?? consumeExpression.inline(reader))
while (consumeNewBlockLineOptional(reader))

consumeBlockEnd(reader)
}

const stringifiedTypes = expressions.map(expr => ChiriType.stringify(expr.valueType))
const valueTypes = expressions.map(expr => expr.type === "list-spread" ? expr.value.valueType.generics[0] : expr.valueType)
const stringifiedTypes = valueTypes.map(type => ChiriType.stringify(type))
if (new Set(stringifiedTypes).size > 1)
throw reader.error(`Lists can only contain a single type. This list contains: ${stringifiedTypes.join(", ")}`)

Expand All @@ -51,11 +60,23 @@ export default TypeDefinition({
return {
type: "literal",
subType: "list",
valueType: ChiriType.of("list", expressions[0]?.valueType ?? "*"),
valueType: ChiriType.of("list", valueTypes[0] ?? "*"),
value: expressions,
position,
}
},
coerce: value => Array.isArray(value) ? value : [value],
is: value => Array.isArray(value),
})

function consumeOptionalSpread (reader: ChiriReader): ChiriLiteralListSpread | undefined {
const position = reader.getPosition()
if (!reader.consumeOptional("..."))
return undefined

return {
type: "list-spread",
value: consumeExpression.inline(reader, TYPE_LIST),
position,
}
}
85 changes: 85 additions & 0 deletions src/chc/type/typeRecord.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type ChiriReader from "../read/ChiriReader"
import type { ChiriPosition } from "../read/ChiriReader"
import consumeBlockEnd from "../read/consume/consumeBlockEnd"
import consumeBlockStartOptional from "../read/consume/consumeBlockStartOptional"
import consumeNewBlockLineOptional from "../read/consume/consumeNewBlockLineOptional"
import type { ChiriLiteralString } from "../read/consume/consumeStringOptional"
import consumeStringOptional from "../read/consume/consumeStringOptional"
import consumeWhiteSpaceOptional from "../read/consume/consumeWhiteSpaceOptional"
import consumeWordInterpolated from "../read/consume/consumeWordInterpolated"
import type { ChiriWordInterpolated } from "../read/consume/consumeWordInterpolatedOptional"
import type { ChiriExpressionOperand, ChiriExpressionResult } from "../read/consume/expression/consumeExpression"
import consumeExpression from "../read/consume/expression/consumeExpression"
import { Record } from "../util/resolveExpression"
import { ChiriType } from "./ChiriType"
import TypeDefinition from "./TypeDefinition"

export type ChiriLiteralRecordKeyValueTuple = [key: ChiriLiteralString | ChiriWordInterpolated, value: ChiriExpressionOperand]

export interface ChiriLiteralRecord {
type: "literal"
subType: "record"
valueType: ChiriType
value: (ChiriLiteralRecordKeyValueTuple | ChiriExpressionResult)[]
position: ChiriPosition
}

const TYPE_RECORD = ChiriType.of("record")
export default TypeDefinition({
type: TYPE_RECORD,
stringable: true,
generics: 1,
consumeOptionalConstructor: (reader): ChiriLiteralRecord | undefined => {
const position = reader.getPosition()
if (!reader.consumeOptional("{"))
return undefined

const expressions: (ChiriLiteralRecordKeyValueTuple | ChiriExpressionResult)[] = []
const multiline = consumeBlockStartOptional(reader)
if (!multiline) {
consumeWhiteSpaceOptional(reader)
do expressions.push(consumeOptionalSpread(reader) ?? consumeRecordKeyValue(reader))
while (reader.consumeOptional(", "))

} else {
do expressions.push(consumeOptionalSpread(reader) ?? consumeRecordKeyValue(reader))
while (consumeNewBlockLineOptional(reader))

consumeBlockEnd(reader)
}

const valueTypes = expressions.map(expr => Array.isArray(expr) ? expr[1].valueType : expr.valueType)
const stringifiedTypes = valueTypes.map(valueType => ChiriType.stringify(valueType))
if (new Set(stringifiedTypes).size > 1)
throw reader.error(`Records can only contain a single type. This record contains: ${stringifiedTypes.join(", ")}`)

if (!multiline) {
consumeWhiteSpaceOptional(reader)
reader.consume("}")
}

return {
type: "literal",
subType: "record",
valueType: ChiriType.of("record", valueTypes[0] ?? "*"),
value: expressions,
position,
}
},
is: value => Record.is(value),
})

function consumeOptionalSpread (reader: ChiriReader): ChiriExpressionOperand | undefined {
if (!reader.consumeOptional("..."))
return undefined

return consumeExpression.inline(reader, TYPE_RECORD)
}

function consumeRecordKeyValue (reader: ChiriReader): ChiriLiteralRecordKeyValueTuple {
const key = consumeStringOptional(reader) ?? consumeWordInterpolated(reader, true)
reader.consume(":")
consumeWhiteSpaceOptional(reader)
const expr = consumeExpression.inline(reader)
return [key, expr]
}
11 changes: 10 additions & 1 deletion src/chc/util/resolveExpression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,17 @@ import type ChiriCompiler from "../write/ChiriCompiler"
import resolveLiteralValue from "./resolveLiteralValue"
import type { default as stringifyTextType } from "./stringifyText"

export const SYMBOL_IS_RECORD = Symbol("IS_RECORD")
export type Literal = undefined | number | boolean | string
export type Value = Literal | Value[]
export type Record = { [KEY in string]: Literal | Literal[] } & { [SYMBOL_IS_RECORD]: true }
export type Value = Literal | Value[] | Record

export namespace Record {
export function is (value: unknown): value is Record {
return typeof value === "object" && !!value && (value as Record)[SYMBOL_IS_RECORD]
}
}

function resolveExpression (compiler: ChiriCompiler, expression?: ChiriExpressionResult): Value {
if (!expression)
return undefined
Expand Down
31 changes: 29 additions & 2 deletions src/chc/util/resolveLiteralValue.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { ChiriLiteralValue } from "../read/consume/consumeTypeConstructorOptional"
import { ChiriType } from "../type/ChiriType"
import type ChiriCompiler from "../write/ChiriCompiler"
import type { default as resolveExpressionType } from "./resolveExpression"
import resolveExpression from "./resolveExpression"
import resolveExpression, { Record } from "./resolveExpression"
import type { default as stringifyExpressionType } from "./stringifyExpression"

function resolveLiteralValue (compiler: ChiriCompiler, expression: ChiriLiteralValue) {
Expand All @@ -19,9 +20,35 @@ function resolveLiteralValue (compiler: ChiriCompiler, expression: ChiriLiteralV
return expression.segments
.map(segment => typeof segment === "string" ? segment : resolveLiteralValue.stringifyExpression?.(compiler, segment))
.join("")

case "list":
return expression.value
.map(value => resolveExpression(compiler, value))
.flatMap(content => {
if (content.type !== "list-spread")
return [resolveExpression(compiler, content)]

const value = resolveExpression(compiler, content.value)
if (!Array.isArray(value))
throw compiler.error(content.position, `Unable to spread a value of type "${ChiriType.stringify(content.value.valueType)}"`)

return value
})

case "record":
return Object.fromEntries(expression.value
.flatMap(content => {
if (Array.isArray(content)) {
const [key, value] = content
return [[resolveLiteralValue.stringifyExpression(compiler, key), resolveExpression(compiler, value)]]
}

const value = resolveLiteralValue.resolveExpression(compiler, content)
if (!Record.is(value))
throw compiler.error(content.position, `Unable to spread a value of type "${ChiriType.stringify(content.valueType)}"`)

return Object.entries(value)
})) as Record

default: {
const e2 = expression as ChiriLiteralValue
throw compiler.error(e2.position, `Unable to resolve literal value type ${e2.subType}`)
Expand Down
Loading

0 comments on commit 9191d08

Please sign in to comment.